|
@@ -0,0 +1,718 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="admin-layout">
|
|
|
|
|
+ <header class="top-nav">
|
|
|
|
|
+ <div class="nav-logo" @click="goHome" title="返回首页">
|
|
|
|
|
+ <img v-if="logImg" :src="logImg" class="logo-img" alt="" />
|
|
|
|
|
+ <div v-else class="logo-icon">
|
|
|
|
|
+ <i class="el-icon-s-platform" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span class="logo-text">{{ title }}</span>
|
|
|
|
|
+ <div class="logo-home-badge" @click.stop="goHome">
|
|
|
|
|
+ <i class="el-icon-s-home" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="nav-divider" />
|
|
|
|
|
+
|
|
|
|
|
+ <nav class="nav-menu">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(menu, index) in visibleTopMenus"
|
|
|
|
|
+ :key="menu.path + index"
|
|
|
|
|
+ class="nav-item"
|
|
|
|
|
+ :class="{ active: activeTopIndex === index }"
|
|
|
|
|
+ @click="switchTopMenu(index)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg-icon v-if="menu.meta && menu.meta.icon" :icon-class="menu.meta.icon" />
|
|
|
|
|
+ <span>{{ menu.meta.title }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-dropdown v-if="overflowTopMenus.length" trigger="click" @command="onMoreMenuCommand">
|
|
|
|
|
+ <div class="nav-item nav-item-more" :class="{ active: activeTopIndex >= visibleTopMenus.length }">
|
|
|
|
|
+ <span>更多菜单</span>
|
|
|
|
|
+ <i class="el-icon-arrow-down el-icon--right" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-dropdown-menu slot="dropdown">
|
|
|
|
|
+ <el-dropdown-item
|
|
|
|
|
+ v-for="(menu, idx) in overflowTopMenus"
|
|
|
|
|
+ :key="menu.path + idx"
|
|
|
|
|
+ :command="visibleTopMenus.length + idx"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg-icon v-if="menu.meta && menu.meta.icon" :icon-class="menu.meta.icon" />
|
|
|
|
|
+ {{ menu.meta.title }}
|
|
|
|
|
+ </el-dropdown-item>
|
|
|
|
|
+ </el-dropdown-menu>
|
|
|
|
|
+ </el-dropdown>
|
|
|
|
|
+ </nav>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="user-area">
|
|
|
|
|
+ <template v-if="device !== 'mobile'">
|
|
|
|
|
+ <search class="header-tool" />
|
|
|
|
|
+ <screenfull class="header-tool" />
|
|
|
|
|
+ <el-tooltip content="布局大小" effect="dark" placement="bottom">
|
|
|
|
|
+ <size-select class="header-tool" />
|
|
|
|
|
+ </el-tooltip>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <div class="user-info">
|
|
|
|
|
+ <div class="user-avatar">{{ userInitial }}</div>
|
|
|
|
|
+ <span class="user-role">管理员</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-dropdown trigger="click" @command="handleCommand">
|
|
|
|
|
+ <div class="nav-user">
|
|
|
|
|
+ <span class="nav-username">{{ name }}</span>
|
|
|
|
|
+ <i class="el-icon-arrow-down dropdown-arrow" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-dropdown-menu slot="dropdown">
|
|
|
|
|
+ <router-link to="/user/profile">
|
|
|
|
|
+ <el-dropdown-item command="profile">
|
|
|
|
|
+ <i class="el-icon-user" /> 个人中心
|
|
|
|
|
+ </el-dropdown-item>
|
|
|
|
|
+ </router-link>
|
|
|
|
|
+ <el-dropdown-item command="logout" divided>
|
|
|
|
|
+ <i class="el-icon-switch-button" /> 退出登录
|
|
|
|
|
+ </el-dropdown-item>
|
|
|
|
|
+ </el-dropdown-menu>
|
|
|
|
|
+ </el-dropdown>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </header>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="main-container">
|
|
|
|
|
+ <aside class="sidebar" :class="{ 'sidebar-dashboard': isDashboard }">
|
|
|
|
|
+ <div v-if="isDashboard" class="dashboard-shortcuts">
|
|
|
|
|
+ <div class="shortcuts-title">
|
|
|
|
|
+ <i class="el-icon-s-grid" />
|
|
|
|
|
+ <span>快捷入口</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(menu, index) in topMenus"
|
|
|
|
|
+ :key="menu.path + index"
|
|
|
|
|
+ class="shortcut-item"
|
|
|
|
|
+ @click="switchTopMenu(index)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg-icon v-if="menu.meta && menu.meta.icon" :icon-class="menu.meta.icon" />
|
|
|
|
|
+ <span>{{ menu.meta.title }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <saas-sidebar v-else class="saas-sidebar-panel" />
|
|
|
|
|
+ </aside>
|
|
|
|
|
+
|
|
|
|
|
+ <main class="content-area">
|
|
|
|
|
+ <tags-view v-if="needTagsView" />
|
|
|
|
|
+ <app-main />
|
|
|
|
|
+ </main>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { mapGetters, mapState } from 'vuex'
|
|
|
|
|
+import { constantRoutes } from '@/router'
|
|
|
|
|
+import SaasSidebar from './components/Sidebar/index.vue'
|
|
|
|
|
+import TagsView from './components/TagsView/index.vue'
|
|
|
|
|
+import AppMain from './components/AppMain'
|
|
|
|
|
+import Search from '@/components/HeaderSearch'
|
|
|
|
|
+import Screenfull from '@/components/Screenfull'
|
|
|
|
|
+import SizeSelect from '@/components/SizeSelect'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'AdminLayout',
|
|
|
|
|
+ components: {
|
|
|
|
|
+ SaasSidebar,
|
|
|
|
|
+ TagsView,
|
|
|
|
|
+ AppMain,
|
|
|
|
|
+ Search,
|
|
|
|
|
+ Screenfull,
|
|
|
|
|
+ SizeSelect
|
|
|
|
|
+ },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ activeTopIndex: 0,
|
|
|
|
|
+ visibleNumber: 12
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ ...mapGetters(['name', 'device', 'sidebarRouters']),
|
|
|
|
|
+ ...mapState({
|
|
|
|
|
+ needTagsView: state => state.settings.tagsView,
|
|
|
|
|
+ topbarRouters: state => state.permission.topbarRouters
|
|
|
|
|
+ }),
|
|
|
|
|
+ title() {
|
|
|
|
|
+ return process.env.VUE_APP_TITLE_INDEX || '云联融智SAAS'
|
|
|
|
|
+ },
|
|
|
|
|
+ logImg() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return require(process.env.VUE_APP_LOG_URL)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return ''
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ userInitial() {
|
|
|
|
|
+ const name = this.name || '管'
|
|
|
|
|
+ return name.charAt(0).toUpperCase()
|
|
|
|
|
+ },
|
|
|
|
|
+ topMenus() {
|
|
|
|
|
+ const list = []
|
|
|
|
|
+ this.topbarRouters.forEach(menu => {
|
|
|
|
|
+ if (menu.hidden !== true) {
|
|
|
|
|
+ if (menu.path === '/') {
|
|
|
|
|
+ list.push(menu.children[0])
|
|
|
|
|
+ } else {
|
|
|
|
|
+ list.push(menu)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ return list
|
|
|
|
|
+ },
|
|
|
|
|
+ visibleTopMenus() {
|
|
|
|
|
+ return this.topMenus.slice(0, this.visibleNumber)
|
|
|
|
|
+ },
|
|
|
|
|
+ overflowTopMenus() {
|
|
|
|
|
+ return this.topMenus.slice(this.visibleNumber)
|
|
|
|
|
+ },
|
|
|
|
|
+ isDashboard() {
|
|
|
|
|
+ const p = this.$route.path
|
|
|
|
|
+ return p === '/index' || p === '/' || p === ''
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ '$route.path'() {
|
|
|
|
|
+ this.syncActiveTopFromRoute()
|
|
|
|
|
+ },
|
|
|
|
|
+ topMenus: {
|
|
|
|
|
+ handler() {
|
|
|
|
|
+ this.syncActiveTopFromRoute()
|
|
|
|
|
+ },
|
|
|
|
|
+ immediate: true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeMount() {
|
|
|
|
|
+ window.addEventListener('resize', this.setVisibleNumber)
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ window.removeEventListener('resize', this.setVisibleNumber)
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ this.setVisibleNumber()
|
|
|
|
|
+ this.syncActiveTopFromRoute()
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ setVisibleNumber() {
|
|
|
|
|
+ const width = document.body.getBoundingClientRect().width / 3
|
|
|
|
|
+ this.visibleNumber = Math.max(4, parseInt(width / 85))
|
|
|
|
|
+ },
|
|
|
|
|
+ goHome() {
|
|
|
|
|
+ if (this.$route.path !== '/index') {
|
|
|
|
|
+ this.$router.push('/index').catch(() => {})
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ switchTopMenu(index) {
|
|
|
|
|
+ this.activeTopIndex = index
|
|
|
|
|
+ const menu = this.topMenus[index]
|
|
|
|
|
+ if (!menu) return
|
|
|
|
|
+ const key = this.resolveTopMenuKey(menu)
|
|
|
|
|
+ if (this.ishttp(key)) {
|
|
|
|
|
+ window.open(key, '_blank')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (key.indexOf('/redirect') !== -1) {
|
|
|
|
|
+ this.$router.push({ path: key.replace('/redirect', '') }).catch(() => {})
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const routes = this.activeRoutes(key)
|
|
|
|
|
+ const firstPath = this.getFirstRoutePath(routes)
|
|
|
|
|
+ if (firstPath && this.$route.path !== firstPath) {
|
|
|
|
|
+ this.$router.push(firstPath).catch(() => {})
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onMoreMenuCommand(index) {
|
|
|
|
|
+ this.switchTopMenu(index)
|
|
|
|
|
+ },
|
|
|
|
|
+ normPath(p) {
|
|
|
|
|
+ if (!p) return ''
|
|
|
|
|
+ const s = String(p)
|
|
|
|
|
+ if (this.ishttp(s)) return s
|
|
|
|
|
+ return s.startsWith('/') ? s : '/' + s
|
|
|
|
|
+ },
|
|
|
|
|
+ resolveTopMenuKey(menu) {
|
|
|
|
|
+ if (!menu) return ''
|
|
|
|
|
+ for (const router of this.topbarRouters) {
|
|
|
|
|
+ if (router.hidden) continue
|
|
|
|
|
+ if (this.normPath(router.path) === this.normPath(menu.path) && router.path !== '/') {
|
|
|
|
|
+ return router.path
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!router.children || !router.children.length) continue
|
|
|
|
|
+ const matched = router.children.some(child => {
|
|
|
|
|
+ if (child.hidden) return false
|
|
|
|
|
+ return child === menu || this.normPath(child.path) === this.normPath(menu.path)
|
|
|
|
|
+ })
|
|
|
|
|
+ if (matched) {
|
|
|
|
|
+ return router.path
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return menu.path
|
|
|
|
|
+ },
|
|
|
|
|
+ activeRoutes(key) {
|
|
|
|
|
+ const routes = []
|
|
|
|
|
+ const normalizedKey = this.normPath(key)
|
|
|
|
|
+ const childrenMenus = this.buildChildrenMenus()
|
|
|
|
|
+ childrenMenus.forEach(item => {
|
|
|
|
|
+ if (this.normPath(item.parentPath) === normalizedKey) {
|
|
|
|
|
+ routes.push(item)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ if (routes.length === 0) {
|
|
|
|
|
+ const router = this.topbarRouters.find(r => !r.hidden && this.normPath(r.path) === normalizedKey)
|
|
|
|
|
+ if (router && router.children) {
|
|
|
|
|
+ router.children.forEach(child => {
|
|
|
|
|
+ if (!child.hidden) routes.push(child)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (routes.length > 0) {
|
|
|
|
|
+ this.$store.commit('SET_SIDEBAR_ROUTERS', routes)
|
|
|
|
|
+ }
|
|
|
|
|
+ return routes
|
|
|
|
|
+ },
|
|
|
|
|
+ getFirstRoutePath(routes) {
|
|
|
|
|
+ if (!routes || !routes.length) return null
|
|
|
|
|
+ for (let i = 0; i < routes.length; i++) {
|
|
|
|
|
+ const route = routes[i]
|
|
|
|
|
+ if (route.hidden) continue
|
|
|
|
|
+ if (route.children && route.children.length) {
|
|
|
|
|
+ const childPath = this.getFirstRoutePath(route.children)
|
|
|
|
|
+ if (childPath) return childPath
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!route.path || this.ishttp(route.path)) continue
|
|
|
|
|
+ if (route.path.indexOf('/redirect') !== -1) {
|
|
|
|
|
+ return route.path.replace('/redirect', '')
|
|
|
|
|
+ }
|
|
|
|
|
+ if (route.path.startsWith('/')) {
|
|
|
|
|
+ return route.path
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null
|
|
|
|
|
+ },
|
|
|
|
|
+ buildChildrenMenus() {
|
|
|
|
|
+ const childrenMenus = []
|
|
|
|
|
+ this.topbarRouters.forEach(router => {
|
|
|
|
|
+ if (!router.children) return
|
|
|
|
|
+ for (const item in router.children) {
|
|
|
|
|
+ const child = router.children[item]
|
|
|
|
|
+ if (child.parentPath === undefined) {
|
|
|
|
|
+ if (router.path === '/') {
|
|
|
|
|
+ const seg = (child.path || '').replace(/^\//, '')
|
|
|
|
|
+ if (seg.indexOf('redirect/') !== 0) {
|
|
|
|
|
+ child.path = '/redirect/' + seg
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (!this.ishttp(child.path)) {
|
|
|
|
|
+ const seg = (child.path || '').replace(/^\//, '')
|
|
|
|
|
+ if (!child.path || !child.path.startsWith('/')) {
|
|
|
|
|
+ child.path = this.normPath(router.path) + '/' + seg
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ child.parentPath = router.path
|
|
|
|
|
+ }
|
|
|
|
|
+ childrenMenus.push(child)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ return constantRoutes.concat(childrenMenus)
|
|
|
|
|
+ },
|
|
|
|
|
+ syncActiveTopFromRoute() {
|
|
|
|
|
+ if (!this.topMenus.length) return
|
|
|
|
|
+ const path = this.$route.path
|
|
|
|
|
+ let topKey = ''
|
|
|
|
|
+ if (path.lastIndexOf('/') > 0) {
|
|
|
|
|
+ const tmp = path.substring(1)
|
|
|
|
|
+ topKey = '/' + tmp.substring(0, tmp.indexOf('/'))
|
|
|
|
|
+ } else if (path === '/index' || path === '/') {
|
|
|
|
|
+ topKey = '/'
|
|
|
|
|
+ }
|
|
|
|
|
+ let found = -1
|
|
|
|
|
+ this.topMenus.forEach((menu, index) => {
|
|
|
|
|
+ const key = this.resolveTopMenuKey(menu)
|
|
|
|
|
+ if (topKey && this.normPath(key) === this.normPath(topKey)) {
|
|
|
|
|
+ found = index
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ if (found >= 0) {
|
|
|
|
|
+ this.activeTopIndex = found
|
|
|
|
|
+ this.activeRoutes(this.resolveTopMenuKey(this.topMenus[found]))
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ ishttp(url) {
|
|
|
|
|
+ return url && (url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1)
|
|
|
|
|
+ },
|
|
|
|
|
+ handleCommand(cmd) {
|
|
|
|
|
+ if (cmd === 'logout') {
|
|
|
|
|
+ this.$confirm('确定注销并退出系统吗?', '提示', {
|
|
|
|
|
+ confirmButtonText: '确定',
|
|
|
|
|
+ cancelButtonText: '取消',
|
|
|
|
|
+ type: 'warning'
|
|
|
|
|
+ }).then(() => {
|
|
|
|
|
+ this.$store.dispatch('LogOut').then(() => {
|
|
|
|
|
+ location.href = '/index'
|
|
|
|
|
+ })
|
|
|
|
|
+ }).catch(() => {})
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.admin-layout {
|
|
|
|
|
+ height: 100vh;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ margin: 0 !important;
|
|
|
|
|
+ padding: 0 !important;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.top-nav {
|
|
|
|
|
+ height: 50px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 0 20px 0 0;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ border-bottom: 1px solid #e8e8e8;
|
|
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-logo {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ padding: 0 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.logo-img {
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ object-fit: contain;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.logo-icon {
|
|
|
|
|
+ width: 32px;
|
|
|
|
|
+ height: 32px;
|
|
|
|
|
+ background: linear-gradient(135deg, #409eff, #1890ff);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 17px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.logo-text {
|
|
|
|
|
+ font-size: 17px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.logo-home-badge {
|
|
|
|
|
+ width: 20px;
|
|
|
|
|
+ height: 20px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background: rgba(64, 158, 255, 0.1);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ margin-left: 4px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.logo-home-badge i {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-divider {
|
|
|
|
|
+ width: 1px;
|
|
|
|
|
+ height: 24px;
|
|
|
|
|
+ background: #e8e8e8;
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-menu {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+ padding: 0 18px;
|
|
|
|
|
+ height: 50px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ border-bottom: 2px solid transparent;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-item:hover {
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-item.active {
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ border-bottom-color: #409eff;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-item .svg-icon {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.user-area {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-left: auto;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ padding-left: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.header-tool {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 0 6px;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ color: #5a5e66;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.header-tool:hover {
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.user-info {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.user-avatar {
|
|
|
|
|
+ width: 30px;
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ background: linear-gradient(135deg, #409eff, #1890ff);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.user-role {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-user {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 5px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ padding: 4px 8px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-user:hover {
|
|
|
|
|
+ background: rgba(64, 158, 255, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-username {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dropdown-arrow {
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.main-container {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ margin: 0 !important;
|
|
|
|
|
+ padding: 0 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar {
|
|
|
|
|
+ width: 200px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ background: #fafbff;
|
|
|
|
|
+ border-right: 1px solid #e8e8e8;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar-dashboard {
|
|
|
|
|
+ background: #fafbff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.saas-sidebar-panel {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ width: 100% !important;
|
|
|
|
|
+ border-right: none !important;
|
|
|
|
|
+ background: transparent !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.dashboard-shortcuts {
|
|
|
|
|
+ padding: 16px 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shortcuts-title {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 7px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ padding: 0 4px 10px;
|
|
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shortcut-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ color: #555;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ margin-bottom: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shortcut-item:hover {
|
|
|
|
|
+ background: #ede9fe;
|
|
|
|
|
+ color: #4f46e5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.shortcut-item .svg-icon {
|
|
|
|
|
+ color: #8b5cf6;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content-area {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #f5f6fa;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content-area >>> .tags-view-container {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content-area >>> .app-main {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ padding: 16px 24px 24px;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+/* 侧栏菜单样式 — 与 adminUI AdminLayout 一致 */
|
|
|
|
|
+.sidebar >>> .sidebar-wrapper {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar >>> .sidebar-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 7px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ letter-spacing: 0.5px;
|
|
|
|
|
+ padding: 16px 16px 10px;
|
|
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
+ background: #fafbff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar >>> .sidebar-menu {
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ background-color: #fafbff !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar >>> .el-menu-item {
|
|
|
|
|
+ height: 50px !important;
|
|
|
|
|
+ line-height: 50px !important;
|
|
|
|
|
+ font-size: 14px !important;
|
|
|
|
|
+ padding-left: 24px !important;
|
|
|
|
|
+ color: #555 !important;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ margin-bottom: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar >>> .el-submenu__title {
|
|
|
|
|
+ height: 50px !important;
|
|
|
|
|
+ line-height: 50px !important;
|
|
|
|
|
+ font-size: 14px !important;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #555 !important;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ margin-bottom: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar >>> .el-menu-item .svg-icon,
|
|
|
|
|
+.sidebar >>> .el-submenu__title .svg-icon {
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+ color: #8b5cf6 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar >>> .el-submenu .el-menu-item {
|
|
|
|
|
+ padding-left: 44px !important;
|
|
|
|
|
+ height: 46px !important;
|
|
|
|
|
+ line-height: 46px !important;
|
|
|
|
|
+ font-size: 13px !important;
|
|
|
|
|
+ min-width: auto !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar >>> .el-menu-item.is-active {
|
|
|
|
|
+ background-color: #ede9fe !important;
|
|
|
|
|
+ color: #4f46e5 !important;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ border-left: 3px solid #4f46e5 !important;
|
|
|
|
|
+ padding-left: 21px !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar >>> .el-menu-item:hover,
|
|
|
|
|
+.sidebar >>> .el-submenu__title:hover {
|
|
|
|
|
+ background-color: #ede9fe !important;
|
|
|
|
|
+ color: #4f46e5 !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.sidebar >>> .el-submenu .el-menu-item:hover {
|
|
|
|
|
+ background-color: #ede9fe !important;
|
|
|
|
|
+ color: #4f46e5 !important;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|