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