| | |
| | | @import './variables.module.scss';
|
| | | @import './mixin.scss';
|
| | | @import './transition.scss';
|
| | | @import './element-ui.scss';
|
| | | @import './sidebar.scss';
|
| | | @import './btn.scss';
|
| | | @import './ruoyi.scss';
|
| | | @import "./variables.module.scss";
|
| | | @import "./mixin.scss";
|
| | | @import "./transition.scss";
|
| | | @import "./element-ui.scss";
|
| | | @import "./sidebar.scss";
|
| | | @import "./btn.scss";
|
| | | @import "./ruoyi.scss";
|
| | |
|
| | | body { |
| | | height: 100%; |
| | |
| | | -webkit-font-smoothing: antialiased;
|
| | | text-rendering: optimizeLegibility; |
| | | font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; |
| | | background: |
| | | radial-gradient(circle at 9% -6%, rgba(59, 130, 246, 0.14), transparent 36%), |
| | | radial-gradient(circle at 88% -8%, rgba(56, 189, 248, 0.12), transparent 30%), |
| | | background: radial-gradient(circle at 9% -6%, rgba(59, 130, 246, 0.14), transparent 36%), radial-gradient(circle at 88% -8%, rgba(56, 189, 248, 0.12), transparent 30%),
|
| | | linear-gradient(165deg, #f3f7fc 0%, #eef5ff 54%, #f8fbff 100%); |
| | | color: var(--text-primary); |
| | | } |
| | |
| | |
|
| | | //main-container全局样式
|
| | | .app-container {
|
| | | padding: 20px 24px 24px;
|
| | | --radius-lg: 0px; |
| | | --radius-md: 0px; |
| | | --radius-sm: 0px; |
| | | --radius-xs: 0px; |
| | | --el-border-radius-base: 0px; |
| | | --el-border-radius-small: 0px; |
| | | --el-border-radius-round: 0px; |
| | | --el-border-radius-circle: 0px; |
| | | |
| | | padding: 20px;
|
| | | background-color: var(--surface-base, #ffffff);
|
| | | border-radius: 0; |
| | | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
| | | border: 1px solid var(--surface-border, #e2e8f0);
|
| | | min-height: calc(100vh - var(--topbar-height, 64px) - var(--content-gap, 16px) - 20px);
|
| | | margin-bottom: 10px;
|
| | |
|
| | | @media (max-width: 768px) {
|
| | | padding: 12px;
|
| | | border-radius: 0; |
| | | }
|
| | | }
|
| | | .search_form { |
| | | display: flex;
|
| | | align-items: center;
|
| | | justify-content: space-between;
|
| | | margin-bottom: 16px;
|
| | | padding-bottom: 16px;
|
| | | border-bottom: 1px dashed var(--surface-border, #e2e8f0);
|
| | | .search_title { |
| | | font-size: 14px;
|
| | | font-weight: 600;
|
| | |
| | | } |
| | | } |
| | | .table_list { |
| | | background: var(--panel-mask); |
| | | border: 1px solid var(--surface-border); |
| | | border-radius: var(--radius-md); |
| | | box-shadow: var(--shadow-sm); |
| | | backdrop-filter: blur(12px); |
| | | padding: 18px; |
| | | background: transparent;
|
| | | border: none;
|
| | | border-radius: 0;
|
| | | box-shadow: none;
|
| | | backdrop-filter: none;
|
| | | padding: 0;
|
| | | } |
| | | .components-container {
|
| | | margin: 30px 50px;
|
| | |
| | | }
|
| | |
|
| | | .text-center {
|
| | | text-align: center
|
| | | text-align: center;
|
| | | }
|
| | |
|
| | | .sub-navbar {
|
| | |
| | | height: 100vh; |
| | | position: fixed; |
| | | top: 0; |
| | | padding-top: var(--topbar-height); |
| | | left: 0; |
| | | z-index: 1001; |
| | | overflow: hidden; |
| | | padding: 0; |
| | | font-size: 0; |
| | | background: var(--sidebar-bg); |
| | | background: var(--surface-base); |
| | | border-right: 1px solid var(--surface-border); |
| | | box-shadow: var(--shadow-md); |
| | | box-shadow: none; |
| | | |
| | | > * { |
| | | position: relative; |
| | |
| | | border: none !important; |
| | | height: 100%; |
| | | width: 100% !important; |
| | | padding: 12px 0; |
| | | padding: 8px 0; |
| | | background: transparent !important; |
| | | } |
| | | |
| | |
| | | .submenu-title-noDropdown, |
| | | .el-sub-menu__title, |
| | | .el-menu-item { |
| | | width: calc(100% - 16px) !important; |
| | | margin: 4px 8px !important; |
| | | width: 100% !important; |
| | | margin: 0 !important; |
| | | height: 44px; |
| | | line-height: 44px; |
| | | border-radius: var(--radius-md); |
| | | padding-left: 16px !important; |
| | | padding-right: 36px !important; // 预留箭头位置 |
| | | border-radius: 0; |
| | | padding-left: 18px !important; |
| | | padding-right: 18px !important; |
| | | box-sizing: border-box; |
| | | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| | | color: var(--sidebar-text); |
| | | color: var(--el-color-primary); |
| | | background: transparent; |
| | | border: none !important; |
| | | position: relative; |
| | | font-size: 14px; |
| | | border-bottom: 1px dashed rgba(var(--el-color-primary-rgb), 0.35) !important; |
| | | |
| | | .svg-icon { |
| | | margin-right: 12px; |
| | | width: 18px; |
| | | height: 18px; |
| | | width: 16px; |
| | | height: 16px; |
| | | vertical-align: middle; |
| | | flex-shrink: 0; |
| | | transition: transform 0.3s ease; |
| | |
| | | .el-sub-menu { |
| | | &.is-opened { |
| | | > .el-sub-menu__title { |
| | | color: var(--menu-active-text) !important; |
| | | color: var(--el-color-primary) !important; |
| | | font-weight: 600; |
| | | |
| | | .el-sub-menu__icon-arrow { |
| | | transform: rotate(180deg) !important; |
| | | color: var(--menu-active-text) !important; |
| | | color: var(--el-color-primary) !important; |
| | | } |
| | | } |
| | | } |
| | |
| | | font-size: 12px !important; |
| | | display: inline-block !important; |
| | | transition: transform 0.3s ease !important; |
| | | color: var(--sidebar-text) !important; |
| | | color: var(--el-color-primary) !important; |
| | | z-index: 2; |
| | | } |
| | | } |
| | |
| | | .submenu-title-noDropdown:hover, |
| | | .el-sub-menu__title:hover, |
| | | .el-menu-item:hover { |
| | | background: var(--menu-hover) !important; |
| | | color: var(--menu-active-text) !important; |
| | | background-color: rgba(var(--el-color-primary-rgb), 0.06) !important; |
| | | color: var(--el-color-primary) !important; |
| | | |
| | | .svg-icon { |
| | | transform: scale(1.1); |
| | | color: var(--el-color-primary) !important; |
| | | } |
| | | |
| | | .el-sub-menu__icon-arrow { |
| | | color: var(--el-color-primary) !important; |
| | | } |
| | | } |
| | | |
| | | .el-menu-item.is-active, |
| | | .el-sub-menu.is-active > .el-sub-menu__title { |
| | | color: var(--menu-active-text) !important; |
| | | background: var(--menu-active-bg) !important; |
| | | box-shadow: var(--menu-active-glow); |
| | | color: var(--el-color-primary) !important; |
| | | background: rgba(var(--el-color-primary-rgb), 0.06) !important; |
| | | box-shadow: none; |
| | | font-weight: 600; |
| | | |
| | | .svg-icon { |
| | | color: var(--menu-active-text) !important; |
| | | color: var(--el-color-primary) !important; |
| | | } |
| | | } |
| | | |
| | | & .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | & .el-sub-menu .el-menu-item { |
| | | height: 40px; |
| | | line-height: 40px; |
| | | margin: 2px 8px !important; |
| | | padding-left: 44px !important; // 增加子菜单缩进 |
| | | border-radius: var(--radius-sm); |
| | | font-size: 13px; |
| | | height: 44px; |
| | | line-height: 44px; |
| | | margin: 0 !important; |
| | | padding-left: 38px !important; |
| | | border-radius: 0; |
| | | font-size: 14px; |
| | | background-color: transparent !important; |
| | | border-bottom: 1px dashed rgba(var(--el-color-primary-rgb), 0.3) !important; |
| | | |
| | | &.is-active { |
| | | background: var(--menu-active-bg) !important; |
| | | background-color: rgba(var(--el-color-primary-rgb), 0.06) !important; |
| | | } |
| | | |
| | | &:hover { |
| | | background-color: rgba(var(--el-color-primary-rgb), 0.06) !important; |
| | | color: var(--el-color-primary) !important; |
| | | |
| | | .svg-icon { |
| | | color: var(--el-color-primary) !important; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | justify-content: center; |
| | | margin: 4px 8px !important; |
| | | width: calc(var(--sidebar-collapsed-width) - 16px) !important; |
| | | border-bottom: none !important; |
| | | |
| | | .svg-icon { |
| | | margin-right: 0; |
| | |
| | | .sidebar-container { |
| | | transition: transform 0.25s; |
| | | width: var(--sidebar-width) !important; |
| | | padding-top: 0; |
| | | z-index: 1004; |
| | | } |
| | | |
| | | &.hideSidebar { |
| | |
| | | .nest-menu .el-sub-menu > .el-sub-menu__title, |
| | | .el-menu-item { |
| | | min-width: 0 !important; |
| | | margin: 4px 10px; |
| | | width: calc(100% - 20px); |
| | | margin: 0; |
| | | width: 100%; |
| | | height: 44px; |
| | | line-height: 44px; |
| | | padding-left: 16px !important; |
| | | padding-right: 16px !important; |
| | | padding-left: 18px !important; |
| | | padding-right: 18px !important; |
| | | box-sizing: border-box; |
| | | border-radius: var(--radius-md); |
| | | color: var(--sidebar-text); |
| | | border-radius: 0; |
| | | color: var(--el-color-primary); |
| | | background: transparent; |
| | | transition: all 0.2s ease; |
| | | border-bottom: 1px dashed rgba(var(--el-color-primary-rgb), 0.35); |
| | | |
| | | &:hover { |
| | | background: var(--menu-hover) !important; |
| | | color: #fff !important; |
| | | background: rgba(var(--el-color-primary-rgb), 0.06) !important; |
| | | color: var(--el-color-primary) !important; |
| | | } |
| | | |
| | | &.is-active { |
| | | background: var(--menu-active-bg) !important; |
| | | color: var(--menu-active-text) !important; |
| | | background: rgba(var(--el-color-primary-rgb), 0.06) !important; |
| | | color: var(--el-color-primary) !important; |
| | | font-weight: 600; |
| | | box-shadow: var(--menu-active-glow); |
| | | box-shadow: none; |
| | | } |
| | | } |
| | | |
| | | > .el-menu--popup { |
| | | max-height: 100vh; |
| | | overflow: hidden; |
| | | padding: 8px; |
| | | border-radius: var(--radius-lg); |
| | | padding: 8px 0; |
| | | border-radius: 0; |
| | | border: 1px solid var(--surface-border); |
| | | box-shadow: var(--shadow-menu); |
| | | background: var(--sidebar-bg); |
| | | backdrop-filter: blur(16px); |
| | | box-shadow: var(--shadow-sm); |
| | | background: var(--surface-base); |
| | | backdrop-filter: none; |
| | | |
| | | > .el-menu { |
| | | max-height: calc(100vh - 16px); |
| | |
| | | --content-radius: 10px; |
| | | --layout-header-z: 20; |
| | | |
| | | --el-color-primary: #374d77; |
| | | --el-color-primary-rgb: 37, 89, 163; |
| | | --el-color-primary: #008c8c; |
| | | --el-color-primary-rgb: 0, 140, 140; |
| | | --el-color-success: #14b8a6; |
| | | --el-color-warning: #f59e0b; |
| | | --el-color-danger: #ef4444; |
| | | |
| | | --sidebar-bg: #1e293b; |
| | | --sidebar-text: #94a3b8; |
| | | --sidebar-muted: #64748b; |
| | | --menu-hover: rgba(255, 255, 255, 0.05); |
| | | --menu-active-bg: #3b82f6; |
| | | --menu-active-text: #ffffff; |
| | | --menu-surface: #1e293b; |
| | | --menu-active-glow: 0 4px 12px rgba(59, 130, 246, 0.3); |
| | | --sidebar-bg: #304156; |
| | | --sidebar-text: #bfcbd9; |
| | | --sidebar-muted: #8b9bb4; |
| | | --menu-hover: #263445; |
| | | --menu-active-bg: transparent; |
| | | --menu-active-text: #409eff; |
| | | --menu-surface: #304156; |
| | | --menu-active-glow: none; |
| | | |
| | | --app-bg: #f8fafc; |
| | | --app-bg-accent: #f1f5f9; |
| | |
| | | --tags-item-hover: #f1f5f9; |
| | | --tags-close-hover: rgba(239, 68, 68, 0.1); |
| | | |
| | | --accent-primary: #374d77; |
| | | --accent-primary: #008c8c; |
| | | --accent-light: #3b82f6; |
| | | --accent-lighter: #60a5fa; |
| | | |
| | |
| | | <template>
|
| | | <el-menu
|
| | | <div class="top-nav"> |
| | | <button v-show="showArrows" |
| | | class="nav-arrow nav-arrow--left" |
| | | type="button" |
| | | :disabled="!canScrollLeft" |
| | | @click="scrollLeft"> |
| | | <el-icon :size="18"> |
| | | <ArrowLeft /> |
| | | </el-icon> |
| | | </button> |
| | | <div ref="scrollWrapRef" |
| | | class="top-nav__scroll" |
| | | @scroll.passive="updateScrollState"> |
| | | <el-menu class="top-nav-menu" |
| | | :default-active="activeMenu"
|
| | | mode="horizontal"
|
| | | @select="handleSelect"
|
| | | :unique-opened="true" |
| | | :ellipsis="false"
|
| | | >
|
| | | <template v-for="(item, index) in topMenus">
|
| | | <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
|
| | | <svg-icon
|
| | | v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
|
| | | :icon-class="item.meta.icon"/>
|
| | | {{ item.meta.title }}
|
| | | </el-menu-item>
|
| | | </template>
|
| | |
|
| | | <!-- 顶部菜单超出数量折叠 -->
|
| | | <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
|
| | | <template #title>更多菜单</template>
|
| | | <template v-for="(item, index) in topMenus">
|
| | | <el-menu-item
|
| | | :index="item.path"
|
| | | :key="index"
|
| | | v-if="index >= visibleNumber">
|
| | | <svg-icon
|
| | | v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
|
| | | :icon-class="item.meta.icon"/>
|
| | | {{ item.meta.title }}
|
| | | </el-menu-item>
|
| | | </template>
|
| | | </el-sub-menu>
|
| | | :active-text-color="theme"> |
| | | <sidebar-item v-for="(routeItem, index) in topbarRouters" |
| | | :key="routeItem.path + index" |
| | | :item="routeItem" |
| | | :base-path="routeItem.path" /> |
| | | </el-menu>
|
| | | </div> |
| | | <button v-show="showArrows" |
| | | class="nav-arrow nav-arrow--right" |
| | | type="button" |
| | | :disabled="!canScrollRight" |
| | | @click="scrollRight"> |
| | | <el-icon :size="18"> |
| | | <ArrowRight /> |
| | | </el-icon> |
| | | </button> |
| | | </div> |
| | | </template>
|
| | |
|
| | | <script setup>
|
| | | import { constantRoutes } from "@/router"
|
| | | import { isHttp } from '@/utils/validate'
|
| | | import useAppStore from '@/store/modules/app'
|
| | | import useSettingsStore from '@/store/modules/settings'
|
| | | import usePermissionStore from '@/store/modules/permission'
|
| | | import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue"; |
| | | import useSettingsStore from "@/store/modules/settings"; |
| | | import usePermissionStore from "@/store/modules/permission"; |
| | | import SidebarItem from "@/layout/components/Sidebar/SidebarItem.vue"; |
| | |
|
| | | // 顶部栏初始数
|
| | | const visibleNumber = ref(null)
|
| | | // 当前激活菜单的 index
|
| | | const currentIndex = ref(null)
|
| | | // 隐藏侧边栏路由
|
| | | const hideList = ['/index', '/user/profile']
|
| | |
|
| | | const appStore = useAppStore()
|
| | | const settingsStore = useSettingsStore()
|
| | | const permissionStore = usePermissionStore()
|
| | | const route = useRoute()
|
| | | const router = useRouter()
|
| | | const settingsStore = useSettingsStore(); |
| | | const permissionStore = usePermissionStore(); |
| | | const route = useRoute(); |
| | |
|
| | | // 主题颜色
|
| | | const theme = computed(() => settingsStore.theme)
|
| | | // 所有的路由信息
|
| | | const routers = computed(() => permissionStore.topbarRouters)
|
| | |
|
| | | // 顶部显示菜单
|
| | | const topMenus = computed(() => {
|
| | | let topMenus = []
|
| | | routers.value.map((menu) => {
|
| | | if (menu.hidden !== true) {
|
| | | // 兼容顶部栏一级菜单内部跳转
|
| | | if (menu.path === '/' && menu.children) {
|
| | | topMenus.push(menu.children[0])
|
| | | } else {
|
| | | topMenus.push(menu)
|
| | | }
|
| | | }
|
| | | })
|
| | | return topMenus
|
| | | })
|
| | |
|
| | | // 设置子路由
|
| | | const childrenMenus = computed(() => {
|
| | | let childrenMenus = []
|
| | | routers.value.map((router) => {
|
| | | for (let item in router.children) {
|
| | | if (router.children[item].parentPath === undefined) {
|
| | | if(router.path === "/") {
|
| | | router.children[item].path = "/" + router.children[item].path
|
| | | } else {
|
| | | if(!isHttp(router.children[item].path)) {
|
| | | router.children[item].path = router.path + "/" + router.children[item].path
|
| | | }
|
| | | }
|
| | | router.children[item].parentPath = router.path
|
| | | }
|
| | | childrenMenus.push(router.children[item])
|
| | | }
|
| | | })
|
| | | return constantRoutes.concat(childrenMenus)
|
| | | })
|
| | | const theme = computed(() => settingsStore.theme); |
| | | const topbarRouters = computed(() => permissionStore.topbarRouters); |
| | | const scrollWrapRef = ref(null); |
| | | const canScrollLeft = ref(false); |
| | | const canScrollRight = ref(false); |
| | | const showArrows = computed(() => canScrollLeft.value || canScrollRight.value); |
| | |
|
| | | // 默认激活的菜单
|
| | | const activeMenu = computed(() => {
|
| | | const path = route.path
|
| | | let activePath = path
|
| | | if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
|
| | | const tmpPath = path.substring(1, path.length)
|
| | | if (!route.meta.link) {
|
| | | activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
|
| | | appStore.toggleSideBarHide(false)
|
| | | }
|
| | | } else if(!route.children) {
|
| | | activePath = path
|
| | | appStore.toggleSideBarHide(true)
|
| | | }
|
| | | activeRoutes(activePath)
|
| | | return activePath
|
| | | })
|
| | | const { meta, path } = route; |
| | | if (meta?.activeMenu) return meta.activeMenu; |
| | | return path; |
| | | }); |
| | |
|
| | | function setVisibleNumber() {
|
| | | const width = document.body.getBoundingClientRect().width / 3
|
| | | visibleNumber.value = parseInt(width / 85)
|
| | | function updateScrollState() { |
| | | const el = scrollWrapRef.value; |
| | | if (!el) return; |
| | | const maxScrollLeft = el.scrollWidth - el.clientWidth; |
| | | canScrollLeft.value = el.scrollLeft > 0; |
| | | canScrollRight.value = el.scrollLeft < maxScrollLeft - 1; |
| | | }
|
| | |
|
| | | function handleSelect(key, keyPath) {
|
| | | currentIndex.value = key
|
| | | const route = routers.value.find(item => item.path === key)
|
| | | if (isHttp(key)) {
|
| | | // http(s):// 路径新窗口打开
|
| | | window.open(key, "_blank")
|
| | | } else if (!route || !route.children) {
|
| | | // 没有子路由路径内部打开
|
| | | const routeMenu = childrenMenus.value.find(item => item.path === key)
|
| | | if (routeMenu && routeMenu.query) {
|
| | | let query = JSON.parse(routeMenu.query)
|
| | | router.push({ path: key, query: query })
|
| | | } else {
|
| | | router.push({ path: key })
|
| | | }
|
| | | appStore.toggleSideBarHide(true)
|
| | | } else {
|
| | | // 显示左侧联动菜单
|
| | | activeRoutes(key)
|
| | | appStore.toggleSideBarHide(false)
|
| | | }
|
| | | function scrollByStep(direction) { |
| | | const el = scrollWrapRef.value; |
| | | if (!el) return; |
| | | const step = Math.max(240, Math.floor(el.clientWidth * 0.6)); |
| | | el.scrollBy({ left: direction * step, behavior: "smooth" }); |
| | | requestAnimationFrame(() => updateScrollState()); |
| | | }
|
| | |
|
| | | function activeRoutes(key) {
|
| | | let routes = []
|
| | | if (childrenMenus.value && childrenMenus.value.length > 0) {
|
| | | childrenMenus.value.map((item) => {
|
| | | if (key == item.parentPath || (key == "index" && "" == item.path)) {
|
| | | routes.push(item)
|
| | | function scrollLeft() { |
| | | scrollByStep(-1); |
| | | }
|
| | | })
|
| | | |
| | | function scrollRight() { |
| | | scrollByStep(1); |
| | | }
|
| | | if(routes.length > 0) {
|
| | | permissionStore.setSidebarRouters(routes)
|
| | | } else {
|
| | | appStore.toggleSideBarHide(true)
|
| | | }
|
| | | return routes
|
| | | |
| | | let resizeRaf = 0; |
| | | function handleResize() { |
| | | if (resizeRaf) cancelAnimationFrame(resizeRaf); |
| | | resizeRaf = requestAnimationFrame(() => { |
| | | updateScrollState(); |
| | | }); |
| | | }
|
| | |
|
| | | onMounted(() => {
|
| | | window.addEventListener('resize', setVisibleNumber)
|
| | | })
|
| | | updateScrollState(); |
| | | window.addEventListener("resize", handleResize, { passive: true }); |
| | | }); |
| | |
|
| | | onBeforeUnmount(() => {
|
| | | window.removeEventListener('resize', setVisibleNumber)
|
| | | })
|
| | |
|
| | | onMounted(() => {
|
| | | setVisibleNumber()
|
| | | })
|
| | | window.removeEventListener("resize", handleResize); |
| | | if (resizeRaf) cancelAnimationFrame(resizeRaf); |
| | | }); |
| | | </script>
|
| | |
|
| | | <style lang="scss">
|
| | | .topmenu-container.el-menu--horizontal > .el-menu-item {
|
| | | float: left;
|
| | | height: 50px !important;
|
| | | line-height: 50px !important;
|
| | | color: #999093 !important;
|
| | | padding: 0 5px !important;
|
| | | margin: 0 10px !important;
|
| | | <style lang="scss" scoped> |
| | | .top-nav { |
| | | position: relative; |
| | | height: var(--topbar-height); |
| | | display: flex; |
| | | align-items: center; |
| | | width: 100%; |
| | | min-width: 0; |
| | | }
|
| | |
|
| | | .topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
|
| | | border-bottom: 2px solid #{'var(--theme)'} !important;
|
| | | color: #303133;
|
| | | .top-nav__scroll { |
| | | flex: 1; |
| | | min-width: 0; |
| | | height: 100%; |
| | | overflow-x: auto; |
| | | overflow-y: hidden; |
| | | scrollbar-width: none; |
| | | -ms-overflow-style: none; |
| | | |
| | | &::-webkit-scrollbar { |
| | | display: none; |
| | | } |
| | | }
|
| | |
|
| | | /* sub-menu item */
|
| | | .topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
|
| | | float: left;
|
| | | height: 50px !important;
|
| | | line-height: 50px !important;
|
| | | color: #999093 !important;
|
| | | padding: 0 5px !important;
|
| | | margin: 0 10px !important;
|
| | | .nav-arrow { |
| | | width: 34px; |
| | | height: 34px; |
| | | flex: 0 0 34px; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 999px; |
| | | border: 1px solid var(--surface-border); |
| | | background: var(--surface-base); |
| | | color: var(--text-secondary); |
| | | cursor: pointer; |
| | | transition: 0.2s ease; |
| | | margin: 0 8px; |
| | | |
| | | &:hover:not(:disabled) { |
| | | color: var(--el-color-primary); |
| | | border-color: rgba(var(--el-color-primary-rgb), 0.35); |
| | | background: rgba(var(--el-color-primary-rgb), 0.06); |
| | | }
|
| | |
|
| | | /* 背景色隐藏 */
|
| | | .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
|
| | | background-color: #ffffff;
|
| | | &:disabled { |
| | | opacity: 0.45; |
| | | cursor: not-allowed; |
| | | } |
| | | }
|
| | |
|
| | | /* 图标右间距 */
|
| | | .topmenu-container .svg-icon {
|
| | | margin-right: 4px;
|
| | | .top-nav-menu { |
| | | width: max-content; |
| | | min-width: 100%; |
| | | height: 100%; |
| | | border-bottom: none; |
| | | background: transparent; |
| | | }
|
| | |
|
| | | /* topmenu more arrow */
|
| | | .topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
|
| | | position: static;
|
| | | vertical-align: middle;
|
| | | margin-left: 8px;
|
| | | margin-top: 0px;
|
| | | :deep(.top-nav-menu.el-menu--horizontal) { |
| | | display: flex; |
| | | align-items: stretch; |
| | | flex-wrap: nowrap; |
| | | height: 100%; |
| | | width: max-content; |
| | | min-width: 100%; |
| | | }
|
| | |
|
| | | :deep(.top-nav-menu.el-menu--horizontal > .el-menu-item), |
| | | :deep(.top-nav-menu.el-menu--horizontal > .el-sub-menu > .el-sub-menu__title) { |
| | | height: var(--topbar-height); |
| | | line-height: var(--topbar-height); |
| | | padding: 0 12px; |
| | | color: var(--text-secondary); |
| | | border-bottom: 2px solid transparent; |
| | | transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease; |
| | | } |
| | |
|
| | | :deep( |
| | | .top-nav-menu.el-menu--horizontal > .el-menu-item:not(.is-disabled):hover |
| | | ), |
| | | :deep( |
| | | .top-nav-menu.el-menu--horizontal > .el-sub-menu > .el-sub-menu__title:hover |
| | | ) { |
| | | background: rgba(var(--el-color-primary-rgb), 0.06); |
| | | color: var(--el-color-primary); |
| | | } |
| | | |
| | | :deep(.top-nav-menu.el-menu--horizontal > .el-menu-item.is-active), |
| | | :deep( |
| | | .top-nav-menu.el-menu--horizontal |
| | | > .el-sub-menu.is-active |
| | | > .el-sub-menu__title |
| | | ) { |
| | | background: rgba(var(--el-color-primary-rgb), 0.06); |
| | | color: var(--el-color-primary); |
| | | font-weight: 600; |
| | | border-bottom-color: var(--el-color-primary); |
| | | } |
| | | |
| | | :deep(.top-nav-menu.el-menu--horizontal > .el-menu-item .svg-icon), |
| | | :deep( |
| | | .top-nav-menu.el-menu--horizontal |
| | | > .el-sub-menu |
| | | > .el-sub-menu__title |
| | | .svg-icon |
| | | ) { |
| | | margin-right: 8px; |
| | | } |
| | | </style>
|
| | |
| | | width: 100%;
|
| | | height: 100%;
|
| | | padding: var(--content-gap);
|
| | | padding-top: 0;
|
| | | /* padding-top: 0; */
|
| | | }
|
| | |
|
| | | .fixed-header + .app-main {
|
| | |
| | | <template> |
| | | <div class="navbar"> |
| | | <div class="left-menu"> |
| | | <hamburger id="hamburger-container" |
| | | :is-active="appStore.sidebar.opened" |
| | | class="hamburger-container" |
| | | @toggleClick="toggleSideBar" /> |
| | | <breadcrumb v-if="!settingsStore.topNav" |
| | | id="breadcrumb-container" |
| | | class="breadcrumb-container" /> |
| | | <div class="navbar-left"> |
| | | <div class="navbar-logo" |
| | | v-if="appStore.device !== 'mobile'" |
| | | :style="{ width: logoWidth }"> |
| | | <logo v-if="settingsStore.sidebarLogo" |
| | | :collapse="logoCollapse" /> |
| | | </div> |
| | | <div v-if="settingsStore.topNav && appStore.device !== 'mobile'" |
| | | class="top-nav-wrapper"> |
| | | <top-nav /> |
| | | </div> |
| | | <div v-else |
| | | class="left-menu-spacer"></div> |
| | | </div> |
| | | <div class="right-menu"> |
| | | <div class="search-wrapper"> |
| | | <el-icon class="search-icon" |
| | | <div class="action-icons"> |
| | | <!-- 搜索图标 --> |
| | | <div class="right-menu-item hover-effect action-icon-btn" |
| | | @click="openHeaderSearch"> |
| | | <el-icon :size="18"> |
| | | <Search /> |
| | | </el-icon> |
| | | <el-input v-model="topSearchKeyword" |
| | | placeholder="快速搜索..." |
| | | clearable |
| | | @keyup.enter="openHeaderSearch" /> |
| | | </div> |
| | | <header-search ref="headerSearchRef" |
| | | :keyword="topSearchKeyword" |
| | | class="search-popup-trigger" /> |
| | | </div> |
| | | <div class="action-icons"> |
| | | class="search-popup-trigger" |
| | | style="display: none;" /> |
| | | <!-- 通知图标 --> |
| | | <el-popover v-model:visible="notificationVisible" |
| | | :width="500" |
| | | placement="bottom-end" |
| | |
| | | :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [0, 10] } }] }" |
| | | popper-class="notification-popover"> |
| | | <template #reference> |
| | | <div class="notification-container right-menu-item hover-effect"> |
| | | <div class="notification-container right-menu-item hover-effect action-icon-btn"> |
| | | <el-badge :value="unreadCount" |
| | | :hidden="unreadCount === 0" |
| | | class="notification-badge"> |
| | |
| | | <NotificationCenter @unreadCountChange="handleUnreadCountChange" |
| | | ref="notificationCenterRef" /> |
| | | </el-popover> |
| | | <div class="right-menu-item hover-effect screenfull-container"> |
| | | <!-- 全屏图标 --> |
| | | <div class="right-menu-item hover-effect screenfull-container action-icon-btn"> |
| | | <screenfull /> |
| | | </div> |
| | | </div> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { Bell, Search } from "@element-plus/icons-vue"; |
| | | import Breadcrumb from "@/components/Breadcrumb"; |
| | | import Hamburger from "@/components/Hamburger"; |
| | | import { |
| | | Bell, |
| | | Search, |
| | | User, |
| | | Setting, |
| | | SwitchButton, |
| | | CaretBottom, |
| | | } from "@element-plus/icons-vue"; |
| | | import Screenfull from "@/components/Screenfull"; |
| | | import HeaderSearch from "@/components/HeaderSearch"; |
| | | import NotificationCenter from "./NotificationCenter/index.vue"; |
| | | import Logo from "./Sidebar/Logo.vue"; |
| | | import TopNav from "@/components/TopNav/index.vue"; |
| | | import useAppStore from "@/store/modules/app"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import useSettingsStore from "@/store/modules/settings"; |
| | |
| | | const userStore = useUserStore(); |
| | | const settingsStore = useSettingsStore(); |
| | | |
| | | const isTopNavLayout = computed( |
| | | () => settingsStore.topNav && appStore.device !== "mobile" |
| | | ); |
| | | const logoWidth = computed(() => { |
| | | if (isTopNavLayout.value) { |
| | | return "var(--sidebar-width)"; |
| | | } |
| | | return appStore.sidebar.opened |
| | | ? "var(--sidebar-width)" |
| | | : "var(--sidebar-collapsed-width)"; |
| | | }); |
| | | const logoCollapse = computed(() => { |
| | | if (isTopNavLayout.value) return false; |
| | | return !appStore.sidebar.opened; |
| | | }); |
| | | |
| | | const topSearchKeyword = ref(""); |
| | | const headerSearchRef = ref(null); |
| | | const notificationVisible = ref(false); |
| | | const notificationCenterRef = ref(null); |
| | | const unreadCount = ref(0); |
| | | |
| | | function toggleSideBar() { |
| | | appStore.toggleSideBar(); |
| | | } |
| | | |
| | | function openHeaderSearch() { |
| | | headerSearchRef.value?.open(topSearchKeyword.value); |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 0 24px; |
| | | background: var(--navbar-bg); |
| | | border-bottom: 1px solid rgba(255, 255, 255, 0.08); |
| | | backdrop-filter: blur(12px); |
| | | padding: 0 24px 0 0; |
| | | background: #fff; |
| | | border-bottom: 1px solid #d8dce5; |
| | | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); |
| | | z-index: var(--layout-header-z); |
| | | } |
| | | |
| | | .left-menu { |
| | | .navbar-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .hamburger-container { |
| | | height: 32px; |
| | | width: 32px; |
| | | border-radius: var(--radius-sm); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: var(--navbar-text); |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | |
| | | &:hover { |
| | | background: var(--navbar-hover); |
| | | color: #fff; |
| | | } |
| | | } |
| | | |
| | | .breadcrumb-container { |
| | | height: 100%; |
| | | flex: 1; |
| | | min-width: 0; |
| | | |
| | | :deep(.el-breadcrumb__inner) { |
| | | color: var(--navbar-text) !important; |
| | | opacity: 0.85; |
| | | |
| | | &:hover { |
| | | color: #fff !important; |
| | | opacity: 1; |
| | | } |
| | | |
| | | a { |
| | | color: inherit !important; |
| | | font-weight: 500 !important; |
| | | } |
| | | .navbar-logo { |
| | | height: 100%; |
| | | display: flex; |
| | | align-items: center; |
| | | transition: width 0.25s ease; |
| | | overflow: hidden; |
| | | flex-shrink: 0; |
| | | background-color: #fff; // 强制设为白色背景 |
| | | // border-right: 1px solid #d8dce5; // 增加右侧边框以区分导航栏主体 |
| | | } |
| | | |
| | | :deep(.no-redirect) { |
| | | color: #fff !important; |
| | | font-weight: 600 !important; |
| | | opacity: 1; |
| | | .top-nav-wrapper { |
| | | display: flex; |
| | | align-items: center; |
| | | height: 100%; |
| | | min-width: 0; |
| | | flex: 1; |
| | | padding: 0 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.el-breadcrumb__separator) { |
| | | color: var(--navbar-text); |
| | | opacity: 0.5; |
| | | } |
| | | .left-menu-spacer { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .right-menu { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 20px; // 增加大组之间的间距 |
| | | |
| | | .search-wrapper { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | height: 34px; |
| | | padding: 0 12px; |
| | | background: var(--navbar-hover); |
| | | border: 1px solid var(--surface-border); |
| | | border-radius: 17px; |
| | | width: 240px; // 搜索框更加精致小巧 |
| | | transition: all 0.3s ease; |
| | | |
| | | &:focus-within { |
| | | width: 300px; |
| | | background: rgba(255, 255, 255, 0.1); |
| | | border-color: var(--accent-primary); |
| | | box-shadow: 0 0 0 2px rgba(var(--el-color-primary-rgb), 0.2); |
| | | } |
| | | |
| | | .search-icon { |
| | | color: var(--sidebar-text); |
| | | font-size: 16px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | :deep(.el-input__wrapper) { |
| | | background: transparent; |
| | | box-shadow: none !important; |
| | | padding: 0; |
| | | } |
| | | |
| | | :deep(.el-input__inner) { |
| | | color: var(--navbar-text); |
| | | font-size: 13px; |
| | | height: 32px; |
| | | |
| | | &::placeholder { |
| | | color: var(--sidebar-text); |
| | | } |
| | | } |
| | | } |
| | | gap: 16px; // 调整组之间的间距 |
| | | |
| | | .action-icons { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | gap: 12px; // 图标之间的间距 |
| | | padding-right: 16px; |
| | | border-right: 1px solid var(--surface-border); // 增加垂直分割线 |
| | | border-right: 1px solid var(--surface-border); |
| | | |
| | | .right-menu-item { |
| | | padding: 0 8px; |
| | | height: 34px; |
| | | .action-icon-btn { |
| | | width: 36px; |
| | | height: 36px; |
| | | padding: 0; |
| | | display: flex; |
| | | align-items: center; |
| | | color: var(--navbar-text); |
| | | border-radius: var(--radius-sm); |
| | | justify-content: center; |
| | | color: var(--text-secondary); |
| | | border-radius: 50%; // 圆形背景 |
| | | background: rgba(0, 0, 0, 0.04); // 浅浅的圆形框底色 |
| | | transition: all 0.2s ease; |
| | | cursor: pointer; |
| | | |
| | | &:hover { |
| | | background: var(--navbar-hover); |
| | | color: var(--menu-active-text); |
| | | background: rgba(0, 0, 0, 0.08); // 悬停加深 |
| | | color: var(--el-color-primary); |
| | | } |
| | | |
| | | :deep(.svg-icon) { |
| | | width: 18px !important; |
| | | height: 18px !important; |
| | | } |
| | | |
| | | :deep(.el-icon) { |
| | | font-size: 18px !important; |
| | | } |
| | | } |
| | | |
| | | .notification-container { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | } |
| | | |
| | |
| | | height: 38px; |
| | | padding: 4px 10px 4px 6px; |
| | | border-radius: 999px; |
| | | border: 1px solid rgba(var(--el-color-primary-rgb), 0.32); |
| | | background: rgba(0, 0, 0, 0.16); |
| | | border: 1px solid transparent; |
| | | background: transparent; |
| | | transition: 0.2s ease; |
| | | |
| | | &:hover { |
| | | background: rgba(0, 0, 0, 0.24); |
| | | border-color: rgba(var(--el-color-primary-rgb), 0.58); |
| | | box-shadow: 0 0 0 2px rgba(var(--el-color-primary-rgb), 0.18); |
| | | background: rgba(0, 0, 0, 0.05); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | .user-name { |
| | | font-size: 13px; |
| | | font-weight: 700; |
| | | color: var(--navbar-text); |
| | | font-weight: 500; |
| | | color: var(--text-primary); |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | |
| | | width: 30px; |
| | | height: 30px; |
| | | border-radius: 999px; |
| | | border: 2px solid rgba(var(--el-color-primary-rgb), 0.28); |
| | | box-shadow: 0 8px 18px rgba(0, 0, 0, 0.18); |
| | | border: 1px solid rgba(0, 0, 0, 0.1); |
| | | object-fit: cover; |
| | | transition: 0.2s ease; |
| | | } |
| | |
| | | width: 9px; |
| | | height: 9px; |
| | | background: #10b981; |
| | | border: 2px solid var(--navbar-bg); |
| | | border: 2px solid #fff; |
| | | border-radius: 999px; |
| | | } |
| | | |
| | | .caret-icon { |
| | | color: var(--navbar-text); |
| | | color: var(--text-secondary); |
| | | opacity: 0.76; |
| | | font-size: 12px; |
| | | transition: 0.2s ease; |
| | |
| | | const sideTheme = ref(settingsStore.sideTheme);
|
| | | const storeSettings = computed(() => settingsStore);
|
| | | const predefineColors = ref([
|
| | | "#374D77",
|
| | | "#008C8C",
|
| | | "#81D8D0",
|
| | | "#E85827",
|
| | | "#008C8C",
|
| | |
| | | <template> |
| | | <div class="sidebar-logo-container" :class="{ collapse }"> |
| | | <div class="sidebar-logo-container" |
| | | :class="{ collapse }"> |
| | | <transition name="sidebarLogoFade"> |
| | | <router-link style="display: flex;" v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> |
| | | <img :src="faviconUrl" class="sidebar-logo sidebar-favicon" alt="站点图标" /> |
| | | <router-link style="display: flex;" |
| | | v-if="collapse" |
| | | key="collapse" |
| | | class="sidebar-logo-link" |
| | | to="/"> |
| | | <img :src="faviconUrl" |
| | | class="sidebar-logo sidebar-favicon" |
| | | alt="站点图标" /> |
| | | </router-link> |
| | | <router-link v-else key="expand" class="sidebar-logo-link" :style="expandLogoLinkStyle" to="/"> |
| | | <img v-if="logoUrl" :src="logoUrl" class="sidebar-logo" @error="handleImageError" alt="公司Logo" /> |
| | | <h1 v-if="!logoUrl" class="sidebar-title">{{ title }}</h1> |
| | | <router-link v-else |
| | | key="expand" |
| | | class="sidebar-logo-link" |
| | | :style="expandLogoLinkStyle" |
| | | to="/"> |
| | | <img v-if="logoUrl" |
| | | :src="logoUrl" |
| | | class="sidebar-logo" |
| | | @error="handleImageError" |
| | | alt="公司Logo" /> |
| | | <h1 v-if="!logoUrl" |
| | | class="sidebar-title">{{ title }}</h1> |
| | | </router-link> |
| | | </transition> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted, watch } from 'vue' |
| | | import useUserStore from '@/store/modules/user' |
| | | import defaultLogo from '@/assets/logo/logo.png' |
| | | import { ref, computed, onMounted, watch } from "vue"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | import defaultLogo from "@/assets/logo/芯导软件(江苏)有限公司.png"; |
| | | |
| | | defineProps({ |
| | | collapse: { |
| | | type: Boolean, |
| | | required: true |
| | | } |
| | | }) |
| | | required: true, |
| | | }, |
| | | }); |
| | | |
| | | const title = import.meta.env.VITE_APP_TITLE |
| | | const userStore = useUserStore() |
| | | const baseUrl = import.meta.env.BASE_URL || '/' |
| | | const faviconUrl = `${baseUrl.replace(/\/?$/, '/') }favicon.ico`.replace(/([^:]\/)\/+/g, '$1') |
| | | const title = import.meta.env.VITE_APP_TITLE; |
| | | const userStore = useUserStore(); |
| | | const baseUrl = import.meta.env.BASE_URL || "/"; |
| | | const faviconUrl = `${baseUrl.replace(/\/?$/, "/")}favicon.ico`.replace( |
| | | /([^:]\/)\/+/g, |
| | | "$1" |
| | | ); |
| | | |
| | | const cleanFactoryName = computed(() => { |
| | | if (!userStore.currentFactoryName) return '' |
| | | return userStore.currentFactoryName.trim() |
| | | }) |
| | | if (!userStore.currentFactoryName) return ""; |
| | | return userStore.currentFactoryName.trim(); |
| | | }); |
| | | |
| | | const logoUrl = ref('') |
| | | const logoUrl = ref(""); |
| | | |
| | | const expandLogoLinkStyle = computed(() => { |
| | | if (!logoUrl.value) { |
| | | return { '--logo-bg-image': 'none' } |
| | | return { "--logo-bg-image": "none" }; |
| | | } |
| | | const escaped = String(logoUrl.value).replace(/"/g, '\\"') |
| | | return { '--logo-bg-image': `url("${escaped}")` } |
| | | }) |
| | | const escaped = String(logoUrl.value).replace(/"/g, '\\"'); |
| | | return { "--logo-bg-image": `url("${escaped}")` }; |
| | | }); |
| | | |
| | | const updateLogoUrl = () => { |
| | | if (!cleanFactoryName.value) { |
| | | logoUrl.value = defaultLogo |
| | | return |
| | | logoUrl.value = defaultLogo; |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | const dynamicLogo = import.meta.glob('/src/assets/logo/*.png', { eager: true }) |
| | | const logoPath = `/src/assets/logo/${cleanFactoryName.value}.png` |
| | | const dynamicLogo = import.meta.glob("/src/assets/logo/*.png", { |
| | | eager: true, |
| | | }); |
| | | const logoPath = `/src/assets/logo/${cleanFactoryName.value}.png`; |
| | | |
| | | if (dynamicLogo[logoPath]) { |
| | | logoUrl.value = dynamicLogo[logoPath].default |
| | | logoUrl.value = dynamicLogo[logoPath].default; |
| | | } else { |
| | | logoUrl.value = defaultLogo |
| | | logoUrl.value = defaultLogo; |
| | | } |
| | | } catch (error) { |
| | | console.error('加载工厂 Logo 失败:', error) |
| | | logoUrl.value = defaultLogo |
| | | console.error("加载工厂 Logo 失败:", error); |
| | | logoUrl.value = defaultLogo; |
| | | } |
| | | } |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | updateLogoUrl() |
| | | watch(() => userStore.currentFactoryName, updateLogoUrl) |
| | | }) |
| | | updateLogoUrl(); |
| | | watch(() => userStore.currentFactoryName, updateLogoUrl); |
| | | }); |
| | | |
| | | const handleImageError = () => { |
| | | logoUrl.value = defaultLogo |
| | | } |
| | | logoUrl.value = defaultLogo; |
| | | }; |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | @import '@/assets/styles/variables.module.scss'; |
| | | @import "@/assets/styles/variables.module.scss"; |
| | | |
| | | .sidebarLogoFade-enter-active { |
| | | transition: opacity 1.5s; |
| | |
| | | .sidebar-logo-container { |
| | | position: relative; |
| | | width: 100% !important; |
| | | height: 64px !important; |
| | | line-height: 64px; |
| | | height: 100% !important; |
| | | line-height: var(--topbar-height, 64px); |
| | | background: transparent; |
| | | border-bottom: 1px solid rgba(255, 255, 255, 0.08); |
| | | text-align: left; |
| | | overflow: hidden; |
| | | transition: all 0.3s ease; |
| | |
| | | width: 100%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-start; // 默认展开时靠左 |
| | | padding: 0 16px; |
| | | justify-content: center; // 始终居中 |
| | | padding: 0; // 移除两边间距,让图片能撑满 |
| | | } |
| | | |
| | | .sidebar-logo { |
| | | height:100%; |
| | | width:100%; |
| | | width: auto; |
| | | max-width: 100%; |
| | | width: 100%; // 宽度始终和侧边栏一致 |
| | | height: auto; // 高度自适应 |
| | | vertical-align: middle; |
| | | object-fit: contain; |
| | | display: block; |
| | |
| | | .sidebar-title { |
| | | display: inline-block; |
| | | margin: 0; |
| | | color: #fff; |
| | | color: #333; // 白色背景下需要深色字体 |
| | | font-weight: 600; |
| | | line-height: 64px; |
| | | line-height: var(--topbar-height, 64px); |
| | | font-size: 16px; |
| | | vertical-align: middle; |
| | | margin-left: 12px; |
| | |
| | | if (isExternal(props.basePath)) {
|
| | | return props.basePath
|
| | | }
|
| | | if (String(routePath || '').startsWith('/')) { |
| | | if (routeQuery) { |
| | | let query = JSON.parse(routeQuery) |
| | | return { path: getNormalPath(routePath), query: query } |
| | | } |
| | | return getNormalPath(routePath) |
| | | } |
| | | if (routeQuery) {
|
| | | let query = JSON.parse(routeQuery)
|
| | | return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
|
| | |
| | | <template> |
| | | <div :class="{ 'has-logo': showLogo }" class="sidebar-container"> |
| | | <logo v-if="showLogo" :collapse="isCollapse" /> |
| | | <div :class="{ 'has-logo': showLogo && appStore.device === 'mobile' }" |
| | | class="sidebar-container"> |
| | | <logo v-if="showLogo && appStore.device === 'mobile'" |
| | | :collapse="false" /> |
| | | <el-scrollbar wrap-class="scrollbar-wrapper"> |
| | | <el-menu |
| | | :default-active="activeMenu" |
| | | <el-menu :default-active="activeMenu" |
| | | :collapse="isCollapse" |
| | | :background-color="getMenuBackground" |
| | | :text-color="getMenuTextColor" |
| | |
| | | :active-text-color="theme" |
| | | :collapse-transition="false" |
| | | mode="vertical" |
| | | :class="sideTheme" |
| | | > |
| | | <sidebar-item |
| | | v-for="(route, index) in sidebarRouters" |
| | | :class="sideTheme"> |
| | | <sidebar-item v-for="(route, index) in sidebarRouters" |
| | | :key="route.path + index" |
| | | :item="route" |
| | | :base-path="route.path" |
| | | /> |
| | | :base-path="route.path" /> |
| | | </el-menu> |
| | | </el-scrollbar> |
| | | </div> |
| | |
| | | background: transparent; |
| | | border-radius: 0; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | |
| | | .scrollbar-wrapper { |
| | | background: transparent; |
| | |
| | | color: var(--menu-active-text) !important; |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <template>
|
| | | <div id="tags-view-container" class="tags-view-container">
|
| | | <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
|
| | | <router-link
|
| | | v-for="tag in visitedViews"
|
| | | <div id="tags-view-container"
|
| | | class="tags-view-container">
|
| | | <scroll-pane ref="scrollPaneRef"
|
| | | class="tags-view-wrapper"
|
| | | @scroll="handleScroll">
|
| | | <router-link v-for="tag in visitedViews"
|
| | | :key="tag.path"
|
| | | :data-path="tag.path"
|
| | | :class="isActive(tag) ? 'active' : ''"
|
| | |
| | | class="tags-view-item"
|
| | | :style="activeStyle(tag)"
|
| | | @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
| | | @contextmenu.prevent="openMenu(tag, $event)"
|
| | | >
|
| | | {{ tag.title }}
|
| | | <span v-if="!isAffix(tag)" class="tags-view-close" @click.prevent.stop="closeSelectedTag(tag)">
|
| | | <close class="el-icon-close" />
|
| | | @contextmenu.prevent="openMenu(tag, $event)">
|
| | | <span class="tags-view-item-title">{{ tag.title }}</span>
|
| | | <span v-if="!isAffix(tag)"
|
| | | class="tags-view-close"
|
| | | @click.prevent.stop="closeSelectedTag(tag)">
|
| | | <el-icon class="el-icon-close">
|
| | | <Close />
|
| | | </el-icon>
|
| | | </span>
|
| | | </router-link>
|
| | | </scroll-pane>
|
| | | <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
| | | <ul v-show="visible"
|
| | | :style="{ left: left + 'px', top: top + 'px' }"
|
| | | class="contextmenu">
|
| | | <li @click="refreshSelectedTag(selectedTag)">
|
| | | <refresh-right style="width: 1em; height: 1em;" /> 刷新页面
|
| | | <el-icon>
|
| | | <RefreshRight />
|
| | | </el-icon> 刷新页面
|
| | | </li>
|
| | | <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
| | | <close style="width: 1em; height: 1em;" /> 关闭当前
|
| | | <li v-if="!isAffix(selectedTag)"
|
| | | @click="closeSelectedTag(selectedTag)">
|
| | | <el-icon>
|
| | | <Close />
|
| | | </el-icon> 关闭当前
|
| | | </li>
|
| | | <li @click="closeOthersTags">
|
| | | <circle-close style="width: 1em; height: 1em;" /> 关闭其他
|
| | | <el-icon>
|
| | | <CircleClose />
|
| | | </el-icon> 关闭其他
|
| | | </li>
|
| | | <li v-if="!isFirstView()" @click="closeLeftTags">
|
| | | <back style="width: 1em; height: 1em;" /> 关闭左侧
|
| | | <li v-if="!isFirstView()"
|
| | | @click="closeLeftTags">
|
| | | <el-icon>
|
| | | <Back />
|
| | | </el-icon> 关闭左侧
|
| | | </li>
|
| | | <li v-if="!isLastView()" @click="closeRightTags">
|
| | | <right style="width: 1em; height: 1em;" /> 关闭右侧
|
| | | <li v-if="!isLastView()"
|
| | | @click="closeRightTags">
|
| | | <el-icon>
|
| | | <Right />
|
| | | </el-icon> 关闭右侧
|
| | | </li>
|
| | | <li @click="closeAllTags(selectedTag)">
|
| | | <circle-close style="width: 1em; height: 1em;" /> 全部关闭
|
| | | <el-icon>
|
| | | <CircleClose />
|
| | | </el-icon> 全部关闭
|
| | | </li>
|
| | | </ul>
|
| | | </div>
|
| | | </template>
|
| | |
|
| | | <script setup>
|
| | | import ScrollPane from './ScrollPane'
|
| | | import { getNormalPath } from '@/utils/ruoyi'
|
| | | import useTagsViewStore from '@/store/modules/tagsView'
|
| | | import useSettingsStore from '@/store/modules/settings'
|
| | | import usePermissionStore from '@/store/modules/permission'
|
| | | import ScrollPane from "./ScrollPane";
|
| | | import { getNormalPath } from "@/utils/ruoyi";
|
| | | import useTagsViewStore from "@/store/modules/tagsView";
|
| | | import useSettingsStore from "@/store/modules/settings";
|
| | | import usePermissionStore from "@/store/modules/permission";
|
| | | import {
|
| | | Close,
|
| | | RefreshRight,
|
| | | CircleClose,
|
| | | Back,
|
| | | Right,
|
| | | } from "@element-plus/icons-vue";
|
| | |
|
| | | const visible = ref(false)
|
| | | const top = ref(0)
|
| | | const left = ref(0)
|
| | | const selectedTag = ref({})
|
| | | const affixTags = ref([])
|
| | | const scrollPaneRef = ref(null)
|
| | | const visible = ref(false);
|
| | | const top = ref(0);
|
| | | const left = ref(0);
|
| | | const selectedTag = ref({});
|
| | | const affixTags = ref([]);
|
| | | const scrollPaneRef = ref(null);
|
| | |
|
| | | const { proxy } = getCurrentInstance()
|
| | | const route = useRoute()
|
| | | const router = useRouter()
|
| | | const { proxy } = getCurrentInstance();
|
| | | const route = useRoute();
|
| | | const router = useRouter();
|
| | |
|
| | | const visitedViews = computed(() => useTagsViewStore().visitedViews)
|
| | | const routes = computed(() => usePermissionStore().routes)
|
| | | const theme = computed(() => useSettingsStore().theme)
|
| | | const visitedViews = computed(() => useTagsViewStore().visitedViews);
|
| | | const routes = computed(() => usePermissionStore().routes);
|
| | | const theme = computed(() => useSettingsStore().theme);
|
| | |
|
| | | watch(route, () => {
|
| | | addTags()
|
| | | moveToCurrentTag()
|
| | | })
|
| | | addTags();
|
| | | moveToCurrentTag();
|
| | | });
|
| | |
|
| | | watch(visible, (value) => {
|
| | | watch(visible, value => {
|
| | | if (value) {
|
| | | document.body.addEventListener('click', closeMenu)
|
| | | document.body.addEventListener("click", closeMenu);
|
| | | } else {
|
| | | document.body.removeEventListener('click', closeMenu)
|
| | | document.body.removeEventListener("click", closeMenu);
|
| | | }
|
| | | })
|
| | | });
|
| | |
|
| | | onMounted(() => {
|
| | | initTags()
|
| | | addTags()
|
| | | })
|
| | | initTags();
|
| | | addTags();
|
| | | });
|
| | |
|
| | | function isActive(r) {
|
| | | return r.path === route.path
|
| | | return r.path === route.path;
|
| | | }
|
| | |
|
| | | function activeStyle(tag) {
|
| | | if (!isActive(tag)) return {}
|
| | | if (!isActive(tag)) return {};
|
| | | return {
|
| | | "background-color": theme.value,
|
| | | "border-color": theme.value
|
| | | }
|
| | | "border-color": theme.value,
|
| | | };
|
| | | }
|
| | |
|
| | | function isAffix(tag) {
|
| | | return tag.meta && tag.meta.affix
|
| | | return tag.meta && tag.meta.affix;
|
| | | }
|
| | |
|
| | | function isFirstView() {
|
| | | try {
|
| | | return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
|
| | | return (
|
| | | selectedTag.value.fullPath === "/index" ||
|
| | | selectedTag.value.fullPath === visitedViews.value[1].fullPath
|
| | | );
|
| | | } catch (err) {
|
| | | return false
|
| | | return false;
|
| | | }
|
| | | }
|
| | |
|
| | | function isLastView() {
|
| | | try {
|
| | | return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
|
| | | return (
|
| | | selectedTag.value.fullPath ===
|
| | | visitedViews.value[visitedViews.value.length - 1].fullPath
|
| | | );
|
| | | } catch (err) {
|
| | | return false
|
| | | return false;
|
| | | }
|
| | | }
|
| | |
|
| | | function filterAffixTags(routes, basePath = '') {
|
| | | let tags = []
|
| | | function filterAffixTags(routes, basePath = "") {
|
| | | let tags = [];
|
| | | routes.forEach(route => {
|
| | | if (route.meta && route.meta.affix) {
|
| | | const tagPath = getNormalPath(basePath + '/' + route.path)
|
| | | const tagPath = getNormalPath(basePath + "/" + route.path);
|
| | | tags.push({
|
| | | fullPath: tagPath,
|
| | | path: tagPath,
|
| | | name: route.name,
|
| | | meta: { ...route.meta }
|
| | | })
|
| | | meta: { ...route.meta },
|
| | | });
|
| | | }
|
| | | if (route.children) {
|
| | | const tempTags = filterAffixTags(route.children, route.path)
|
| | | const tempTags = filterAffixTags(route.children, route.path);
|
| | | if (tempTags.length >= 1) {
|
| | | tags = [...tags, ...tempTags]
|
| | | tags = [...tags, ...tempTags];
|
| | | }
|
| | | }
|
| | | })
|
| | | return tags
|
| | | });
|
| | | return tags;
|
| | | }
|
| | |
|
| | | function initTags() {
|
| | | const res = filterAffixTags(routes.value)
|
| | | affixTags.value = res
|
| | | const res = filterAffixTags(routes.value);
|
| | | affixTags.value = res;
|
| | | for (const tag of res) {
|
| | | // Must have tag name
|
| | | if (tag.name) {
|
| | | useTagsViewStore().addVisitedView(tag)
|
| | | useTagsViewStore().addVisitedView(tag);
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | function addTags() {
|
| | | const { name } = route
|
| | | const { name } = route;
|
| | | if (name) {
|
| | | useTagsViewStore().addView(route)
|
| | | useTagsViewStore().addView(route);
|
| | | }
|
| | | }
|
| | |
|
| | |
| | | nextTick(() => {
|
| | | for (const r of visitedViews.value) {
|
| | | if (r.path === route.path) {
|
| | | scrollPaneRef.value.moveToTarget(r)
|
| | | scrollPaneRef.value.moveToTarget(r);
|
| | | // when query is different then update
|
| | | if (r.fullPath !== route.fullPath) {
|
| | | useTagsViewStore().updateVisitedView(route)
|
| | | useTagsViewStore().updateVisitedView(route);
|
| | | }
|
| | | }
|
| | | }
|
| | | })
|
| | | });
|
| | | }
|
| | |
|
| | | function refreshSelectedTag(view) {
|
| | | proxy.$tab.refreshPage(view)
|
| | | proxy.$tab.refreshPage(view);
|
| | | if (route.meta.link) {
|
| | | useTagsViewStore().delIframeView(route)
|
| | | useTagsViewStore().delIframeView(route);
|
| | | }
|
| | | }
|
| | |
|
| | | function closeSelectedTag(view) {
|
| | | proxy.$tab.closePage(view).then(({ visitedViews }) => {
|
| | | if (isActive(view)) {
|
| | | toLastView(visitedViews, view)
|
| | | toLastView(visitedViews, view);
|
| | | }
|
| | | })
|
| | | });
|
| | | }
|
| | |
|
| | | function closeRightTags() {
|
| | | proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
|
| | | if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
|
| | | toLastView(visitedViews)
|
| | | toLastView(visitedViews);
|
| | | }
|
| | | })
|
| | | });
|
| | | }
|
| | |
|
| | | function closeLeftTags() {
|
| | | proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
|
| | | if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
|
| | | toLastView(visitedViews)
|
| | | toLastView(visitedViews);
|
| | | }
|
| | | })
|
| | | });
|
| | | }
|
| | |
|
| | | function closeOthersTags() {
|
| | | router.push(selectedTag.value).catch(() => { })
|
| | | router.push(selectedTag.value).catch(() => {});
|
| | | proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
|
| | | moveToCurrentTag()
|
| | | })
|
| | | moveToCurrentTag();
|
| | | });
|
| | | }
|
| | |
|
| | | function closeAllTags(view) {
|
| | | proxy.$tab.closeAllPage().then(({ visitedViews }) => {
|
| | | if (affixTags.value.some(tag => tag.path === route.path)) {
|
| | | return
|
| | | return;
|
| | | }
|
| | | toLastView(visitedViews, view)
|
| | | })
|
| | | toLastView(visitedViews, view);
|
| | | });
|
| | | }
|
| | |
|
| | | function toLastView(visitedViews, view) {
|
| | | const latestView = visitedViews.slice(-1)[0]
|
| | | const latestView = visitedViews.slice(-1)[0];
|
| | | if (latestView) {
|
| | | router.push(latestView.fullPath)
|
| | | router.push(latestView.fullPath);
|
| | | } else {
|
| | | // now the default is to redirect to the home page if there is no tags-view,
|
| | | // you can adjust it according to your needs.
|
| | | if (view.name === 'Dashboard') {
|
| | | if (view.name === "Dashboard") {
|
| | | // to reload home page
|
| | | router.replace({ path: '/redirect' + view.fullPath })
|
| | | router.replace({ path: "/redirect" + view.fullPath });
|
| | | } else {
|
| | | router.push('/')
|
| | | router.push("/");
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | function openMenu(tag, e) {
|
| | | const menuMinWidth = 105
|
| | | const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
|
| | | const offsetWidth = proxy.$el.offsetWidth // container width
|
| | | const maxLeft = offsetWidth - menuMinWidth // left boundary
|
| | | const l = e.clientX - offsetLeft + 15 // 15: margin right
|
| | | const menuMinWidth = 105;
|
| | | const offsetLeft = proxy.$el.getBoundingClientRect().left; // container margin left
|
| | | const offsetWidth = proxy.$el.offsetWidth; // container width
|
| | | const maxLeft = offsetWidth - menuMinWidth; // left boundary
|
| | | const l = e.clientX - offsetLeft + 15; // 15: margin right
|
| | |
|
| | | if (l > maxLeft) {
|
| | | left.value = maxLeft
|
| | | left.value = maxLeft;
|
| | | } else {
|
| | | left.value = l
|
| | | left.value = l;
|
| | | }
|
| | |
|
| | | top.value = e.clientY
|
| | | visible.value = true
|
| | | selectedTag.value = tag
|
| | | top.value = e.clientY;
|
| | | visible.value = true;
|
| | | selectedTag.value = tag;
|
| | | }
|
| | |
|
| | | function closeMenu() {
|
| | | visible.value = false
|
| | | visible.value = false;
|
| | | }
|
| | |
|
| | | function handleScroll() {
|
| | | closeMenu()
|
| | | closeMenu();
|
| | | }
|
| | | </script>
|
| | |
|
| | | <style lang="scss" scoped>
|
| | | .tags-view-container {
|
| | | height: 42px; // 增加容器高度,提供更多呼吸感
|
| | | height: 34px;
|
| | | width: 100%;
|
| | | background: var(--tags-bg);
|
| | | border-bottom: 1px solid var(--surface-border);
|
| | | padding: 0 16px;
|
| | | background: #fff;
|
| | | border-bottom: 1px solid #d8dce5;
|
| | | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
|
| | | padding: 0;
|
| | | display: flex;
|
| | | align-items: center;
|
| | | position: relative;
|
| | | z-index: 10;
|
| | |
|
| | | .tags-view-wrapper {
|
| | | flex: 1;
|
| | |
| | | align-items: center;
|
| | | position: relative;
|
| | | cursor: pointer;
|
| | | height: 30px; // 增加页签高度
|
| | | line-height: 30px;
|
| | | color: var(--tags-item-text);
|
| | | background: var(--tags-item-bg);
|
| | | border: 1px solid var(--tags-item-border);
|
| | | border-radius: 6px; // 稍微圆润一点
|
| | | padding: 0 12px;
|
| | | height: 26px;
|
| | | line-height: 26px;
|
| | | color: #495060;
|
| | | background-color: #fff;
|
| | | border: 1px solid #d8dce5;
|
| | | border-radius: 2px;
|
| | | padding: 0 8px;
|
| | | font-size: 12px;
|
| | | margin-right: 6px;
|
| | | transition: all 0.2s ease;
|
| | | gap: 6px;
|
| | | margin-right: 5px;
|
| | | margin-top: 4px;
|
| | | margin-left: 5px;
|
| | | transition: all 0.3s ease;
|
| | | text-decoration: none;
|
| | |
|
| | | .tags-view-item-title {
|
| | | margin-right: 2px;
|
| | | display: inline-block;
|
| | | vertical-align: middle;
|
| | | }
|
| | |
|
| | | &:hover {
|
| | | background: var(--tags-item-hover);
|
| | | color: var(--accent-primary);
|
| | | border-color: var(--accent-primary);
|
| | | background-color: #f4f4f5;
|
| | | }
|
| | |
|
| | | &.active {
|
| | | background: var(--accent-primary);
|
| | | background-color: var(--el-color-primary, #42b983);
|
| | | color: #fff;
|
| | | border-color: var(--accent-primary);
|
| | | box-shadow: 0 2px 8px rgba(37, 99, 235, 0.15);
|
| | | font-weight: 500;
|
| | | border-color: var(--el-color-primary, #42b983);
|
| | | box-shadow: none;
|
| | | font-weight: 400;
|
| | |
|
| | | &::before {
|
| | | content: "";
|
| | | background: #fff;
|
| | | background-color: #fff;
|
| | | display: inline-block;
|
| | | width: 6px;
|
| | | height: 6px;
|
| | | width: 8px;
|
| | | height: 8px;
|
| | | border-radius: 50%;
|
| | | margin-right: 2px;
|
| | | margin-right: 4px;
|
| | | }
|
| | |
|
| | | .tags-view-close {
|
| | | color: #fff;
|
| | | display: inline-block;
|
| | | vertical-align: middle;
|
| | | &:hover {
|
| | | background-color: rgba(255, 255, 255, 0.3);
|
| | | }
|
| | | }
|
| | | }
|
| | | }
|
| | |
| | | z-index: 3000;
|
| | | position: absolute;
|
| | | list-style-type: none;
|
| | | padding: 4px 0;
|
| | | border-radius: var(--radius-md);
|
| | | padding: 5px 0;
|
| | | border-radius: 4px;
|
| | | font-size: 12px;
|
| | | color: var(--text-secondary);
|
| | | box-shadow: var(--shadow-md);
|
| | | border: 1px solid var(--surface-border);
|
| | | font-weight: 400;
|
| | | color: #333;
|
| | | box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
|
| | |
|
| | | li {
|
| | | margin: 0;
|
| | | padding: 8px 16px;
|
| | | padding: 7px 16px;
|
| | | cursor: pointer;
|
| | | display: flex;
|
| | | align-items: center;
|
| | |
| | | transition: all 0.2s ease;
|
| | | white-space: nowrap;
|
| | |
|
| | | svg {
|
| | | width: 14px;
|
| | | height: 14px;
|
| | | color: inherit;
|
| | | .el-icon {
|
| | | font-size: 14px;
|
| | | }
|
| | |
|
| | | &:hover {
|
| | | background: #f1f5f9;
|
| | | color: var(--accent-primary);
|
| | | background: #eee;
|
| | | }
|
| | | }
|
| | | }
|
| | |
| | | .el-scrollbar__view {
|
| | | display: flex;
|
| | | align-items: center;
|
| | | height: 34px;
|
| | | }
|
| | |
|
| | | .tags-view-item {
|
| | |
| | | display: inline-flex;
|
| | | align-items: center;
|
| | | justify-content: center;
|
| | | width: 12px;
|
| | | height: 12px;
|
| | | width: 14px;
|
| | | height: 14px;
|
| | | line-height: 1;
|
| | | align-self: center;
|
| | | transform: translateY(1px);
|
| | | transform: translateY(0px);
|
| | | margin-left: 2px;
|
| | | }
|
| | |
|
| | | .el-icon-close {
|
| | | display: inline-flex;
|
| | | align-items: center;
|
| | | justify-content: center;
|
| | | width: 12px;
|
| | | height: 12px;
|
| | | width: 14px;
|
| | | height: 14px;
|
| | | line-height: 1;
|
| | | vertical-align: initial !important;
|
| | | vertical-align: middle;
|
| | | border-radius: 50%;
|
| | | text-align: center;
|
| | | transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
| | | transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
| | | transform-origin: 100% 50%;
|
| | | align-self: center;
|
| | |
|
| | | &:before {
|
| | | transform: scale(.6);
|
| | | transform: scale(0.8);
|
| | | display: inline-flex;
|
| | | align-items: center;
|
| | | justify-content: center;
|
| | |
| | | <div v-if="device === 'mobile' && sidebar.opened" |
| | | class="drawer-bg" |
| | | @click="handleClickOutside" /> |
| | | <sidebar v-if="!sidebar.hide" |
| | | <div :class="{ 'fixed-header': fixedHeader }" |
| | | class="top-header"> |
| | | <navbar @setLayout="setLayout" /> |
| | | </div> |
| | | <sidebar v-if="!sidebar.hide && (!settingsStore.topNav || device === 'mobile')" |
| | | class="sidebar-container" /> |
| | | <div :class="{ hasTagsView: showTagsView, sidebarHide: sidebar.hide }" |
| | | class="main-container main-layout"> |
| | | <div :class="{ 'fixed-header': fixedHeader, 'with-tags': showTagsView }"> |
| | | <navbar @setLayout="setLayout" /> |
| | | <div v-if="showTagsView" |
| | | class="tags-view-wrapper" |
| | | :class="{ 'fixed-tags-view': fixedHeader }"> |
| | | <tags-view /> |
| | | </div> |
| | | <div class="breadcrumb-wrapper" |
| | | :class="{ 'fixed-breadcrumb': fixedHeader }"> |
| | | <hamburger v-if="!sidebar.hide && (!settingsStore.topNav || device === 'mobile')" |
| | | id="hamburger-container" |
| | | :is-active="sidebar.opened" |
| | | class="hamburger-container" |
| | | @toggleClick="toggleSideBar" /> |
| | | <breadcrumb id="breadcrumb-container" |
| | | class="breadcrumb-container" /> |
| | | </div> |
| | | <app-main /> |
| | | <settings ref="settingRef" /> |
| | |
| | | import { useRoute } from "vue-router"; |
| | | import Sidebar from "./components/Sidebar/index.vue"; |
| | | import { AppMain, Navbar, Settings, TagsView } from "./components"; |
| | | import Breadcrumb from "@/components/Breadcrumb"; |
| | | import Hamburger from "@/components/Hamburger"; |
| | | import AIChatSidebar from "@/components/AIChatSidebar/index.vue"; |
| | | import defaultSettings from "@/settings"; |
| | | |
| | |
| | | import useTagsViewStore from "@/store/modules/tagsView"; |
| | | |
| | | const settingsStore = useSettingsStore(); |
| | | const appStore = useAppStore(); |
| | | const tagsViewStore = useTagsViewStore(); |
| | | const userStore = useUserStore(); |
| | | const route = useRoute(); |
| | | const theme = computed(() => settingsStore.theme); |
| | | const sideTheme = computed(() => settingsStore.sideTheme); |
| | | const sidebar = computed(() => useAppStore().sidebar); |
| | | const device = computed(() => useAppStore().device); |
| | | const sidebar = computed(() => appStore.sidebar); |
| | | const device = computed(() => appStore.device); |
| | | const needTagsView = computed(() => settingsStore.tagsView); |
| | | const showTagsView = computed( |
| | | () => needTagsView.value && tagsViewStore.visitedViews.length > 1 |
| | | ); |
| | | const showTagsView = computed(() => needTagsView.value); |
| | | const fixedHeader = computed(() => settingsStore.fixedHeader); |
| | | const aiEnabled = computed(() => Number(userStore.aiEnabled) === 1); |
| | | const showGlobalAiChat = computed(() => { |
| | |
| | | () => device.value, |
| | | () => { |
| | | if (device.value === "mobile" && sidebar.value.opened) { |
| | | useAppStore().closeSideBar({ withoutAnimation: false }); |
| | | appStore.closeSideBar({ withoutAnimation: false }); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | watchEffect(() => { |
| | | if (width.value - 1 < WIDTH) { |
| | | useAppStore().toggleDevice("mobile"); |
| | | useAppStore().closeSideBar({ withoutAnimation: true }); |
| | | appStore.toggleDevice("mobile"); |
| | | appStore.closeSideBar({ withoutAnimation: true }); |
| | | } else { |
| | | useAppStore().toggleDevice("desktop"); |
| | | appStore.toggleDevice("desktop"); |
| | | } |
| | | }); |
| | | |
| | | watchEffect(() => { |
| | | if (settingsStore.topNav && device.value !== "mobile") { |
| | | appStore.toggleSideBarHide(true); |
| | | appStore.closeSideBar({ withoutAnimation: true }); |
| | | } else { |
| | | appStore.toggleSideBarHide(false); |
| | | } |
| | | }); |
| | | |
| | | function handleClickOutside() { |
| | | useAppStore().closeSideBar({ withoutAnimation: false }); |
| | | appStore.closeSideBar({ withoutAnimation: false }); |
| | | } |
| | | |
| | | function toggleSideBar() { |
| | | appStore.toggleSideBar(); |
| | | } |
| | | |
| | | const settingRef = ref(null); |
| | |
| | | width: 100%; |
| | | top: 0; |
| | | height: 100%; |
| | | position: absolute; |
| | | z-index: 999; |
| | | position: fixed; |
| | | z-index: 1003; |
| | | } |
| | | |
| | | .top-header { |
| | | width: 100%; |
| | | z-index: 1002; |
| | | } |
| | | |
| | | .top-header.fixed-header { |
| | | position: sticky; |
| | | top: 0; |
| | | } |
| | | |
| | | .main-layout { |
| | |
| | | transition: margin-left 0.25s ease; |
| | | display: flex; |
| | | flex-direction: column; |
| | | padding: 0; |
| | | } |
| | | |
| | | .fixed-header { |
| | | position: sticky; |
| | | top: 0; |
| | | z-index: var(--layout-header-z); |
| | | .breadcrumb-wrapper { |
| | | width: 100%; |
| | | padding: 0; |
| | | background: #fff; |
| | | padding: 10px 24px; |
| | | background: transparent; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2px; // 在 Navbar 和 TagsView 之间增加极小的空隙 |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .fixed-header.with-tags { |
| | | padding-bottom: 4px; // 底部留出一点距离,不要直接贴着主体内容 |
| | | .hamburger-container { |
| | | height: 32px; |
| | | width: 32px; |
| | | border-radius: var(--radius-sm); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | color: var(--text-secondary); |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | flex-shrink: 0; |
| | | |
| | | &:hover { |
| | | background: rgba(0, 0, 0, 0.05); |
| | | color: var(--el-color-primary); |
| | | } |
| | | } |
| | | |
| | | .hideSidebar .fixed-header { |
| | | .breadcrumb-container { |
| | | min-width: 0; |
| | | |
| | | :deep(.el-breadcrumb__inner) { |
| | | color: var(--text-secondary) !important; |
| | | opacity: 0.85; |
| | | |
| | | &:hover { |
| | | color: var(--el-color-primary) !important; |
| | | opacity: 1; |
| | | } |
| | | |
| | | a { |
| | | color: inherit !important; |
| | | font-weight: 500 !important; |
| | | } |
| | | } |
| | | |
| | | :deep(.no-redirect) { |
| | | color: var(--text-primary) !important; |
| | | font-weight: 600 !important; |
| | | opacity: 1; |
| | | } |
| | | |
| | | :deep(.el-breadcrumb__separator) { |
| | | color: var(--text-tertiary); |
| | | opacity: 0.5; |
| | | } |
| | | } |
| | | |
| | | .fixed-breadcrumb { |
| | | position: sticky; |
| | | top: var(--topbar-height); |
| | | z-index: calc(var(--layout-header-z) - 1); |
| | | background: var(--app-bg); |
| | | } |
| | | |
| | | .hasTagsView { |
| | | .fixed-breadcrumb { |
| | | top: calc(var(--topbar-height) + var(--tagsbar-height)); |
| | | } |
| | | } |
| | | |
| | | .tags-view-wrapper.fixed-tags-view { |
| | | position: sticky; |
| | | top: var(--topbar-height); |
| | | z-index: calc(var(--layout-header-z) - 1); |
| | | background: var(--app-bg); |
| | | } |
| | | |
| | | .hideSidebar .breadcrumb-wrapper { |
| | | width: 100%; |
| | | } |
| | | |
| | |
| | | margin-left: var(--sidebar-collapsed-width); |
| | | } |
| | | |
| | | .mobile .fixed-header { |
| | | .mobile .top-header.fixed-header { |
| | | width: 100%; |
| | | padding: 8px 10px 0; |
| | | } |
| | | |
| | | .mobile .main-layout, |
| | |
| | | /**
|
| | | * 是否显示顶部导航
|
| | | */
|
| | | topNav: false,
|
| | | topNav: true, |
| | |
|
| | | /**
|
| | | * 是否显示 tagsView
|
| | |
| | | |
| | | const useSettingsStore = defineStore("settings", () => { |
| | | const title = ref(""); |
| | | const theme = ref(storageSetting.theme || "#374D77"); |
| | | const theme = ref(storageSetting.theme || "#008C8C"); |
| | | const sideThemeValue = ref(storageSetting.sideTheme || sideTheme); |
| | | const showSettingsValue = ref(showSettings); |
| | | const topNavValue = ref(storageSetting.topNav === undefined ? topNav : storageSetting.topNav); |
| | |
| | | transform: translateY(-50%); |
| | | width: 4px; |
| | | height: 1rem; |
| | | background-color: #374d77; |
| | | background-color: #008c8c; |
| | | border-radius: 2px; |
| | | } |
| | | </style> |
| | |
| | | transform: translateY(-50%); |
| | | width: 4px; |
| | | height: 1rem; |
| | | background-color: #374d77; /* Element 默认红色 */ |
| | | background-color: #008c8c; /* Element 默认红色 */ |
| | | border-radius: 2px; |
| | | } |
| | | </style> |
| | |
| | | |
| | | <style scoped lang="scss"> |
| | | .login-page { |
| | | --accent: var(--accent-primary, var(--el-color-primary, #374d77)); |
| | | --accent: var(--accent-primary, var(--el-color-primary, #008c8c)); |
| | | --accent-rgb: var(--el-color-primary-rgb, 22, 116, 88); |
| | | --text: #0f172a; |
| | | --muted: #64748b; |