index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. <script lang="ts" setup>
  2. import { useTabsStore } from '@/store/modules/tabs'
  3. import { useRoutesStore } from '@/store/modules/routes'
  4. import { useSettingsStore } from '@/store/modules/settings'
  5. import { handleActivePath, handleTabs } from '@/utils/routes'
  6. import { translate } from '@/i18n'
  7. import { VabRoute, VabRouteRecord } from '/#/router'
  8. defineProps({
  9. layout: {
  10. type: String,
  11. default: '',
  12. },
  13. })
  14. const $pub: any = inject('$pub')
  15. const route: VabRoute = useRoute()
  16. const router = useRouter()
  17. const settingsStore = useSettingsStore()
  18. const { theme } = storeToRefs(settingsStore)
  19. const routesStore = useRoutesStore()
  20. const { getRoutes: routes } = storeToRefs(routesStore)
  21. const tabsStore = useTabsStore()
  22. const { getVisitedRoutes: visitedRoutes } = storeToRefs(tabsStore)
  23. const {
  24. addVisitedRoute,
  25. delVisitedRoute,
  26. delOthersVisitedRoutes,
  27. delLeftVisitedRoutes,
  28. delRightVisitedRoutes,
  29. delAllVisitedRoutes,
  30. } = tabsStore
  31. const tabActive = ref('')
  32. const active = ref(false)
  33. const hoverRoute = ref()
  34. const visible = ref(false)
  35. const top = ref(0)
  36. const left = ref(0)
  37. const isActive = (path: string) => path === handleActivePath(route, true)
  38. const isNoCLosable = (tag: any) => tag.meta.noClosable
  39. const handleTabClick = (tab: any) => {
  40. if (!isActive(tab.name)) router.push(visitedRoutes.value[tab.index])
  41. }
  42. const handleVisibleChange = (val: boolean) => {
  43. active.value = val
  44. }
  45. const initNoCLosableTabs = (routes: VabRouteRecord[]) => {
  46. routes.forEach((_route) => {
  47. if (_route.meta.noClosable) addTabs(_route)
  48. if (_route.children) initNoCLosableTabs(_route.children)
  49. })
  50. }
  51. /**
  52. * 添加标签页
  53. * @param tag route
  54. * @returns {Promise<void>}
  55. */
  56. const addTabs = async (tag: VabRoute | VabRouteRecord) => {
  57. const tab = handleTabs(tag)
  58. if (tab) {
  59. await addVisitedRoute(tab)
  60. tabActive.value = tab.path
  61. }
  62. }
  63. /**
  64. * 根据原生路径删除标签中的标签
  65. * @param rawPath 原生路径
  66. * @returns {Promise<void>}
  67. */
  68. const handleTabRemove: any = async (rawPath: string) => {
  69. if (isActive(rawPath)) await toLastTab()
  70. await delVisitedRoute(rawPath)
  71. }
  72. const handleCommand = (command: string) => {
  73. switch (command) {
  74. case 'refreshThisTab':
  75. refreshThisTab()
  76. break
  77. case 'closeOthersTabs':
  78. closeOthersTabs()
  79. break
  80. case 'closeLeftTabs':
  81. closeLeftTabs()
  82. break
  83. case 'closeRightTabs':
  84. closeRightTabs()
  85. break
  86. case 'closeAllTabs':
  87. closeAllTabs()
  88. break
  89. }
  90. }
  91. /**
  92. * 刷新当前标签页
  93. * @returns {Promise<void>}
  94. */
  95. const refreshThisTab = () => {
  96. $pub('reload-router-view')
  97. }
  98. /**
  99. * 删除其他标签页
  100. * @returns {Promise<void>}
  101. */
  102. const closeOthersTabs = async () => {
  103. if (hoverRoute.value) {
  104. await router.push(hoverRoute.value)
  105. await delOthersVisitedRoutes(hoverRoute.value.path)
  106. } else await delOthersVisitedRoutes(handleActivePath(route, true))
  107. await closeMenu()
  108. }
  109. /**
  110. * 删除左侧标签页
  111. * @returns {Promise<void>}
  112. */
  113. const closeLeftTabs = async () => {
  114. if (hoverRoute.value) {
  115. await router.push(hoverRoute.value)
  116. await delLeftVisitedRoutes(hoverRoute.value.path)
  117. } else await delLeftVisitedRoutes(handleActivePath(route, true))
  118. await closeMenu()
  119. }
  120. /**
  121. * 删除右侧标签页
  122. * @returns {Promise<void>}
  123. */
  124. const closeRightTabs = async () => {
  125. if (hoverRoute.value) {
  126. await router.push(hoverRoute.value)
  127. await delRightVisitedRoutes(hoverRoute.value.path)
  128. } else await delRightVisitedRoutes(handleActivePath(route, true))
  129. await closeMenu()
  130. }
  131. /**
  132. * 删除所有标签页
  133. * @returns {Promise<void>}
  134. */
  135. const closeAllTabs = async () => {
  136. await delAllVisitedRoutes()
  137. await toLastTab()
  138. await closeMenu()
  139. }
  140. /**
  141. * 跳转最后一个标签页
  142. */
  143. const toLastTab = async () => {
  144. const latestView = visitedRoutes.value
  145. .filter((_: any) => _.path !== handleActivePath(route, true))
  146. .slice(-1)[0]
  147. if (latestView) await router.push(latestView)
  148. else await router.push('/')
  149. }
  150. const { x, y } = useMouse()
  151. const openMenu = () => {
  152. left.value = x.value
  153. top.value = y.value
  154. visible.value = true
  155. }
  156. const closeMenu = () => {
  157. visible.value = false
  158. hoverRoute.value = null
  159. }
  160. initNoCLosableTabs(routes.value)
  161. watch(
  162. () => route.fullPath,
  163. () => {
  164. addTabs(route)
  165. },
  166. {
  167. immediate: true,
  168. }
  169. )
  170. watchEffect(() => {
  171. if (visible.value) document.body.addEventListener('click', closeMenu)
  172. else document.body.removeEventListener('click', closeMenu)
  173. })
  174. </script>
  175. <template>
  176. <div class="vab-tabs">
  177. <vab-fold v-if="layout === 'common'" />
  178. <el-tabs
  179. v-model="tabActive"
  180. class="vab-tabs-content"
  181. :class="{
  182. ['vab-tabs-content-' + theme.tabsBarStyle]: true,
  183. }"
  184. type="card"
  185. @tab-click="handleTabClick"
  186. @tab-remove="handleTabRemove"
  187. >
  188. <el-tab-pane
  189. v-for="item in visitedRoutes"
  190. :key="item.path"
  191. :closable="!isNoCLosable(item)"
  192. :name="item.path"
  193. >
  194. <template #label>
  195. <span style="display: inline-block" @contextmenu.prevent="openMenu">
  196. <template v-if="theme.showTabsIcon">
  197. <vab-icon
  198. v-if="item.meta && item.meta.icon"
  199. :icon="item.meta.icon"
  200. :is-custom-svg="item.meta.isCustomSvg"
  201. />
  202. <!-- 如果没有图标那么取第二级的图标 -->
  203. <vab-icon v-else :icon="item.parentIcon" />
  204. </template>
  205. <span v-if="item.meta && item.meta.title">
  206. {{ translate(item.meta.title) }}
  207. </span>
  208. </span>
  209. </template>
  210. </el-tab-pane>
  211. </el-tabs>
  212. <el-dropdown
  213. placement="bottom-end"
  214. popper-class="vab-tabs-more-dropdown"
  215. @command="handleCommand"
  216. @visible-change="handleVisibleChange"
  217. >
  218. <span class="vab-tabs-more" :class="{ 'vab-tabs-more-active': active }">
  219. <span class="vab-tabs-more-icon">
  220. <i class="box box-t"></i>
  221. <i class="box box-b"></i>
  222. </span>
  223. </span>
  224. <template #dropdown>
  225. <el-dropdown-menu class="tabs-more">
  226. <el-dropdown-item command="refreshThisTab">
  227. <vab-icon icon="refresh-line" />
  228. <span>
  229. {{ translate('刷新') }}
  230. </span>
  231. </el-dropdown-item>
  232. <el-dropdown-item command="closeOthersTabs">
  233. <vab-icon icon="close-line" />
  234. <span>
  235. {{ translate('关闭其他') }}
  236. </span>
  237. </el-dropdown-item>
  238. <el-dropdown-item command="closeLeftTabs">
  239. <vab-icon icon="arrow-left-line" />
  240. <span>
  241. {{ translate('关闭左侧') }}
  242. </span>
  243. </el-dropdown-item>
  244. <el-dropdown-item command="closeRightTabs">
  245. <vab-icon icon="arrow-right-line" />
  246. <span>
  247. {{ translate('关闭右侧') }}
  248. </span>
  249. </el-dropdown-item>
  250. <el-dropdown-item command="closeAllTabs">
  251. <vab-icon icon="close-line" />
  252. <span>
  253. {{ translate('关闭全部') }}
  254. </span>
  255. </el-dropdown-item>
  256. </el-dropdown-menu>
  257. </template>
  258. </el-dropdown>
  259. <ul
  260. v-if="visible"
  261. class="contextmenu el-dropdown-menu"
  262. :style="{ left: left + 'px', top: top + 'px' }"
  263. >
  264. <li class="el-dropdown-menu__item" @click="refreshThisTab">
  265. <vab-icon icon="refresh-line" />
  266. <span>{{ translate('刷新') }}</span>
  267. </li>
  268. <li
  269. class="el-dropdown-menu__item"
  270. :class="{ 'is-disabled': visitedRoutes.length === 1 }"
  271. @click="closeOthersTabs"
  272. >
  273. <vab-icon icon="close-line" />
  274. <span>{{ translate('关闭其他') }}</span>
  275. </li>
  276. <li
  277. class="el-dropdown-menu__item"
  278. :class="{ 'is-disabled': !visitedRoutes.indexOf(hoverRoute) }"
  279. @click="closeLeftTabs"
  280. >
  281. <vab-icon icon="arrow-left-line" />
  282. <span>{{ translate('关闭左侧') }}</span>
  283. </li>
  284. <li
  285. class="el-dropdown-menu__item"
  286. :class="{
  287. 'is-disabled':
  288. visitedRoutes.indexOf(hoverRoute) === visitedRoutes.length - 1,
  289. }"
  290. @click="closeRightTabs"
  291. >
  292. <vab-icon icon="arrow-right-line" />
  293. <span>{{ translate('关闭右侧') }}</span>
  294. </li>
  295. <li class="el-dropdown-menu__item" @click="closeAllTabs">
  296. <vab-icon icon="close-line" />
  297. <span>{{ translate('关闭全部') }}</span>
  298. </li>
  299. </ul>
  300. </div>
  301. </template>
  302. <style lang="scss" scoped>
  303. @use 'sass:math';
  304. .vab-tabs {
  305. position: relative;
  306. box-sizing: border-box;
  307. display: flex;
  308. align-content: center;
  309. align-items: center;
  310. justify-content: space-between;
  311. min-height: $base-tabs-height;
  312. padding-right: $base-padding;
  313. padding-left: $base-padding;
  314. user-select: none;
  315. background: var(--el-color-white);
  316. border-top: 1px solid #f6f6f6;
  317. :deep() {
  318. .fold-unfold {
  319. margin-right: $base-margin;
  320. }
  321. [class*='ri'] {
  322. margin-right: 3px;
  323. }
  324. .vab-icon {
  325. vertical-align: -3px;
  326. }
  327. }
  328. &-content {
  329. width: calc(100% - 40px);
  330. &-card {
  331. height: $base-tag-item-height;
  332. :deep() {
  333. .el-tabs__nav-next,
  334. .el-tabs__nav-prev {
  335. height: $base-tag-item-height;
  336. line-height: $base-tag-item-height;
  337. }
  338. .el-tabs__header {
  339. border-bottom: 0;
  340. .el-tabs__nav {
  341. border: 0;
  342. }
  343. .el-tabs__item {
  344. box-sizing: border-box;
  345. height: $base-tag-item-height;
  346. margin-right: 5px;
  347. line-height: $base-tag-item-height;
  348. border: 1px solid $base-border-color !important;
  349. border-radius: $base-border-radius;
  350. transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
  351. &.is-active {
  352. color: var(--el-color-primary);
  353. background: var(--el-color-primary-light-9);
  354. border: 1px solid var(--el-color-primary);
  355. outline: none;
  356. }
  357. &:hover {
  358. border: 1px solid var(--el-color-primary);
  359. }
  360. }
  361. }
  362. }
  363. }
  364. &-smart {
  365. height: $base-tag-item-height;
  366. :deep() {
  367. .el-tabs__nav-next,
  368. .el-tabs__nav-prev {
  369. height: $base-tag-item-height;
  370. line-height: $base-tag-item-height;
  371. }
  372. .el-tabs__header {
  373. border-bottom: 0;
  374. .el-tabs__nav {
  375. border: 0;
  376. }
  377. .el-tabs__item {
  378. height: $base-tag-item-height;
  379. margin-right: 5px;
  380. line-height: $base-tag-item-height;
  381. border: 0;
  382. outline: none;
  383. transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
  384. &.is-active {
  385. background: var(--el-color-primary-light-9);
  386. outline: none;
  387. &:after {
  388. width: 100%;
  389. transition: $base-transition;
  390. }
  391. }
  392. &:after {
  393. position: absolute;
  394. bottom: 0;
  395. left: 0;
  396. width: 0;
  397. height: 2px;
  398. content: '';
  399. background-color: var(--el-color-primary);
  400. transition: $base-transition;
  401. }
  402. &:hover {
  403. background: var(--el-color-primary-light-9);
  404. &:after {
  405. width: 100%;
  406. transition: $base-transition;
  407. }
  408. }
  409. }
  410. }
  411. }
  412. }
  413. &-smooth {
  414. height: $base-tag-item-height + 4;
  415. :deep() {
  416. .el-tabs__nav-next,
  417. .el-tabs__nav-prev {
  418. height: $base-tag-item-height + 14;
  419. line-height: $base-tag-item-height + 14;
  420. }
  421. .el-tabs__header {
  422. border-bottom: 0;
  423. .el-tabs__nav {
  424. border: 0;
  425. }
  426. .el-tabs__item {
  427. height: $base-tag-item-height + 4;
  428. padding: 0 30px 0 30px;
  429. margin-top: #{math.div(
  430. $base-tabs-height - $base-tag-item-height - 4.1,
  431. 2
  432. )};
  433. margin-right: -18px;
  434. line-height: $base-tag-item-height + 4;
  435. text-align: center;
  436. border: 0;
  437. outline: none;
  438. transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
  439. &.is-closable:hover {
  440. padding: 0 30px 0 30px;
  441. }
  442. &.is-active {
  443. padding: 0 30px 0 30px;
  444. color: var(--el-color-primary);
  445. background: var(--el-color-primary-light-9);
  446. outline: none;
  447. mask: url('~@/assets/tabs_images/vab-tab.png');
  448. mask-size: 100% 100%;
  449. &:hover {
  450. padding: 0 30px 0 30px;
  451. color: var(--el-color-primary);
  452. background: var(--el-color-primary-light-9);
  453. mask: url('~@/assets/tabs_images/vab-tab.png');
  454. mask-size: 100% 100%;
  455. }
  456. &.is-closable {
  457. padding: 0 30px 0 30px;
  458. }
  459. }
  460. &:hover {
  461. padding: 0 30px 0 30px;
  462. color: var(--el-color-black);
  463. background: #dee1e6;
  464. mask: url('~@/assets/tabs_images/vab-tab.png');
  465. mask-size: 100% 100%;
  466. }
  467. }
  468. }
  469. }
  470. }
  471. }
  472. .contextmenu {
  473. position: fixed;
  474. top: 0;
  475. left: 0;
  476. z-index: 10;
  477. .el-dropdown-menu__item:hover {
  478. color: var(--el-color-primary);
  479. background: var(--el-color-primary-light-9);
  480. }
  481. }
  482. &-more {
  483. position: relative;
  484. box-sizing: border-box;
  485. display: block;
  486. text-align: left;
  487. &-active,
  488. &:hover {
  489. &:after {
  490. position: absolute;
  491. bottom: 0;
  492. left: 0;
  493. height: 0;
  494. content: '';
  495. }
  496. .vab-tabs-more-icon {
  497. transform: rotate(90deg);
  498. .box-t {
  499. &:before {
  500. transform: rotate(45deg);
  501. }
  502. }
  503. .box:before,
  504. .box:after {
  505. background: var(--el-color-primary);
  506. }
  507. }
  508. }
  509. &-icon {
  510. display: inline-block;
  511. color: #9a9a9a;
  512. cursor: pointer;
  513. transition: transform 0.3s ease-out;
  514. .box {
  515. position: relative;
  516. display: block;
  517. width: 14px;
  518. height: 8px;
  519. &:before {
  520. position: absolute;
  521. top: 2px;
  522. left: 0;
  523. width: 6px;
  524. height: 6px;
  525. content: '';
  526. background: #9a9a9a;
  527. }
  528. &:after {
  529. position: absolute;
  530. top: 2px;
  531. left: 8px;
  532. width: 6px;
  533. height: 6px;
  534. content: '';
  535. background: #9a9a9a;
  536. }
  537. }
  538. .box-t {
  539. &:before {
  540. transition: transform 0.3s ease-out 0.3s;
  541. }
  542. }
  543. }
  544. }
  545. }
  546. </style>