src/assets/styles/sidebar.scss | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/assets/styles/variables.module.scss | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/layout/components/Navbar.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/layout/components/Sidebar/Logo.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/layout/components/Sidebar/SidebarItem.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/layout/components/Sidebar/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/layout/components/TagsView/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/layout/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/settings.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/store/modules/app.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/store/modules/settings.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/assets/styles/sidebar.scss
@@ -12,21 +12,18 @@ } .sidebar-container { -webkit-transition: width .28s; transition: width 0.28s; width: $base-sidebar-width !important; background-color: $base-menu-background; height: 100%; position: fixed; font-size: 0px; top: 50px; top: 0; bottom: 0; left: 0; z-index: 1001; overflow: hidden; -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35); box-shadow: none; margin: 0 auto; box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); // reset element-ui css .horizontal-collapse-transition { @@ -42,7 +39,7 @@ } .el-scrollbar { height: 92%; height: 100%; } &.has-logo { @@ -54,29 +51,24 @@ .is-horizontal { display: none; } a { display: inline-block; width: 80px; height: 80px; width: 100%; overflow: hidden; margin-top: 20px; } .svg-icon { //margin-right: 16px; margin-right: 16px; } .el-menu { border: none; height: 100%; width: 100% !important; padding: 0 20px !important; } .el-menu-item, .menu-title { line-height: 20px; font-size: 14px; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; @@ -89,69 +81,52 @@ // menu hover .sub-menu-title-noDropdown, .el-sub-menu__title { border-radius: 10px 10px 10px 10px; &:hover { background-color: #ffffff !important; border-radius: 10px 10px 10px 10px; color: $base-menu-color-active !important; background-color: rgba(0, 0, 0, 0.06) !important; } } & .theme-dark .is-active > .el-sub-menu__title { color: $base-menu-color-active !important; } & .nest-menu > a { right: 20px; position: relative; } & .nest-menu> .el-sub-menu > .el-sub-menu__title { right: 20px; position: relative; } & .nest-menu .el-sub-menu>.el-sub-menu__title, & .el-sub-menu .el-menu-item { min-width: $base-sidebar-width !important; height: 80px; min-width: 80px !important; border-radius: 10px 10px 10px 10px; &:hover { background-color: #ffffff !important; border-radius: 10px 10px 10px 10px; color: $base-menu-color-active !important; background-color: rgba(0, 0, 0, 0.06) !important; } } & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title, & .theme-dark .el-sub-menu .el-menu-item { background-color: $base-sub-menu-background; border-radius: 10px 10px 10px 10px; &:hover { background-color: #ffffff !important; border-radius: 10px 10px 10px 10px; color: $base-menu-color-active !important; background-color: $base-sub-menu-hover !important; } } } .hideSidebar { .sidebar-container { width: 120px !important; width: 54px !important; } .main-container { margin-left: 120px; margin-left: 54px; } .sub-menu-title-noDropdown { padding: 0 0 0 20px !important; padding: 0 !important; position: relative; .el-tooltip { padding: 0 !important; .svg-icon { //margin-left: 20px; margin-left: 20px; } } } @@ -163,7 +138,7 @@ padding: 0 !important; .svg-icon { //margin-left: 20px; margin-left: 20px; } } @@ -226,21 +201,17 @@ // when menu collapsed .el-menu--vertical { width: 120px !important; /* 设置一个固定的宽度 */ &>.el-menu { .svg-icon { //margin-right: 16px; margin-right: 16px; } } .nest-menu .el-sub-menu>.el-sub-menu__title, .el-menu-item { border-radius: 10px 10px 10px 10px; &:hover { // you can use $sub-menuHover background-color: #ffffff !important; border-radius: 10px 10px 10px 10px; color: $base-menu-color-active !important; background-color: rgba(0, 0, 0, 0.06) !important; } } src/assets/styles/variables.module.scss
@@ -9,30 +9,30 @@ $panGreen: #30B08F; // 默认主题变量 $menuText: #ffffff; $menuActiveText: #165DFF; $menuBg: #165DFF; $menuHover: #ffffff; $menuText: #bfcbd9; $menuActiveText: #409eff; $menuBg: #304156; $menuHover: #263445; // 浅色主题theme-light $menuLightBg: #165DFF; $menuLightHover: #ffffff; $menuLightText: #ffffff; $menuLightActiveText: #165DFF; $menuLightBg: #ffffff; $menuLightHover: #f0f1f5; $menuLightText: #303133; $menuLightActiveText: #409EFF; // 基础变量 $base-sidebar-width: 120px; $sideBarWidth: 120px; $base-sidebar-width: 200px; $sideBarWidth: 200px; // 菜单暗色变量 $base-menu-color: #bfcbd9; $base-menu-color-active: #165DFF; $base-menu-background: #165DFF; $base-menu-color-active: #f4f4f5; $base-menu-background: #304156; $base-sub-menu-background: #1f2d3d; $base-sub-menu-hover: #001528; // 组件变量 $--color-primary: #165DFF; $--color-primary: #409EFF; $--color-success: #67C23A; $--color-warning: #E6A23C; $--color-danger: #F56C6C; @@ -71,10 +71,10 @@ --sidebar-bg: #{$menuBg}; --sidebar-text: #{$menuText}; --menu-hover: #{$menuHover}; --navbar-bg: #ffffff; --navbar-text: #303133; /* splitpanes default-theme 变量 */ --splitpanes-default-bg: #ffffff; @@ -119,7 +119,7 @@ --blockquote-bg: #1d1e1f; --blockquote-border: #303030; --blockquote-text: #d0d0d0; /* Cron 时间表达式 模式变量 */ --cron-border: #303030; @@ -127,7 +127,7 @@ --splitpanes-default-bg: #141414; /* 侧边栏菜单覆盖 */ .sidebar-container { .sidebar-container { .el-menu-item, .menu-title { color: var(--el-text-color-regular); } @@ -199,7 +199,7 @@ background-color: var(--el-bg-color-overlay); } } /* 下拉菜单样式覆盖 */ .el-dropdown-menu__item:not(.is-disabled):focus, .el-dropdown-menu__item:not(.is-disabled):hover{ background-color: var(--navbar-hover) !important; @@ -211,7 +211,7 @@ border-left-color: var(--blockquote-border) !important; color: var(--blockquote-text) !important; } /* 时间表达式标题样式覆盖 */ .popup-result .title { background: var(--cron-border); src/layout/components/Navbar.vue
@@ -1,13 +1,13 @@ <template> <div class="navbar"> <!-- <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 v-if="sidebar.hide"> <top-nav id="topmenu-container" class="topmenu-container" /> </div> <div class="logo" v-if="!sidebar.hide"> <img src="@/assets/logo/logo.png" alt=""/> </div> <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 v-if="sidebar.hide">--> <!-- <top-nav id="topmenu-container" class="topmenu-container" />--> <!-- </div>--> <!-- <div class="logo" v-if="!sidebar.hide">--> <!-- <img src="@/assets/logo/logo.png" alt=""/>--> <!-- </div>--> <div class="right-menu"> <template v-if="appStore.device !== 'mobile'"> <header-search id="header-search" class="right-menu-item" /> @@ -23,9 +23,9 @@ <router-link to="/user/profile"> <el-dropdown-item>个人中心</el-dropdown-item> </router-link> <!-- <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">--> <!-- <span>布局设置</span>--> <!-- </el-dropdown-item>--> <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings"> <span>布局设置</span> </el-dropdown-item> <el-dropdown-item divided command="logout"> <span>退出登录</span> </el-dropdown-item> @@ -135,116 +135,114 @@ <style lang='scss' scoped> .navbar { height: 50px; overflow: hidden; position: fixed; /* 将头部固定 */ top: 0; /* 在顶部固定 */ width: 100%; /* 宽度100%,覆盖整个视口 */ //background-color: #f8f9fa; /* 设置背景颜色,以便更明显地看到效果 */ z-index: 1000; /* 确保头部在其他内容之上 */ background: var(--navbar-bg); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); display: flex; justify-content: space-between; padding: 0 20px; .logo { height: 50px; line-height: 50px; img { cursor: pointer; width: 146px; height: 46px; } } .breadcrumb-container { float: left; } .topmenu-container { position: absolute; } .errLog-container { display: inline-block; vertical-align: top; } .right-menu { float: right; height: 100%; line-height: 50px; display: flex; &:focus { outline: none; } .right-menu-item { display: inline-block; padding: 0 8px; height: 100%; font-size: 18px; color: #5a5e66; vertical-align: text-bottom; &.hover-effect { cursor: pointer; transition: background 0.3s; &:hover { background: rgba(0, 0, 0, 0.025); } } &.theme-switch-wrapper { display: flex; align-items: center; svg { transition: transform 0.3s; &:hover { transform: scale(1.15); } } } } .avatar-container { margin-right: 0px; padding-right: 0px; .avatar-wrapper { margin-top: 10px; right: 5px; position: relative; .user-avatar { cursor: pointer; width: 30px; height: 30px; border-radius: 50%; } .user-nickname{ position: relative; left: 5px; bottom: 10px; font-size: 14px; font-weight: bold; } i { cursor: pointer; position: absolute; right: -20px; top: 25px; font-size: 12px; } } } } height: 50px; overflow: hidden; position: relative; background: var(--navbar-bg); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); .hamburger-container { line-height: 46px; height: 100%; float: left; cursor: pointer; transition: background 0.3s; -webkit-tap-highlight-color: transparent; &:hover { background: rgba(0, 0, 0, 0.025); } } .breadcrumb-container { float: left; } .topmenu-container { position: absolute; left: 50px; } .errLog-container { display: inline-block; vertical-align: top; } .right-menu { float: right; height: 100%; line-height: 50px; display: flex; margin-right: 30px; &:focus { outline: none; } .right-menu-item { display: inline-block; padding: 0 8px; height: 100%; font-size: 18px; color: #5a5e66; vertical-align: text-bottom; &.hover-effect { cursor: pointer; transition: background 0.3s; &:hover { background: rgba(0, 0, 0, 0.025); } } &.theme-switch-wrapper { display: flex; align-items: center; svg { transition: transform 0.3s; &:hover { transform: scale(1.15); } } } } .avatar-container { margin-right: 0px; padding-right: 0px; .avatar-wrapper { margin-top: 10px; right: 5px; position: relative; .user-avatar { cursor: pointer; width: 30px; height: 30px; border-radius: 50%; } .user-nickname{ position: relative; left: 5px; bottom: 10px; font-size: 14px; font-weight: bold; } i { cursor: pointer; position: absolute; right: -20px; top: 25px; font-size: 12px; } } } } } </style> src/layout/components/Sidebar/Logo.vue
@@ -1,16 +1,16 @@ <template> <div class="sidebar-logo-container" :class="{ 'collapse': collapse }"> <transition name="sidebarLogoFade"> <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> <img v-if="logo" :src="logo" class="sidebar-logo" /> <h1 v-else class="sidebar-title">{{ title }}</h1> </router-link> <router-link v-else key="expand" class="sidebar-logo-link" to="/"> <img v-if="logo" :src="logo" class="sidebar-logo" /> <h1 class="sidebar-title">{{ title }}</h1> </router-link> </transition> </div> <div class="sidebar-logo-container" :class="{ 'collapse': collapse }"> <transition name="sidebarLogoFade"> <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> <img v-if="logo" :src="logo" class="sidebar-logo" /> <h1 v-else class="sidebar-title">{{ title }}</h1> </router-link> <router-link v-else key="expand" class="sidebar-logo-link" to="/"> <img v-if="logo" :src="logo" class="sidebar-logo" /> <h1 class="sidebar-title">{{ title }}</h1> </router-link> </transition> </div> </template> <script setup> @@ -19,10 +19,10 @@ import variables from '@/assets/styles/variables.module.scss' defineProps({ collapse: { type: Boolean, required: true } collapse: { type: Boolean, required: true } }) const title = import.meta.env.VITE_APP_TITLE @@ -31,18 +31,18 @@ // 获取Logo背景色 const getLogoBackground = computed(() => { if (settingsStore.isDark) { return 'var(--sidebar-bg)' } return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg if (settingsStore.isDark) { return 'var(--sidebar-bg)' } return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg }) // 获取Logo文字颜色 const getLogoTextColor = computed(() => { if (settingsStore.isDark) { return 'var(--sidebar-text)' } return sideTheme.value === 'theme-dark' ? '#fff' : variables.menuLightText if (settingsStore.isDark) { return 'var(--sidebar-text)' } return sideTheme.value === 'theme-dark' ? '#fff' : variables.menuLightText }) </script> @@ -50,50 +50,49 @@ @import '@/assets/styles/variables.module.scss'; .sidebarLogoFade-enter-active { transition: opacity 1.5s; transition: opacity 1.5s; } .sidebarLogoFade-enter, .sidebarLogoFade-leave-to { opacity: 0; opacity: 0; } .sidebar-logo-container { position: relative; width: 100%; height: 50px; line-height: 50px; background: v-bind(getLogoBackground); text-align: center; overflow: hidden; & .sidebar-logo-link { height: 100%; width: 100%; & .sidebar-logo { width: 32px; height: 32px; vertical-align: middle; margin-right: 12px; } & .sidebar-title { display: inline-block; margin: 0; color: v-bind(getLogoTextColor); font-weight: 600; line-height: 50px; font-size: 14px; font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; vertical-align: middle; } } &.collapse { .sidebar-logo { margin-right: 0px; } } position: relative; width: 100%; height: 50px; line-height: 50px; background: v-bind(getLogoBackground); text-align: center; overflow: hidden; & .sidebar-logo-link { height: 100%; width: 100%; & .sidebar-logo { height: 32px; vertical-align: middle; margin-right: 12px; } & .sidebar-title { display: inline-block; margin: 0; color: v-bind(getLogoTextColor); font-weight: 600; line-height: 50px; font-size: 14px; font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; vertical-align: middle; } } &.collapse { .sidebar-logo { margin-right: 0px; } } } </style> src/layout/components/Sidebar/SidebarItem.vue
@@ -1,30 +1,30 @@ <template> <div v-if="!item.hidden"> <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow"> <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }" style="display: flex;flex-direction: column;justify-content: center;height: 80px;padding: 0;width: 80px"> <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" style="width: 30px;height: 30px;margin-bottom: 6px"/> <template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template> </el-menu-item> </app-link> </template> <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported> <template v-if="item.meta" #title> <svg-icon :icon-class="item.meta && item.meta.icon" style="width: 30px;height: 30px;margin-bottom: 6px"/> <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title}}</span> </template> <sidebar-item v-for="(child, index) in item.children" :key="child.path + index" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" /> </el-sub-menu> </div> <div v-if="!item.hidden"> <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow"> <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }"> <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/> <template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template> </el-menu-item> </app-link> </template> <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported> <template v-if="item.meta" #title> <svg-icon :icon-class="item.meta && item.meta.icon" /> <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span> </template> <sidebar-item v-for="(child, index) in item.children" :key="child.path + index" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" /> </el-sub-menu> </div> </template> <script setup> @@ -33,93 +33,71 @@ import { getNormalPath } from '@/utils/ruoyi' const props = defineProps({ // route object item: { type: Object, required: true }, isNest: { type: Boolean, default: false }, basePath: { type: String, default: '' } // route object item: { type: Object, required: true }, isNest: { type: Boolean, default: false }, basePath: { type: String, default: '' } }) const onlyOneChild = ref({}) function hasOneShowingChild(children = [], parent) { if (!children) { children = [] } const showingChildren = children.filter(item => { if (item.hidden) { return false } onlyOneChild.value = item return true }) // When there is only one child router, the child router is displayed by default if (showingChildren.length === 1) { return true } // Show parent if there are no child router to display if (showingChildren.length === 0) { onlyOneChild.value = { ...parent, path: '', noShowingChildren: true } return true } return false if (!children) { children = [] } const showingChildren = children.filter(item => { if (item.hidden) { return false } onlyOneChild.value = item return true }) // When there is only one child router, the child router is displayed by default if (showingChildren.length === 1) { return true } // Show parent if there are no child router to display if (showingChildren.length === 0) { onlyOneChild.value = { ...parent, path: '', noShowingChildren: true } return true } return false } function resolvePath(routePath, routeQuery) { if (isExternal(routePath)) { return routePath } if (isExternal(props.basePath)) { return props.basePath } if (routeQuery) { let query = JSON.parse(routeQuery) return { path: getNormalPath(props.basePath + '/' + routePath), query: query } } return getNormalPath(props.basePath + '/' + routePath) if (isExternal(routePath)) { return routePath } if (isExternal(props.basePath)) { return props.basePath } if (routeQuery) { let query = JSON.parse(routeQuery) return { path: getNormalPath(props.basePath + '/' + routePath), query: query } } return getNormalPath(props.basePath + '/' + routePath) } function hasTitle(title){ if (title.length > 5) { return title } else { return "" } if (title.length > 5) { return title } else { return "" } } </script> <style scoped> :deep(.el-sub-menu__title) { display: flex; flex-direction: column; justify-content: center; padding: 0 !important; height: 80px; margin-top: 20px; } :deep(.submenu-title-noDropdown) { padding: 0 !important; } :deep(.router-link-exact-active) { width: 80px; height: 80px; background: #FFFFFF; border-radius: 10px 10px 10px 10px; } :deep(.el-sub-menu__icon-arrow) { right: -12px !important; &:hover { color: #ffffff !important; } } </style> src/layout/components/Sidebar/index.vue
@@ -1,25 +1,27 @@ <template> <div :class="{ 'has-logo': showLogo }" class="sidebar-container"> <!-- <logo v-if="showLogo" :collapse="isCollapse" />--> <el-scrollbar wrap-class="scrollbar-wrapper"> <el-menu :default-active="activeMenu" :background-color="getMenuBackground" :text-color="getMenuTextColor" :unique-opened="true" :active-text-color="theme" :collapse="false" mode="vertical" > <sidebar-item v-for="(route, index) in sidebarRouters" :key="route.path + index" :item="route" :base-path="route.path" /> </el-menu> </el-scrollbar> </div> <div :class="{ 'has-logo': showLogo }" class="sidebar-container"> <logo v-if="showLogo" :collapse="isCollapse" /> <el-scrollbar wrap-class="scrollbar-wrapper"> <el-menu :default-active="activeMenu" :collapse="isCollapse" :background-color="getMenuBackground" :text-color="getMenuTextColor" :unique-opened="true" :active-text-color="theme" :collapse-transition="false" mode="vertical" :class="sideTheme" > <sidebar-item v-for="(route, index) in sidebarRouters" :key="route.path + index" :item="route" :base-path="route.path" /> </el-menu> </el-scrollbar> </div> </template> <script setup> @@ -39,62 +41,64 @@ const showLogo = computed(() => settingsStore.sidebarLogo) const sideTheme = computed(() => settingsStore.sideTheme) const theme = computed(() => settingsStore.theme) const isCollapse = computed(() => !appStore.sidebar.opened) // 获取菜单背景色 const getMenuBackground = computed(() => { if (settingsStore.isDark) { return 'var(--sidebar-bg)' } return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg if (settingsStore.isDark) { return 'var(--sidebar-bg)' } return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg }) // 获取菜单文字颜色 const getMenuTextColor = computed(() => { if (settingsStore.isDark) { return 'var(--sidebar-text)' } return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText if (settingsStore.isDark) { return 'var(--sidebar-text)' } return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText }) const activeMenu = computed(() => { const { meta, path } = route if (meta.activeMenu) { return meta.activeMenu } return path const { meta, path } = route if (meta.activeMenu) { return meta.activeMenu } return path }) </script> <style lang="scss" scoped> .sidebar-container { background-color: v-bind(getMenuBackground); .scrollbar-wrapper { background-color: v-bind(getMenuBackground); } .el-menu { border: none; height: 100%; width: 100% !important; .el-menu-item, .el-sub-menu__title { &:hover { background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important; } } .el-menu-item { color: v-bind(getMenuTextColor); &.is-active { color: var(--menu-active-text, #409eff); background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important; } } .el-sub-menu__title { color: v-bind(getMenuTextColor); } } background-color: v-bind(getMenuBackground); .scrollbar-wrapper { background-color: v-bind(getMenuBackground); } .el-menu { border: none; height: 100%; width: 100% !important; .el-menu-item, .el-sub-menu__title { &:hover { background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important; } } .el-menu-item { color: v-bind(getMenuTextColor); &.is-active { color: var(--menu-active-text, #409eff); background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important; } } .el-sub-menu__title { color: v-bind(getMenuTextColor); } } } </style> src/layout/components/TagsView/index.vue
@@ -1,44 +1,44 @@ <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" :key="tag.path" :data-path="tag.path" :class="isActive(tag) ? 'active' : ''" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" 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)" @click.prevent.stop="closeSelectedTag(tag)"> <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' : ''" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" 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)" @click.prevent.stop="closeSelectedTag(tag)"> <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" /> </span> </router-link> </scroll-pane> <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu"> <li @click="refreshSelectedTag(selectedTag)"> <refresh-right style="width: 1em; height: 1em;" /> 刷新页面 </li> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"> <close style="width: 1em; height: 1em;" /> 关闭当前 </li> <li @click="closeOthersTags"> <circle-close style="width: 1em; height: 1em;" /> 关闭其他 </li> <li v-if="!isFirstView()" @click="closeLeftTags"> <back style="width: 1em; height: 1em;" /> 关闭左侧 </li> <li v-if="!isLastView()" @click="closeRightTags"> <right style="width: 1em; height: 1em;" /> 关闭右侧 </li> <li @click="closeAllTags(selectedTag)"> <circle-close style="width: 1em; height: 1em;" /> 全部关闭 </li> </ul> </div> </router-link> </scroll-pane> <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu"> <li @click="refreshSelectedTag(selectedTag)"> <refresh-right style="width: 1em; height: 1em;" /> 刷新页面 </li> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"> <close style="width: 1em; height: 1em;" /> 关闭当前 </li> <li @click="closeOthersTags"> <circle-close style="width: 1em; height: 1em;" /> 关闭其他 </li> <li v-if="!isFirstView()" @click="closeLeftTags"> <back style="width: 1em; height: 1em;" /> 关闭左侧 </li> <li v-if="!isLastView()" @click="closeRightTags"> <right style="width: 1em; height: 1em;" /> 关闭右侧 </li> <li @click="closeAllTags(selectedTag)"> <circle-close style="width: 1em; height: 1em;" /> 全部关闭 </li> </ul> </div> </template> <script setup> @@ -64,293 +64,302 @@ const theme = computed(() => useSettingsStore().theme) watch(route, () => { addTags() moveToCurrentTag() addTags() moveToCurrentTag() }) watch(visible, (value) => { if (value) { document.body.addEventListener('click', closeMenu) } else { document.body.removeEventListener('click', closeMenu) } if (value) { document.body.addEventListener('click', closeMenu) } else { 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 {} return { "background-color": theme.value, "border-color": theme.value } if (!isActive(tag)) return {} return { "background-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 } catch (err) { return false } try { return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath } catch (err) { return false } } function isLastView() { try { return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath } catch (err) { return false } try { return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath } catch (err) { return false } } function filterAffixTags(routes, basePath = '') { let tags = [] routes.forEach(route => { if (route.meta && route.meta.affix) { const tagPath = getNormalPath(basePath + '/' + route.path) tags.push({ fullPath: tagPath, path: tagPath, name: route.name, meta: { ...route.meta } }) } if (route.children) { const tempTags = filterAffixTags(route.children, route.path) if (tempTags.length >= 1) { tags = [...tags, ...tempTags] } } }) return tags let tags = [] routes.forEach(route => { if (route.meta && route.meta.affix) { const tagPath = getNormalPath(basePath + '/' + route.path) tags.push({ fullPath: tagPath, path: tagPath, name: route.name, meta: { ...route.meta } }) } if (route.children) { const tempTags = filterAffixTags(route.children, route.path) if (tempTags.length >= 1) { tags = [...tags, ...tempTags] } } }) return tags } function initTags() { const res = filterAffixTags(routes.value) affixTags.value = res for (const tag of res) { // Must have tag name if (tag.name) { useTagsViewStore().addVisitedView(tag) } } const res = filterAffixTags(routes.value) affixTags.value = res for (const tag of res) { // Must have tag name if (tag.name) { useTagsViewStore().addVisitedView(tag) } } } function addTags() { const { name } = route if (name) { useTagsViewStore().addView(route) } const { name } = route if (name) { useTagsViewStore().addView(route) } } function moveToCurrentTag() { nextTick(() => { for (const r of visitedViews.value) { if (r.path === route.path) { scrollPaneRef.value.moveToTarget(r) // when query is different then update if (r.fullPath !== route.fullPath) { useTagsViewStore().updateVisitedView(route) } } } }) nextTick(() => { for (const r of visitedViews.value) { if (r.path === route.path) { scrollPaneRef.value.moveToTarget(r) // when query is different then update if (r.fullPath !== route.fullPath) { useTagsViewStore().updateVisitedView(route) } } } }) } function refreshSelectedTag(view) { proxy.$tab.refreshPage(view) if (route.meta.link) { useTagsViewStore().delIframeView(route) } proxy.$tab.refreshPage(view) if (route.meta.link) { useTagsViewStore().delIframeView(route) } } function closeSelectedTag(view) { proxy.$tab.closePage(view).then(({ visitedViews }) => { if (isActive(view)) { toLastView(visitedViews, view) } }) proxy.$tab.closePage(view).then(({ visitedViews }) => { if (isActive(view)) { toLastView(visitedViews, view) } }) } function closeRightTags() { proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => { if (!visitedViews.find(i => i.fullPath === route.fullPath)) { toLastView(visitedViews) } }) proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => { if (!visitedViews.find(i => i.fullPath === route.fullPath)) { toLastView(visitedViews) } }) } function closeLeftTags() { proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => { if (!visitedViews.find(i => i.fullPath === route.fullPath)) { toLastView(visitedViews) } }) proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => { if (!visitedViews.find(i => i.fullPath === route.fullPath)) { toLastView(visitedViews) } }) } function closeOthersTags() { router.push(selectedTag.value).catch(() => { }) proxy.$tab.closeOtherPage(selectedTag.value).then(() => { moveToCurrentTag() }) router.push(selectedTag.value).catch(() => { }) proxy.$tab.closeOtherPage(selectedTag.value).then(() => { moveToCurrentTag() }) } function closeAllTags(view) { proxy.$tab.closeAllPage().then(({ visitedViews }) => { if (affixTags.value.some(tag => tag.path === route.path)) { return } toLastView(visitedViews, view) }) proxy.$tab.closeAllPage().then(({ visitedViews }) => { if (affixTags.value.some(tag => tag.path === route.path)) { return } toLastView(visitedViews, view) }) } function toLastView(visitedViews, view) { const latestView = visitedViews.slice(-1)[0] if (latestView) { 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') { // to reload home page router.replace({ path: '/redirect' + view.fullPath }) } else { router.push('/') } } const latestView = visitedViews.slice(-1)[0] if (latestView) { 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') { // to reload home page router.replace({ path: '/redirect' + view.fullPath }) } else { 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 if (l > maxLeft) { left.value = maxLeft } else { left.value = l } top.value = e.clientY visible.value = true selectedTag.value = tag 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 } else { left.value = l } 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: 34px; width: 100%; position: fixed; /* 将头部固定 */ top: 50px; /* 在顶部固定 */ z-index: 1000; /* 确保头部在其他内容之上 */ background: #fff; box-shadow: none; .tags-view-wrapper { .tags-view-item { display: inline-block; position: relative; cursor: pointer; height: 30px; line-height: 26px; //border: 1px solid var(--tags-item-border, #d8dce5); color: var(--tags-item-text, #495060); background: var(--tags-item-bg, #fff); padding: 2px 16px; font-size: 12px; //margin-left: 5px; margin-top: 4px; &:first-of-type { margin-left: 15px; } &:last-of-type { margin-right: 15px; } &.active { border-radius: 10px 10px 0px 0px; background-color: #F7F7F7 !important; color: #165DFF; } } } .contextmenu { margin: 0; background: var(--el-bg-color-overlay, #fff); z-index: 3000; position: absolute; list-style-type: none; padding: 5px 0; border-radius: 4px; font-size: 12px; font-weight: 400; color: var(--tags-item-text, #333); box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); border: 1px solid var(--el-border-color-light, #e4e7ed); li { margin: 0; padding: 7px 16px; cursor: pointer; &:hover { background: var(--tags-item-hover, #eee); } } } height: 34px; width: 100%; background: var(--tags-bg, #fff); border-bottom: 1px solid var(--tags-item-border, #d8dce5); box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04); .tags-view-wrapper { .tags-view-item { display: inline-block; position: relative; cursor: pointer; height: 26px; line-height: 26px; border: 1px solid var(--tags-item-border, #d8dce5); color: var(--tags-item-text, #495060); background: var(--tags-item-bg, #fff); padding: 0 8px; font-size: 12px; margin-left: 5px; margin-top: 4px; &:first-of-type { margin-left: 15px; } &:last-of-type { margin-right: 15px; } &.active { background-color: #42b983; color: #fff; border-color: #42b983; &::before { content: ''; background: #fff; display: inline-block; width: 8px; height: 8px; border-radius: 50%; position: relative; margin-right: 5px; } } } } .contextmenu { margin: 0; background: var(--el-bg-color-overlay, #fff); z-index: 3000; position: absolute; list-style-type: none; padding: 5px 0; border-radius: 4px; font-size: 12px; font-weight: 400; color: var(--tags-item-text, #333); box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); border: 1px solid var(--el-border-color-light, #e4e7ed); li { margin: 0; padding: 7px 16px; cursor: pointer; &:hover { background: var(--tags-item-hover, #eee); } } } } </style> <style lang="scss"> //reset element css of el-icon-close .tags-view-wrapper { .tags-view-item { .el-icon-close { width: 16px; height: 16px; vertical-align: 2px; border-radius: 50%; text-align: center; transition: all .3s cubic-bezier(.645, .045, .355, 1); transform-origin: 100% 50%; &:before { transform: scale(.6); display: inline-block; vertical-align: -3px; } &:hover { background-color: var(--tags-close-hover, #b4bccc); color: #fff; width: 12px !important; height: 12px !important; } } } .tags-view-item { .el-icon-close { width: 16px; height: 16px; vertical-align: 2px; border-radius: 50%; text-align: center; transition: all .3s cubic-bezier(.645, .045, .355, 1); transform-origin: 100% 50%; &:before { transform: scale(.6); display: inline-block; vertical-align: -3px; } &:hover { background-color: var(--tags-close-hover, #b4bccc); color: #fff; width: 12px !important; height: 12px !important; } } } } </style> src/layout/index.vue
@@ -1,16 +1,16 @@ <template> <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }"> <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/> <navbar @setLayout="setLayout" /> <sidebar v-if="!sidebar.hide" class="sidebar-container" /> <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container"> <div :class="{ 'fixed-header': fixedHeader }"> <tags-view v-if="needTagsView" /> </div> <app-main /> <settings ref="settingRef" /> </div> </div> <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }"> <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/> <sidebar v-if="!sidebar.hide" class="sidebar-container" /> <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container"> <div :class="{ 'fixed-header': fixedHeader }"> <navbar @setLayout="setLayout" /> <tags-view v-if="needTagsView" /> </div> <app-main /> <settings ref="settingRef" /> </div> </div> </template> <script setup> @@ -29,84 +29,84 @@ const fixedHeader = computed(() => settingsStore.fixedHeader) const classObj = computed(() => ({ hideSidebar: !sidebar.value.opened, openSidebar: sidebar.value.opened, withoutAnimation: sidebar.value.withoutAnimation, mobile: device.value === 'mobile' hideSidebar: !sidebar.value.opened, openSidebar: sidebar.value.opened, withoutAnimation: sidebar.value.withoutAnimation, mobile: device.value === 'mobile' })) const { width, height } = useWindowSize() const WIDTH = 992 // refer to Bootstrap's responsive design watch(() => device.value, () => { if (device.value === 'mobile' && sidebar.value.opened) { useAppStore().closeSideBar({ withoutAnimation: false }) } if (device.value === 'mobile' && sidebar.value.opened) { useAppStore().closeSideBar({ withoutAnimation: false }) } }) watchEffect(() => { if (width.value - 1 < WIDTH) { useAppStore().toggleDevice('mobile') useAppStore().closeSideBar({ withoutAnimation: true }) } else { useAppStore().toggleDevice('desktop') } if (width.value - 1 < WIDTH) { useAppStore().toggleDevice('mobile') useAppStore().closeSideBar({ withoutAnimation: true }) } else { useAppStore().toggleDevice('desktop') } }) function handleClickOutside() { useAppStore().closeSideBar({ withoutAnimation: false }) useAppStore().closeSideBar({ withoutAnimation: false }) } const settingRef = ref(null) function setLayout() { settingRef.value.openSetting() settingRef.value.openSetting() } </script> <style lang="scss" scoped> @import "@/assets/styles/mixin.scss"; @import "@/assets/styles/variables.module.scss"; @import "@/assets/styles/mixin.scss"; @import "@/assets/styles/variables.module.scss"; .app-wrapper { @include clearfix; position: relative; height: 100%; width: 100%; &.mobile.openSidebar { position: fixed; top: 0; } @include clearfix; position: relative; height: 100%; width: 100%; &.mobile.openSidebar { position: fixed; top: 0; } } .drawer-bg { background: #000; opacity: 0.3; width: 100%; top: 0; height: 100%; position: absolute; z-index: 999; background: #000; opacity: 0.3; width: 100%; top: 0; height: 100%; position: absolute; z-index: 999; } .fixed-header { position: fixed; top: 0; right: 0; z-index: 9; width: calc(100% - #{$base-sidebar-width}); transition: width 0.28s; position: fixed; top: 0; right: 0; z-index: 9; width: calc(100% - #{$base-sidebar-width}); transition: width 0.28s; } .hideSidebar .fixed-header { width: calc(100% - 120px); width: calc(100% - 54px); } .sidebarHide .fixed-header { width: 100%; width: 100%; } .mobile .fixed-header { width: 100%; width: 100%; } </style> src/settings.js
@@ -37,7 +37,7 @@ /** * 是否显示动态标题 */ dynamicTitle: false, dynamicTitle: true, /** * @type {string | array} 'production' | ['production', 'development'] src/store/modules/app.js
@@ -5,7 +5,7 @@ { state: () => ({ sidebar: { opened: true, opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, withoutAnimation: false, hide: false }, src/store/modules/settings.js
@@ -14,7 +14,7 @@ { state: () => ({ title: '', theme: storageSetting.theme || '#165DFF', theme: storageSetting.theme || '#409EFF', sideTheme: storageSetting.sideTheme || sideTheme, showSettings: showSettings, topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,