潘志宝
9 天以前 273cb85713ed1d194eb4cc245daf29cd33be89e7
提交 | 用户 | 时间
820397 1 <script lang="ts" setup>
3e359e 2 import { computed, nextTick, onMounted, ref, unref, watch } from 'vue'
820397 3 import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
3e359e 4 import { useRouter } from 'vue-router'
820397 5 import { usePermissionStore } from '@/store/modules/permission'
H 6 import { useTagsViewStore } from '@/store/modules/tagsView'
7 import { useAppStore } from '@/store/modules/app'
8 import { useI18n } from '@/hooks/web/useI18n'
9 import { filterAffixTags } from './helper'
10 import { ContextMenu, ContextMenuExpose } from '@/layout/components/ContextMenu'
11 import { useDesign } from '@/hooks/web/useDesign'
12 import { useTemplateRefsList } from '@vueuse/core'
13 import { ElScrollbar } from 'element-plus'
14 import { useScrollTo } from '@/hooks/event/useScrollTo'
15
16 const { getPrefixCls } = useDesign()
17
18 const prefixCls = getPrefixCls('tags-view')
19
20 const { t } = useI18n()
21
22 const { currentRoute, push, replace } = useRouter()
23
24 const permissionStore = usePermissionStore()
25
26 const routers = computed(() => permissionStore.getRouters)
27
28 const tagsViewStore = useTagsViewStore()
29
30 const visitedViews = computed(() => tagsViewStore.getVisitedViews)
31
32 const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
33
34 const appStore = useAppStore()
3e359e 35
H 36 const tagsViewImmerse = computed(() => appStore.getTagsViewImmerse)
820397 37
H 38 const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
39
40 const isDark = computed(() => appStore.getIsDark)
41
42 // 初始化tag
43 const initTags = () => {
44   affixTagArr.value = filterAffixTags(unref(routers))
45   for (const tag of unref(affixTagArr)) {
46     // Must have tag name
47     if (tag.name) {
48       tagsViewStore.addVisitedView(tag)
49     }
50   }
51 }
52
53 const selectedTag = ref<RouteLocationNormalizedLoaded>()
54
55 // 新增tag
56 const addTags = () => {
57   const { name } = unref(currentRoute)
58   if (name) {
59     selectedTag.value = unref(currentRoute)
60     tagsViewStore.addView(unref(currentRoute))
61   }
62   return false
63 }
64
65 // 关闭选中的tag
66 const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
67   if (view?.meta?.affix) return
68   tagsViewStore.delView(view)
69   if (isActive(view)) {
70     toLastView()
71   }
72 }
73
74 // 关闭全部
75 const closeAllTags = () => {
76   tagsViewStore.delAllViews()
77   toLastView()
78 }
79
80 // 关闭其它
81 const closeOthersTags = () => {
82   tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
83 }
84
85 // 重新加载
86 const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
87   if (!view) return
88   tagsViewStore.delCachedView()
89   const { path, query } = view
90   await nextTick()
91   replace({
92     path: '/redirect' + path,
93     query: query
94   })
95 }
96
97 // 关闭左侧
98 const closeLeftTags = () => {
99   tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
100 }
101
102 // 关闭右侧
103 const closeRightTags = () => {
104   tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
105 }
106
107 // 跳转到最后一个
108 const toLastView = () => {
109   const visitedViews = tagsViewStore.getVisitedViews
110   const latestView = visitedViews.slice(-1)[0]
111   if (latestView) {
112     push(latestView)
113   } else {
114     if (
115       unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
116       unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
117     ) {
118       addTags()
119       return
120     }
121     // TODO: You can set another route
122     push('/')
123   }
124 }
125
126 // 滚动到选中的tag
127 const moveToCurrentTag = async () => {
128   await nextTick()
129   for (const v of unref(visitedViews)) {
3e359e 130     if (v.fullPath === unref(currentRoute).fullPath) {
820397 131       moveToTarget(v)
H 132       break
133     }
134   }
135 }
136
137 const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
138
139 const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
140   const wrap$ = unref(scrollbarRef)?.wrapRef
141   let firstTag: Nullable<RouterLinkProps> = null
142   let lastTag: Nullable<RouterLinkProps> = null
143
144   const tagList = unref(tagLinksRefs)
145   // find first tag and last tag
146   if (tagList.length > 0) {
147     firstTag = tagList[0]
148     lastTag = tagList[tagList.length - 1]
149   }
150   if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
151     // 直接滚动到0的位置
152     const { start } = useScrollTo({
153       el: wrap$!,
154       position: 'scrollLeft',
155       to: 0,
156       duration: 500
157     })
158     start()
159   } else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
160     // 滚动到最后的位置
161     const { start } = useScrollTo({
162       el: wrap$!,
163       position: 'scrollLeft',
164       to: wrap$!.scrollWidth - wrap$!.offsetWidth,
165       duration: 500
166     })
167     start()
168   } else {
169     // find preTag and nextTag
170     const currentIndex: number = tagList.findIndex(
171       (item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath
172     )
173     const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
174
175     const prevTag = tgsRefs[currentIndex - 1] as HTMLElement
176     const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
177
178     // the tag's offsetLeft after of nextTag
179     const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
180
181     // the tag's offsetLeft before of prevTag
182     const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
183
184     if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
185       const { start } = useScrollTo({
186         el: wrap$!,
187         position: 'scrollLeft',
188         to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
189         duration: 500
190       })
191       start()
192     } else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
193       const { start } = useScrollTo({
194         el: wrap$!,
195         position: 'scrollLeft',
196         to: beforePrevTagOffsetLeft,
197         duration: 500
198       })
199       start()
200     }
201   }
202 }
203
204 // 是否是当前tag
205 const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
3e359e 206   return route.fullPath === unref(currentRoute).fullPath
820397 207 }
H 208
209 // 所有右键菜单组件的元素
210 const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
211
212 // 右键菜单装填改变的时候
213 const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
214   if (visible) {
215     for (const v of unref(itemRefs)) {
216       const elDropdownMenuRef = v.elDropdownMenuRef
217       if (tagItem.fullPath !== v.tagItem.fullPath) {
218         elDropdownMenuRef?.handleClose()
219       }
220     }
221   }
222 }
223
224 // elscroll 实例
225 const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
226
227 // 保存滚动位置
228 const scrollLeftNumber = ref(0)
229
230 const scroll = ({ scrollLeft }) => {
231   scrollLeftNumber.value = scrollLeft as number
232 }
233
234 // 移动到某个位置
235 const move = (to: number) => {
236   const wrap$ = unref(scrollbarRef)?.wrapRef
237   const { start } = useScrollTo({
238     el: wrap$!,
239     position: 'scrollLeft',
240     to: unref(scrollLeftNumber) + to,
241     duration: 500
242   })
243   start()
244 }
245
246 onMounted(() => {
247   initTags()
248   addTags()
249 })
250
251 watch(
252   () => currentRoute.value,
253   () => {
254     addTags()
255     moveToCurrentTag()
256   }
257 )
258 </script>
259
260 <template>
261   <div
262     :id="prefixCls"
263     :class="prefixCls"
264     class="relative w-full flex bg-[#fff] dark:bg-[var(--el-bg-color)]"
265   >
266     <span
3e359e 267       :class="tagsViewImmerse ? '' : `${prefixCls}__tool ${prefixCls}__tool--first`"
820397 268       class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
H 269       @click="move(-200)"
270     >
271       <Icon
272         :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
3e359e 273         color="var(--el-text-color-placeholder)"
H 274         icon="ep:d-arrow-left"
820397 275       />
H 276     </span>
277     <div class="flex-1 overflow-hidden">
278       <ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
3e359e 279         <div class="h-[var(--tags-view-height)] flex">
820397 280           <ContextMenu
3e359e 281             v-for="item in visitedViews"
H 282             :key="item.fullPath"
820397 283             :ref="itemRefs.set"
3e359e 284             :class="[
H 285               `${prefixCls}__item`,
286               tagsViewImmerse ? `${prefixCls}__item--immerse` : '',
287               tagsViewIcon ? `${prefixCls}__item--icon` : '',
288               tagsViewImmerse && tagsViewIcon ? `${prefixCls}__item--immerse--icon` : '',
289               item?.meta?.affix ? `${prefixCls}__item--affix` : '',
290               {
291                 'is-active': isActive(item)
292               }
293             ]"
820397 294             :schema="[
H 295               {
296                 icon: 'ep:refresh',
297                 label: t('common.reload'),
298                 disabled: selectedTag?.fullPath !== item.fullPath,
299                 command: () => {
300                   refreshSelectedTag(item)
301                 }
302               },
303               {
304                 icon: 'ep:close',
305                 label: t('common.closeTab'),
306                 disabled: !!visitedViews?.length && selectedTag?.meta.affix,
307                 command: () => {
308                   closeSelectedTag(item)
309                 }
310               },
311               {
312                 divided: true,
313                 icon: 'ep:d-arrow-left',
314                 label: t('common.closeTheLeftTab'),
315                 disabled:
316                   !!visitedViews?.length &&
317                   (item.fullPath === visitedViews[0].fullPath ||
318                     selectedTag?.fullPath !== item.fullPath),
319                 command: () => {
320                   closeLeftTags()
321                 }
322               },
323               {
324                 icon: 'ep:d-arrow-right',
325                 label: t('common.closeTheRightTab'),
326                 disabled:
327                   !!visitedViews?.length &&
328                   (item.fullPath === visitedViews[visitedViews.length - 1].fullPath ||
329                     selectedTag?.fullPath !== item.fullPath),
330                 command: () => {
331                   closeRightTags()
332                 }
333               },
334               {
335                 divided: true,
336                 icon: 'ep:discount',
337                 label: t('common.closeOther'),
338                 disabled: selectedTag?.fullPath !== item.fullPath,
339                 command: () => {
340                   closeOthersTags()
341                 }
342               },
343               {
344                 icon: 'ep:minus',
345                 label: t('common.closeAll'),
346                 command: () => {
347                   closeAllTags()
348                 }
349               }
350             ]"
351             :tag-item="item"
352             @visible-change="visibleChange"
353           >
354             <div>
3e359e 355               <router-link :ref="tagLinksRefs.set" v-slot="{ navigate }" :to="{ ...item }" custom>
820397 356                 <div
3e359e 357                   :class="`h-full flex items-center justify-center whitespace-nowrap pl-15px ${prefixCls}__item--label`"
820397 358                   @click="navigate"
H 359                 >
360                   <Icon
361                     v-if="
3e359e 362                       tagsViewIcon &&
H 363                       (item?.meta?.icon ||
364                         (item?.matched &&
365                           item.matched[0] &&
366                           item.matched[item.matched.length - 1].meta?.icon))
820397 367                     "
3e359e 368                     :icon="item?.meta?.icon || item.matched[item.matched.length - 1].meta.icon"
820397 369                     :size="12"
H 370                     class="mr-5px"
371                   />
3e359e 372                   {{
H 373                     t(item?.meta?.title as string) +
374                     (item?.meta?.titleSuffix ? ` (${item?.meta?.titleSuffix})` : '')
375                   }}
820397 376                   <Icon
H 377                     :class="`${prefixCls}__item--close`"
3e359e 378                     :size="12"
820397 379                     color="#333"
H 380                     icon="ep:close"
381                     @click.prevent.stop="closeSelectedTag(item)"
382                   />
383                 </div>
384               </router-link>
385             </div>
386           </ContextMenu>
387         </div>
388       </ElScrollbar>
389     </div>
390     <span
3e359e 391       :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
820397 392       class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
H 393       @click="move(200)"
394     >
395       <Icon
396         :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
3e359e 397         color="var(--el-text-color-placeholder)"
H 398         icon="ep:d-arrow-right"
820397 399       />
H 400     </span>
401     <span
3e359e 402       :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
820397 403       class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
H 404       @click="refreshSelectedTag(selectedTag)"
405     >
406       <Icon
407         :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
3e359e 408         color="var(--el-text-color-placeholder)"
H 409         icon="ep:refresh-right"
820397 410       />
H 411     </span>
412     <ContextMenu
413       :schema="[
414         {
415           icon: 'ep:refresh',
416           label: t('common.reload'),
417           command: () => {
418             refreshSelectedTag(selectedTag)
419           }
420         },
421         {
422           icon: 'ep:close',
423           label: t('common.closeTab'),
424           disabled: !!visitedViews?.length && selectedTag?.meta.affix,
425           command: () => {
426             closeSelectedTag(selectedTag!)
427           }
428         },
429         {
430           divided: true,
431           icon: 'ep:d-arrow-left',
432           label: t('common.closeTheLeftTab'),
433           disabled: !!visitedViews?.length && selectedTag?.fullPath === visitedViews[0].fullPath,
434           command: () => {
435             closeLeftTags()
436           }
437         },
438         {
439           icon: 'ep:d-arrow-right',
440           label: t('common.closeTheRightTab'),
441           disabled:
442             !!visitedViews?.length &&
443             selectedTag?.fullPath === visitedViews[visitedViews.length - 1].fullPath,
444           command: () => {
445             closeRightTags()
446           }
447         },
448         {
449           divided: true,
450           icon: 'ep:discount',
451           label: t('common.closeOther'),
452           command: () => {
453             closeOthersTags()
454           }
455         },
456         {
457           icon: 'ep:minus',
458           label: t('common.closeAll'),
459           command: () => {
460             closeAllTags()
461           }
462         }
463       ]"
3e359e 464       trigger="click"
820397 465     >
H 466       <span
3e359e 467         :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
820397 468         class="block h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
H 469       >
470         <Icon
471           :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
3e359e 472           color="var(--el-text-color-placeholder)"
H 473           icon="ep:menu"
820397 474         />
H 475       </span>
476     </ContextMenu>
477   </div>
478 </template>
479
480 <style lang="scss" scoped>
481 $prefix-cls: #{$namespace}-tags-view;
482
483 .#{$prefix-cls} {
484   :deep(.#{$elNamespace}-scrollbar__view) {
485     height: 100%;
486   }
487
488   &__tool {
489     position: relative;
490
491     &::before {
492       position: absolute;
3e359e 493       top: 0;
820397 494       left: 0;
H 495       width: 100%;
3e359e 496       height: 100%;
820397 497       border-left: 1px solid var(--el-border-color);
H 498       content: '';
499     }
500
501     &--first {
502       &::before {
503         position: absolute;
3e359e 504         top: 0;
820397 505         left: 0;
H 506         width: 100%;
3e359e 507         height: 100%;
820397 508         border-right: 1px solid var(--el-border-color);
H 509         border-left: none;
510         content: '';
511       }
512     }
513   }
514
515   &__item {
516     position: relative;
3e359e 517     top: 3px;
820397 518     height: calc(100% - 6px);
3e359e 519     padding-right: 15px;
820397 520     margin-left: 4px;
H 521     font-size: 12px;
522     cursor: pointer;
523     border: 1px solid #d9d9d9;
524     border-radius: 2px;
3e359e 525     box-sizing: border-box;
820397 526
H 527     &--close {
528       position: absolute;
529       top: 50%;
530       right: 5px;
531       display: none;
532       transform: translate(0, -50%);
533     }
3e359e 534
820397 535     &:not(.#{$prefix-cls}__item--affix):hover {
H 536       .#{$prefix-cls}__item--close {
537         display: block;
538       }
539     }
3e359e 540   }
H 541
542   &__item--icon {
543     padding-right: 20px;
820397 544   }
H 545
546   &__item:not(.is-active) {
547     &:hover {
548       color: var(--el-color-primary);
549     }
550   }
551
552   &__item.is-active {
553     color: var(--el-color-white);
554     background-color: var(--el-color-primary);
555     border: 1px solid var(--el-color-primary);
3e359e 556
820397 557     .#{$prefix-cls}__item--close {
H 558       :deep(span) {
559         color: var(--el-color-white) !important;
3e359e 560       }
H 561     }
562   }
563
564   &__item--immerse {
565     top: 2px;
566     height: calc(100% - 3px);
567     padding-right: 35px;
568     margin: 0 -10px;
569     border: none !important;
570     -webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='68' height='34' viewBox='0 0 68 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m27,0c-7.99582,0 -11.95105,0.00205 -12,12l0,6c0,8.284 -0.48549,16.49691 -8.76949,16.49691l54.37857,-0.11145c-8.284,0 -8.60908,-8.10146 -8.60908,-16.38546l0,-6c0.11145,-12.08445 -4.38441,-12 -12,-12l-13,0z' fill='%23409eff'/%3E%3C/svg%3E")
571       12 27 15;
572
573     .#{$prefix-cls}__item--label {
574       padding-left: 35px;
575     }
576
577     .#{$prefix-cls}__item--close {
578       right: 20px;
579     }
580   }
581
582   &__item--immerse--icon {
583     padding-right: 35px;
584   }
585
586   &__item--immerse:not(.is-active) {
587     &:hover {
588       color: var(--el-color-white);
589       background-color: var(--el-color-primary);
590
591       .#{$prefix-cls}__item--close {
592         :deep(span) {
593           color: var(--el-color-white) !important;
594         }
820397 595       }
H 596     }
597   }
598 }
599
600 .dark {
601   .#{$prefix-cls} {
602     &__tool {
603       &--first {
604         &::after {
605           display: none;
606         }
607       }
608     }
609
610     &__item {
611       border: 1px solid var(--el-border-color);
612     }
613
614     &__item:not(.is-active) {
615       &:hover {
616         color: var(--el-color-primary);
617       }
618     }
619
620     &__item.is-active {
621       color: var(--el-color-white);
622       background-color: var(--el-color-primary);
623       border: 1px solid var(--el-color-primary);
3e359e 624
820397 625       .#{$prefix-cls}__item--close {
H 626         :deep(span) {
627           color: var(--el-color-white) !important;
628         }
629       }
630     }
3e359e 631
H 632     &__item--immerse:not(.is-active) {
633       &:hover {
634         color: var(--el-color-white);
635       }
636     }
820397 637   }
H 638 }
639 </style>