index.vue 14 KB

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