AdminLayout.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. <template>
  2. <div class="admin-layout">
  3. <header class="top-nav">
  4. <div class="nav-logo" @click="goHome" title="返回首页">
  5. <img v-if="logImg" :src="logImg" class="logo-img" alt="" />
  6. <div v-else class="logo-icon">
  7. <i class="el-icon-s-platform" />
  8. </div>
  9. <span class="logo-text">{{ title }}</span>
  10. <div class="logo-home-badge" @click.stop="goHome">
  11. <i class="el-icon-s-home" />
  12. </div>
  13. </div>
  14. <div class="nav-divider" />
  15. <nav class="nav-menu">
  16. <div
  17. v-for="(menu, index) in visibleTopMenus"
  18. :key="menu.path + index"
  19. class="nav-item"
  20. :class="{ active: activeTopIndex === index }"
  21. @click="switchTopMenu(index)"
  22. >
  23. <svg-icon v-if="menu.meta && menu.meta.icon" :icon-class="menu.meta.icon" />
  24. <span>{{ menu.meta.title }}</span>
  25. </div>
  26. <el-dropdown v-if="overflowTopMenus.length" trigger="click" @command="onMoreMenuCommand">
  27. <div class="nav-item nav-item-more" :class="{ active: activeTopIndex >= visibleTopMenus.length }">
  28. <span>更多菜单</span>
  29. <i class="el-icon-arrow-down el-icon--right" />
  30. </div>
  31. <el-dropdown-menu slot="dropdown">
  32. <el-dropdown-item
  33. v-for="(menu, idx) in overflowTopMenus"
  34. :key="menu.path + idx"
  35. :command="visibleTopMenus.length + idx"
  36. >
  37. <svg-icon v-if="menu.meta && menu.meta.icon" :icon-class="menu.meta.icon" />
  38. {{ menu.meta.title }}
  39. </el-dropdown-item>
  40. </el-dropdown-menu>
  41. </el-dropdown>
  42. </nav>
  43. <div class="user-area">
  44. <template v-if="device !== 'mobile'">
  45. <search class="header-tool" />
  46. <screenfull class="header-tool" />
  47. <el-tooltip content="布局大小" effect="dark" placement="bottom">
  48. <size-select class="header-tool" />
  49. </el-tooltip>
  50. </template>
  51. <div class="user-info">
  52. <div class="user-avatar">{{ userInitial }}</div>
  53. <span class="user-role">管理员</span>
  54. </div>
  55. <el-dropdown trigger="click" @command="handleCommand">
  56. <div class="nav-user">
  57. <span class="nav-username">{{ name }}</span>
  58. <i class="el-icon-arrow-down dropdown-arrow" />
  59. </div>
  60. <el-dropdown-menu slot="dropdown">
  61. <router-link to="/user/profile">
  62. <el-dropdown-item command="profile">
  63. <i class="el-icon-user" /> 个人中心
  64. </el-dropdown-item>
  65. </router-link>
  66. <el-dropdown-item command="logout" divided>
  67. <i class="el-icon-switch-button" /> 退出登录
  68. </el-dropdown-item>
  69. </el-dropdown-menu>
  70. </el-dropdown>
  71. </div>
  72. </header>
  73. <div class="main-container">
  74. <aside class="sidebar" :class="{ 'sidebar-dashboard': isDashboard }">
  75. <div v-if="isDashboard" class="dashboard-shortcuts">
  76. <div class="shortcuts-title">
  77. <i class="el-icon-s-grid" />
  78. <span>快捷入口</span>
  79. </div>
  80. <div
  81. v-for="(menu, index) in topMenus"
  82. :key="menu.path + index"
  83. class="shortcut-item"
  84. @click="switchTopMenu(index)"
  85. >
  86. <svg-icon v-if="menu.meta && menu.meta.icon" :icon-class="menu.meta.icon" />
  87. <span>{{ menu.meta.title }}</span>
  88. </div>
  89. </div>
  90. <saas-sidebar v-else class="saas-sidebar-panel" />
  91. </aside>
  92. <main class="content-area">
  93. <tags-view v-if="needTagsView" />
  94. <app-main />
  95. </main>
  96. </div>
  97. </div>
  98. </template>
  99. <script>
  100. import { mapGetters, mapState } from 'vuex'
  101. import { constantRoutes } from '@/router'
  102. import SaasSidebar from './components/Sidebar/index.vue'
  103. import TagsView from './components/TagsView/index.vue'
  104. import AppMain from './components/AppMain'
  105. import Search from '@/components/HeaderSearch'
  106. import Screenfull from '@/components/Screenfull'
  107. import SizeSelect from '@/components/SizeSelect'
  108. export default {
  109. name: 'AdminLayout',
  110. components: {
  111. SaasSidebar,
  112. TagsView,
  113. AppMain,
  114. Search,
  115. Screenfull,
  116. SizeSelect
  117. },
  118. data() {
  119. return {
  120. activeTopIndex: 0,
  121. visibleNumber: 12
  122. }
  123. },
  124. computed: {
  125. ...mapGetters(['name', 'device', 'sidebarRouters']),
  126. ...mapState({
  127. needTagsView: state => state.settings.tagsView,
  128. topbarRouters: state => state.permission.topbarRouters
  129. }),
  130. title() {
  131. return process.env.VUE_APP_TITLE_INDEX || '云联融智SAAS'
  132. },
  133. logImg() {
  134. try {
  135. return require(process.env.VUE_APP_LOG_URL)
  136. } catch (e) {
  137. return ''
  138. }
  139. },
  140. userInitial() {
  141. const name = this.name || '管'
  142. return name.charAt(0).toUpperCase()
  143. },
  144. topMenus() {
  145. const list = []
  146. this.topbarRouters.forEach(menu => {
  147. if (menu.hidden !== true) {
  148. if (menu.path === '/') {
  149. list.push(menu.children[0])
  150. } else {
  151. list.push(menu)
  152. }
  153. }
  154. })
  155. return list
  156. },
  157. visibleTopMenus() {
  158. return this.topMenus.slice(0, this.visibleNumber)
  159. },
  160. overflowTopMenus() {
  161. return this.topMenus.slice(this.visibleNumber)
  162. },
  163. isDashboard() {
  164. const p = this.$route.path
  165. return p === '/index' || p === '/' || p === ''
  166. }
  167. },
  168. watch: {
  169. '$route.path'() {
  170. this.syncActiveTopFromRoute()
  171. },
  172. topMenus: {
  173. handler() {
  174. this.syncActiveTopFromRoute()
  175. },
  176. immediate: true
  177. }
  178. },
  179. beforeMount() {
  180. window.addEventListener('resize', this.setVisibleNumber)
  181. },
  182. beforeDestroy() {
  183. window.removeEventListener('resize', this.setVisibleNumber)
  184. },
  185. mounted() {
  186. this.setVisibleNumber()
  187. this.syncActiveTopFromRoute()
  188. },
  189. methods: {
  190. setVisibleNumber() {
  191. const width = document.body.getBoundingClientRect().width / 3
  192. this.visibleNumber = Math.max(4, parseInt(width / 85))
  193. },
  194. goHome() {
  195. if (this.$route.path !== '/index') {
  196. this.$router.push('/index').catch(() => {})
  197. }
  198. },
  199. switchTopMenu(index) {
  200. this.activeTopIndex = index
  201. const menu = this.topMenus[index]
  202. if (!menu) return
  203. const key = this.resolveTopMenuKey(menu)
  204. if (this.ishttp(key)) {
  205. window.open(key, '_blank')
  206. return
  207. }
  208. if (key.indexOf('/redirect') !== -1) {
  209. this.$router.push({ path: key.replace('/redirect', '') }).catch(() => {})
  210. return
  211. }
  212. const routes = this.activeRoutes(key)
  213. const firstPath = this.getFirstRoutePath(routes)
  214. if (firstPath && this.$route.path !== firstPath) {
  215. this.$router.push(firstPath).catch(() => {})
  216. }
  217. },
  218. onMoreMenuCommand(index) {
  219. this.switchTopMenu(index)
  220. },
  221. normPath(p) {
  222. if (!p) return ''
  223. const s = String(p)
  224. if (this.ishttp(s)) return s
  225. return s.startsWith('/') ? s : '/' + s
  226. },
  227. resolveTopMenuKey(menu) {
  228. if (!menu) return ''
  229. for (const router of this.topbarRouters) {
  230. if (router.hidden) continue
  231. if (this.normPath(router.path) === this.normPath(menu.path) && router.path !== '/') {
  232. return router.path
  233. }
  234. if (!router.children || !router.children.length) continue
  235. const matched = router.children.some(child => {
  236. if (child.hidden) return false
  237. return child === menu || this.normPath(child.path) === this.normPath(menu.path)
  238. })
  239. if (matched) {
  240. return router.path
  241. }
  242. }
  243. return menu.path
  244. },
  245. activeRoutes(key) {
  246. const routes = []
  247. const normalizedKey = this.normPath(key)
  248. const childrenMenus = this.buildChildrenMenus()
  249. childrenMenus.forEach(item => {
  250. if (this.normPath(item.parentPath) === normalizedKey) {
  251. routes.push(item)
  252. }
  253. })
  254. if (routes.length === 0) {
  255. const router = this.topbarRouters.find(r => !r.hidden && this.normPath(r.path) === normalizedKey)
  256. if (router && router.children) {
  257. router.children.forEach(child => {
  258. if (!child.hidden) routes.push(child)
  259. })
  260. }
  261. }
  262. if (routes.length > 0) {
  263. this.$store.commit('SET_SIDEBAR_ROUTERS', routes)
  264. }
  265. return routes
  266. },
  267. getFirstRoutePath(routes) {
  268. if (!routes || !routes.length) return null
  269. for (let i = 0; i < routes.length; i++) {
  270. const route = routes[i]
  271. if (route.hidden) continue
  272. if (route.children && route.children.length) {
  273. const childPath = this.getFirstRoutePath(route.children)
  274. if (childPath) return childPath
  275. }
  276. if (!route.path || this.ishttp(route.path)) continue
  277. if (route.path.indexOf('/redirect') !== -1) {
  278. return route.path.replace('/redirect', '')
  279. }
  280. if (route.path.startsWith('/')) {
  281. return route.path
  282. }
  283. }
  284. return null
  285. },
  286. buildChildrenMenus() {
  287. const childrenMenus = []
  288. this.topbarRouters.forEach(router => {
  289. if (!router.children) return
  290. for (const item in router.children) {
  291. const child = router.children[item]
  292. if (child.parentPath === undefined) {
  293. if (router.path === '/') {
  294. const seg = (child.path || '').replace(/^\//, '')
  295. if (seg.indexOf('redirect/') !== 0) {
  296. child.path = '/redirect/' + seg
  297. }
  298. } else if (!this.ishttp(child.path)) {
  299. const seg = (child.path || '').replace(/^\//, '')
  300. if (!child.path || !child.path.startsWith('/')) {
  301. child.path = this.normPath(router.path) + '/' + seg
  302. }
  303. }
  304. child.parentPath = router.path
  305. }
  306. childrenMenus.push(child)
  307. }
  308. })
  309. return constantRoutes.concat(childrenMenus)
  310. },
  311. syncActiveTopFromRoute() {
  312. if (!this.topMenus.length) return
  313. const path = this.$route.path
  314. let topKey = ''
  315. if (path.lastIndexOf('/') > 0) {
  316. const tmp = path.substring(1)
  317. topKey = '/' + tmp.substring(0, tmp.indexOf('/'))
  318. } else if (path === '/index' || path === '/') {
  319. topKey = '/'
  320. }
  321. let found = -1
  322. this.topMenus.forEach((menu, index) => {
  323. const key = this.resolveTopMenuKey(menu)
  324. if (topKey && this.normPath(key) === this.normPath(topKey)) {
  325. found = index
  326. }
  327. })
  328. if (found >= 0) {
  329. this.activeTopIndex = found
  330. this.activeRoutes(this.resolveTopMenuKey(this.topMenus[found]))
  331. }
  332. },
  333. ishttp(url) {
  334. return url && (url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1)
  335. },
  336. handleCommand(cmd) {
  337. if (cmd === 'logout') {
  338. this.$confirm('确定注销并退出系统吗?', '提示', {
  339. confirmButtonText: '确定',
  340. cancelButtonText: '取消',
  341. type: 'warning'
  342. }).then(() => {
  343. this.$store.dispatch('LogOut').then(() => {
  344. location.href = '/index'
  345. })
  346. }).catch(() => {})
  347. }
  348. }
  349. }
  350. }
  351. </script>
  352. <style scoped>
  353. .admin-layout {
  354. height: 100vh;
  355. display: flex;
  356. flex-direction: column;
  357. overflow: hidden;
  358. margin: 0 !important;
  359. padding: 0 !important;
  360. position: relative;
  361. }
  362. .top-nav {
  363. height: 50px;
  364. background: #fff;
  365. display: flex;
  366. align-items: center;
  367. padding: 0 20px 0 0;
  368. flex-shrink: 0;
  369. z-index: 100;
  370. border-bottom: 1px solid #e8e8e8;
  371. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  372. }
  373. .nav-logo {
  374. display: flex;
  375. align-items: center;
  376. gap: 10px;
  377. cursor: pointer;
  378. margin-right: 8px;
  379. flex-shrink: 0;
  380. padding: 0 16px;
  381. }
  382. .logo-img {
  383. width: 32px;
  384. height: 32px;
  385. object-fit: contain;
  386. flex-shrink: 0;
  387. }
  388. .logo-icon {
  389. width: 32px;
  390. height: 32px;
  391. background: linear-gradient(135deg, #409eff, #1890ff);
  392. border-radius: 8px;
  393. display: flex;
  394. align-items: center;
  395. justify-content: center;
  396. color: #fff;
  397. font-size: 17px;
  398. flex-shrink: 0;
  399. }
  400. .logo-text {
  401. font-size: 17px;
  402. font-weight: 600;
  403. color: #303133;
  404. white-space: nowrap;
  405. }
  406. .logo-home-badge {
  407. width: 20px;
  408. height: 20px;
  409. border-radius: 4px;
  410. background: rgba(64, 158, 255, 0.1);
  411. display: flex;
  412. align-items: center;
  413. justify-content: center;
  414. margin-left: 4px;
  415. flex-shrink: 0;
  416. }
  417. .logo-home-badge i {
  418. font-size: 13px;
  419. color: #409eff;
  420. }
  421. .nav-divider {
  422. width: 1px;
  423. height: 24px;
  424. background: #e8e8e8;
  425. margin-right: 8px;
  426. flex-shrink: 0;
  427. }
  428. .nav-menu {
  429. display: flex;
  430. align-items: center;
  431. flex: 1;
  432. overflow: hidden;
  433. min-width: 0;
  434. }
  435. .nav-item {
  436. display: flex;
  437. align-items: center;
  438. gap: 6px;
  439. padding: 0 18px;
  440. height: 50px;
  441. cursor: pointer;
  442. color: #606266;
  443. font-size: 14px;
  444. white-space: nowrap;
  445. flex-shrink: 0;
  446. border-bottom: 2px solid transparent;
  447. box-sizing: border-box;
  448. }
  449. .nav-item:hover {
  450. color: #409eff;
  451. }
  452. .nav-item.active {
  453. color: #409eff;
  454. border-bottom-color: #409eff;
  455. font-weight: 500;
  456. }
  457. .nav-item .svg-icon {
  458. font-size: 16px;
  459. }
  460. .user-area {
  461. display: flex;
  462. align-items: center;
  463. gap: 8px;
  464. margin-left: auto;
  465. flex-shrink: 0;
  466. padding-left: 12px;
  467. }
  468. .header-tool {
  469. display: inline-flex;
  470. align-items: center;
  471. justify-content: center;
  472. padding: 0 6px;
  473. font-size: 18px;
  474. color: #5a5e66;
  475. cursor: pointer;
  476. }
  477. .header-tool:hover {
  478. color: #409eff;
  479. }
  480. .user-info {
  481. display: flex;
  482. align-items: center;
  483. gap: 8px;
  484. }
  485. .user-avatar {
  486. width: 30px;
  487. height: 30px;
  488. border-radius: 8px;
  489. background: linear-gradient(135deg, #409eff, #1890ff);
  490. color: #fff;
  491. display: flex;
  492. align-items: center;
  493. justify-content: center;
  494. font-size: 13px;
  495. font-weight: 600;
  496. }
  497. .user-role {
  498. font-size: 13px;
  499. color: #909399;
  500. }
  501. .nav-user {
  502. display: flex;
  503. align-items: center;
  504. gap: 5px;
  505. cursor: pointer;
  506. padding: 4px 8px;
  507. border-radius: 6px;
  508. }
  509. .nav-user:hover {
  510. background: rgba(64, 158, 255, 0.08);
  511. }
  512. .nav-username {
  513. font-size: 14px;
  514. color: #303133;
  515. }
  516. .dropdown-arrow {
  517. color: #909399;
  518. font-size: 12px;
  519. }
  520. .main-container {
  521. flex: 1;
  522. display: flex;
  523. overflow: hidden;
  524. margin: 0 !important;
  525. padding: 0 !important;
  526. }
  527. .sidebar {
  528. width: 200px;
  529. flex-shrink: 0;
  530. background: #fafbff;
  531. border-right: 1px solid #e8e8e8;
  532. overflow: hidden;
  533. display: flex;
  534. flex-direction: column;
  535. }
  536. .sidebar-dashboard {
  537. background: #fafbff;
  538. }
  539. .saas-sidebar-panel {
  540. flex: 1;
  541. min-height: 0;
  542. width: 100% !important;
  543. border-right: none !important;
  544. background: transparent !important;
  545. }
  546. .dashboard-shortcuts {
  547. padding: 16px 12px;
  548. }
  549. .shortcuts-title {
  550. display: flex;
  551. align-items: center;
  552. gap: 7px;
  553. font-size: 12px;
  554. font-weight: 600;
  555. color: #999;
  556. padding: 0 4px 10px;
  557. border-bottom: 1px solid #f0f0f0;
  558. margin-bottom: 8px;
  559. }
  560. .shortcut-item {
  561. display: flex;
  562. align-items: center;
  563. gap: 10px;
  564. padding: 10px 12px;
  565. border-radius: 8px;
  566. cursor: pointer;
  567. color: #555;
  568. font-size: 14px;
  569. margin-bottom: 2px;
  570. }
  571. .shortcut-item:hover {
  572. background: #ede9fe;
  573. color: #4f46e5;
  574. }
  575. .shortcut-item .svg-icon {
  576. color: #8b5cf6;
  577. }
  578. .content-area {
  579. flex: 1;
  580. display: flex;
  581. flex-direction: column;
  582. overflow: hidden;
  583. background: #f5f6fa;
  584. min-width: 0;
  585. }
  586. .content-area >>> .tags-view-container {
  587. flex-shrink: 0;
  588. }
  589. .content-area >>> .app-main {
  590. flex: 1;
  591. overflow: auto;
  592. padding: 16px 24px 24px;
  593. }
  594. </style>
  595. <style scoped>
  596. /* 侧栏菜单样式 — 与 adminUI AdminLayout 一致 */
  597. .sidebar >>> .sidebar-wrapper {
  598. width: 100%;
  599. height: 100%;
  600. background: transparent;
  601. border: none;
  602. }
  603. .sidebar >>> .sidebar-header {
  604. display: flex;
  605. align-items: center;
  606. gap: 7px;
  607. font-size: 12px;
  608. font-weight: 600;
  609. color: #999;
  610. letter-spacing: 0.5px;
  611. padding: 16px 16px 10px;
  612. border-bottom: 1px solid #f0f0f0;
  613. background: #fafbff;
  614. }
  615. .sidebar >>> .sidebar-menu {
  616. border: none;
  617. padding: 8px 12px;
  618. background-color: #fafbff !important;
  619. }
  620. .sidebar >>> .el-menu-item {
  621. height: 50px !important;
  622. line-height: 50px !important;
  623. font-size: 14px !important;
  624. padding-left: 24px !important;
  625. color: #555 !important;
  626. border-radius: 8px;
  627. margin-bottom: 2px;
  628. }
  629. .sidebar >>> .el-submenu__title {
  630. height: 50px !important;
  631. line-height: 50px !important;
  632. font-size: 14px !important;
  633. font-weight: 500;
  634. color: #555 !important;
  635. border-radius: 8px;
  636. margin-bottom: 2px;
  637. }
  638. .sidebar >>> .el-menu-item .svg-icon,
  639. .sidebar >>> .el-submenu__title .svg-icon {
  640. margin-right: 8px;
  641. color: #8b5cf6 !important;
  642. }
  643. .sidebar >>> .el-submenu .el-menu-item {
  644. padding-left: 44px !important;
  645. height: 46px !important;
  646. line-height: 46px !important;
  647. font-size: 13px !important;
  648. min-width: auto !important;
  649. }
  650. .sidebar >>> .el-menu-item.is-active {
  651. background-color: #ede9fe !important;
  652. color: #4f46e5 !important;
  653. font-weight: 600;
  654. border-left: 3px solid #4f46e5 !important;
  655. padding-left: 21px !important;
  656. }
  657. .sidebar >>> .el-menu-item:hover,
  658. .sidebar >>> .el-submenu__title:hover {
  659. background-color: #ede9fe !important;
  660. color: #4f46e5 !important;
  661. }
  662. .sidebar >>> .el-submenu .el-menu-item:hover {
  663. background-color: #ede9fe !important;
  664. color: #4f46e5 !important;
  665. }
  666. </style>