index.vue 15 KB

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