@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.
Files changed (122) hide show
  1. package/CHANGELOG.md +88 -76
  2. package/LICENSE +21 -0
  3. package/README.md +314 -294
  4. package/dist/C_AvatarGroup.cjs +1 -1
  5. package/dist/C_AvatarGroup.d.cts +1 -1
  6. package/dist/C_AvatarGroup.d.ts +1 -1
  7. package/dist/C_AvatarGroup.js +1 -1
  8. package/dist/C_Cascade-CmCrLVft.css.map +1 -0
  9. package/dist/C_Cascade-U6A5AJJb.css.map +1 -0
  10. package/dist/C_Cascade.css +3 -3
  11. package/dist/C_Cascade.d.cts +2 -2
  12. package/dist/C_Cascade.d.ts +2 -2
  13. package/dist/C_Cascade.js.map +1 -1
  14. package/dist/C_Cascade2.js +1 -1
  15. package/dist/C_Cascade2.js.map +1 -1
  16. package/dist/{C_Form-SCq-DZaZ.css.map → C_Form-B_2ql62V.css.map} +1 -1
  17. package/dist/{C_Form-mcd1nKvF.css.map → C_Form-CX-7xsz3.css.map} +1 -1
  18. package/dist/C_Form.css +60 -60
  19. package/dist/C_Form.d.cts +2 -2
  20. package/dist/C_Form.d.ts +2 -2
  21. package/dist/C_Form.js.map +1 -1
  22. package/dist/C_Form2.js +1 -1
  23. package/dist/C_Form2.js.map +1 -1
  24. package/dist/C_FullCalendar.js.map +1 -1
  25. package/dist/C_FullCalendar2.js +1 -1
  26. package/dist/C_FullCalendar2.js.map +1 -1
  27. package/dist/C_Icon.d.cts +1 -1
  28. package/dist/C_Icon.d.ts +1 -1
  29. package/dist/C_Icon.js.map +1 -1
  30. package/dist/C_Icon2.js.map +1 -1
  31. package/dist/C_Language.d.cts +1 -1
  32. package/dist/C_Language.d.ts +1 -1
  33. package/dist/C_Language.js.map +1 -1
  34. package/dist/C_Language2.js.map +1 -1
  35. package/dist/C_Steps-27Exh7PM.css.map +1 -0
  36. package/dist/C_Steps-DU9ZsFrs.css.map +1 -0
  37. package/dist/C_Steps.css +38 -38
  38. package/dist/C_Steps.d.cts +1 -1
  39. package/dist/C_Steps.d.ts +1 -1
  40. package/dist/C_Steps.js.map +1 -1
  41. package/dist/C_Steps2.js +1 -1
  42. package/dist/C_Steps2.js.map +1 -1
  43. package/dist/C_Table.js.map +1 -1
  44. package/dist/C_Table2.js +3 -3
  45. package/dist/C_Table2.js.map +1 -1
  46. package/dist/{C_TagsView-COjGNM9h.css.map → C_TagsView-BN8vMRsY.css.map} +1 -1
  47. package/dist/{C_TagsView-zUDj9S06.css.map → C_TagsView-s1dnSJAV.css.map} +1 -1
  48. package/dist/C_TagsView.css +36 -36
  49. package/dist/C_TagsView.d.cts +1 -1
  50. package/dist/C_TagsView.d.ts +1 -1
  51. package/dist/C_TagsView.js.map +1 -1
  52. package/dist/C_TagsView2.js +1 -1
  53. package/dist/C_TagsView2.js.map +1 -1
  54. package/dist/C_Theme.d.cts +1 -1
  55. package/dist/C_Theme.d.ts +1 -1
  56. package/dist/C_Theme.js.map +1 -1
  57. package/dist/C_Theme2.js.map +1 -1
  58. package/dist/C_Timeline.cjs +1 -1
  59. package/dist/C_Timeline.d.cts +1 -1
  60. package/dist/C_Timeline.d.ts +1 -1
  61. package/dist/C_Timeline.js +1 -1
  62. package/dist/constants.d.ts +5 -5
  63. package/dist/constants2.d.ts +5 -5
  64. package/dist/constants3.d.ts +2 -2
  65. package/dist/constants4.d.ts +5 -5
  66. package/dist/constants5.d.ts +2 -2
  67. package/dist/index.cjs +1 -1
  68. package/dist/index.d.cts +9 -9
  69. package/dist/index.d.ts +9 -9
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/index.vue.d.ts +1 -1
  73. package/dist/index10.vue.d.ts +1 -1
  74. package/dist/index11.vue.d.ts +2 -2
  75. package/dist/index12.vue.d.ts +1 -1
  76. package/dist/index13.vue.d.ts +4 -4
  77. package/dist/index15.vue.d.ts +30 -10
  78. package/dist/index16.vue.d.ts +13 -6
  79. package/dist/index17.vue.d.ts +1 -1
  80. package/dist/index18.vue.d.ts +5 -5
  81. package/dist/index2.vue.d.ts +1 -1
  82. package/dist/index20.vue.d.ts +1 -1
  83. package/dist/index21.vue.d.ts +2 -2
  84. package/dist/index22.vue.d.ts +1 -1
  85. package/dist/index23.vue.d.ts +14 -3
  86. package/dist/index24.vue.d.ts +13 -4
  87. package/dist/index25.vue.d.ts +4 -2
  88. package/dist/index26.vue.d.ts +1 -1
  89. package/dist/index27.vue.d.ts +2 -2
  90. package/dist/index28.vue.d.ts +3 -3
  91. package/dist/index3.vue.d.ts +2 -2
  92. package/dist/index4.vue.d.ts +2 -2
  93. package/dist/index6.vue.d.ts +1 -1
  94. package/dist/index7.vue.d.ts +9 -7
  95. package/dist/index8.vue.d.ts +3 -3
  96. package/dist/index9.vue.d.ts +2 -2
  97. package/dist/resolver.cjs +1 -1
  98. package/dist/resolver.cjs.map +1 -1
  99. package/dist/resolver.d.cts +2 -21
  100. package/dist/resolver.d.ts +2 -21
  101. package/dist/resolver.js +1 -1
  102. package/dist/resolver.js.map +1 -1
  103. package/dist/style.css +142 -142
  104. package/dist/useCalendarEvents.d.ts +9 -6
  105. package/dist/useCollapsePanel.d.ts +3 -3
  106. package/dist/useCropperCore.d.ts +6 -6
  107. package/dist/useDraggableLayout.d.ts +2 -2
  108. package/dist/useDynamicFormState.d.ts +29 -3724
  109. package/dist/useEdgeInteraction.d.ts +1 -1
  110. package/dist/useGlobalSearch.d.ts +1 -1
  111. package/dist/useInfiniteScroll.d.ts +1 -1
  112. package/dist/useQRCode.d.ts +4 -4
  113. package/dist/useSignatureHistory.d.ts +3 -3
  114. package/dist/useSplitResize.d.ts +2 -2
  115. package/dist/useTimeSelection.d.ts +2 -2
  116. package/dist/useTreeOperations.d.ts +5 -5
  117. package/dist/useWorkflowValidation.d.ts +3 -3
  118. package/package.json +11 -2
  119. package/dist/C_Cascade-BE7t1OX5.css.map +0 -1
  120. package/dist/C_Cascade-CpDiau1y.css.map +0 -1
  121. package/dist/C_Steps-Buyg7Axn.css.map +0 -1
  122. package/dist/C_Steps-DNY91VbQ.css.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"C_TagsView-COjGNM9h.css","names":[],"sources":["../src/components/C_TagsView/index.vue?vue&type=style&index=0&scoped=c25966b3&lang.scss"],"sourcesContent":["/* unplugin-vue-components disabled *//* ===========================================\n * 标签组件样式 - 精简版\n * =========================================== */\n/* --------- 标签视图容器 --------- */\n.tags-view-container[data-v-c25966b3] {\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-c25966b3] {\n width: 100%;\n overflow: hidden;\n position: relative;\n}\n\n/* --------- 滚动容器 --------- */\n.tags-container[data-v-c25966b3] {\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-c25966b3]::-webkit-scrollbar {\n display: none;\n}\n\n/* --------- 现代化标签样式 --------- */\n.n-tag[data-v-c25966b3] {\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-c25966b3]::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-c25966b3]: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-c25966b3]:hover:not([type=primary])::before {\n opacity: 0.8;\n}\n.n-tag[data-v-c25966b3]:hover:not([type=primary]) .c-icon,\n.n-tag[data-v-c25966b3]:hover:not([type=primary]) [class*=\"c-icon\"] {\n color: var(--color-primary) !important;\n}\n.n-tag:hover:not([type=primary]) i[data-v-c25966b3],\n.n-tag:hover:not([type=primary]) .icon[data-v-c25966b3] {\n color: var(--color-primary) !important;\n}\n.n-tag[data-v-c25966b3]: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-c25966b3] {\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-c25966b3]::before {\n opacity: 0.6;\n}\n.n-tag[type=primary][data-v-c25966b3] .n-tag__content,\n.n-tag[type=primary][data-v-c25966b3] *:not(.n-tag__close) {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary][data-v-c25966b3] .c-icon,\n.n-tag[type=primary][data-v-c25966b3] [class*=\"c-icon\"] {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary] i[data-v-c25966b3],\n.n-tag[type=primary] .icon[data-v-c25966b3] {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary][data-v-c25966b3]: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-c25966b3] .n-tag__close {\n color: var(--color-primary) !important;\n opacity: 0.8;\n}\n.n-tag[data-v-c25966b3] .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-c25966b3] .n-tag__close:not(:hover) {\n transform: scale(0.9);\n}\n.n-tag[data-v-c25966b3] .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-c25966b3] .n-tag__close:active {\n transform: scale(0.95) !important;\n transition-duration: 0.1s !important;\n}\n\n/* --------- C_Icon --------- */\n[data-v-c25966b3] .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-c25966b3] {\n padding: 6px 12px 0;\n}\n.n-tag[data-v-c25966b3] {\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-c25966b3] .n-tag__close {\n width: 14px;\n height: 14px;\n font-size: 11px;\n margin-left: 4px;\n}\n.tags-container[data-v-c25966b3] {\n padding: 0 12px;\n min-height: 36px;\n}\n[data-v-c25966b3] .c-icon {\n font-size: 11px;\n margin-right: 3px;\n}\n}\n/* --------- 暗色主题 --------- */\n[data-theme=dark] .n-tag[data-v-c25966b3] .n-tag__close {\n background: rgba(255, 255, 255, 0.03) !important;\n}\n[data-theme=dark] .n-tag[data-v-c25966b3] .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
+ {"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-zUDj9S06.css","names":[],"sources":["../src/components/C_TagsView/index.vue?vue&type=style&index=0&scoped=c25966b3&lang.scss"],"sourcesContent":["/* unplugin-vue-components disabled *//* ===========================================\n * 标签组件样式 - 精简版\n * =========================================== */\n/* --------- 标签视图容器 --------- */\n.tags-view-container[data-v-c25966b3] {\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-c25966b3] {\n width: 100%;\n overflow: hidden;\n position: relative;\n}\n\n/* --------- 滚动容器 --------- */\n.tags-container[data-v-c25966b3] {\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-c25966b3]::-webkit-scrollbar {\n display: none;\n}\n\n/* --------- 现代化标签样式 --------- */\n.n-tag[data-v-c25966b3] {\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-c25966b3]::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-c25966b3]: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-c25966b3]:hover:not([type=primary])::before {\n opacity: 0.8;\n}\n.n-tag[data-v-c25966b3]:hover:not([type=primary]) .c-icon,\n.n-tag[data-v-c25966b3]:hover:not([type=primary]) [class*=\"c-icon\"] {\n color: var(--color-primary) !important;\n}\n.n-tag:hover:not([type=primary]) i[data-v-c25966b3],\n.n-tag:hover:not([type=primary]) .icon[data-v-c25966b3] {\n color: var(--color-primary) !important;\n}\n.n-tag[data-v-c25966b3]: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-c25966b3] {\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-c25966b3]::before {\n opacity: 0.6;\n}\n.n-tag[type=primary][data-v-c25966b3] .n-tag__content,\n.n-tag[type=primary][data-v-c25966b3] *:not(.n-tag__close) {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary][data-v-c25966b3] .c-icon,\n.n-tag[type=primary][data-v-c25966b3] [class*=\"c-icon\"] {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary] i[data-v-c25966b3],\n.n-tag[type=primary] .icon[data-v-c25966b3] {\n color: var(--color-primary) !important;\n}\n.n-tag[type=primary][data-v-c25966b3]: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-c25966b3] .n-tag__close {\n color: var(--color-primary) !important;\n opacity: 0.8;\n}\n.n-tag[data-v-c25966b3] .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-c25966b3] .n-tag__close:not(:hover) {\n transform: scale(0.9);\n}\n.n-tag[data-v-c25966b3] .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-c25966b3] .n-tag__close:active {\n transform: scale(0.95) !important;\n transition-duration: 0.1s !important;\n}\n\n/* --------- C_Icon --------- */\n[data-v-c25966b3] .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-c25966b3] {\n padding: 6px 12px 0;\n}\n.n-tag[data-v-c25966b3] {\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-c25966b3] .n-tag__close {\n width: 14px;\n height: 14px;\n font-size: 11px;\n margin-left: 4px;\n}\n.tags-container[data-v-c25966b3] {\n padding: 0 12px;\n min-height: 36px;\n}\n[data-v-c25966b3] .c-icon {\n font-size: 11px;\n margin-right: 3px;\n}\n}\n/* --------- 暗色主题 --------- */\n[data-theme=dark] .n-tag[data-v-c25966b3] .n-tag__close {\n background: rgba(255, 255, 255, 0.03) !important;\n}\n[data-theme=dark] .n-tag[data-v-c25966b3] .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
+ {"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"}
@@ -2,7 +2,7 @@
2
2
  * 标签组件样式 - 精简版
3
3
  * =========================================== */
4
4
  /* --------- 标签视图容器 --------- */
5
- .tags-view-container[data-v-c25966b3] {
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-c25966b3] {
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-c25966b3] {
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-c25966b3]::-webkit-scrollbar {
30
+ .tags-container[data-v-1c8f8663]::-webkit-scrollbar {
31
31
  display: none;
32
32
  }
33
33
 
34
34
  /* --------- 现代化标签样式 --------- */
35
- .n-tag[data-v-c25966b3] {
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-c25966b3]::before {
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-c25966b3]:hover:not([type=primary]) {
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-c25966b3]:hover:not([type=primary])::before {
72
+ .n-tag[data-v-1c8f8663]:hover:not([type=primary])::before {
73
73
  opacity: 0.8;
74
74
  }
75
- .n-tag[data-v-c25966b3]:hover:not([type=primary]) .c-icon,
76
- .n-tag[data-v-c25966b3]:hover:not([type=primary]) [class*="c-icon"] {
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-c25966b3],
80
- .n-tag:hover:not([type=primary]) .icon[data-v-c25966b3] {
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-c25966b3]:hover:not([type=primary]) .n-tag__close {
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-c25966b3] {
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-c25966b3]::before {
95
+ .n-tag[type=primary][data-v-1c8f8663]::before {
96
96
  opacity: 0.6;
97
97
  }
98
- .n-tag[type=primary][data-v-c25966b3] .n-tag__content,
99
- .n-tag[type=primary][data-v-c25966b3] *:not(.n-tag__close) {
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-c25966b3] .c-icon,
103
- .n-tag[type=primary][data-v-c25966b3] [class*="c-icon"] {
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-c25966b3],
107
- .n-tag[type=primary] .icon[data-v-c25966b3] {
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-c25966b3]:hover {
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-c25966b3] .n-tag__close {
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-c25966b3] .n-tag__close {
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-c25966b3] .n-tag__close:not(:hover) {
133
+ .n-tag[data-v-1c8f8663] .n-tag__close:not(:hover) {
134
134
  transform: scale(0.9);
135
135
  }
136
- .n-tag[data-v-c25966b3] .n-tag__close:hover {
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-c25966b3] .n-tag__close:active {
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-c25966b3] .c-icon {
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-c25966b3] {
160
+ .tags-view-container[data-v-1c8f8663] {
161
161
  padding: 6px 12px 0;
162
162
  }
163
- .n-tag[data-v-c25966b3] {
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-c25966b3] .n-tag__close {
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-c25966b3] {
176
+ .tags-container[data-v-1c8f8663] {
177
177
  padding: 0 12px;
178
178
  min-height: 36px;
179
179
  }
180
- [data-v-c25966b3] .c-icon {
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-c25966b3] .n-tag__close {
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-c25966b3] .n-tag__close:hover {
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-COjGNM9h.css.map*/
194
+ /*# sourceMappingURL=C_TagsView-BN8vMRsY.css.map*/
@@ -1,2 +1,2 @@
1
- import { i as useTagsView, n as _default, r as UseTagsViewOptions, t as ContextMenuLabels } from "./index24.vue.js";
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 };
@@ -1,3 +1,3 @@
1
1
  import "./menuAdapter.js";
2
- import { i as useTagsView, n as _default, r as UseTagsViewOptions, t as ContextMenuLabels } from "./index24.vue.js";
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 };
@@ -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"}
@@ -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-c25966b3`]]);export{E as n,O as t};
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
@@ -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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"}