@robot-admin/naive-ui-components 0.9.3 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -76
- package/LICENSE +21 -0
- package/README.md +314 -294
- package/dist/C_AvatarGroup.cjs +1 -1
- package/dist/C_AvatarGroup.d.cts +1 -1
- package/dist/C_AvatarGroup.d.ts +1 -1
- package/dist/C_AvatarGroup.js +1 -1
- package/dist/C_Cascade-CmCrLVft.css.map +1 -0
- package/dist/C_Cascade-U6A5AJJb.css.map +1 -0
- package/dist/C_Cascade.css +3 -3
- package/dist/C_Cascade.d.cts +2 -2
- package/dist/C_Cascade.d.ts +2 -2
- package/dist/C_Cascade.js.map +1 -1
- package/dist/C_Cascade2.js +1 -1
- package/dist/C_Cascade2.js.map +1 -1
- package/dist/{C_Form-SCq-DZaZ.css.map → C_Form-B_2ql62V.css.map} +1 -1
- package/dist/{C_Form-mcd1nKvF.css.map → C_Form-CX-7xsz3.css.map} +1 -1
- package/dist/C_Form.css +60 -60
- package/dist/C_Form.d.cts +2 -2
- package/dist/C_Form.d.ts +2 -2
- package/dist/C_Form.js.map +1 -1
- package/dist/C_Form2.js +1 -1
- package/dist/C_Form2.js.map +1 -1
- package/dist/C_FullCalendar.js.map +1 -1
- package/dist/C_FullCalendar2.js +1 -1
- package/dist/C_FullCalendar2.js.map +1 -1
- package/dist/C_Icon.d.cts +1 -1
- package/dist/C_Icon.d.ts +1 -1
- package/dist/C_Icon.js.map +1 -1
- package/dist/C_Icon2.js.map +1 -1
- package/dist/C_Language.d.cts +1 -1
- package/dist/C_Language.d.ts +1 -1
- package/dist/C_Language.js.map +1 -1
- package/dist/C_Language2.js.map +1 -1
- package/dist/C_Steps-27Exh7PM.css.map +1 -0
- package/dist/C_Steps-DU9ZsFrs.css.map +1 -0
- package/dist/C_Steps.css +38 -38
- package/dist/C_Steps.d.cts +1 -1
- package/dist/C_Steps.d.ts +1 -1
- package/dist/C_Steps.js.map +1 -1
- package/dist/C_Steps2.js +1 -1
- package/dist/C_Steps2.js.map +1 -1
- package/dist/C_Table.js.map +1 -1
- package/dist/C_Table2.js +3 -3
- package/dist/C_Table2.js.map +1 -1
- package/dist/{C_TagsView-COjGNM9h.css.map → C_TagsView-BN8vMRsY.css.map} +1 -1
- package/dist/{C_TagsView-zUDj9S06.css.map → C_TagsView-s1dnSJAV.css.map} +1 -1
- package/dist/C_TagsView.css +36 -36
- package/dist/C_TagsView.d.cts +1 -1
- package/dist/C_TagsView.d.ts +1 -1
- package/dist/C_TagsView.js.map +1 -1
- package/dist/C_TagsView2.js +1 -1
- package/dist/C_TagsView2.js.map +1 -1
- package/dist/C_Theme.d.cts +1 -1
- package/dist/C_Theme.d.ts +1 -1
- package/dist/C_Theme.js.map +1 -1
- package/dist/C_Theme2.js.map +1 -1
- package/dist/C_Timeline.cjs +1 -1
- package/dist/C_Timeline.d.cts +1 -1
- package/dist/C_Timeline.d.ts +1 -1
- package/dist/C_Timeline.js +1 -1
- package/dist/constants.d.ts +5 -5
- package/dist/constants2.d.ts +5 -5
- package/dist/constants3.d.ts +2 -2
- package/dist/constants4.d.ts +5 -5
- package/dist/constants5.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +9 -9
- package/dist/index.d.ts +9 -9
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.vue.d.ts +1 -1
- package/dist/index10.vue.d.ts +1 -1
- package/dist/index11.vue.d.ts +2 -2
- package/dist/index12.vue.d.ts +1 -1
- package/dist/index13.vue.d.ts +4 -4
- package/dist/index15.vue.d.ts +30 -10
- package/dist/index16.vue.d.ts +13 -6
- package/dist/index17.vue.d.ts +1 -1
- package/dist/index18.vue.d.ts +5 -5
- package/dist/index2.vue.d.ts +1 -1
- package/dist/index20.vue.d.ts +1 -1
- package/dist/index21.vue.d.ts +2 -2
- package/dist/index22.vue.d.ts +1 -1
- package/dist/index23.vue.d.ts +14 -3
- package/dist/index24.vue.d.ts +13 -4
- package/dist/index25.vue.d.ts +4 -2
- package/dist/index26.vue.d.ts +1 -1
- package/dist/index27.vue.d.ts +2 -2
- package/dist/index28.vue.d.ts +3 -3
- package/dist/index3.vue.d.ts +2 -2
- package/dist/index4.vue.d.ts +2 -2
- package/dist/index6.vue.d.ts +1 -1
- package/dist/index7.vue.d.ts +9 -7
- package/dist/index8.vue.d.ts +3 -3
- package/dist/index9.vue.d.ts +2 -2
- package/dist/resolver.cjs +1 -1
- package/dist/resolver.cjs.map +1 -1
- package/dist/resolver.d.cts +2 -21
- package/dist/resolver.d.ts +2 -21
- package/dist/resolver.js +1 -1
- package/dist/resolver.js.map +1 -1
- package/dist/style.css +142 -142
- package/dist/useCalendarEvents.d.ts +9 -6
- package/dist/useCollapsePanel.d.ts +3 -3
- package/dist/useCropperCore.d.ts +6 -6
- package/dist/useDraggableLayout.d.ts +2 -2
- package/dist/useDynamicFormState.d.ts +29 -3724
- package/dist/useEdgeInteraction.d.ts +1 -1
- package/dist/useGlobalSearch.d.ts +1 -1
- package/dist/useInfiniteScroll.d.ts +1 -1
- package/dist/useQRCode.d.ts +4 -4
- package/dist/useSignatureHistory.d.ts +3 -3
- package/dist/useSplitResize.d.ts +2 -2
- package/dist/useTimeSelection.d.ts +2 -2
- package/dist/useTreeOperations.d.ts +5 -5
- package/dist/useWorkflowValidation.d.ts +3 -3
- package/package.json +11 -2
- package/dist/C_Cascade-BE7t1OX5.css.map +0 -1
- package/dist/C_Cascade-CpDiau1y.css.map +0 -1
- package/dist/C_Steps-Buyg7Axn.css.map +0 -1
- package/dist/C_Steps-DNY91VbQ.css.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_TagsView-
|
|
1
|
+
{"version":3,"file":"C_TagsView-BN8vMRsY.css","names":[],"sources":["../src/components/C_TagsView/index.vue?vue&type=style&index=0&scoped=1c8f8663&lang.scss"],"sourcesContent":["/* unplugin-vue-components disabled *//* ===========================================\n * 标签组件样式 - 精简版\n * =========================================== */\n/* --------- 标签视图容器 --------- */\n.tags-view-container[data-v-1c8f8663] {\n border-bottom: 1px solid var(--color-border);\n padding: 8px 16px 0;\n position: relative;\n width: 100%;\n}\n\n/* --------- 滚动轨道容器 --------- */\n.tags-track[data-v-1c8f8663] {\n width: 100%;\n overflow: hidden;\n position: relative;\n}\n\n/* --------- 滚动容器 --------- */\n.tags-container[data-v-1c8f8663] {\n display: flex;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none;\n -ms-overflow-style: none;\n padding: 0 20px;\n align-items: center;\n min-height: 40px;\n}\n.tags-container[data-v-1c8f8663]::-webkit-scrollbar {\n display: none;\n}\n\n/* --------- 现代化标签样式 --------- */\n.n-tag[data-v-1c8f8663] {\n position: relative;\n display: inline-flex;\n align-items: center;\n padding: 0 12px;\n height: 28px;\n margin-right: 4px;\n font-size: 12px;\n font-weight: 500;\n border-radius: 14px;\n border: 1px solid var(--color-border);\n cursor: pointer;\n transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1);\n flex-shrink: 0;\n white-space: nowrap;\n /* 内部高光效果 */\n /* --------- 悬停效果 --------- */\n /* --------- 激活状态 --------- */\n /* --------- 关闭按钮 --------- */\n}\n.n-tag[data-v-1c8f8663]::before {\n content: \"\";\n position: absolute;\n top: 1px;\n left: 1px;\n right: 1px;\n height: 50%;\n border-radius: 13px 13px 6px 6px;\n background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), transparent);\n opacity: 0.5;\n transition: opacity 0.22s ease;\n}\n.n-tag[data-v-1c8f8663]:hover:not([type=primary]) {\n color: var(--color-primary) !important;\n border-color: var(--color-primary);\n transform: scale(1.02);\n}\n.n-tag[data-v-1c8f8663]:hover:not([type=primary])::before {\n opacity: 0.8;\n}\n.n-tag[data-v-1c8f8663]:hover:not([type=primary]) .c-icon,\n.n-tag[data-v-1c8f8663]:hover:not([type=primary]) [class*=\"c-icon\"] {\n color: var(--color-primary) !important;\n}\n.n-tag:hover:not([type=primary]) i[data-v-1c8f8663],\n.n-tag:hover:not([type=primary]) .icon[data-v-1c8f8663] {\n color: var(--color-primary) !important;\n}\n.n-tag[data-v-1c8f8663]:hover:not([type=primary]) .n-tag__close {\n color: var(--color-primary) !important;\n opacity: 0.8;\n transform: scale(1.05);\n}\n.n-tag[type=primary][data-v-1c8f8663] {\n background: var(--tag-active-bg, rgba(32, 128, 240, 0.08)) !important;\n color: var(--color-primary) !important;\n border-color: var(--color-primary) !important;\n font-weight: 600;\n transform: scale(1.02);\n}\n.n-tag[type=primary][data-v-1c8f8663]::before {\n opacity: 0.6;\n}\n.n-tag[type=primary][data-v-1c8f8663] .n-tag__content,\n.n-tag[type=primary][data-v-1c8f8663] *:not(.n-tag__close) {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary][data-v-1c8f8663] .c-icon,\n.n-tag[type=primary][data-v-1c8f8663] [class*=\"c-icon\"] {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary] i[data-v-1c8f8663],\n.n-tag[type=primary] .icon[data-v-1c8f8663] {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary][data-v-1c8f8663]:hover {\n background: var(--tag-active-hover-bg, rgba(32, 128, 240, 0.12)) !important;\n transform: scale(1.03);\n}\n.n-tag[type=primary][data-v-1c8f8663] .n-tag__close {\n color: var(--color-primary) !important;\n opacity: 0.8;\n}\n.n-tag[data-v-1c8f8663] .n-tag__close {\n width: 16px !important;\n height: 16px !important;\n border-radius: 8px !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n margin-left: 6px !important;\n font-size: 12px !important;\n font-weight: 600 !important;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;\n opacity: 0.6;\n background: rgba(0, 0, 0, 0.02) !important;\n border: 1px solid transparent;\n}\n.n-tag[data-v-1c8f8663] .n-tag__close:not(:hover) {\n transform: scale(0.9);\n}\n.n-tag[data-v-1c8f8663] .n-tag__close:hover {\n background: linear-gradient(135deg, rgba(248, 113, 113, 0.1), rgba(239, 68, 68, 0.08)) !important;\n border-color: rgba(239, 68, 68, 0.15) !important;\n color: #ef4444 !important;\n opacity: 1 !important;\n transform: scale(0.6) !important;\n box-shadow: 0 2px 6px rgba(239, 68, 68, 0.2) !important;\n}\n.n-tag[data-v-1c8f8663] .n-tag__close:active {\n transform: scale(0.95) !important;\n transition-duration: 0.1s !important;\n}\n\n/* --------- C_Icon --------- */\n[data-v-1c8f8663] .c-icon {\n margin-right: 4px;\n font-size: 12px;\n transition: color 0.22s ease;\n display: inline-flex;\n align-items: center;\n}\n\n/* --------- 响应式 --------- */\n@media (max-width: 768px) {\n.tags-view-container[data-v-1c8f8663] {\n padding: 6px 12px 0;\n}\n.n-tag[data-v-1c8f8663] {\n height: 26px;\n padding: 0 10px;\n font-size: 11px;\n margin-right: 3px;\n border-radius: 13px;\n}\n.n-tag[data-v-1c8f8663] .n-tag__close {\n width: 14px;\n height: 14px;\n font-size: 11px;\n margin-left: 4px;\n}\n.tags-container[data-v-1c8f8663] {\n padding: 0 12px;\n min-height: 36px;\n}\n[data-v-1c8f8663] .c-icon {\n font-size: 11px;\n margin-right: 3px;\n}\n}\n/* --------- 暗色主题 --------- */\n[data-theme=dark] .n-tag[data-v-1c8f8663] .n-tag__close {\n background: rgba(255, 255, 255, 0.03) !important;\n}\n[data-theme=dark] .n-tag[data-v-1c8f8663] .n-tag__close:hover {\n background: linear-gradient(135deg, rgba(248, 113, 113, 0.12), rgba(239, 68, 68, 0.1)) !important;\n box-shadow: 0 2px 6px rgba(239, 68, 68, 0.25) !important;\n}"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_TagsView-
|
|
1
|
+
{"version":3,"file":"C_TagsView-s1dnSJAV.css","names":[],"sources":["../src/components/C_TagsView/index.vue?vue&type=style&index=0&scoped=1c8f8663&lang.scss"],"sourcesContent":["/* unplugin-vue-components disabled *//* ===========================================\n * 标签组件样式 - 精简版\n * =========================================== */\n/* --------- 标签视图容器 --------- */\n.tags-view-container[data-v-1c8f8663] {\n border-bottom: 1px solid var(--color-border);\n padding: 8px 16px 0;\n position: relative;\n width: 100%;\n}\n\n/* --------- 滚动轨道容器 --------- */\n.tags-track[data-v-1c8f8663] {\n width: 100%;\n overflow: hidden;\n position: relative;\n}\n\n/* --------- 滚动容器 --------- */\n.tags-container[data-v-1c8f8663] {\n display: flex;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none;\n -ms-overflow-style: none;\n padding: 0 20px;\n align-items: center;\n min-height: 40px;\n}\n.tags-container[data-v-1c8f8663]::-webkit-scrollbar {\n display: none;\n}\n\n/* --------- 现代化标签样式 --------- */\n.n-tag[data-v-1c8f8663] {\n position: relative;\n display: inline-flex;\n align-items: center;\n padding: 0 12px;\n height: 28px;\n margin-right: 4px;\n font-size: 12px;\n font-weight: 500;\n border-radius: 14px;\n border: 1px solid var(--color-border);\n cursor: pointer;\n transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1);\n flex-shrink: 0;\n white-space: nowrap;\n /* 内部高光效果 */\n /* --------- 悬停效果 --------- */\n /* --------- 激活状态 --------- */\n /* --------- 关闭按钮 --------- */\n}\n.n-tag[data-v-1c8f8663]::before {\n content: \"\";\n position: absolute;\n top: 1px;\n left: 1px;\n right: 1px;\n height: 50%;\n border-radius: 13px 13px 6px 6px;\n background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), transparent);\n opacity: 0.5;\n transition: opacity 0.22s ease;\n}\n.n-tag[data-v-1c8f8663]:hover:not([type=primary]) {\n color: var(--color-primary) !important;\n border-color: var(--color-primary);\n transform: scale(1.02);\n}\n.n-tag[data-v-1c8f8663]:hover:not([type=primary])::before {\n opacity: 0.8;\n}\n.n-tag[data-v-1c8f8663]:hover:not([type=primary]) .c-icon,\n.n-tag[data-v-1c8f8663]:hover:not([type=primary]) [class*=\"c-icon\"] {\n color: var(--color-primary) !important;\n}\n.n-tag:hover:not([type=primary]) i[data-v-1c8f8663],\n.n-tag:hover:not([type=primary]) .icon[data-v-1c8f8663] {\n color: var(--color-primary) !important;\n}\n.n-tag[data-v-1c8f8663]:hover:not([type=primary]) .n-tag__close {\n color: var(--color-primary) !important;\n opacity: 0.8;\n transform: scale(1.05);\n}\n.n-tag[type=primary][data-v-1c8f8663] {\n background: var(--tag-active-bg, rgba(32, 128, 240, 0.08)) !important;\n color: var(--color-primary) !important;\n border-color: var(--color-primary) !important;\n font-weight: 600;\n transform: scale(1.02);\n}\n.n-tag[type=primary][data-v-1c8f8663]::before {\n opacity: 0.6;\n}\n.n-tag[type=primary][data-v-1c8f8663] .n-tag__content,\n.n-tag[type=primary][data-v-1c8f8663] *:not(.n-tag__close) {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary][data-v-1c8f8663] .c-icon,\n.n-tag[type=primary][data-v-1c8f8663] [class*=\"c-icon\"] {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary] i[data-v-1c8f8663],\n.n-tag[type=primary] .icon[data-v-1c8f8663] {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary][data-v-1c8f8663]:hover {\n background: var(--tag-active-hover-bg, rgba(32, 128, 240, 0.12)) !important;\n transform: scale(1.03);\n}\n.n-tag[type=primary][data-v-1c8f8663] .n-tag__close {\n color: var(--color-primary) !important;\n opacity: 0.8;\n}\n.n-tag[data-v-1c8f8663] .n-tag__close {\n width: 16px !important;\n height: 16px !important;\n border-radius: 8px !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n margin-left: 6px !important;\n font-size: 12px !important;\n font-weight: 600 !important;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;\n opacity: 0.6;\n background: rgba(0, 0, 0, 0.02) !important;\n border: 1px solid transparent;\n}\n.n-tag[data-v-1c8f8663] .n-tag__close:not(:hover) {\n transform: scale(0.9);\n}\n.n-tag[data-v-1c8f8663] .n-tag__close:hover {\n background: linear-gradient(135deg, rgba(248, 113, 113, 0.1), rgba(239, 68, 68, 0.08)) !important;\n border-color: rgba(239, 68, 68, 0.15) !important;\n color: #ef4444 !important;\n opacity: 1 !important;\n transform: scale(0.6) !important;\n box-shadow: 0 2px 6px rgba(239, 68, 68, 0.2) !important;\n}\n.n-tag[data-v-1c8f8663] .n-tag__close:active {\n transform: scale(0.95) !important;\n transition-duration: 0.1s !important;\n}\n\n/* --------- C_Icon --------- */\n[data-v-1c8f8663] .c-icon {\n margin-right: 4px;\n font-size: 12px;\n transition: color 0.22s ease;\n display: inline-flex;\n align-items: center;\n}\n\n/* --------- 响应式 --------- */\n@media (max-width: 768px) {\n.tags-view-container[data-v-1c8f8663] {\n padding: 6px 12px 0;\n}\n.n-tag[data-v-1c8f8663] {\n height: 26px;\n padding: 0 10px;\n font-size: 11px;\n margin-right: 3px;\n border-radius: 13px;\n}\n.n-tag[data-v-1c8f8663] .n-tag__close {\n width: 14px;\n height: 14px;\n font-size: 11px;\n margin-left: 4px;\n}\n.tags-container[data-v-1c8f8663] {\n padding: 0 12px;\n min-height: 36px;\n}\n[data-v-1c8f8663] .c-icon {\n font-size: 11px;\n margin-right: 3px;\n}\n}\n/* --------- 暗色主题 --------- */\n[data-theme=dark] .n-tag[data-v-1c8f8663] .n-tag__close {\n background: rgba(255, 255, 255, 0.03) !important;\n}\n[data-theme=dark] .n-tag[data-v-1c8f8663] .n-tag__close:hover {\n background: linear-gradient(135deg, rgba(248, 113, 113, 0.12), rgba(239, 68, 68, 0.1)) !important;\n box-shadow: 0 2px 6px rgba(239, 68, 68, 0.25) !important;\n}"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA"}
|
package/dist/C_TagsView.css
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 标签组件样式 - 精简版
|
|
3
3
|
* =========================================== */
|
|
4
4
|
/* --------- 标签视图容器 --------- */
|
|
5
|
-
.tags-view-container[data-v-
|
|
5
|
+
.tags-view-container[data-v-1c8f8663] {
|
|
6
6
|
border-bottom: 1px solid var(--color-border);
|
|
7
7
|
padding: 8px 16px 0;
|
|
8
8
|
position: relative;
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/* --------- 滚动轨道容器 --------- */
|
|
13
|
-
.tags-track[data-v-
|
|
13
|
+
.tags-track[data-v-1c8f8663] {
|
|
14
14
|
width: 100%;
|
|
15
15
|
overflow: hidden;
|
|
16
16
|
position: relative;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/* --------- 滚动容器 --------- */
|
|
20
|
-
.tags-container[data-v-
|
|
20
|
+
.tags-container[data-v-1c8f8663] {
|
|
21
21
|
display: flex;
|
|
22
22
|
overflow-x: auto;
|
|
23
23
|
overflow-y: hidden;
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
align-items: center;
|
|
28
28
|
min-height: 40px;
|
|
29
29
|
}
|
|
30
|
-
.tags-container[data-v-
|
|
30
|
+
.tags-container[data-v-1c8f8663]::-webkit-scrollbar {
|
|
31
31
|
display: none;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/* --------- 现代化标签样式 --------- */
|
|
35
|
-
.n-tag[data-v-
|
|
35
|
+
.n-tag[data-v-1c8f8663] {
|
|
36
36
|
position: relative;
|
|
37
37
|
display: inline-flex;
|
|
38
38
|
align-items: center;
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
/* --------- 激活状态 --------- */
|
|
53
53
|
/* --------- 关闭按钮 --------- */
|
|
54
54
|
}
|
|
55
|
-
.n-tag[data-v-
|
|
55
|
+
.n-tag[data-v-1c8f8663]::before {
|
|
56
56
|
content: "";
|
|
57
57
|
position: absolute;
|
|
58
58
|
top: 1px;
|
|
@@ -64,58 +64,58 @@
|
|
|
64
64
|
opacity: 0.5;
|
|
65
65
|
transition: opacity 0.22s ease;
|
|
66
66
|
}
|
|
67
|
-
.n-tag[data-v-
|
|
67
|
+
.n-tag[data-v-1c8f8663]:hover:not([type=primary]) {
|
|
68
68
|
color: var(--color-primary) !important;
|
|
69
69
|
border-color: var(--color-primary);
|
|
70
70
|
transform: scale(1.02);
|
|
71
71
|
}
|
|
72
|
-
.n-tag[data-v-
|
|
72
|
+
.n-tag[data-v-1c8f8663]:hover:not([type=primary])::before {
|
|
73
73
|
opacity: 0.8;
|
|
74
74
|
}
|
|
75
|
-
.n-tag[data-v-
|
|
76
|
-
.n-tag[data-v-
|
|
75
|
+
.n-tag[data-v-1c8f8663]:hover:not([type=primary]) .c-icon,
|
|
76
|
+
.n-tag[data-v-1c8f8663]:hover:not([type=primary]) [class*="c-icon"] {
|
|
77
77
|
color: var(--color-primary) !important;
|
|
78
78
|
}
|
|
79
|
-
.n-tag:hover:not([type=primary]) i[data-v-
|
|
80
|
-
.n-tag:hover:not([type=primary]) .icon[data-v-
|
|
79
|
+
.n-tag:hover:not([type=primary]) i[data-v-1c8f8663],
|
|
80
|
+
.n-tag:hover:not([type=primary]) .icon[data-v-1c8f8663] {
|
|
81
81
|
color: var(--color-primary) !important;
|
|
82
82
|
}
|
|
83
|
-
.n-tag[data-v-
|
|
83
|
+
.n-tag[data-v-1c8f8663]:hover:not([type=primary]) .n-tag__close {
|
|
84
84
|
color: var(--color-primary) !important;
|
|
85
85
|
opacity: 0.8;
|
|
86
86
|
transform: scale(1.05);
|
|
87
87
|
}
|
|
88
|
-
.n-tag[type=primary][data-v-
|
|
88
|
+
.n-tag[type=primary][data-v-1c8f8663] {
|
|
89
89
|
background: var(--tag-active-bg, rgba(32, 128, 240, 0.08)) !important;
|
|
90
90
|
color: var(--color-primary) !important;
|
|
91
91
|
border-color: var(--color-primary) !important;
|
|
92
92
|
font-weight: 600;
|
|
93
93
|
transform: scale(1.02);
|
|
94
94
|
}
|
|
95
|
-
.n-tag[type=primary][data-v-
|
|
95
|
+
.n-tag[type=primary][data-v-1c8f8663]::before {
|
|
96
96
|
opacity: 0.6;
|
|
97
97
|
}
|
|
98
|
-
.n-tag[type=primary][data-v-
|
|
99
|
-
.n-tag[type=primary][data-v-
|
|
98
|
+
.n-tag[type=primary][data-v-1c8f8663] .n-tag__content,
|
|
99
|
+
.n-tag[type=primary][data-v-1c8f8663] *:not(.n-tag__close) {
|
|
100
100
|
color: var(--color-primary) !important;
|
|
101
101
|
}
|
|
102
|
-
.n-tag[type=primary][data-v-
|
|
103
|
-
.n-tag[type=primary][data-v-
|
|
102
|
+
.n-tag[type=primary][data-v-1c8f8663] .c-icon,
|
|
103
|
+
.n-tag[type=primary][data-v-1c8f8663] [class*="c-icon"] {
|
|
104
104
|
color: var(--color-primary) !important;
|
|
105
105
|
}
|
|
106
|
-
.n-tag[type=primary] i[data-v-
|
|
107
|
-
.n-tag[type=primary] .icon[data-v-
|
|
106
|
+
.n-tag[type=primary] i[data-v-1c8f8663],
|
|
107
|
+
.n-tag[type=primary] .icon[data-v-1c8f8663] {
|
|
108
108
|
color: var(--color-primary) !important;
|
|
109
109
|
}
|
|
110
|
-
.n-tag[type=primary][data-v-
|
|
110
|
+
.n-tag[type=primary][data-v-1c8f8663]:hover {
|
|
111
111
|
background: var(--tag-active-hover-bg, rgba(32, 128, 240, 0.12)) !important;
|
|
112
112
|
transform: scale(1.03);
|
|
113
113
|
}
|
|
114
|
-
.n-tag[type=primary][data-v-
|
|
114
|
+
.n-tag[type=primary][data-v-1c8f8663] .n-tag__close {
|
|
115
115
|
color: var(--color-primary) !important;
|
|
116
116
|
opacity: 0.8;
|
|
117
117
|
}
|
|
118
|
-
.n-tag[data-v-
|
|
118
|
+
.n-tag[data-v-1c8f8663] .n-tag__close {
|
|
119
119
|
width: 16px !important;
|
|
120
120
|
height: 16px !important;
|
|
121
121
|
border-radius: 8px !important;
|
|
@@ -130,10 +130,10 @@
|
|
|
130
130
|
background: rgba(0, 0, 0, 0.02) !important;
|
|
131
131
|
border: 1px solid transparent;
|
|
132
132
|
}
|
|
133
|
-
.n-tag[data-v-
|
|
133
|
+
.n-tag[data-v-1c8f8663] .n-tag__close:not(:hover) {
|
|
134
134
|
transform: scale(0.9);
|
|
135
135
|
}
|
|
136
|
-
.n-tag[data-v-
|
|
136
|
+
.n-tag[data-v-1c8f8663] .n-tag__close:hover {
|
|
137
137
|
background: linear-gradient(135deg, rgba(248, 113, 113, 0.1), rgba(239, 68, 68, 0.08)) !important;
|
|
138
138
|
border-color: rgba(239, 68, 68, 0.15) !important;
|
|
139
139
|
color: #ef4444 !important;
|
|
@@ -141,13 +141,13 @@
|
|
|
141
141
|
transform: scale(0.6) !important;
|
|
142
142
|
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.2) !important;
|
|
143
143
|
}
|
|
144
|
-
.n-tag[data-v-
|
|
144
|
+
.n-tag[data-v-1c8f8663] .n-tag__close:active {
|
|
145
145
|
transform: scale(0.95) !important;
|
|
146
146
|
transition-duration: 0.1s !important;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/* --------- C_Icon --------- */
|
|
150
|
-
[data-v-
|
|
150
|
+
[data-v-1c8f8663] .c-icon {
|
|
151
151
|
margin-right: 4px;
|
|
152
152
|
font-size: 12px;
|
|
153
153
|
transition: color 0.22s ease;
|
|
@@ -157,38 +157,38 @@
|
|
|
157
157
|
|
|
158
158
|
/* --------- 响应式 --------- */
|
|
159
159
|
@media (max-width: 768px) {
|
|
160
|
-
.tags-view-container[data-v-
|
|
160
|
+
.tags-view-container[data-v-1c8f8663] {
|
|
161
161
|
padding: 6px 12px 0;
|
|
162
162
|
}
|
|
163
|
-
.n-tag[data-v-
|
|
163
|
+
.n-tag[data-v-1c8f8663] {
|
|
164
164
|
height: 26px;
|
|
165
165
|
padding: 0 10px;
|
|
166
166
|
font-size: 11px;
|
|
167
167
|
margin-right: 3px;
|
|
168
168
|
border-radius: 13px;
|
|
169
169
|
}
|
|
170
|
-
.n-tag[data-v-
|
|
170
|
+
.n-tag[data-v-1c8f8663] .n-tag__close {
|
|
171
171
|
width: 14px;
|
|
172
172
|
height: 14px;
|
|
173
173
|
font-size: 11px;
|
|
174
174
|
margin-left: 4px;
|
|
175
175
|
}
|
|
176
|
-
.tags-container[data-v-
|
|
176
|
+
.tags-container[data-v-1c8f8663] {
|
|
177
177
|
padding: 0 12px;
|
|
178
178
|
min-height: 36px;
|
|
179
179
|
}
|
|
180
|
-
[data-v-
|
|
180
|
+
[data-v-1c8f8663] .c-icon {
|
|
181
181
|
font-size: 11px;
|
|
182
182
|
margin-right: 3px;
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
/* --------- 暗色主题 --------- */
|
|
186
|
-
[data-theme=dark] .n-tag[data-v-
|
|
186
|
+
[data-theme=dark] .n-tag[data-v-1c8f8663] .n-tag__close {
|
|
187
187
|
background: rgba(255, 255, 255, 0.03) !important;
|
|
188
188
|
}
|
|
189
|
-
[data-theme=dark] .n-tag[data-v-
|
|
189
|
+
[data-theme=dark] .n-tag[data-v-1c8f8663] .n-tag__close:hover {
|
|
190
190
|
background: linear-gradient(135deg, rgba(248, 113, 113, 0.12), rgba(239, 68, 68, 0.1)) !important;
|
|
191
191
|
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.25) !important;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
/*# sourceMappingURL=C_TagsView-
|
|
194
|
+
/*# sourceMappingURL=C_TagsView-BN8vMRsY.css.map*/
|
package/dist/C_TagsView.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as useTagsView, n as
|
|
1
|
+
import { i as useTagsView, n as ContextMenuLabels, r as UseTagsViewOptions, t as _default } from "./index24.vue.js";
|
|
2
2
|
export { _default as C_TagsView, type ContextMenuLabels, type UseTagsViewOptions, useTagsView };
|
package/dist/C_TagsView.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import "./menuAdapter.js";
|
|
2
|
-
import { i as useTagsView, n as
|
|
2
|
+
import { i as useTagsView, n as ContextMenuLabels, r as UseTagsViewOptions, t as _default } from "./index24.vue.js";
|
|
3
3
|
export { _default as C_TagsView, type ContextMenuLabels, type UseTagsViewOptions, useTagsView };
|
package/dist/C_TagsView.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_TagsView.js","names":[],"sources":["../src/components/C_TagsView/useTagsView.ts","../src/components/C_TagsView/index.vue","../src/components/C_TagsView/index.vue"],"sourcesContent":["import { ref, type Ref } from 'vue'\nimport type { TagItem } from '../_shared'\n\nexport interface UseTagsViewOptions {\n /** localStorage 持久化 key,传空字符串则不持久化 */\n persistKey?: string\n /** 首页路径,用于 removeAll 后的回退 */\n homePath?: string\n}\n\nconst STORAGE_KEY_DEFAULT = '__tags_view_list__'\n\n/**\n * 标签页管理 composable\n *\n * 独立于组件使用 — 可在任何地方管理标签状态\n *\n * @example 独立使用\n * ```ts\n * const tags = useTagsView({ persistKey: 'my-tags' })\n * tags.addTag({ path: '/home', title: '首页', meta: { affix: true } })\n * ```\n */\nexport function useTagsView(options: UseTagsViewOptions = {}) {\n const { persistKey = STORAGE_KEY_DEFAULT, homePath = '/home' } = options\n\n // ====== 持久化 ======\n const loadPersistedTags = (): TagItem[] => {\n if (!persistKey) return []\n try {\n const raw = localStorage.getItem(persistKey)\n return raw ? JSON.parse(raw) : []\n } catch {\n return []\n }\n }\n\n const persistTags = (tags: TagItem[]) => {\n if (!persistKey) return\n try {\n localStorage.setItem(persistKey, JSON.stringify(tags))\n } catch {\n /* silently fail */\n }\n }\n\n // ====== State ======\n const tagsViewList: Ref<TagItem[]> = ref(loadPersistedTags())\n const activeTag = ref('')\n\n const syncPersist = () => persistTags(tagsViewList.value)\n\n // ====== Actions ======\n\n const initTags = (tags?: TagItem[]) => {\n if (tags) {\n tagsViewList.value = tags\n } else {\n // 去重\n tagsViewList.value = [\n ...new Map(tagsViewList.value.map(tag => [tag.path, tag])).values(),\n ]\n }\n syncPersist()\n }\n\n const setActiveTag = (path: string) => {\n activeTag.value = path\n }\n\n const addTag = (tag: TagItem) => {\n const existingIndex = tagsViewList.value.findIndex(\n item => item.path === tag.path\n )\n\n if (existingIndex === -1) {\n tagsViewList.value.push(tag)\n } else {\n const existing = tagsViewList.value[existingIndex]\n if (existing.title !== tag.title || !existing.originalTitle) {\n existing.title = tag.title\n existing.originalTitle = tag.originalTitle || existing.originalTitle\n }\n }\n\n setActiveTag(tag.path)\n syncPersist()\n }\n\n const removeTag = (index: number): string | null => {\n if (index >= 0 && index < tagsViewList.value.length) {\n const removed = tagsViewList.value.splice(index, 1)[0]?.path ?? null\n syncPersist()\n return removed\n }\n return null\n }\n\n const removeOtherTags = (index: number) => {\n if (index === 0) {\n tagsViewList.value = [tagsViewList.value[0]].filter(Boolean)\n } else {\n tagsViewList.value = [\n tagsViewList.value[0],\n tagsViewList.value[index],\n ].filter(Boolean)\n }\n syncPersist()\n }\n\n const removeLeftTags = (index: number) => {\n tagsViewList.value = [\n tagsViewList.value[0],\n ...tagsViewList.value.slice(index),\n ]\n syncPersist()\n }\n\n const removeRightTags = (index: number) => {\n tagsViewList.value = tagsViewList.value.slice(0, index + 1)\n syncPersist()\n }\n\n const removeAllTags = () => {\n tagsViewList.value = tagsViewList.value.filter(tag => tag.meta?.affix)\n syncPersist()\n }\n\n return {\n tagsViewList,\n activeTag,\n homePath,\n initTags,\n setActiveTag,\n addTag,\n removeTag,\n removeOtherTags,\n removeLeftTags,\n removeRightTags,\n removeAllTags,\n }\n}\n","<!--\n * @Description: 通用标签页导航组件 — 路由感知的多标签管理\n *\n * 内置功能:\n * - 路由变化自动添加标签\n * - 持久化(localStorage,可关闭)\n * - 右键菜单(关闭/关闭其他/关闭左侧/关闭右侧/关闭所有)\n * - 水平滚轮滚动\n * - i18n 支持(labelFormatter)\n *\n * @example 基础用法\n * ```vue\n * <C_TagsView\n * :label-formatter=\"$t\"\n * @select=\"router.push\"\n * />\n * ```\n *\n * @example 自定义首页 + 菜单文案\n * ```vue\n * <C_TagsView\n * home-path=\"/dashboard\"\n * home-title=\"Dashboard\"\n * :context-menu-labels=\"{ close: 'Close', closeAll: 'Close All', ... }\"\n * />\n * ```\n *\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\n-->\n<template>\n <div class=\"tags-view-container\">\n <div\n class=\"tags-track\"\n ref=\"tagsTrack\"\n @wheel=\"handleWheel\"\n >\n <div\n class=\"tags-container\"\n ref=\"tagsContainer\"\n >\n <NTag\n v-for=\"(tag, index) in tags.tagsViewList.value\"\n :key=\"tag.path\"\n :type=\"isActive(tag) ? 'primary' : 'default'\"\n :closable=\"!isAffix(tag)\"\n :data-path=\"tag.path\"\n @close.stop=\"handleClose(tag, index)\"\n @click=\"navigateToTag(tag)\"\n @contextmenu.prevent=\"\n (e: MouseEvent) => showContextMenu(e, tag, index)\n \"\n >\n <template #icon>\n <C_Icon\n :name=\"tag.icon\"\n :size=\"12\"\n />\n </template>\n {{ tag.title }}\n </NTag>\n </div>\n </div>\n <NDropdown\n v-if=\"contextMenuVisible\"\n :show=\"contextMenuVisible\"\n :options=\"contextMenuOptions\"\n :x=\"contextMenuX\"\n :y=\"contextMenuY\"\n @clickoutside=\"closeContextMenu\"\n @select=\"handleContextMenuSelect\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\n import { ref, computed, watch, nextTick, h } from 'vue'\n import { useRoute, useRouter } from 'vue-router'\n import { C_Icon } from '../C_Icon'\n import type { TagItem } from '../_shared'\n import { useTagsView, type UseTagsViewOptions } from './useTagsView'\n\n defineOptions({ name: 'C_TagsView' })\n\n export interface ContextMenuLabels {\n close?: string\n closeOthers?: string\n closeLeft?: string\n closeRight?: string\n closeAll?: string\n }\n\n const props = withDefaults(\n defineProps<{\n /**\n * 标签文本格式化函数(用于 i18n)\n */\n labelFormatter?: (label: string) => string\n /** 首页路径 */\n homePath?: string\n /** 首页标题(原始文本) */\n homeTitle?: string\n /** 首页图标 */\n homeIcon?: string\n /** 持久化 localStorage key,传空字符串禁用 */\n persistKey?: string\n /** 右键菜单文案(国际化适配) */\n contextMenuLabels?: ContextMenuLabels\n /** useTagsView 配置项(优先级最高) */\n tagsViewOptions?: UseTagsViewOptions\n }>(),\n {\n homePath: '/home',\n homeTitle: '首页',\n homeIcon: 'mdi:home',\n persistKey: '__tags_view_list__',\n contextMenuLabels: () => ({\n close: '关闭',\n closeOthers: '关闭其他',\n closeLeft: '关闭左侧',\n closeRight: '关闭右侧',\n closeAll: '关闭所有',\n }),\n }\n )\n\n const emit = defineEmits<{\n /** 标签被关闭后跳转 */\n close: [fallbackPath: string]\n /** 关闭全部后 */\n closeAll: []\n }>()\n\n const route = useRoute()\n const router = useRouter()\n\n // ====== Tags composable ======\n const tags = useTagsView(\n props.tagsViewOptions ?? {\n persistKey: props.persistKey,\n homePath: props.homePath,\n }\n )\n\n // ====== 首次渲染前重新翻译持久化标签 ======\n if (tags.tagsViewList.value.length > 0 && props.labelFormatter) {\n const allRoutes = router.getRoutes()\n const translatedTags = tags.tagsViewList.value.map(tag => {\n const matchedRoute = allRoutes.find(r => r.path === tag.path)\n const originalTitle =\n tag.originalTitle || (matchedRoute?.meta?.title as string) || tag.title\n return {\n ...tag,\n originalTitle,\n title: props.labelFormatter!(originalTitle),\n }\n })\n tags.initTags(translatedTags)\n }\n\n const formatLabel = (title: string): string =>\n props.labelFormatter ? props.labelFormatter(title) : title\n\n // ====== UI State ======\n\n const contextMenuVisible = ref(false)\n const contextMenuX = ref(0)\n const contextMenuY = ref(0)\n const selectedTag = ref<TagItem | null>(null)\n const selectedIndex = ref(-1)\n const tagsTrack = ref<HTMLElement>()\n const tagsContainer = ref<HTMLElement>()\n\n // ====== Helpers ======\n const isActive = (tag: TagItem): boolean => tag.path === route.path\n const isAffix = (tag: TagItem) => tag.meta?.affix\n\n const handleWheel = (e: WheelEvent) => {\n const container = tagsContainer.value\n if (!container) return\n e.preventDefault()\n container.scrollTo({\n left: container.scrollLeft + e.deltaY,\n behavior: 'smooth',\n })\n }\n\n const scrollToTag = (path: string) => {\n nextTick(() => {\n const targetTag = tagsContainer.value?.querySelector(\n `[data-path=\"${path}\"]`\n ) as HTMLElement\n if (targetTag) {\n targetTag.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline: 'center',\n })\n }\n })\n }\n\n // ====== Actions ======\n\n const navigateToTag = (tag: TagItem) => {\n if (tag.path !== route.path) {\n router.push(tag.path)\n }\n scrollToTag(tag.path)\n }\n\n const handleClose = (tag: TagItem, index: number) => {\n if (isAffix(tag)) return\n\n const wasActive = isActive(tag)\n const tagsBeforeRemove = tags.tagsViewList.value.length\n tags.removeTag(index)\n\n if (wasActive) {\n const remaining = tags.tagsViewList.value\n const wasLastTag = index === tagsBeforeRemove - 1\n let fallbackPath: string\n\n if (wasLastTag) {\n fallbackPath = remaining[index - 1]?.path || props.homePath\n } else {\n fallbackPath = remaining[remaining.length - 1]?.path || props.homePath\n }\n\n router.push(fallbackPath)\n emit('close', fallbackPath)\n }\n }\n\n const showContextMenu = (event: MouseEvent, tag: TagItem, index: number) => {\n event.preventDefault()\n selectedTag.value = tag\n selectedIndex.value = index\n contextMenuVisible.value = true\n contextMenuX.value = event.clientX\n contextMenuY.value = event.clientY\n }\n\n const closeContextMenu = () => (contextMenuVisible.value = false)\n\n const contextMenuOptions = computed(() => {\n const labels = props.contextMenuLabels\n return [\n {\n type: 'option' as const,\n label: labels.close ?? '关闭',\n key: 'close',\n disabled: isAffix(selectedTag.value as TagItem),\n icon: () => h('span', { class: 'i-mdi:close' }),\n },\n {\n type: 'option' as const,\n label: labels.closeOthers ?? '关闭其他',\n key: 'closeOthers',\n icon: () => h('span', { class: 'i-mdi:arrow-left-right-bold' }),\n },\n {\n type: 'option' as const,\n label: labels.closeLeft ?? '关闭左侧',\n key: 'closeLeft',\n icon: () => h('span', { class: 'i-mdi:align-horizontal-left' }),\n },\n {\n type: 'option' as const,\n label: labels.closeRight ?? '关闭右侧',\n key: 'closeRight',\n icon: () => h('span', { class: 'i-mdi:align-horizontal-right' }),\n },\n {\n type: 'option' as const,\n label: labels.closeAll ?? '关闭所有',\n key: 'closeAll',\n icon: () => h('span', { class: 'i-mdi:tally-mark-5' }),\n },\n ]\n })\n\n const handleContextMenuSelect = (key: string) => {\n if (!selectedTag.value || selectedIndex.value === -1) return\n\n switch (key) {\n case 'close':\n handleClose(selectedTag.value, selectedIndex.value)\n break\n case 'closeOthers':\n tags.removeOtherTags(selectedIndex.value)\n break\n case 'closeLeft':\n tags.removeLeftTags(selectedIndex.value)\n break\n case 'closeRight':\n tags.removeRightTags(selectedIndex.value)\n break\n case 'closeAll':\n tags.removeAllTags()\n router.push(props.homePath)\n emit('closeAll')\n break\n }\n closeContextMenu()\n }\n\n // ====== 路由自动跟踪 ======\n watch(\n () => route.path,\n (newPath: string) => {\n const originalTitle =\n newPath === props.homePath\n ? props.homeTitle\n : (route.meta.title as string) || 'Unnamed Page'\n\n tags.addTag({\n path: newPath,\n originalTitle,\n title: formatLabel(originalTitle),\n icon:\n newPath === props.homePath\n ? props.homeIcon\n : (route.meta.icon as string),\n meta: { affix: route.meta.affix as boolean },\n })\n\n tags.setActiveTag(newPath)\n scrollToTag(newPath)\n },\n { immediate: true }\n )\n\n // ====== 暴露 ======\n defineExpose({\n /** useTagsView composable 实例 */\n tags,\n /** 滚动到指定标签 */\n scrollToTag,\n })\n</script>\n\n<style lang=\"scss\" scoped>\n @use './index.scss';\n</style>\n","<!--\n * @Description: 通用标签页导航组件 — 路由感知的多标签管理\n *\n * 内置功能:\n * - 路由变化自动添加标签\n * - 持久化(localStorage,可关闭)\n * - 右键菜单(关闭/关闭其他/关闭左侧/关闭右侧/关闭所有)\n * - 水平滚轮滚动\n * - i18n 支持(labelFormatter)\n *\n * @example 基础用法\n * ```vue\n * <C_TagsView\n * :label-formatter=\"$t\"\n * @select=\"router.push\"\n * />\n * ```\n *\n * @example 自定义首页 + 菜单文案\n * ```vue\n * <C_TagsView\n * home-path=\"/dashboard\"\n * home-title=\"Dashboard\"\n * :context-menu-labels=\"{ close: 'Close', closeAll: 'Close All', ... }\"\n * />\n * ```\n *\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\n-->\n<template>\n <div class=\"tags-view-container\">\n <div\n class=\"tags-track\"\n ref=\"tagsTrack\"\n @wheel=\"handleWheel\"\n >\n <div\n class=\"tags-container\"\n ref=\"tagsContainer\"\n >\n <NTag\n v-for=\"(tag, index) in tags.tagsViewList.value\"\n :key=\"tag.path\"\n :type=\"isActive(tag) ? 'primary' : 'default'\"\n :closable=\"!isAffix(tag)\"\n :data-path=\"tag.path\"\n @close.stop=\"handleClose(tag, index)\"\n @click=\"navigateToTag(tag)\"\n @contextmenu.prevent=\"\n (e: MouseEvent) => showContextMenu(e, tag, index)\n \"\n >\n <template #icon>\n <C_Icon\n :name=\"tag.icon\"\n :size=\"12\"\n />\n </template>\n {{ tag.title }}\n </NTag>\n </div>\n </div>\n <NDropdown\n v-if=\"contextMenuVisible\"\n :show=\"contextMenuVisible\"\n :options=\"contextMenuOptions\"\n :x=\"contextMenuX\"\n :y=\"contextMenuY\"\n @clickoutside=\"closeContextMenu\"\n @select=\"handleContextMenuSelect\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\n import { ref, computed, watch, nextTick, h } from 'vue'\n import { useRoute, useRouter } from 'vue-router'\n import { C_Icon } from '../C_Icon'\n import type { TagItem } from '../_shared'\n import { useTagsView, type UseTagsViewOptions } from './useTagsView'\n\n defineOptions({ name: 'C_TagsView' })\n\n export interface ContextMenuLabels {\n close?: string\n closeOthers?: string\n closeLeft?: string\n closeRight?: string\n closeAll?: string\n }\n\n const props = withDefaults(\n defineProps<{\n /**\n * 标签文本格式化函数(用于 i18n)\n */\n labelFormatter?: (label: string) => string\n /** 首页路径 */\n homePath?: string\n /** 首页标题(原始文本) */\n homeTitle?: string\n /** 首页图标 */\n homeIcon?: string\n /** 持久化 localStorage key,传空字符串禁用 */\n persistKey?: string\n /** 右键菜单文案(国际化适配) */\n contextMenuLabels?: ContextMenuLabels\n /** useTagsView 配置项(优先级最高) */\n tagsViewOptions?: UseTagsViewOptions\n }>(),\n {\n homePath: '/home',\n homeTitle: '首页',\n homeIcon: 'mdi:home',\n persistKey: '__tags_view_list__',\n contextMenuLabels: () => ({\n close: '关闭',\n closeOthers: '关闭其他',\n closeLeft: '关闭左侧',\n closeRight: '关闭右侧',\n closeAll: '关闭所有',\n }),\n }\n )\n\n const emit = defineEmits<{\n /** 标签被关闭后跳转 */\n close: [fallbackPath: string]\n /** 关闭全部后 */\n closeAll: []\n }>()\n\n const route = useRoute()\n const router = useRouter()\n\n // ====== Tags composable ======\n const tags = useTagsView(\n props.tagsViewOptions ?? {\n persistKey: props.persistKey,\n homePath: props.homePath,\n }\n )\n\n // ====== 首次渲染前重新翻译持久化标签 ======\n if (tags.tagsViewList.value.length > 0 && props.labelFormatter) {\n const allRoutes = router.getRoutes()\n const translatedTags = tags.tagsViewList.value.map(tag => {\n const matchedRoute = allRoutes.find(r => r.path === tag.path)\n const originalTitle =\n tag.originalTitle || (matchedRoute?.meta?.title as string) || tag.title\n return {\n ...tag,\n originalTitle,\n title: props.labelFormatter!(originalTitle),\n }\n })\n tags.initTags(translatedTags)\n }\n\n const formatLabel = (title: string): string =>\n props.labelFormatter ? props.labelFormatter(title) : title\n\n // ====== UI State ======\n\n const contextMenuVisible = ref(false)\n const contextMenuX = ref(0)\n const contextMenuY = ref(0)\n const selectedTag = ref<TagItem | null>(null)\n const selectedIndex = ref(-1)\n const tagsTrack = ref<HTMLElement>()\n const tagsContainer = ref<HTMLElement>()\n\n // ====== Helpers ======\n const isActive = (tag: TagItem): boolean => tag.path === route.path\n const isAffix = (tag: TagItem) => tag.meta?.affix\n\n const handleWheel = (e: WheelEvent) => {\n const container = tagsContainer.value\n if (!container) return\n e.preventDefault()\n container.scrollTo({\n left: container.scrollLeft + e.deltaY,\n behavior: 'smooth',\n })\n }\n\n const scrollToTag = (path: string) => {\n nextTick(() => {\n const targetTag = tagsContainer.value?.querySelector(\n `[data-path=\"${path}\"]`\n ) as HTMLElement\n if (targetTag) {\n targetTag.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline: 'center',\n })\n }\n })\n }\n\n // ====== Actions ======\n\n const navigateToTag = (tag: TagItem) => {\n if (tag.path !== route.path) {\n router.push(tag.path)\n }\n scrollToTag(tag.path)\n }\n\n const handleClose = (tag: TagItem, index: number) => {\n if (isAffix(tag)) return\n\n const wasActive = isActive(tag)\n const tagsBeforeRemove = tags.tagsViewList.value.length\n tags.removeTag(index)\n\n if (wasActive) {\n const remaining = tags.tagsViewList.value\n const wasLastTag = index === tagsBeforeRemove - 1\n let fallbackPath: string\n\n if (wasLastTag) {\n fallbackPath = remaining[index - 1]?.path || props.homePath\n } else {\n fallbackPath = remaining[remaining.length - 1]?.path || props.homePath\n }\n\n router.push(fallbackPath)\n emit('close', fallbackPath)\n }\n }\n\n const showContextMenu = (event: MouseEvent, tag: TagItem, index: number) => {\n event.preventDefault()\n selectedTag.value = tag\n selectedIndex.value = index\n contextMenuVisible.value = true\n contextMenuX.value = event.clientX\n contextMenuY.value = event.clientY\n }\n\n const closeContextMenu = () => (contextMenuVisible.value = false)\n\n const contextMenuOptions = computed(() => {\n const labels = props.contextMenuLabels\n return [\n {\n type: 'option' as const,\n label: labels.close ?? '关闭',\n key: 'close',\n disabled: isAffix(selectedTag.value as TagItem),\n icon: () => h('span', { class: 'i-mdi:close' }),\n },\n {\n type: 'option' as const,\n label: labels.closeOthers ?? '关闭其他',\n key: 'closeOthers',\n icon: () => h('span', { class: 'i-mdi:arrow-left-right-bold' }),\n },\n {\n type: 'option' as const,\n label: labels.closeLeft ?? '关闭左侧',\n key: 'closeLeft',\n icon: () => h('span', { class: 'i-mdi:align-horizontal-left' }),\n },\n {\n type: 'option' as const,\n label: labels.closeRight ?? '关闭右侧',\n key: 'closeRight',\n icon: () => h('span', { class: 'i-mdi:align-horizontal-right' }),\n },\n {\n type: 'option' as const,\n label: labels.closeAll ?? '关闭所有',\n key: 'closeAll',\n icon: () => h('span', { class: 'i-mdi:tally-mark-5' }),\n },\n ]\n })\n\n const handleContextMenuSelect = (key: string) => {\n if (!selectedTag.value || selectedIndex.value === -1) return\n\n switch (key) {\n case 'close':\n handleClose(selectedTag.value, selectedIndex.value)\n break\n case 'closeOthers':\n tags.removeOtherTags(selectedIndex.value)\n break\n case 'closeLeft':\n tags.removeLeftTags(selectedIndex.value)\n break\n case 'closeRight':\n tags.removeRightTags(selectedIndex.value)\n break\n case 'closeAll':\n tags.removeAllTags()\n router.push(props.homePath)\n emit('closeAll')\n break\n }\n closeContextMenu()\n }\n\n // ====== 路由自动跟踪 ======\n watch(\n () => route.path,\n (newPath: string) => {\n const originalTitle =\n newPath === props.homePath\n ? props.homeTitle\n : (route.meta.title as string) || 'Unnamed Page'\n\n tags.addTag({\n path: newPath,\n originalTitle,\n title: formatLabel(originalTitle),\n icon:\n newPath === props.homePath\n ? props.homeIcon\n : (route.meta.icon as string),\n meta: { affix: route.meta.affix as boolean },\n })\n\n tags.setActiveTag(newPath)\n scrollToTag(newPath)\n },\n { immediate: true }\n )\n\n // ====== 暴露 ======\n defineExpose({\n /** useTagsView composable 实例 */\n tags,\n /** 滚动到指定标签 */\n scrollToTag,\n })\n</script>\n\n<style lang=\"scss\" scoped>\n @use './index.scss';\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"C_TagsView.js","names":[],"sources":["../src/components/C_TagsView/useTagsView.ts","../src/components/C_TagsView/index.vue","../src/components/C_TagsView/index.vue"],"sourcesContent":["import { ref, type Ref } from 'vue'\nimport type { TagItem } from '../_shared'\n\nexport interface UseTagsViewOptions {\n /** localStorage 持久化 key,传空字符串则不持久化 */\n persistKey?: string\n /** 首页路径,用于 removeAll 后的回退 */\n homePath?: string\n}\n\nconst STORAGE_KEY_DEFAULT = '__tags_view_list__'\n\n/**\n * 标签页管理 composable\n *\n * 独立于组件使用 — 可在任何地方管理标签状态\n *\n * @example 独立使用\n * ```ts\n * const tags = useTagsView({ persistKey: 'my-tags' })\n * tags.addTag({ path: '/home', title: '首页', meta: { affix: true } })\n * ```\n */\nexport function useTagsView(options: UseTagsViewOptions = {}) {\n const { persistKey = STORAGE_KEY_DEFAULT, homePath = '/home' } = options\n\n // ====== 持久化 ======\n const loadPersistedTags = (): TagItem[] => {\n if (!persistKey) return []\n try {\n const raw = localStorage.getItem(persistKey)\n return raw ? JSON.parse(raw) : []\n } catch {\n return []\n }\n }\n\n const persistTags = (tags: TagItem[]) => {\n if (!persistKey) return\n try {\n localStorage.setItem(persistKey, JSON.stringify(tags))\n } catch {\n /* silently fail */\n }\n }\n\n // ====== State ======\n const tagsViewList: Ref<TagItem[]> = ref(loadPersistedTags())\n const activeTag = ref('')\n\n const syncPersist = () => persistTags(tagsViewList.value)\n\n // ====== Actions ======\n\n const initTags = (tags?: TagItem[]) => {\n if (tags) {\n tagsViewList.value = tags\n } else {\n // 去重\n tagsViewList.value = [\n ...new Map(tagsViewList.value.map(tag => [tag.path, tag])).values(),\n ]\n }\n syncPersist()\n }\n\n const setActiveTag = (path: string) => {\n activeTag.value = path\n }\n\n const addTag = (tag: TagItem) => {\n const existingIndex = tagsViewList.value.findIndex(\n item => item.path === tag.path\n )\n\n if (existingIndex === -1) {\n tagsViewList.value.push(tag)\n } else {\n const existing = tagsViewList.value[existingIndex]\n if (existing.title !== tag.title || !existing.originalTitle) {\n existing.title = tag.title\n existing.originalTitle = tag.originalTitle || existing.originalTitle\n }\n }\n\n setActiveTag(tag.path)\n syncPersist()\n }\n\n const removeTag = (index: number): string | null => {\n if (index >= 0 && index < tagsViewList.value.length) {\n const removed = tagsViewList.value.splice(index, 1)[0]?.path ?? null\n syncPersist()\n return removed\n }\n return null\n }\n\n const removeOtherTags = (index: number) => {\n if (index === 0) {\n tagsViewList.value = [tagsViewList.value[0]].filter(Boolean)\n } else {\n tagsViewList.value = [\n tagsViewList.value[0],\n tagsViewList.value[index],\n ].filter(Boolean)\n }\n syncPersist()\n }\n\n const removeLeftTags = (index: number) => {\n tagsViewList.value = [\n tagsViewList.value[0],\n ...tagsViewList.value.slice(index),\n ]\n syncPersist()\n }\n\n const removeRightTags = (index: number) => {\n tagsViewList.value = tagsViewList.value.slice(0, index + 1)\n syncPersist()\n }\n\n const removeAllTags = () => {\n tagsViewList.value = tagsViewList.value.filter(tag => tag.meta?.affix)\n syncPersist()\n }\n\n return {\n tagsViewList,\n activeTag,\n homePath,\n initTags,\n setActiveTag,\n addTag,\n removeTag,\n removeOtherTags,\n removeLeftTags,\n removeRightTags,\n removeAllTags,\n }\n}\n","<!--\r\n * @Description: 通用标签页导航组件 — 路由感知的多标签管理\r\n *\r\n * 内置功能:\r\n * - 路由变化自动添加标签\r\n * - 持久化(localStorage,可关闭)\r\n * - 右键菜单(关闭/关闭其他/关闭左侧/关闭右侧/关闭所有)\r\n * - 水平滚轮滚动\r\n * - i18n 支持(labelFormatter)\r\n *\r\n * @example 基础用法\r\n * ```vue\r\n * <C_TagsView\r\n * :label-formatter=\"$t\"\r\n * @select=\"router.push\"\r\n * />\r\n * ```\r\n *\r\n * @example 自定义首页 + 菜单文案\r\n * ```vue\r\n * <C_TagsView\r\n * home-path=\"/dashboard\"\r\n * home-title=\"Dashboard\"\r\n * :context-menu-labels=\"{ close: 'Close', closeAll: 'Close All', ... }\"\r\n * />\r\n * ```\r\n *\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n<template>\r\n <div class=\"tags-view-container\">\r\n <div\r\n class=\"tags-track\"\r\n ref=\"tagsTrack\"\r\n @wheel=\"handleWheel\"\r\n >\r\n <div\r\n class=\"tags-container\"\r\n ref=\"tagsContainer\"\r\n >\r\n <NTag\r\n v-for=\"(tag, index) in tags.tagsViewList.value\"\r\n :key=\"tag.path\"\r\n :type=\"isActive(tag) ? 'primary' : 'default'\"\r\n :closable=\"!isAffix(tag)\"\r\n :data-path=\"tag.path\"\r\n @close.stop=\"handleClose(tag, index)\"\r\n @click=\"navigateToTag(tag)\"\r\n @contextmenu.prevent=\"\r\n (e: MouseEvent) => showContextMenu(e, tag, index)\r\n \"\r\n >\r\n <template #icon>\r\n <C_Icon\r\n :name=\"tag.icon\"\r\n :size=\"12\"\r\n />\r\n </template>\r\n {{ tag.title }}\r\n </NTag>\r\n </div>\r\n </div>\r\n <NDropdown\r\n v-if=\"contextMenuVisible\"\r\n :show=\"contextMenuVisible\"\r\n :options=\"contextMenuOptions\"\r\n :x=\"contextMenuX\"\r\n :y=\"contextMenuY\"\r\n @clickoutside=\"closeContextMenu\"\r\n @select=\"handleContextMenuSelect\"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n import { ref, computed, watch, nextTick, h } from 'vue'\r\n import { useRoute, useRouter } from 'vue-router'\r\n import { C_Icon } from '../C_Icon'\r\n import type { TagItem } from '../_shared'\r\n import { useTagsView, type UseTagsViewOptions } from './useTagsView'\r\n import type { ContextMenuLabels } from './types'\r\n\r\n defineOptions({ name: 'C_TagsView' })\r\n\r\n interface LegacyContextMenuLabels extends ContextMenuLabels {\r\n close?: string\r\n closeOthers?: string\r\n closeLeft?: string\r\n closeRight?: string\r\n closeAll?: string\r\n }\r\n\r\n const props = withDefaults(\r\n defineProps<{\r\n /**\r\n * 标签文本格式化函数(用于 i18n)\r\n */\r\n labelFormatter?: (label: string) => string\r\n /** 首页路径 */\r\n homePath?: string\r\n /** 首页标题(原始文本) */\r\n homeTitle?: string\r\n /** 首页图标 */\r\n homeIcon?: string\r\n /** 持久化 localStorage key,传空字符串禁用 */\r\n persistKey?: string\r\n /** 右键菜单文案(国际化适配) */\r\n contextMenuLabels?: LegacyContextMenuLabels\r\n /** useTagsView 配置项(优先级最高) */\r\n tagsViewOptions?: UseTagsViewOptions\r\n }>(),\r\n {\r\n homePath: '/home',\r\n homeTitle: '首页',\r\n homeIcon: 'mdi:home',\r\n persistKey: '__tags_view_list__',\r\n contextMenuLabels: () => ({\r\n close: '关闭',\r\n closeOthers: '关闭其他',\r\n closeLeft: '关闭左侧',\r\n closeRight: '关闭右侧',\r\n closeAll: '关闭所有',\r\n }),\r\n }\r\n )\r\n\r\n const emit = defineEmits<{\r\n /** 标签被关闭后跳转 */\r\n close: [fallbackPath: string]\r\n /** 关闭全部后 */\r\n closeAll: []\r\n }>()\r\n\r\n const route = useRoute()\r\n const router = useRouter()\r\n\r\n // ====== Tags composable ======\r\n const tags = useTagsView(\r\n props.tagsViewOptions ?? {\r\n persistKey: props.persistKey,\r\n homePath: props.homePath,\r\n }\r\n )\r\n\r\n // ====== 首次渲染前重新翻译持久化标签 ======\r\n if (tags.tagsViewList.value.length > 0 && props.labelFormatter) {\r\n const allRoutes = router.getRoutes()\r\n const translatedTags = tags.tagsViewList.value.map(tag => {\r\n const matchedRoute = allRoutes.find(r => r.path === tag.path)\r\n const originalTitle =\r\n tag.originalTitle || (matchedRoute?.meta?.title as string) || tag.title\r\n return {\r\n ...tag,\r\n originalTitle,\r\n title: props.labelFormatter!(originalTitle),\r\n }\r\n })\r\n tags.initTags(translatedTags)\r\n }\r\n\r\n const formatLabel = (title: string): string =>\r\n props.labelFormatter ? props.labelFormatter(title) : title\r\n\r\n // ====== UI State ======\r\n\r\n const contextMenuVisible = ref(false)\r\n const contextMenuX = ref(0)\r\n const contextMenuY = ref(0)\r\n const selectedTag = ref<TagItem | null>(null)\r\n const selectedIndex = ref(-1)\r\n const tagsTrack = ref<HTMLElement>()\r\n const tagsContainer = ref<HTMLElement>()\r\n\r\n // ====== Helpers ======\r\n const isActive = (tag: TagItem): boolean => tag.path === route.path\r\n const isAffix = (tag: TagItem) => tag.meta?.affix\r\n\r\n const handleWheel = (e: WheelEvent) => {\r\n const container = tagsContainer.value\r\n if (!container) return\r\n e.preventDefault()\r\n container.scrollTo({\r\n left: container.scrollLeft + e.deltaY,\r\n behavior: 'smooth',\r\n })\r\n }\r\n\r\n const scrollToTag = (path: string) => {\r\n nextTick(() => {\r\n const targetTag = tagsContainer.value?.querySelector(\r\n `[data-path=\"${path}\"]`\r\n ) as HTMLElement\r\n if (targetTag) {\r\n targetTag.scrollIntoView({\r\n behavior: 'smooth',\r\n block: 'nearest',\r\n inline: 'center',\r\n })\r\n }\r\n })\r\n }\r\n\r\n // ====== Actions ======\r\n\r\n const navigateToTag = (tag: TagItem) => {\r\n if (tag.path !== route.path) {\r\n router.push(tag.path)\r\n }\r\n scrollToTag(tag.path)\r\n }\r\n\r\n const handleClose = (tag: TagItem, index: number) => {\r\n if (isAffix(tag)) return\r\n\r\n const wasActive = isActive(tag)\r\n const tagsBeforeRemove = tags.tagsViewList.value.length\r\n tags.removeTag(index)\r\n\r\n if (wasActive) {\r\n const remaining = tags.tagsViewList.value\r\n const wasLastTag = index === tagsBeforeRemove - 1\r\n let fallbackPath: string\r\n\r\n if (wasLastTag) {\r\n fallbackPath = remaining[index - 1]?.path || props.homePath\r\n } else {\r\n fallbackPath = remaining[remaining.length - 1]?.path || props.homePath\r\n }\r\n\r\n router.push(fallbackPath)\r\n emit('close', fallbackPath)\r\n }\r\n }\r\n\r\n const showContextMenu = (event: MouseEvent, tag: TagItem, index: number) => {\r\n event.preventDefault()\r\n selectedTag.value = tag\r\n selectedIndex.value = index\r\n contextMenuVisible.value = true\r\n contextMenuX.value = event.clientX\r\n contextMenuY.value = event.clientY\r\n }\r\n\r\n const closeContextMenu = () => (contextMenuVisible.value = false)\r\n\r\n const contextMenuOptions = computed(() => {\r\n const labels = props.contextMenuLabels\r\n return [\r\n {\r\n type: 'option' as const,\r\n label: labels.close ?? '关闭',\r\n key: 'close',\r\n disabled: isAffix(selectedTag.value as TagItem),\r\n icon: () => h('span', { class: 'i-mdi:close' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeOthers ?? '关闭其他',\r\n key: 'closeOthers',\r\n icon: () => h('span', { class: 'i-mdi:arrow-left-right-bold' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeLeft ?? '关闭左侧',\r\n key: 'closeLeft',\r\n icon: () => h('span', { class: 'i-mdi:align-horizontal-left' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeRight ?? '关闭右侧',\r\n key: 'closeRight',\r\n icon: () => h('span', { class: 'i-mdi:align-horizontal-right' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeAll ?? '关闭所有',\r\n key: 'closeAll',\r\n icon: () => h('span', { class: 'i-mdi:tally-mark-5' }),\r\n },\r\n ]\r\n })\r\n\r\n const handleContextMenuSelect = (key: string) => {\r\n if (!selectedTag.value || selectedIndex.value === -1) return\r\n\r\n switch (key) {\r\n case 'close':\r\n handleClose(selectedTag.value, selectedIndex.value)\r\n break\r\n case 'closeOthers':\r\n tags.removeOtherTags(selectedIndex.value)\r\n break\r\n case 'closeLeft':\r\n tags.removeLeftTags(selectedIndex.value)\r\n break\r\n case 'closeRight':\r\n tags.removeRightTags(selectedIndex.value)\r\n break\r\n case 'closeAll':\r\n tags.removeAllTags()\r\n router.push(props.homePath)\r\n emit('closeAll')\r\n break\r\n }\r\n closeContextMenu()\r\n }\r\n\r\n // ====== 路由自动跟踪 ======\r\n watch(\r\n () => route.path,\r\n (newPath: string) => {\r\n const originalTitle =\r\n newPath === props.homePath\r\n ? props.homeTitle\r\n : (route.meta.title as string) || 'Unnamed Page'\r\n\r\n tags.addTag({\r\n path: newPath,\r\n originalTitle,\r\n title: formatLabel(originalTitle),\r\n icon:\r\n newPath === props.homePath\r\n ? props.homeIcon\r\n : (route.meta.icon as string),\r\n meta: { affix: route.meta.affix as boolean },\r\n })\r\n\r\n tags.setActiveTag(newPath)\r\n scrollToTag(newPath)\r\n },\r\n { immediate: true }\r\n )\r\n\r\n // ====== 暴露 ======\r\n defineExpose({\r\n /** useTagsView composable 实例 */\r\n tags,\r\n /** 滚动到指定标签 */\r\n scrollToTag,\r\n })\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n @use './index.scss';\r\n</style>\r\n","<!--\r\n * @Description: 通用标签页导航组件 — 路由感知的多标签管理\r\n *\r\n * 内置功能:\r\n * - 路由变化自动添加标签\r\n * - 持久化(localStorage,可关闭)\r\n * - 右键菜单(关闭/关闭其他/关闭左侧/关闭右侧/关闭所有)\r\n * - 水平滚轮滚动\r\n * - i18n 支持(labelFormatter)\r\n *\r\n * @example 基础用法\r\n * ```vue\r\n * <C_TagsView\r\n * :label-formatter=\"$t\"\r\n * @select=\"router.push\"\r\n * />\r\n * ```\r\n *\r\n * @example 自定义首页 + 菜单文案\r\n * ```vue\r\n * <C_TagsView\r\n * home-path=\"/dashboard\"\r\n * home-title=\"Dashboard\"\r\n * :context-menu-labels=\"{ close: 'Close', closeAll: 'Close All', ... }\"\r\n * />\r\n * ```\r\n *\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n<template>\r\n <div class=\"tags-view-container\">\r\n <div\r\n class=\"tags-track\"\r\n ref=\"tagsTrack\"\r\n @wheel=\"handleWheel\"\r\n >\r\n <div\r\n class=\"tags-container\"\r\n ref=\"tagsContainer\"\r\n >\r\n <NTag\r\n v-for=\"(tag, index) in tags.tagsViewList.value\"\r\n :key=\"tag.path\"\r\n :type=\"isActive(tag) ? 'primary' : 'default'\"\r\n :closable=\"!isAffix(tag)\"\r\n :data-path=\"tag.path\"\r\n @close.stop=\"handleClose(tag, index)\"\r\n @click=\"navigateToTag(tag)\"\r\n @contextmenu.prevent=\"\r\n (e: MouseEvent) => showContextMenu(e, tag, index)\r\n \"\r\n >\r\n <template #icon>\r\n <C_Icon\r\n :name=\"tag.icon\"\r\n :size=\"12\"\r\n />\r\n </template>\r\n {{ tag.title }}\r\n </NTag>\r\n </div>\r\n </div>\r\n <NDropdown\r\n v-if=\"contextMenuVisible\"\r\n :show=\"contextMenuVisible\"\r\n :options=\"contextMenuOptions\"\r\n :x=\"contextMenuX\"\r\n :y=\"contextMenuY\"\r\n @clickoutside=\"closeContextMenu\"\r\n @select=\"handleContextMenuSelect\"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n import { ref, computed, watch, nextTick, h } from 'vue'\r\n import { useRoute, useRouter } from 'vue-router'\r\n import { C_Icon } from '../C_Icon'\r\n import type { TagItem } from '../_shared'\r\n import { useTagsView, type UseTagsViewOptions } from './useTagsView'\r\n import type { ContextMenuLabels } from './types'\r\n\r\n defineOptions({ name: 'C_TagsView' })\r\n\r\n interface LegacyContextMenuLabels extends ContextMenuLabels {\r\n close?: string\r\n closeOthers?: string\r\n closeLeft?: string\r\n closeRight?: string\r\n closeAll?: string\r\n }\r\n\r\n const props = withDefaults(\r\n defineProps<{\r\n /**\r\n * 标签文本格式化函数(用于 i18n)\r\n */\r\n labelFormatter?: (label: string) => string\r\n /** 首页路径 */\r\n homePath?: string\r\n /** 首页标题(原始文本) */\r\n homeTitle?: string\r\n /** 首页图标 */\r\n homeIcon?: string\r\n /** 持久化 localStorage key,传空字符串禁用 */\r\n persistKey?: string\r\n /** 右键菜单文案(国际化适配) */\r\n contextMenuLabels?: LegacyContextMenuLabels\r\n /** useTagsView 配置项(优先级最高) */\r\n tagsViewOptions?: UseTagsViewOptions\r\n }>(),\r\n {\r\n homePath: '/home',\r\n homeTitle: '首页',\r\n homeIcon: 'mdi:home',\r\n persistKey: '__tags_view_list__',\r\n contextMenuLabels: () => ({\r\n close: '关闭',\r\n closeOthers: '关闭其他',\r\n closeLeft: '关闭左侧',\r\n closeRight: '关闭右侧',\r\n closeAll: '关闭所有',\r\n }),\r\n }\r\n )\r\n\r\n const emit = defineEmits<{\r\n /** 标签被关闭后跳转 */\r\n close: [fallbackPath: string]\r\n /** 关闭全部后 */\r\n closeAll: []\r\n }>()\r\n\r\n const route = useRoute()\r\n const router = useRouter()\r\n\r\n // ====== Tags composable ======\r\n const tags = useTagsView(\r\n props.tagsViewOptions ?? {\r\n persistKey: props.persistKey,\r\n homePath: props.homePath,\r\n }\r\n )\r\n\r\n // ====== 首次渲染前重新翻译持久化标签 ======\r\n if (tags.tagsViewList.value.length > 0 && props.labelFormatter) {\r\n const allRoutes = router.getRoutes()\r\n const translatedTags = tags.tagsViewList.value.map(tag => {\r\n const matchedRoute = allRoutes.find(r => r.path === tag.path)\r\n const originalTitle =\r\n tag.originalTitle || (matchedRoute?.meta?.title as string) || tag.title\r\n return {\r\n ...tag,\r\n originalTitle,\r\n title: props.labelFormatter!(originalTitle),\r\n }\r\n })\r\n tags.initTags(translatedTags)\r\n }\r\n\r\n const formatLabel = (title: string): string =>\r\n props.labelFormatter ? props.labelFormatter(title) : title\r\n\r\n // ====== UI State ======\r\n\r\n const contextMenuVisible = ref(false)\r\n const contextMenuX = ref(0)\r\n const contextMenuY = ref(0)\r\n const selectedTag = ref<TagItem | null>(null)\r\n const selectedIndex = ref(-1)\r\n const tagsTrack = ref<HTMLElement>()\r\n const tagsContainer = ref<HTMLElement>()\r\n\r\n // ====== Helpers ======\r\n const isActive = (tag: TagItem): boolean => tag.path === route.path\r\n const isAffix = (tag: TagItem) => tag.meta?.affix\r\n\r\n const handleWheel = (e: WheelEvent) => {\r\n const container = tagsContainer.value\r\n if (!container) return\r\n e.preventDefault()\r\n container.scrollTo({\r\n left: container.scrollLeft + e.deltaY,\r\n behavior: 'smooth',\r\n })\r\n }\r\n\r\n const scrollToTag = (path: string) => {\r\n nextTick(() => {\r\n const targetTag = tagsContainer.value?.querySelector(\r\n `[data-path=\"${path}\"]`\r\n ) as HTMLElement\r\n if (targetTag) {\r\n targetTag.scrollIntoView({\r\n behavior: 'smooth',\r\n block: 'nearest',\r\n inline: 'center',\r\n })\r\n }\r\n })\r\n }\r\n\r\n // ====== Actions ======\r\n\r\n const navigateToTag = (tag: TagItem) => {\r\n if (tag.path !== route.path) {\r\n router.push(tag.path)\r\n }\r\n scrollToTag(tag.path)\r\n }\r\n\r\n const handleClose = (tag: TagItem, index: number) => {\r\n if (isAffix(tag)) return\r\n\r\n const wasActive = isActive(tag)\r\n const tagsBeforeRemove = tags.tagsViewList.value.length\r\n tags.removeTag(index)\r\n\r\n if (wasActive) {\r\n const remaining = tags.tagsViewList.value\r\n const wasLastTag = index === tagsBeforeRemove - 1\r\n let fallbackPath: string\r\n\r\n if (wasLastTag) {\r\n fallbackPath = remaining[index - 1]?.path || props.homePath\r\n } else {\r\n fallbackPath = remaining[remaining.length - 1]?.path || props.homePath\r\n }\r\n\r\n router.push(fallbackPath)\r\n emit('close', fallbackPath)\r\n }\r\n }\r\n\r\n const showContextMenu = (event: MouseEvent, tag: TagItem, index: number) => {\r\n event.preventDefault()\r\n selectedTag.value = tag\r\n selectedIndex.value = index\r\n contextMenuVisible.value = true\r\n contextMenuX.value = event.clientX\r\n contextMenuY.value = event.clientY\r\n }\r\n\r\n const closeContextMenu = () => (contextMenuVisible.value = false)\r\n\r\n const contextMenuOptions = computed(() => {\r\n const labels = props.contextMenuLabels\r\n return [\r\n {\r\n type: 'option' as const,\r\n label: labels.close ?? '关闭',\r\n key: 'close',\r\n disabled: isAffix(selectedTag.value as TagItem),\r\n icon: () => h('span', { class: 'i-mdi:close' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeOthers ?? '关闭其他',\r\n key: 'closeOthers',\r\n icon: () => h('span', { class: 'i-mdi:arrow-left-right-bold' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeLeft ?? '关闭左侧',\r\n key: 'closeLeft',\r\n icon: () => h('span', { class: 'i-mdi:align-horizontal-left' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeRight ?? '关闭右侧',\r\n key: 'closeRight',\r\n icon: () => h('span', { class: 'i-mdi:align-horizontal-right' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeAll ?? '关闭所有',\r\n key: 'closeAll',\r\n icon: () => h('span', { class: 'i-mdi:tally-mark-5' }),\r\n },\r\n ]\r\n })\r\n\r\n const handleContextMenuSelect = (key: string) => {\r\n if (!selectedTag.value || selectedIndex.value === -1) return\r\n\r\n switch (key) {\r\n case 'close':\r\n handleClose(selectedTag.value, selectedIndex.value)\r\n break\r\n case 'closeOthers':\r\n tags.removeOtherTags(selectedIndex.value)\r\n break\r\n case 'closeLeft':\r\n tags.removeLeftTags(selectedIndex.value)\r\n break\r\n case 'closeRight':\r\n tags.removeRightTags(selectedIndex.value)\r\n break\r\n case 'closeAll':\r\n tags.removeAllTags()\r\n router.push(props.homePath)\r\n emit('closeAll')\r\n break\r\n }\r\n closeContextMenu()\r\n }\r\n\r\n // ====== 路由自动跟踪 ======\r\n watch(\r\n () => route.path,\r\n (newPath: string) => {\r\n const originalTitle =\r\n newPath === props.homePath\r\n ? props.homeTitle\r\n : (route.meta.title as string) || 'Unnamed Page'\r\n\r\n tags.addTag({\r\n path: newPath,\r\n originalTitle,\r\n title: formatLabel(originalTitle),\r\n icon:\r\n newPath === props.homePath\r\n ? props.homeIcon\r\n : (route.meta.icon as string),\r\n meta: { affix: route.meta.affix as boolean },\r\n })\r\n\r\n tags.setActiveTag(newPath)\r\n scrollToTag(newPath)\r\n },\r\n { immediate: true }\r\n )\r\n\r\n // ====== 暴露 ======\r\n defineExpose({\r\n /** useTagsView composable 实例 */\r\n tags,\r\n /** 滚动到指定标签 */\r\n scrollToTag,\r\n })\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n @use './index.scss';\r\n</style>\r\n"],"mappings":"wJAuBA,SAAgB,EAAY,EAA8B,EAAE,CAAE,CAC5D,GAAM,CAAE,aAAa,qBAAqB,WAAW,SAAY,EAG3D,MAAqC,CACzC,GAAI,CAAC,EAAY,MAAO,EAAE,CAC1B,GAAI,CACF,IAAM,EAAM,aAAa,QAAQ,EAAW,CAC5C,OAAO,EAAM,KAAK,MAAM,EAAI,CAAG,EAAE,MAC3B,CACN,MAAO,EAAE,GAIP,EAAe,GAAoB,CAClC,KACL,GAAI,CACF,aAAa,QAAQ,EAAY,KAAK,UAAU,EAAK,CAAC,MAChD,IAMJ,GAAA,EAAA,EAAA,KAAmC,GAAmB,CAAC,CACvD,GAAA,EAAA,EAAA,KAAgB,GAAG,CAEnB,MAAoB,EAAY,EAAa,MAAM,CAInD,EAAY,GAAqB,CACjC,EACF,EAAa,MAAQ,EAGrB,EAAa,MAAQ,CACnB,GAAG,IAAI,IAAI,EAAa,MAAM,IAAI,GAAO,CAAC,EAAI,KAAM,EAAI,CAAC,CAAC,CAAC,QAAQ,CACpE,CAEH,GAAa,EAGT,EAAgB,GAAiB,CACrC,EAAU,MAAQ,GA6DpB,MAAO,CACL,eACA,YACA,WACA,WACA,eACA,OAhEc,GAAiB,CAC/B,IAAM,EAAgB,EAAa,MAAM,UACvC,GAAQ,EAAK,OAAS,EAAI,KAC3B,CAED,GAAI,IAAkB,GACpB,EAAa,MAAM,KAAK,EAAI,KACvB,CACL,IAAM,EAAW,EAAa,MAAM,IAChC,EAAS,QAAU,EAAI,OAAS,CAAC,EAAS,iBAC5C,EAAS,MAAQ,EAAI,MACrB,EAAS,cAAgB,EAAI,eAAiB,EAAS,eAI3D,EAAa,EAAI,KAAK,CACtB,GAAa,EAiDb,UA9CiB,GAAiC,CAClD,GAAI,GAAS,GAAK,EAAQ,EAAa,MAAM,OAAQ,CACnD,IAAM,EAAU,EAAa,MAAM,OAAO,EAAO,EAAE,CAAC,IAAI,MAAQ,KAEhE,OADA,GAAa,CACN,EAET,OAAO,MAyCP,gBAtCuB,GAAkB,CACrC,IAAU,EACZ,EAAa,MAAQ,CAAC,EAAa,MAAM,GAAG,CAAC,OAAO,QAAQ,CAE5D,EAAa,MAAQ,CACnB,EAAa,MAAM,GACnB,EAAa,MAAM,GACpB,CAAC,OAAO,QAAQ,CAEnB,GAAa,EA8Bb,eA3BsB,GAAkB,CACxC,EAAa,MAAQ,CACnB,EAAa,MAAM,GACnB,GAAG,EAAa,MAAM,MAAM,EAAM,CACnC,CACD,GAAa,EAuBb,gBApBuB,GAAkB,CACzC,EAAa,MAAQ,EAAa,MAAM,MAAM,EAAG,EAAQ,EAAE,CAC3D,GAAa,EAmBb,kBAhB0B,CAC1B,EAAa,MAAQ,EAAa,MAAM,OAAO,GAAO,EAAI,MAAM,MAAM,CACtE,GAAa,EAed,qbChDD,IAAM,EAAQ,EAkCR,EAAO,EAOP,GAAA,EAAA,EAAA,WAAkB,CAClB,GAAA,EAAA,EAAA,YAAoB,CAGpB,EAAO,EACX,EAAM,iBAAmB,CACvB,WAAY,EAAM,WAClB,SAAU,EAAM,SACjB,CACF,CAGD,GAAI,EAAK,aAAa,MAAM,OAAS,GAAK,EAAM,eAAgB,CAC9D,IAAM,EAAY,EAAO,WAAW,CAC9B,EAAiB,EAAK,aAAa,MAAM,IAAI,GAAO,CACxD,IAAM,EAAe,EAAU,KAAK,GAAK,EAAE,OAAS,EAAI,KAAK,CACvD,EACJ,EAAI,eAAkB,GAAc,MAAM,OAAoB,EAAI,MACpE,MAAO,CACL,GAAG,EACH,gBACA,MAAO,EAAM,eAAgB,EAAA,CAC9B,EACD,CACF,EAAK,SAAS,EAAe,CAG/B,IAAM,EAAe,GACnB,EAAM,eAAiB,EAAM,eAAe,EAAM,CAAG,EAIjD,GAAA,EAAA,EAAA,KAAyB,GAAM,CAC/B,GAAA,EAAA,EAAA,KAAmB,EAAE,CACrB,GAAA,EAAA,EAAA,KAAmB,EAAE,CACrB,GAAA,EAAA,EAAA,KAAkC,KAAK,CACvC,GAAA,EAAA,EAAA,KAAoB,GAAG,CACvB,GAAA,EAAA,EAAA,MAA8B,CAC9B,GAAA,EAAA,EAAA,MAAkC,CAGlC,EAAY,GAA0B,EAAI,OAAS,EAAM,KACzD,EAAW,GAAiB,EAAI,MAAM,MAEtC,EAAe,GAAkB,CACrC,IAAM,EAAY,EAAc,MAC3B,IACL,EAAE,gBAAgB,CAClB,EAAU,SAAS,CACjB,KAAM,EAAU,WAAa,EAAE,OAC/B,SAAU,SACX,CAAC,GAGE,EAAe,GAAiB,EACpC,EAAA,EAAA,cAAe,CACb,IAAM,EAAY,EAAc,OAAO,cACrC,eAAe,EAAK,IACrB,CACG,GACF,EAAU,eAAe,CACvB,SAAU,SACV,MAAO,UACP,OAAQ,SACT,CAAC,EAEJ,EAKE,EAAiB,GAAiB,CAClC,EAAI,OAAS,EAAM,MACrB,EAAO,KAAK,EAAI,KAAK,CAEvB,EAAY,EAAI,KAAK,EAGjB,GAAe,EAAc,IAAkB,CACnD,GAAI,EAAQ,EAAI,CAAE,OAElB,IAAM,EAAY,EAAS,EAAI,CACzB,EAAmB,EAAK,aAAa,MAAM,OAGjD,GAFA,EAAK,UAAU,EAAM,CAEjB,EAAW,CACb,IAAM,EAAY,EAAK,aAAa,MAC9B,EAAa,IAAU,EAAmB,EAC5C,EAEJ,AAGE,EAHE,EACa,EAAU,EAAQ,IAAI,MAAQ,EAAM,SAEpC,EAAU,EAAU,OAAS,IAAI,MAAQ,EAAM,SAGhE,EAAO,KAAK,EAAa,CACzB,EAAK,QAAS,EAAa,GAIzB,GAAmB,EAAmB,EAAc,IAAkB,CAC1E,EAAM,gBAAgB,CACtB,EAAY,MAAQ,EACpB,EAAc,MAAQ,EACtB,EAAmB,MAAQ,GAC3B,EAAa,MAAQ,EAAM,QAC3B,EAAa,MAAQ,EAAM,SAGvB,MAA0B,EAAmB,MAAQ,GAErD,GAAA,EAAA,EAAA,cAAoC,CACxC,IAAM,EAAS,EAAM,kBACrB,MAAO,CACL,CACE,KAAM,SACN,MAAO,EAAO,OAAS,KACvB,IAAK,QACL,SAAU,EAAQ,EAAY,MAAiB,CAC/C,UAAA,EAAA,EAAA,GAAc,OAAQ,CAAE,MAAO,cAAe,CAAA,CAC/C,CACD,CACE,KAAM,SACN,MAAO,EAAO,aAAe,OAC7B,IAAK,cACL,UAAA,EAAA,EAAA,GAAc,OAAQ,CAAE,MAAO,8BAA+B,CAAA,CAC/D,CACD,CACE,KAAM,SACN,MAAO,EAAO,WAAa,OAC3B,IAAK,YACL,UAAA,EAAA,EAAA,GAAc,OAAQ,CAAE,MAAO,8BAA+B,CAAA,CAC/D,CACD,CACE,KAAM,SACN,MAAO,EAAO,YAAc,OAC5B,IAAK,aACL,UAAA,EAAA,EAAA,GAAc,OAAQ,CAAE,MAAO,+BAAgC,CAAA,CAChE,CACD,CACE,KAAM,SACN,MAAO,EAAO,UAAY,OAC1B,IAAK,WACL,UAAA,EAAA,EAAA,GAAc,OAAQ,CAAE,MAAO,qBAAsB,CAAA,EAExD,EACD,CAEI,EAA2B,GAAgB,CAC3C,MAAC,EAAY,OAAS,EAAc,QAAU,IAElD,QAAQ,EAAR,CACE,IAAK,QACH,EAAY,EAAY,MAAO,EAAc,MAAM,CACnD,MACF,IAAK,cACH,EAAK,gBAAgB,EAAc,MAAM,CACzC,MACF,IAAK,YACH,EAAK,eAAe,EAAc,MAAM,CACxC,MACF,IAAK,aACH,EAAK,gBAAgB,EAAc,MAAM,CACzC,MACF,IAAK,WACH,EAAK,eAAe,CACpB,EAAO,KAAK,EAAM,SAAS,CAC3B,EAAK,WAAW,CAChB,MAEJ,GAAkB,UAIpB,EAAA,EAAA,WACQ,EAAM,KACX,GAAoB,CACnB,IAAM,EACJ,IAAY,EAAM,SACd,EAAM,UACL,EAAM,KAAK,OAAoB,eAEtC,EAAK,OAAO,CACV,KAAM,EACN,gBACA,MAAO,EAAY,EAAc,CACjC,KACE,IAAY,EAAM,SACd,EAAM,SACL,EAAM,KAAK,KAClB,KAAM,CAAE,MAAO,EAAM,KAAK,MAAA,CAC3B,CAAC,CAEF,EAAK,aAAa,EAAQ,CAC1B,EAAY,EAAQ,EAEtB,CAAE,UAAW,GAAM,CACpB,CAGD,EAAa,CAEX,OAEA,cACD,CAAC,qFC5QI,MAzCN,EAyCM,EAAA,EAAA,EAAA,oBAVE,MAAA,CA7BJ,MAAM,qBACF,YAAJ,IAAI,EACH,QAAO,6BA0BF,MAAA,CAvBJ,MAAM,yBACF,gBAAJ,IAAI,kDAqBG,EAAA,SAAA,MAAA,EAAA,EAAA,aAAA,EAAA,EAAA,OAlBkB,EAAI,CAAC,aAAa,OAAjC,EAAK,yCAkBR,EAAA,CAjBJ,IAAK,EAAI,KACT,KAAM,EAAS,EAAG,CAAA,UAAA,UAClB,SAAQ,CAAG,EAAQ,EAAG,CACtB,YAAW,EAAI,KACf,SAAA,EAAA,EAAA,eAAK,GAAO,EAAY,EAAK,EAAK,CAAA,CAAA,OAAA,CAAA,CAClC,QAAK,GAAE,EAAc,EAAG,CACxB,eAAA,EAAA,EAAA,eAAoC,GAAkB,EAAgB,EAAG,EAAK,EAAK,CAAA,CAAA,UAAA,CAAA,GAIzE,MAAA,EAAA,EAAA,aAIP,EAAA,EAAA,EAAA,cAAA,EAAA,EAAA,OAAA,EAAA,EAAA,CAAA,CAFC,KAAM,EAAI,KACV,KAAM,iDAGX,EAAA,EAAA,EAAA,iBADW,KAAA,EAAA,EAAA,iBACR,EAAI,MAAK,CAAA,EAAA,CAAA,CAAA,oGAKV,EAAA,QAAA,EAAA,EAAA,YAAA,EAAA,EAAA,EAAA,aAON,EAAA,OANC,KAAM,EAAA,MACN,QAAS,EAAA,MACT,EAAG,EAAA,MACH,EAAG,EAAA,MACH,eAAc,EACd,SAAQ"}
|
package/dist/C_TagsView2.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./C_Icon2.js";import{t}from"./export-helper.js";import{NDropdown as n,NTag as r}from"naive-ui";import{Fragment as i,computed as a,createBlock as o,createCommentVNode as s,createElementBlock as c,createElementVNode as l,createTextVNode as u,createVNode as d,defineComponent as f,h as p,nextTick as m,openBlock as h,ref as g,renderList as _,resolveComponent as v,toDisplayString as y,unref as b,watch as x,withCtx as S,withModifiers as C}from"vue";import{useRoute as w,useRouter as T}from"vue-router";function E(e={}){let{persistKey:t=`__tags_view_list__`,homePath:n=`/home`}=e,r=()=>{if(!t)return[];try{let e=localStorage.getItem(t);return e?JSON.parse(e):[]}catch{return[]}},i=e=>{if(t)try{localStorage.setItem(t,JSON.stringify(e))}catch{}},a=g(r()),o=g(``),s=()=>i(a.value),c=e=>{e?a.value=e:a.value=[...new Map(a.value.map(e=>[e.path,e])).values()],s()},l=e=>{o.value=e};return{tagsViewList:a,activeTag:o,homePath:n,initTags:c,setActiveTag:l,addTag:e=>{let t=a.value.findIndex(t=>t.path===e.path);if(t===-1)a.value.push(e);else{let n=a.value[t];(n.title!==e.title||!n.originalTitle)&&(n.title=e.title,n.originalTitle=e.originalTitle||n.originalTitle)}l(e.path),s()},removeTag:e=>{if(e>=0&&e<a.value.length){let t=a.value.splice(e,1)[0]?.path??null;return s(),t}return null},removeOtherTags:e=>{e===0?a.value=[a.value[0]].filter(Boolean):a.value=[a.value[0],a.value[e]].filter(Boolean),s()},removeLeftTags:e=>{a.value=[a.value[0],...a.value.slice(e)],s()},removeRightTags:e=>{a.value=a.value.slice(0,e+1),s()},removeAllTags:()=>{a.value=a.value.filter(e=>e.meta?.affix),s()}}}const D={class:`tags-view-container`};var O=t(f({name:`C_TagsView`,__name:`index`,props:{labelFormatter:{},homePath:{default:`/home`},homeTitle:{default:`首页`},homeIcon:{default:`mdi:home`},persistKey:{default:`__tags_view_list__`},contextMenuLabels:{default:()=>({close:`关闭`,closeOthers:`关闭其他`,closeLeft:`关闭左侧`,closeRight:`关闭右侧`,closeAll:`关闭所有`})},tagsViewOptions:{}},emits:[`close`,`closeAll`],setup(t,{expose:f,emit:v}){let O=t,k=v,A=w(),j=T(),M=E(O.tagsViewOptions??{persistKey:O.persistKey,homePath:O.homePath});if(M.tagsViewList.value.length>0&&O.labelFormatter){let e=j.getRoutes(),t=M.tagsViewList.value.map(t=>{let n=e.find(e=>e.path===t.path),r=t.originalTitle||n?.meta?.title||t.title;return{...t,originalTitle:r,title:O.labelFormatter(r)}});M.initTags(t)}let N=e=>O.labelFormatter?O.labelFormatter(e):e,P=g(!1),F=g(0),I=g(0),L=g(null),R=g(-1),z=g(),B=g(),V=e=>e.path===A.path,H=e=>e.meta?.affix,U=e=>{let t=B.value;t&&(e.preventDefault(),t.scrollTo({left:t.scrollLeft+e.deltaY,behavior:`smooth`}))},W=e=>{m(()=>{let t=B.value?.querySelector(`[data-path="${e}"]`);t&&t.scrollIntoView({behavior:`smooth`,block:`nearest`,inline:`center`})})},G=e=>{e.path!==A.path&&j.push(e.path),W(e.path)},K=(e,t)=>{if(H(e))return;let n=V(e),r=M.tagsViewList.value.length;if(M.removeTag(t),n){let e=M.tagsViewList.value,n=t===r-1,i;i=n?e[t-1]?.path||O.homePath:e[e.length-1]?.path||O.homePath,j.push(i),k(`close`,i)}},q=(e,t,n)=>{e.preventDefault(),L.value=t,R.value=n,P.value=!0,F.value=e.clientX,I.value=e.clientY},J=()=>P.value=!1,Y=a(()=>{let e=O.contextMenuLabels;return[{type:`option`,label:e.close??`关闭`,key:`close`,disabled:H(L.value),icon:()=>p(`span`,{class:`i-mdi:close`})},{type:`option`,label:e.closeOthers??`关闭其他`,key:`closeOthers`,icon:()=>p(`span`,{class:`i-mdi:arrow-left-right-bold`})},{type:`option`,label:e.closeLeft??`关闭左侧`,key:`closeLeft`,icon:()=>p(`span`,{class:`i-mdi:align-horizontal-left`})},{type:`option`,label:e.closeRight??`关闭右侧`,key:`closeRight`,icon:()=>p(`span`,{class:`i-mdi:align-horizontal-right`})},{type:`option`,label:e.closeAll??`关闭所有`,key:`closeAll`,icon:()=>p(`span`,{class:`i-mdi:tally-mark-5`})}]}),X=e=>{if(!(!L.value||R.value===-1)){switch(e){case`close`:K(L.value,R.value);break;case`closeOthers`:M.removeOtherTags(R.value);break;case`closeLeft`:M.removeLeftTags(R.value);break;case`closeRight`:M.removeRightTags(R.value);break;case`closeAll`:M.removeAllTags(),j.push(O.homePath),k(`closeAll`);break}J()}};return x(()=>A.path,e=>{let t=e===O.homePath?O.homeTitle:A.meta.title||`Unnamed Page`;M.addTag({path:e,originalTitle:t,title:N(t),icon:e===O.homePath?O.homeIcon:A.meta.icon,meta:{affix:A.meta.affix}}),M.setActiveTag(e),W(e)},{immediate:!0}),f({tags:M,scrollToTag:W}),(t,a)=>{let f=r,p=n;return h(),c(`div`,D,[l(`div`,{class:`tags-track`,ref_key:`tagsTrack`,ref:z,onWheel:U},[l(`div`,{class:`tags-container`,ref_key:`tagsContainer`,ref:B},[(h(!0),c(i,null,_(b(M).tagsViewList.value,(t,n)=>(h(),o(f,{key:t.path,type:V(t)?`primary`:`default`,closable:!H(t),"data-path":t.path,onClose:C(e=>K(t,n),[`stop`]),onClick:e=>G(t),onContextmenu:C(e=>q(e,t,n),[`prevent`])},{icon:S(()=>[d(b(e),{name:t.icon,size:12},null,8,[`name`])]),default:S(()=>[u(` `+y(t.title),1)]),_:2},1032,[`type`,`closable`,`data-path`,`onClose`,`onClick`,`onContextmenu`]))),128))],512)],544),P.value?(h(),o(p,{key:0,show:P.value,options:Y.value,x:F.value,y:I.value,onClickoutside:J,onSelect:X},null,8,[`show`,`options`,`x`,`y`])):s(`v-if`,!0)])}}}),[[`__scopeId`,`data-v-
|
|
1
|
+
import{t as e}from"./C_Icon2.js";import{t}from"./export-helper.js";import{NDropdown as n,NTag as r}from"naive-ui";import{Fragment as i,computed as a,createBlock as o,createCommentVNode as s,createElementBlock as c,createElementVNode as l,createTextVNode as u,createVNode as d,defineComponent as f,h as p,nextTick as m,openBlock as h,ref as g,renderList as _,resolveComponent as v,toDisplayString as y,unref as b,watch as x,withCtx as S,withModifiers as C}from"vue";import{useRoute as w,useRouter as T}from"vue-router";function E(e={}){let{persistKey:t=`__tags_view_list__`,homePath:n=`/home`}=e,r=()=>{if(!t)return[];try{let e=localStorage.getItem(t);return e?JSON.parse(e):[]}catch{return[]}},i=e=>{if(t)try{localStorage.setItem(t,JSON.stringify(e))}catch{}},a=g(r()),o=g(``),s=()=>i(a.value),c=e=>{e?a.value=e:a.value=[...new Map(a.value.map(e=>[e.path,e])).values()],s()},l=e=>{o.value=e};return{tagsViewList:a,activeTag:o,homePath:n,initTags:c,setActiveTag:l,addTag:e=>{let t=a.value.findIndex(t=>t.path===e.path);if(t===-1)a.value.push(e);else{let n=a.value[t];(n.title!==e.title||!n.originalTitle)&&(n.title=e.title,n.originalTitle=e.originalTitle||n.originalTitle)}l(e.path),s()},removeTag:e=>{if(e>=0&&e<a.value.length){let t=a.value.splice(e,1)[0]?.path??null;return s(),t}return null},removeOtherTags:e=>{e===0?a.value=[a.value[0]].filter(Boolean):a.value=[a.value[0],a.value[e]].filter(Boolean),s()},removeLeftTags:e=>{a.value=[a.value[0],...a.value.slice(e)],s()},removeRightTags:e=>{a.value=a.value.slice(0,e+1),s()},removeAllTags:()=>{a.value=a.value.filter(e=>e.meta?.affix),s()}}}const D={class:`tags-view-container`};var O=t(f({name:`C_TagsView`,__name:`index`,props:{labelFormatter:{},homePath:{default:`/home`},homeTitle:{default:`首页`},homeIcon:{default:`mdi:home`},persistKey:{default:`__tags_view_list__`},contextMenuLabels:{default:()=>({close:`关闭`,closeOthers:`关闭其他`,closeLeft:`关闭左侧`,closeRight:`关闭右侧`,closeAll:`关闭所有`})},tagsViewOptions:{}},emits:[`close`,`closeAll`],setup(t,{expose:f,emit:v}){let O=t,k=v,A=w(),j=T(),M=E(O.tagsViewOptions??{persistKey:O.persistKey,homePath:O.homePath});if(M.tagsViewList.value.length>0&&O.labelFormatter){let e=j.getRoutes(),t=M.tagsViewList.value.map(t=>{let n=e.find(e=>e.path===t.path),r=t.originalTitle||n?.meta?.title||t.title;return{...t,originalTitle:r,title:O.labelFormatter(r)}});M.initTags(t)}let N=e=>O.labelFormatter?O.labelFormatter(e):e,P=g(!1),F=g(0),I=g(0),L=g(null),R=g(-1),z=g(),B=g(),V=e=>e.path===A.path,H=e=>e.meta?.affix,U=e=>{let t=B.value;t&&(e.preventDefault(),t.scrollTo({left:t.scrollLeft+e.deltaY,behavior:`smooth`}))},W=e=>{m(()=>{let t=B.value?.querySelector(`[data-path="${e}"]`);t&&t.scrollIntoView({behavior:`smooth`,block:`nearest`,inline:`center`})})},G=e=>{e.path!==A.path&&j.push(e.path),W(e.path)},K=(e,t)=>{if(H(e))return;let n=V(e),r=M.tagsViewList.value.length;if(M.removeTag(t),n){let e=M.tagsViewList.value,n=t===r-1,i;i=n?e[t-1]?.path||O.homePath:e[e.length-1]?.path||O.homePath,j.push(i),k(`close`,i)}},q=(e,t,n)=>{e.preventDefault(),L.value=t,R.value=n,P.value=!0,F.value=e.clientX,I.value=e.clientY},J=()=>P.value=!1,Y=a(()=>{let e=O.contextMenuLabels;return[{type:`option`,label:e.close??`关闭`,key:`close`,disabled:H(L.value),icon:()=>p(`span`,{class:`i-mdi:close`})},{type:`option`,label:e.closeOthers??`关闭其他`,key:`closeOthers`,icon:()=>p(`span`,{class:`i-mdi:arrow-left-right-bold`})},{type:`option`,label:e.closeLeft??`关闭左侧`,key:`closeLeft`,icon:()=>p(`span`,{class:`i-mdi:align-horizontal-left`})},{type:`option`,label:e.closeRight??`关闭右侧`,key:`closeRight`,icon:()=>p(`span`,{class:`i-mdi:align-horizontal-right`})},{type:`option`,label:e.closeAll??`关闭所有`,key:`closeAll`,icon:()=>p(`span`,{class:`i-mdi:tally-mark-5`})}]}),X=e=>{if(!(!L.value||R.value===-1)){switch(e){case`close`:K(L.value,R.value);break;case`closeOthers`:M.removeOtherTags(R.value);break;case`closeLeft`:M.removeLeftTags(R.value);break;case`closeRight`:M.removeRightTags(R.value);break;case`closeAll`:M.removeAllTags(),j.push(O.homePath),k(`closeAll`);break}J()}};return x(()=>A.path,e=>{let t=e===O.homePath?O.homeTitle:A.meta.title||`Unnamed Page`;M.addTag({path:e,originalTitle:t,title:N(t),icon:e===O.homePath?O.homeIcon:A.meta.icon,meta:{affix:A.meta.affix}}),M.setActiveTag(e),W(e)},{immediate:!0}),f({tags:M,scrollToTag:W}),(t,a)=>{let f=r,p=n;return h(),c(`div`,D,[l(`div`,{class:`tags-track`,ref_key:`tagsTrack`,ref:z,onWheel:U},[l(`div`,{class:`tags-container`,ref_key:`tagsContainer`,ref:B},[(h(!0),c(i,null,_(b(M).tagsViewList.value,(t,n)=>(h(),o(f,{key:t.path,type:V(t)?`primary`:`default`,closable:!H(t),"data-path":t.path,onClose:C(e=>K(t,n),[`stop`]),onClick:e=>G(t),onContextmenu:C(e=>q(e,t,n),[`prevent`])},{icon:S(()=>[d(b(e),{name:t.icon,size:12},null,8,[`name`])]),default:S(()=>[u(` `+y(t.title),1)]),_:2},1032,[`type`,`closable`,`data-path`,`onClose`,`onClick`,`onContextmenu`]))),128))],512)],544),P.value?(h(),o(p,{key:0,show:P.value,options:Y.value,x:F.value,y:I.value,onClickoutside:J,onSelect:X},null,8,[`show`,`options`,`x`,`y`])):s(`v-if`,!0)])}}}),[[`__scopeId`,`data-v-1c8f8663`]]);export{E as n,O as t};
|
|
2
2
|
//# sourceMappingURL=C_TagsView2.js.map
|
package/dist/C_TagsView2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"C_TagsView2.js","names":[],"sources":["../src/components/C_TagsView/useTagsView.ts","../src/components/C_TagsView/index.vue","../src/components/C_TagsView/index.vue"],"sourcesContent":["import { ref, type Ref } from 'vue'\nimport type { TagItem } from '../_shared'\n\nexport interface UseTagsViewOptions {\n /** localStorage 持久化 key,传空字符串则不持久化 */\n persistKey?: string\n /** 首页路径,用于 removeAll 后的回退 */\n homePath?: string\n}\n\nconst STORAGE_KEY_DEFAULT = '__tags_view_list__'\n\n/**\n * 标签页管理 composable\n *\n * 独立于组件使用 — 可在任何地方管理标签状态\n *\n * @example 独立使用\n * ```ts\n * const tags = useTagsView({ persistKey: 'my-tags' })\n * tags.addTag({ path: '/home', title: '首页', meta: { affix: true } })\n * ```\n */\nexport function useTagsView(options: UseTagsViewOptions = {}) {\n const { persistKey = STORAGE_KEY_DEFAULT, homePath = '/home' } = options\n\n // ====== 持久化 ======\n const loadPersistedTags = (): TagItem[] => {\n if (!persistKey) return []\n try {\n const raw = localStorage.getItem(persistKey)\n return raw ? JSON.parse(raw) : []\n } catch {\n return []\n }\n }\n\n const persistTags = (tags: TagItem[]) => {\n if (!persistKey) return\n try {\n localStorage.setItem(persistKey, JSON.stringify(tags))\n } catch {\n /* silently fail */\n }\n }\n\n // ====== State ======\n const tagsViewList: Ref<TagItem[]> = ref(loadPersistedTags())\n const activeTag = ref('')\n\n const syncPersist = () => persistTags(tagsViewList.value)\n\n // ====== Actions ======\n\n const initTags = (tags?: TagItem[]) => {\n if (tags) {\n tagsViewList.value = tags\n } else {\n // 去重\n tagsViewList.value = [\n ...new Map(tagsViewList.value.map(tag => [tag.path, tag])).values(),\n ]\n }\n syncPersist()\n }\n\n const setActiveTag = (path: string) => {\n activeTag.value = path\n }\n\n const addTag = (tag: TagItem) => {\n const existingIndex = tagsViewList.value.findIndex(\n item => item.path === tag.path\n )\n\n if (existingIndex === -1) {\n tagsViewList.value.push(tag)\n } else {\n const existing = tagsViewList.value[existingIndex]\n if (existing.title !== tag.title || !existing.originalTitle) {\n existing.title = tag.title\n existing.originalTitle = tag.originalTitle || existing.originalTitle\n }\n }\n\n setActiveTag(tag.path)\n syncPersist()\n }\n\n const removeTag = (index: number): string | null => {\n if (index >= 0 && index < tagsViewList.value.length) {\n const removed = tagsViewList.value.splice(index, 1)[0]?.path ?? null\n syncPersist()\n return removed\n }\n return null\n }\n\n const removeOtherTags = (index: number) => {\n if (index === 0) {\n tagsViewList.value = [tagsViewList.value[0]].filter(Boolean)\n } else {\n tagsViewList.value = [\n tagsViewList.value[0],\n tagsViewList.value[index],\n ].filter(Boolean)\n }\n syncPersist()\n }\n\n const removeLeftTags = (index: number) => {\n tagsViewList.value = [\n tagsViewList.value[0],\n ...tagsViewList.value.slice(index),\n ]\n syncPersist()\n }\n\n const removeRightTags = (index: number) => {\n tagsViewList.value = tagsViewList.value.slice(0, index + 1)\n syncPersist()\n }\n\n const removeAllTags = () => {\n tagsViewList.value = tagsViewList.value.filter(tag => tag.meta?.affix)\n syncPersist()\n }\n\n return {\n tagsViewList,\n activeTag,\n homePath,\n initTags,\n setActiveTag,\n addTag,\n removeTag,\n removeOtherTags,\n removeLeftTags,\n removeRightTags,\n removeAllTags,\n }\n}\n","<!--\n * @Description: 通用标签页导航组件 — 路由感知的多标签管理\n *\n * 内置功能:\n * - 路由变化自动添加标签\n * - 持久化(localStorage,可关闭)\n * - 右键菜单(关闭/关闭其他/关闭左侧/关闭右侧/关闭所有)\n * - 水平滚轮滚动\n * - i18n 支持(labelFormatter)\n *\n * @example 基础用法\n * ```vue\n * <C_TagsView\n * :label-formatter=\"$t\"\n * @select=\"router.push\"\n * />\n * ```\n *\n * @example 自定义首页 + 菜单文案\n * ```vue\n * <C_TagsView\n * home-path=\"/dashboard\"\n * home-title=\"Dashboard\"\n * :context-menu-labels=\"{ close: 'Close', closeAll: 'Close All', ... }\"\n * />\n * ```\n *\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\n-->\n<template>\n <div class=\"tags-view-container\">\n <div\n class=\"tags-track\"\n ref=\"tagsTrack\"\n @wheel=\"handleWheel\"\n >\n <div\n class=\"tags-container\"\n ref=\"tagsContainer\"\n >\n <NTag\n v-for=\"(tag, index) in tags.tagsViewList.value\"\n :key=\"tag.path\"\n :type=\"isActive(tag) ? 'primary' : 'default'\"\n :closable=\"!isAffix(tag)\"\n :data-path=\"tag.path\"\n @close.stop=\"handleClose(tag, index)\"\n @click=\"navigateToTag(tag)\"\n @contextmenu.prevent=\"\n (e: MouseEvent) => showContextMenu(e, tag, index)\n \"\n >\n <template #icon>\n <C_Icon\n :name=\"tag.icon\"\n :size=\"12\"\n />\n </template>\n {{ tag.title }}\n </NTag>\n </div>\n </div>\n <NDropdown\n v-if=\"contextMenuVisible\"\n :show=\"contextMenuVisible\"\n :options=\"contextMenuOptions\"\n :x=\"contextMenuX\"\n :y=\"contextMenuY\"\n @clickoutside=\"closeContextMenu\"\n @select=\"handleContextMenuSelect\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\n import { ref, computed, watch, nextTick, h } from 'vue'\n import { useRoute, useRouter } from 'vue-router'\n import { C_Icon } from '../C_Icon'\n import type { TagItem } from '../_shared'\n import { useTagsView, type UseTagsViewOptions } from './useTagsView'\n\n defineOptions({ name: 'C_TagsView' })\n\n export interface ContextMenuLabels {\n close?: string\n closeOthers?: string\n closeLeft?: string\n closeRight?: string\n closeAll?: string\n }\n\n const props = withDefaults(\n defineProps<{\n /**\n * 标签文本格式化函数(用于 i18n)\n */\n labelFormatter?: (label: string) => string\n /** 首页路径 */\n homePath?: string\n /** 首页标题(原始文本) */\n homeTitle?: string\n /** 首页图标 */\n homeIcon?: string\n /** 持久化 localStorage key,传空字符串禁用 */\n persistKey?: string\n /** 右键菜单文案(国际化适配) */\n contextMenuLabels?: ContextMenuLabels\n /** useTagsView 配置项(优先级最高) */\n tagsViewOptions?: UseTagsViewOptions\n }>(),\n {\n homePath: '/home',\n homeTitle: '首页',\n homeIcon: 'mdi:home',\n persistKey: '__tags_view_list__',\n contextMenuLabels: () => ({\n close: '关闭',\n closeOthers: '关闭其他',\n closeLeft: '关闭左侧',\n closeRight: '关闭右侧',\n closeAll: '关闭所有',\n }),\n }\n )\n\n const emit = defineEmits<{\n /** 标签被关闭后跳转 */\n close: [fallbackPath: string]\n /** 关闭全部后 */\n closeAll: []\n }>()\n\n const route = useRoute()\n const router = useRouter()\n\n // ====== Tags composable ======\n const tags = useTagsView(\n props.tagsViewOptions ?? {\n persistKey: props.persistKey,\n homePath: props.homePath,\n }\n )\n\n // ====== 首次渲染前重新翻译持久化标签 ======\n if (tags.tagsViewList.value.length > 0 && props.labelFormatter) {\n const allRoutes = router.getRoutes()\n const translatedTags = tags.tagsViewList.value.map(tag => {\n const matchedRoute = allRoutes.find(r => r.path === tag.path)\n const originalTitle =\n tag.originalTitle || (matchedRoute?.meta?.title as string) || tag.title\n return {\n ...tag,\n originalTitle,\n title: props.labelFormatter!(originalTitle),\n }\n })\n tags.initTags(translatedTags)\n }\n\n const formatLabel = (title: string): string =>\n props.labelFormatter ? props.labelFormatter(title) : title\n\n // ====== UI State ======\n\n const contextMenuVisible = ref(false)\n const contextMenuX = ref(0)\n const contextMenuY = ref(0)\n const selectedTag = ref<TagItem | null>(null)\n const selectedIndex = ref(-1)\n const tagsTrack = ref<HTMLElement>()\n const tagsContainer = ref<HTMLElement>()\n\n // ====== Helpers ======\n const isActive = (tag: TagItem): boolean => tag.path === route.path\n const isAffix = (tag: TagItem) => tag.meta?.affix\n\n const handleWheel = (e: WheelEvent) => {\n const container = tagsContainer.value\n if (!container) return\n e.preventDefault()\n container.scrollTo({\n left: container.scrollLeft + e.deltaY,\n behavior: 'smooth',\n })\n }\n\n const scrollToTag = (path: string) => {\n nextTick(() => {\n const targetTag = tagsContainer.value?.querySelector(\n `[data-path=\"${path}\"]`\n ) as HTMLElement\n if (targetTag) {\n targetTag.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline: 'center',\n })\n }\n })\n }\n\n // ====== Actions ======\n\n const navigateToTag = (tag: TagItem) => {\n if (tag.path !== route.path) {\n router.push(tag.path)\n }\n scrollToTag(tag.path)\n }\n\n const handleClose = (tag: TagItem, index: number) => {\n if (isAffix(tag)) return\n\n const wasActive = isActive(tag)\n const tagsBeforeRemove = tags.tagsViewList.value.length\n tags.removeTag(index)\n\n if (wasActive) {\n const remaining = tags.tagsViewList.value\n const wasLastTag = index === tagsBeforeRemove - 1\n let fallbackPath: string\n\n if (wasLastTag) {\n fallbackPath = remaining[index - 1]?.path || props.homePath\n } else {\n fallbackPath = remaining[remaining.length - 1]?.path || props.homePath\n }\n\n router.push(fallbackPath)\n emit('close', fallbackPath)\n }\n }\n\n const showContextMenu = (event: MouseEvent, tag: TagItem, index: number) => {\n event.preventDefault()\n selectedTag.value = tag\n selectedIndex.value = index\n contextMenuVisible.value = true\n contextMenuX.value = event.clientX\n contextMenuY.value = event.clientY\n }\n\n const closeContextMenu = () => (contextMenuVisible.value = false)\n\n const contextMenuOptions = computed(() => {\n const labels = props.contextMenuLabels\n return [\n {\n type: 'option' as const,\n label: labels.close ?? '关闭',\n key: 'close',\n disabled: isAffix(selectedTag.value as TagItem),\n icon: () => h('span', { class: 'i-mdi:close' }),\n },\n {\n type: 'option' as const,\n label: labels.closeOthers ?? '关闭其他',\n key: 'closeOthers',\n icon: () => h('span', { class: 'i-mdi:arrow-left-right-bold' }),\n },\n {\n type: 'option' as const,\n label: labels.closeLeft ?? '关闭左侧',\n key: 'closeLeft',\n icon: () => h('span', { class: 'i-mdi:align-horizontal-left' }),\n },\n {\n type: 'option' as const,\n label: labels.closeRight ?? '关闭右侧',\n key: 'closeRight',\n icon: () => h('span', { class: 'i-mdi:align-horizontal-right' }),\n },\n {\n type: 'option' as const,\n label: labels.closeAll ?? '关闭所有',\n key: 'closeAll',\n icon: () => h('span', { class: 'i-mdi:tally-mark-5' }),\n },\n ]\n })\n\n const handleContextMenuSelect = (key: string) => {\n if (!selectedTag.value || selectedIndex.value === -1) return\n\n switch (key) {\n case 'close':\n handleClose(selectedTag.value, selectedIndex.value)\n break\n case 'closeOthers':\n tags.removeOtherTags(selectedIndex.value)\n break\n case 'closeLeft':\n tags.removeLeftTags(selectedIndex.value)\n break\n case 'closeRight':\n tags.removeRightTags(selectedIndex.value)\n break\n case 'closeAll':\n tags.removeAllTags()\n router.push(props.homePath)\n emit('closeAll')\n break\n }\n closeContextMenu()\n }\n\n // ====== 路由自动跟踪 ======\n watch(\n () => route.path,\n (newPath: string) => {\n const originalTitle =\n newPath === props.homePath\n ? props.homeTitle\n : (route.meta.title as string) || 'Unnamed Page'\n\n tags.addTag({\n path: newPath,\n originalTitle,\n title: formatLabel(originalTitle),\n icon:\n newPath === props.homePath\n ? props.homeIcon\n : (route.meta.icon as string),\n meta: { affix: route.meta.affix as boolean },\n })\n\n tags.setActiveTag(newPath)\n scrollToTag(newPath)\n },\n { immediate: true }\n )\n\n // ====== 暴露 ======\n defineExpose({\n /** useTagsView composable 实例 */\n tags,\n /** 滚动到指定标签 */\n scrollToTag,\n })\n</script>\n\n<style lang=\"scss\" scoped>\n @use './index.scss';\n</style>\n","<!--\n * @Description: 通用标签页导航组件 — 路由感知的多标签管理\n *\n * 内置功能:\n * - 路由变化自动添加标签\n * - 持久化(localStorage,可关闭)\n * - 右键菜单(关闭/关闭其他/关闭左侧/关闭右侧/关闭所有)\n * - 水平滚轮滚动\n * - i18n 支持(labelFormatter)\n *\n * @example 基础用法\n * ```vue\n * <C_TagsView\n * :label-formatter=\"$t\"\n * @select=\"router.push\"\n * />\n * ```\n *\n * @example 自定义首页 + 菜单文案\n * ```vue\n * <C_TagsView\n * home-path=\"/dashboard\"\n * home-title=\"Dashboard\"\n * :context-menu-labels=\"{ close: 'Close', closeAll: 'Close All', ... }\"\n * />\n * ```\n *\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\n-->\n<template>\n <div class=\"tags-view-container\">\n <div\n class=\"tags-track\"\n ref=\"tagsTrack\"\n @wheel=\"handleWheel\"\n >\n <div\n class=\"tags-container\"\n ref=\"tagsContainer\"\n >\n <NTag\n v-for=\"(tag, index) in tags.tagsViewList.value\"\n :key=\"tag.path\"\n :type=\"isActive(tag) ? 'primary' : 'default'\"\n :closable=\"!isAffix(tag)\"\n :data-path=\"tag.path\"\n @close.stop=\"handleClose(tag, index)\"\n @click=\"navigateToTag(tag)\"\n @contextmenu.prevent=\"\n (e: MouseEvent) => showContextMenu(e, tag, index)\n \"\n >\n <template #icon>\n <C_Icon\n :name=\"tag.icon\"\n :size=\"12\"\n />\n </template>\n {{ tag.title }}\n </NTag>\n </div>\n </div>\n <NDropdown\n v-if=\"contextMenuVisible\"\n :show=\"contextMenuVisible\"\n :options=\"contextMenuOptions\"\n :x=\"contextMenuX\"\n :y=\"contextMenuY\"\n @clickoutside=\"closeContextMenu\"\n @select=\"handleContextMenuSelect\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\n import { ref, computed, watch, nextTick, h } from 'vue'\n import { useRoute, useRouter } from 'vue-router'\n import { C_Icon } from '../C_Icon'\n import type { TagItem } from '../_shared'\n import { useTagsView, type UseTagsViewOptions } from './useTagsView'\n\n defineOptions({ name: 'C_TagsView' })\n\n export interface ContextMenuLabels {\n close?: string\n closeOthers?: string\n closeLeft?: string\n closeRight?: string\n closeAll?: string\n }\n\n const props = withDefaults(\n defineProps<{\n /**\n * 标签文本格式化函数(用于 i18n)\n */\n labelFormatter?: (label: string) => string\n /** 首页路径 */\n homePath?: string\n /** 首页标题(原始文本) */\n homeTitle?: string\n /** 首页图标 */\n homeIcon?: string\n /** 持久化 localStorage key,传空字符串禁用 */\n persistKey?: string\n /** 右键菜单文案(国际化适配) */\n contextMenuLabels?: ContextMenuLabels\n /** useTagsView 配置项(优先级最高) */\n tagsViewOptions?: UseTagsViewOptions\n }>(),\n {\n homePath: '/home',\n homeTitle: '首页',\n homeIcon: 'mdi:home',\n persistKey: '__tags_view_list__',\n contextMenuLabels: () => ({\n close: '关闭',\n closeOthers: '关闭其他',\n closeLeft: '关闭左侧',\n closeRight: '关闭右侧',\n closeAll: '关闭所有',\n }),\n }\n )\n\n const emit = defineEmits<{\n /** 标签被关闭后跳转 */\n close: [fallbackPath: string]\n /** 关闭全部后 */\n closeAll: []\n }>()\n\n const route = useRoute()\n const router = useRouter()\n\n // ====== Tags composable ======\n const tags = useTagsView(\n props.tagsViewOptions ?? {\n persistKey: props.persistKey,\n homePath: props.homePath,\n }\n )\n\n // ====== 首次渲染前重新翻译持久化标签 ======\n if (tags.tagsViewList.value.length > 0 && props.labelFormatter) {\n const allRoutes = router.getRoutes()\n const translatedTags = tags.tagsViewList.value.map(tag => {\n const matchedRoute = allRoutes.find(r => r.path === tag.path)\n const originalTitle =\n tag.originalTitle || (matchedRoute?.meta?.title as string) || tag.title\n return {\n ...tag,\n originalTitle,\n title: props.labelFormatter!(originalTitle),\n }\n })\n tags.initTags(translatedTags)\n }\n\n const formatLabel = (title: string): string =>\n props.labelFormatter ? props.labelFormatter(title) : title\n\n // ====== UI State ======\n\n const contextMenuVisible = ref(false)\n const contextMenuX = ref(0)\n const contextMenuY = ref(0)\n const selectedTag = ref<TagItem | null>(null)\n const selectedIndex = ref(-1)\n const tagsTrack = ref<HTMLElement>()\n const tagsContainer = ref<HTMLElement>()\n\n // ====== Helpers ======\n const isActive = (tag: TagItem): boolean => tag.path === route.path\n const isAffix = (tag: TagItem) => tag.meta?.affix\n\n const handleWheel = (e: WheelEvent) => {\n const container = tagsContainer.value\n if (!container) return\n e.preventDefault()\n container.scrollTo({\n left: container.scrollLeft + e.deltaY,\n behavior: 'smooth',\n })\n }\n\n const scrollToTag = (path: string) => {\n nextTick(() => {\n const targetTag = tagsContainer.value?.querySelector(\n `[data-path=\"${path}\"]`\n ) as HTMLElement\n if (targetTag) {\n targetTag.scrollIntoView({\n behavior: 'smooth',\n block: 'nearest',\n inline: 'center',\n })\n }\n })\n }\n\n // ====== Actions ======\n\n const navigateToTag = (tag: TagItem) => {\n if (tag.path !== route.path) {\n router.push(tag.path)\n }\n scrollToTag(tag.path)\n }\n\n const handleClose = (tag: TagItem, index: number) => {\n if (isAffix(tag)) return\n\n const wasActive = isActive(tag)\n const tagsBeforeRemove = tags.tagsViewList.value.length\n tags.removeTag(index)\n\n if (wasActive) {\n const remaining = tags.tagsViewList.value\n const wasLastTag = index === tagsBeforeRemove - 1\n let fallbackPath: string\n\n if (wasLastTag) {\n fallbackPath = remaining[index - 1]?.path || props.homePath\n } else {\n fallbackPath = remaining[remaining.length - 1]?.path || props.homePath\n }\n\n router.push(fallbackPath)\n emit('close', fallbackPath)\n }\n }\n\n const showContextMenu = (event: MouseEvent, tag: TagItem, index: number) => {\n event.preventDefault()\n selectedTag.value = tag\n selectedIndex.value = index\n contextMenuVisible.value = true\n contextMenuX.value = event.clientX\n contextMenuY.value = event.clientY\n }\n\n const closeContextMenu = () => (contextMenuVisible.value = false)\n\n const contextMenuOptions = computed(() => {\n const labels = props.contextMenuLabels\n return [\n {\n type: 'option' as const,\n label: labels.close ?? '关闭',\n key: 'close',\n disabled: isAffix(selectedTag.value as TagItem),\n icon: () => h('span', { class: 'i-mdi:close' }),\n },\n {\n type: 'option' as const,\n label: labels.closeOthers ?? '关闭其他',\n key: 'closeOthers',\n icon: () => h('span', { class: 'i-mdi:arrow-left-right-bold' }),\n },\n {\n type: 'option' as const,\n label: labels.closeLeft ?? '关闭左侧',\n key: 'closeLeft',\n icon: () => h('span', { class: 'i-mdi:align-horizontal-left' }),\n },\n {\n type: 'option' as const,\n label: labels.closeRight ?? '关闭右侧',\n key: 'closeRight',\n icon: () => h('span', { class: 'i-mdi:align-horizontal-right' }),\n },\n {\n type: 'option' as const,\n label: labels.closeAll ?? '关闭所有',\n key: 'closeAll',\n icon: () => h('span', { class: 'i-mdi:tally-mark-5' }),\n },\n ]\n })\n\n const handleContextMenuSelect = (key: string) => {\n if (!selectedTag.value || selectedIndex.value === -1) return\n\n switch (key) {\n case 'close':\n handleClose(selectedTag.value, selectedIndex.value)\n break\n case 'closeOthers':\n tags.removeOtherTags(selectedIndex.value)\n break\n case 'closeLeft':\n tags.removeLeftTags(selectedIndex.value)\n break\n case 'closeRight':\n tags.removeRightTags(selectedIndex.value)\n break\n case 'closeAll':\n tags.removeAllTags()\n router.push(props.homePath)\n emit('closeAll')\n break\n }\n closeContextMenu()\n }\n\n // ====== 路由自动跟踪 ======\n watch(\n () => route.path,\n (newPath: string) => {\n const originalTitle =\n newPath === props.homePath\n ? props.homeTitle\n : (route.meta.title as string) || 'Unnamed Page'\n\n tags.addTag({\n path: newPath,\n originalTitle,\n title: formatLabel(originalTitle),\n icon:\n newPath === props.homePath\n ? props.homeIcon\n : (route.meta.icon as string),\n meta: { affix: route.meta.affix as boolean },\n })\n\n tags.setActiveTag(newPath)\n scrollToTag(newPath)\n },\n { immediate: true }\n )\n\n // ====== 暴露 ======\n defineExpose({\n /** useTagsView composable 实例 */\n tags,\n /** 滚动到指定标签 */\n scrollToTag,\n })\n</script>\n\n<style lang=\"scss\" scoped>\n @use './index.scss';\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"C_TagsView2.js","names":[],"sources":["../src/components/C_TagsView/useTagsView.ts","../src/components/C_TagsView/index.vue","../src/components/C_TagsView/index.vue"],"sourcesContent":["import { ref, type Ref } from 'vue'\nimport type { TagItem } from '../_shared'\n\nexport interface UseTagsViewOptions {\n /** localStorage 持久化 key,传空字符串则不持久化 */\n persistKey?: string\n /** 首页路径,用于 removeAll 后的回退 */\n homePath?: string\n}\n\nconst STORAGE_KEY_DEFAULT = '__tags_view_list__'\n\n/**\n * 标签页管理 composable\n *\n * 独立于组件使用 — 可在任何地方管理标签状态\n *\n * @example 独立使用\n * ```ts\n * const tags = useTagsView({ persistKey: 'my-tags' })\n * tags.addTag({ path: '/home', title: '首页', meta: { affix: true } })\n * ```\n */\nexport function useTagsView(options: UseTagsViewOptions = {}) {\n const { persistKey = STORAGE_KEY_DEFAULT, homePath = '/home' } = options\n\n // ====== 持久化 ======\n const loadPersistedTags = (): TagItem[] => {\n if (!persistKey) return []\n try {\n const raw = localStorage.getItem(persistKey)\n return raw ? JSON.parse(raw) : []\n } catch {\n return []\n }\n }\n\n const persistTags = (tags: TagItem[]) => {\n if (!persistKey) return\n try {\n localStorage.setItem(persistKey, JSON.stringify(tags))\n } catch {\n /* silently fail */\n }\n }\n\n // ====== State ======\n const tagsViewList: Ref<TagItem[]> = ref(loadPersistedTags())\n const activeTag = ref('')\n\n const syncPersist = () => persistTags(tagsViewList.value)\n\n // ====== Actions ======\n\n const initTags = (tags?: TagItem[]) => {\n if (tags) {\n tagsViewList.value = tags\n } else {\n // 去重\n tagsViewList.value = [\n ...new Map(tagsViewList.value.map(tag => [tag.path, tag])).values(),\n ]\n }\n syncPersist()\n }\n\n const setActiveTag = (path: string) => {\n activeTag.value = path\n }\n\n const addTag = (tag: TagItem) => {\n const existingIndex = tagsViewList.value.findIndex(\n item => item.path === tag.path\n )\n\n if (existingIndex === -1) {\n tagsViewList.value.push(tag)\n } else {\n const existing = tagsViewList.value[existingIndex]\n if (existing.title !== tag.title || !existing.originalTitle) {\n existing.title = tag.title\n existing.originalTitle = tag.originalTitle || existing.originalTitle\n }\n }\n\n setActiveTag(tag.path)\n syncPersist()\n }\n\n const removeTag = (index: number): string | null => {\n if (index >= 0 && index < tagsViewList.value.length) {\n const removed = tagsViewList.value.splice(index, 1)[0]?.path ?? null\n syncPersist()\n return removed\n }\n return null\n }\n\n const removeOtherTags = (index: number) => {\n if (index === 0) {\n tagsViewList.value = [tagsViewList.value[0]].filter(Boolean)\n } else {\n tagsViewList.value = [\n tagsViewList.value[0],\n tagsViewList.value[index],\n ].filter(Boolean)\n }\n syncPersist()\n }\n\n const removeLeftTags = (index: number) => {\n tagsViewList.value = [\n tagsViewList.value[0],\n ...tagsViewList.value.slice(index),\n ]\n syncPersist()\n }\n\n const removeRightTags = (index: number) => {\n tagsViewList.value = tagsViewList.value.slice(0, index + 1)\n syncPersist()\n }\n\n const removeAllTags = () => {\n tagsViewList.value = tagsViewList.value.filter(tag => tag.meta?.affix)\n syncPersist()\n }\n\n return {\n tagsViewList,\n activeTag,\n homePath,\n initTags,\n setActiveTag,\n addTag,\n removeTag,\n removeOtherTags,\n removeLeftTags,\n removeRightTags,\n removeAllTags,\n }\n}\n","<!--\r\n * @Description: 通用标签页导航组件 — 路由感知的多标签管理\r\n *\r\n * 内置功能:\r\n * - 路由变化自动添加标签\r\n * - 持久化(localStorage,可关闭)\r\n * - 右键菜单(关闭/关闭其他/关闭左侧/关闭右侧/关闭所有)\r\n * - 水平滚轮滚动\r\n * - i18n 支持(labelFormatter)\r\n *\r\n * @example 基础用法\r\n * ```vue\r\n * <C_TagsView\r\n * :label-formatter=\"$t\"\r\n * @select=\"router.push\"\r\n * />\r\n * ```\r\n *\r\n * @example 自定义首页 + 菜单文案\r\n * ```vue\r\n * <C_TagsView\r\n * home-path=\"/dashboard\"\r\n * home-title=\"Dashboard\"\r\n * :context-menu-labels=\"{ close: 'Close', closeAll: 'Close All', ... }\"\r\n * />\r\n * ```\r\n *\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n<template>\r\n <div class=\"tags-view-container\">\r\n <div\r\n class=\"tags-track\"\r\n ref=\"tagsTrack\"\r\n @wheel=\"handleWheel\"\r\n >\r\n <div\r\n class=\"tags-container\"\r\n ref=\"tagsContainer\"\r\n >\r\n <NTag\r\n v-for=\"(tag, index) in tags.tagsViewList.value\"\r\n :key=\"tag.path\"\r\n :type=\"isActive(tag) ? 'primary' : 'default'\"\r\n :closable=\"!isAffix(tag)\"\r\n :data-path=\"tag.path\"\r\n @close.stop=\"handleClose(tag, index)\"\r\n @click=\"navigateToTag(tag)\"\r\n @contextmenu.prevent=\"\r\n (e: MouseEvent) => showContextMenu(e, tag, index)\r\n \"\r\n >\r\n <template #icon>\r\n <C_Icon\r\n :name=\"tag.icon\"\r\n :size=\"12\"\r\n />\r\n </template>\r\n {{ tag.title }}\r\n </NTag>\r\n </div>\r\n </div>\r\n <NDropdown\r\n v-if=\"contextMenuVisible\"\r\n :show=\"contextMenuVisible\"\r\n :options=\"contextMenuOptions\"\r\n :x=\"contextMenuX\"\r\n :y=\"contextMenuY\"\r\n @clickoutside=\"closeContextMenu\"\r\n @select=\"handleContextMenuSelect\"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n import { ref, computed, watch, nextTick, h } from 'vue'\r\n import { useRoute, useRouter } from 'vue-router'\r\n import { C_Icon } from '../C_Icon'\r\n import type { TagItem } from '../_shared'\r\n import { useTagsView, type UseTagsViewOptions } from './useTagsView'\r\n import type { ContextMenuLabels } from './types'\r\n\r\n defineOptions({ name: 'C_TagsView' })\r\n\r\n interface LegacyContextMenuLabels extends ContextMenuLabels {\r\n close?: string\r\n closeOthers?: string\r\n closeLeft?: string\r\n closeRight?: string\r\n closeAll?: string\r\n }\r\n\r\n const props = withDefaults(\r\n defineProps<{\r\n /**\r\n * 标签文本格式化函数(用于 i18n)\r\n */\r\n labelFormatter?: (label: string) => string\r\n /** 首页路径 */\r\n homePath?: string\r\n /** 首页标题(原始文本) */\r\n homeTitle?: string\r\n /** 首页图标 */\r\n homeIcon?: string\r\n /** 持久化 localStorage key,传空字符串禁用 */\r\n persistKey?: string\r\n /** 右键菜单文案(国际化适配) */\r\n contextMenuLabels?: LegacyContextMenuLabels\r\n /** useTagsView 配置项(优先级最高) */\r\n tagsViewOptions?: UseTagsViewOptions\r\n }>(),\r\n {\r\n homePath: '/home',\r\n homeTitle: '首页',\r\n homeIcon: 'mdi:home',\r\n persistKey: '__tags_view_list__',\r\n contextMenuLabels: () => ({\r\n close: '关闭',\r\n closeOthers: '关闭其他',\r\n closeLeft: '关闭左侧',\r\n closeRight: '关闭右侧',\r\n closeAll: '关闭所有',\r\n }),\r\n }\r\n )\r\n\r\n const emit = defineEmits<{\r\n /** 标签被关闭后跳转 */\r\n close: [fallbackPath: string]\r\n /** 关闭全部后 */\r\n closeAll: []\r\n }>()\r\n\r\n const route = useRoute()\r\n const router = useRouter()\r\n\r\n // ====== Tags composable ======\r\n const tags = useTagsView(\r\n props.tagsViewOptions ?? {\r\n persistKey: props.persistKey,\r\n homePath: props.homePath,\r\n }\r\n )\r\n\r\n // ====== 首次渲染前重新翻译持久化标签 ======\r\n if (tags.tagsViewList.value.length > 0 && props.labelFormatter) {\r\n const allRoutes = router.getRoutes()\r\n const translatedTags = tags.tagsViewList.value.map(tag => {\r\n const matchedRoute = allRoutes.find(r => r.path === tag.path)\r\n const originalTitle =\r\n tag.originalTitle || (matchedRoute?.meta?.title as string) || tag.title\r\n return {\r\n ...tag,\r\n originalTitle,\r\n title: props.labelFormatter!(originalTitle),\r\n }\r\n })\r\n tags.initTags(translatedTags)\r\n }\r\n\r\n const formatLabel = (title: string): string =>\r\n props.labelFormatter ? props.labelFormatter(title) : title\r\n\r\n // ====== UI State ======\r\n\r\n const contextMenuVisible = ref(false)\r\n const contextMenuX = ref(0)\r\n const contextMenuY = ref(0)\r\n const selectedTag = ref<TagItem | null>(null)\r\n const selectedIndex = ref(-1)\r\n const tagsTrack = ref<HTMLElement>()\r\n const tagsContainer = ref<HTMLElement>()\r\n\r\n // ====== Helpers ======\r\n const isActive = (tag: TagItem): boolean => tag.path === route.path\r\n const isAffix = (tag: TagItem) => tag.meta?.affix\r\n\r\n const handleWheel = (e: WheelEvent) => {\r\n const container = tagsContainer.value\r\n if (!container) return\r\n e.preventDefault()\r\n container.scrollTo({\r\n left: container.scrollLeft + e.deltaY,\r\n behavior: 'smooth',\r\n })\r\n }\r\n\r\n const scrollToTag = (path: string) => {\r\n nextTick(() => {\r\n const targetTag = tagsContainer.value?.querySelector(\r\n `[data-path=\"${path}\"]`\r\n ) as HTMLElement\r\n if (targetTag) {\r\n targetTag.scrollIntoView({\r\n behavior: 'smooth',\r\n block: 'nearest',\r\n inline: 'center',\r\n })\r\n }\r\n })\r\n }\r\n\r\n // ====== Actions ======\r\n\r\n const navigateToTag = (tag: TagItem) => {\r\n if (tag.path !== route.path) {\r\n router.push(tag.path)\r\n }\r\n scrollToTag(tag.path)\r\n }\r\n\r\n const handleClose = (tag: TagItem, index: number) => {\r\n if (isAffix(tag)) return\r\n\r\n const wasActive = isActive(tag)\r\n const tagsBeforeRemove = tags.tagsViewList.value.length\r\n tags.removeTag(index)\r\n\r\n if (wasActive) {\r\n const remaining = tags.tagsViewList.value\r\n const wasLastTag = index === tagsBeforeRemove - 1\r\n let fallbackPath: string\r\n\r\n if (wasLastTag) {\r\n fallbackPath = remaining[index - 1]?.path || props.homePath\r\n } else {\r\n fallbackPath = remaining[remaining.length - 1]?.path || props.homePath\r\n }\r\n\r\n router.push(fallbackPath)\r\n emit('close', fallbackPath)\r\n }\r\n }\r\n\r\n const showContextMenu = (event: MouseEvent, tag: TagItem, index: number) => {\r\n event.preventDefault()\r\n selectedTag.value = tag\r\n selectedIndex.value = index\r\n contextMenuVisible.value = true\r\n contextMenuX.value = event.clientX\r\n contextMenuY.value = event.clientY\r\n }\r\n\r\n const closeContextMenu = () => (contextMenuVisible.value = false)\r\n\r\n const contextMenuOptions = computed(() => {\r\n const labels = props.contextMenuLabels\r\n return [\r\n {\r\n type: 'option' as const,\r\n label: labels.close ?? '关闭',\r\n key: 'close',\r\n disabled: isAffix(selectedTag.value as TagItem),\r\n icon: () => h('span', { class: 'i-mdi:close' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeOthers ?? '关闭其他',\r\n key: 'closeOthers',\r\n icon: () => h('span', { class: 'i-mdi:arrow-left-right-bold' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeLeft ?? '关闭左侧',\r\n key: 'closeLeft',\r\n icon: () => h('span', { class: 'i-mdi:align-horizontal-left' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeRight ?? '关闭右侧',\r\n key: 'closeRight',\r\n icon: () => h('span', { class: 'i-mdi:align-horizontal-right' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeAll ?? '关闭所有',\r\n key: 'closeAll',\r\n icon: () => h('span', { class: 'i-mdi:tally-mark-5' }),\r\n },\r\n ]\r\n })\r\n\r\n const handleContextMenuSelect = (key: string) => {\r\n if (!selectedTag.value || selectedIndex.value === -1) return\r\n\r\n switch (key) {\r\n case 'close':\r\n handleClose(selectedTag.value, selectedIndex.value)\r\n break\r\n case 'closeOthers':\r\n tags.removeOtherTags(selectedIndex.value)\r\n break\r\n case 'closeLeft':\r\n tags.removeLeftTags(selectedIndex.value)\r\n break\r\n case 'closeRight':\r\n tags.removeRightTags(selectedIndex.value)\r\n break\r\n case 'closeAll':\r\n tags.removeAllTags()\r\n router.push(props.homePath)\r\n emit('closeAll')\r\n break\r\n }\r\n closeContextMenu()\r\n }\r\n\r\n // ====== 路由自动跟踪 ======\r\n watch(\r\n () => route.path,\r\n (newPath: string) => {\r\n const originalTitle =\r\n newPath === props.homePath\r\n ? props.homeTitle\r\n : (route.meta.title as string) || 'Unnamed Page'\r\n\r\n tags.addTag({\r\n path: newPath,\r\n originalTitle,\r\n title: formatLabel(originalTitle),\r\n icon:\r\n newPath === props.homePath\r\n ? props.homeIcon\r\n : (route.meta.icon as string),\r\n meta: { affix: route.meta.affix as boolean },\r\n })\r\n\r\n tags.setActiveTag(newPath)\r\n scrollToTag(newPath)\r\n },\r\n { immediate: true }\r\n )\r\n\r\n // ====== 暴露 ======\r\n defineExpose({\r\n /** useTagsView composable 实例 */\r\n tags,\r\n /** 滚动到指定标签 */\r\n scrollToTag,\r\n })\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n @use './index.scss';\r\n</style>\r\n","<!--\r\n * @Description: 通用标签页导航组件 — 路由感知的多标签管理\r\n *\r\n * 内置功能:\r\n * - 路由变化自动添加标签\r\n * - 持久化(localStorage,可关闭)\r\n * - 右键菜单(关闭/关闭其他/关闭左侧/关闭右侧/关闭所有)\r\n * - 水平滚轮滚动\r\n * - i18n 支持(labelFormatter)\r\n *\r\n * @example 基础用法\r\n * ```vue\r\n * <C_TagsView\r\n * :label-formatter=\"$t\"\r\n * @select=\"router.push\"\r\n * />\r\n * ```\r\n *\r\n * @example 自定义首页 + 菜单文案\r\n * ```vue\r\n * <C_TagsView\r\n * home-path=\"/dashboard\"\r\n * home-title=\"Dashboard\"\r\n * :context-menu-labels=\"{ close: 'Close', closeAll: 'Close All', ... }\"\r\n * />\r\n * ```\r\n *\r\n * Copyright (c) 2025 by CHENY, All Rights Reserved.\r\n-->\r\n<template>\r\n <div class=\"tags-view-container\">\r\n <div\r\n class=\"tags-track\"\r\n ref=\"tagsTrack\"\r\n @wheel=\"handleWheel\"\r\n >\r\n <div\r\n class=\"tags-container\"\r\n ref=\"tagsContainer\"\r\n >\r\n <NTag\r\n v-for=\"(tag, index) in tags.tagsViewList.value\"\r\n :key=\"tag.path\"\r\n :type=\"isActive(tag) ? 'primary' : 'default'\"\r\n :closable=\"!isAffix(tag)\"\r\n :data-path=\"tag.path\"\r\n @close.stop=\"handleClose(tag, index)\"\r\n @click=\"navigateToTag(tag)\"\r\n @contextmenu.prevent=\"\r\n (e: MouseEvent) => showContextMenu(e, tag, index)\r\n \"\r\n >\r\n <template #icon>\r\n <C_Icon\r\n :name=\"tag.icon\"\r\n :size=\"12\"\r\n />\r\n </template>\r\n {{ tag.title }}\r\n </NTag>\r\n </div>\r\n </div>\r\n <NDropdown\r\n v-if=\"contextMenuVisible\"\r\n :show=\"contextMenuVisible\"\r\n :options=\"contextMenuOptions\"\r\n :x=\"contextMenuX\"\r\n :y=\"contextMenuY\"\r\n @clickoutside=\"closeContextMenu\"\r\n @select=\"handleContextMenuSelect\"\r\n />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n import { ref, computed, watch, nextTick, h } from 'vue'\r\n import { useRoute, useRouter } from 'vue-router'\r\n import { C_Icon } from '../C_Icon'\r\n import type { TagItem } from '../_shared'\r\n import { useTagsView, type UseTagsViewOptions } from './useTagsView'\r\n import type { ContextMenuLabels } from './types'\r\n\r\n defineOptions({ name: 'C_TagsView' })\r\n\r\n interface LegacyContextMenuLabels extends ContextMenuLabels {\r\n close?: string\r\n closeOthers?: string\r\n closeLeft?: string\r\n closeRight?: string\r\n closeAll?: string\r\n }\r\n\r\n const props = withDefaults(\r\n defineProps<{\r\n /**\r\n * 标签文本格式化函数(用于 i18n)\r\n */\r\n labelFormatter?: (label: string) => string\r\n /** 首页路径 */\r\n homePath?: string\r\n /** 首页标题(原始文本) */\r\n homeTitle?: string\r\n /** 首页图标 */\r\n homeIcon?: string\r\n /** 持久化 localStorage key,传空字符串禁用 */\r\n persistKey?: string\r\n /** 右键菜单文案(国际化适配) */\r\n contextMenuLabels?: LegacyContextMenuLabels\r\n /** useTagsView 配置项(优先级最高) */\r\n tagsViewOptions?: UseTagsViewOptions\r\n }>(),\r\n {\r\n homePath: '/home',\r\n homeTitle: '首页',\r\n homeIcon: 'mdi:home',\r\n persistKey: '__tags_view_list__',\r\n contextMenuLabels: () => ({\r\n close: '关闭',\r\n closeOthers: '关闭其他',\r\n closeLeft: '关闭左侧',\r\n closeRight: '关闭右侧',\r\n closeAll: '关闭所有',\r\n }),\r\n }\r\n )\r\n\r\n const emit = defineEmits<{\r\n /** 标签被关闭后跳转 */\r\n close: [fallbackPath: string]\r\n /** 关闭全部后 */\r\n closeAll: []\r\n }>()\r\n\r\n const route = useRoute()\r\n const router = useRouter()\r\n\r\n // ====== Tags composable ======\r\n const tags = useTagsView(\r\n props.tagsViewOptions ?? {\r\n persistKey: props.persistKey,\r\n homePath: props.homePath,\r\n }\r\n )\r\n\r\n // ====== 首次渲染前重新翻译持久化标签 ======\r\n if (tags.tagsViewList.value.length > 0 && props.labelFormatter) {\r\n const allRoutes = router.getRoutes()\r\n const translatedTags = tags.tagsViewList.value.map(tag => {\r\n const matchedRoute = allRoutes.find(r => r.path === tag.path)\r\n const originalTitle =\r\n tag.originalTitle || (matchedRoute?.meta?.title as string) || tag.title\r\n return {\r\n ...tag,\r\n originalTitle,\r\n title: props.labelFormatter!(originalTitle),\r\n }\r\n })\r\n tags.initTags(translatedTags)\r\n }\r\n\r\n const formatLabel = (title: string): string =>\r\n props.labelFormatter ? props.labelFormatter(title) : title\r\n\r\n // ====== UI State ======\r\n\r\n const contextMenuVisible = ref(false)\r\n const contextMenuX = ref(0)\r\n const contextMenuY = ref(0)\r\n const selectedTag = ref<TagItem | null>(null)\r\n const selectedIndex = ref(-1)\r\n const tagsTrack = ref<HTMLElement>()\r\n const tagsContainer = ref<HTMLElement>()\r\n\r\n // ====== Helpers ======\r\n const isActive = (tag: TagItem): boolean => tag.path === route.path\r\n const isAffix = (tag: TagItem) => tag.meta?.affix\r\n\r\n const handleWheel = (e: WheelEvent) => {\r\n const container = tagsContainer.value\r\n if (!container) return\r\n e.preventDefault()\r\n container.scrollTo({\r\n left: container.scrollLeft + e.deltaY,\r\n behavior: 'smooth',\r\n })\r\n }\r\n\r\n const scrollToTag = (path: string) => {\r\n nextTick(() => {\r\n const targetTag = tagsContainer.value?.querySelector(\r\n `[data-path=\"${path}\"]`\r\n ) as HTMLElement\r\n if (targetTag) {\r\n targetTag.scrollIntoView({\r\n behavior: 'smooth',\r\n block: 'nearest',\r\n inline: 'center',\r\n })\r\n }\r\n })\r\n }\r\n\r\n // ====== Actions ======\r\n\r\n const navigateToTag = (tag: TagItem) => {\r\n if (tag.path !== route.path) {\r\n router.push(tag.path)\r\n }\r\n scrollToTag(tag.path)\r\n }\r\n\r\n const handleClose = (tag: TagItem, index: number) => {\r\n if (isAffix(tag)) return\r\n\r\n const wasActive = isActive(tag)\r\n const tagsBeforeRemove = tags.tagsViewList.value.length\r\n tags.removeTag(index)\r\n\r\n if (wasActive) {\r\n const remaining = tags.tagsViewList.value\r\n const wasLastTag = index === tagsBeforeRemove - 1\r\n let fallbackPath: string\r\n\r\n if (wasLastTag) {\r\n fallbackPath = remaining[index - 1]?.path || props.homePath\r\n } else {\r\n fallbackPath = remaining[remaining.length - 1]?.path || props.homePath\r\n }\r\n\r\n router.push(fallbackPath)\r\n emit('close', fallbackPath)\r\n }\r\n }\r\n\r\n const showContextMenu = (event: MouseEvent, tag: TagItem, index: number) => {\r\n event.preventDefault()\r\n selectedTag.value = tag\r\n selectedIndex.value = index\r\n contextMenuVisible.value = true\r\n contextMenuX.value = event.clientX\r\n contextMenuY.value = event.clientY\r\n }\r\n\r\n const closeContextMenu = () => (contextMenuVisible.value = false)\r\n\r\n const contextMenuOptions = computed(() => {\r\n const labels = props.contextMenuLabels\r\n return [\r\n {\r\n type: 'option' as const,\r\n label: labels.close ?? '关闭',\r\n key: 'close',\r\n disabled: isAffix(selectedTag.value as TagItem),\r\n icon: () => h('span', { class: 'i-mdi:close' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeOthers ?? '关闭其他',\r\n key: 'closeOthers',\r\n icon: () => h('span', { class: 'i-mdi:arrow-left-right-bold' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeLeft ?? '关闭左侧',\r\n key: 'closeLeft',\r\n icon: () => h('span', { class: 'i-mdi:align-horizontal-left' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeRight ?? '关闭右侧',\r\n key: 'closeRight',\r\n icon: () => h('span', { class: 'i-mdi:align-horizontal-right' }),\r\n },\r\n {\r\n type: 'option' as const,\r\n label: labels.closeAll ?? '关闭所有',\r\n key: 'closeAll',\r\n icon: () => h('span', { class: 'i-mdi:tally-mark-5' }),\r\n },\r\n ]\r\n })\r\n\r\n const handleContextMenuSelect = (key: string) => {\r\n if (!selectedTag.value || selectedIndex.value === -1) return\r\n\r\n switch (key) {\r\n case 'close':\r\n handleClose(selectedTag.value, selectedIndex.value)\r\n break\r\n case 'closeOthers':\r\n tags.removeOtherTags(selectedIndex.value)\r\n break\r\n case 'closeLeft':\r\n tags.removeLeftTags(selectedIndex.value)\r\n break\r\n case 'closeRight':\r\n tags.removeRightTags(selectedIndex.value)\r\n break\r\n case 'closeAll':\r\n tags.removeAllTags()\r\n router.push(props.homePath)\r\n emit('closeAll')\r\n break\r\n }\r\n closeContextMenu()\r\n }\r\n\r\n // ====== 路由自动跟踪 ======\r\n watch(\r\n () => route.path,\r\n (newPath: string) => {\r\n const originalTitle =\r\n newPath === props.homePath\r\n ? props.homeTitle\r\n : (route.meta.title as string) || 'Unnamed Page'\r\n\r\n tags.addTag({\r\n path: newPath,\r\n originalTitle,\r\n title: formatLabel(originalTitle),\r\n icon:\r\n newPath === props.homePath\r\n ? props.homeIcon\r\n : (route.meta.icon as string),\r\n meta: { affix: route.meta.affix as boolean },\r\n })\r\n\r\n tags.setActiveTag(newPath)\r\n scrollToTag(newPath)\r\n },\r\n { immediate: true }\r\n )\r\n\r\n // ====== 暴露 ======\r\n defineExpose({\r\n /** useTagsView composable 实例 */\r\n tags,\r\n /** 滚动到指定标签 */\r\n scrollToTag,\r\n })\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n @use './index.scss';\r\n</style>\r\n"],"mappings":"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"}
|