vome-core 0.1.76 → 0.1.78

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 (195) hide show
  1. package/dist/admin/api/client.js +2 -3
  2. package/dist/admin/components/layout/vm-app-header.vue +341 -0
  3. package/dist/admin/components/layout/vm-app-sidebar.vue +187 -0
  4. package/dist/admin/components/layout/vm-sidebar-flyout.vue +268 -0
  5. package/dist/admin/components/layout/vm-sidebar-nav-item.vue +490 -0
  6. package/dist/admin/components/layout/vm-sidebar-nav.vue +104 -0
  7. package/dist/admin/components/layout/vm-tags-view.vue +233 -0
  8. package/dist/admin/components/ui/avatar/Avatar.vue +18 -0
  9. package/dist/admin/components/ui/avatar/AvatarFallback.vue +21 -0
  10. package/dist/admin/components/ui/avatar/AvatarImage.vue +16 -0
  11. package/dist/admin/components/ui/avatar/index.d.ts +3 -0
  12. package/dist/admin/components/ui/avatar/index.js +1 -0
  13. package/dist/admin/components/ui/badge/Badge.vue +26 -0
  14. package/dist/admin/components/ui/badge/index.d.ts +6 -0
  15. package/dist/admin/components/ui/badge/index.js +1 -0
  16. package/dist/admin/components/ui/button/Button.vue +31 -0
  17. package/dist/admin/components/ui/button/index.d.ts +7 -0
  18. package/dist/admin/components/ui/button/index.js +1 -0
  19. package/dist/admin/components/ui/checkbox/Checkbox.vue +35 -0
  20. package/dist/admin/components/ui/checkbox/index.d.ts +1 -0
  21. package/dist/admin/components/ui/checkbox/index.js +1 -0
  22. package/dist/admin/components/ui/dialog/Dialog.vue +19 -0
  23. package/dist/admin/components/ui/dialog/DialogClose.vue +15 -0
  24. package/dist/admin/components/ui/dialog/DialogContent.vue +135 -0
  25. package/dist/admin/components/ui/dialog/DialogDescription.vue +23 -0
  26. package/dist/admin/components/ui/dialog/DialogFooter.vue +27 -0
  27. package/dist/admin/components/ui/dialog/DialogHeader.vue +17 -0
  28. package/dist/admin/components/ui/dialog/DialogOverlay.vue +21 -0
  29. package/dist/admin/components/ui/dialog/DialogScrollContent.vue +60 -0
  30. package/dist/admin/components/ui/dialog/DialogTitle.vue +23 -0
  31. package/dist/admin/components/ui/dialog/DialogTrigger.vue +15 -0
  32. package/dist/admin/components/ui/dialog/index.d.ts +10 -0
  33. package/dist/admin/components/ui/dialog/index.js +1 -0
  34. package/dist/admin/components/ui/dropdown-menu/DropdownMenu.vue +19 -0
  35. package/dist/admin/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -0
  36. package/dist/admin/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -0
  37. package/dist/admin/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -0
  38. package/dist/admin/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -0
  39. package/dist/admin/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -0
  40. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -0
  41. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
  42. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -0
  43. package/dist/admin/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -0
  44. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -0
  45. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -0
  46. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -0
  47. package/dist/admin/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -0
  48. package/dist/admin/components/ui/dropdown-menu/index.d.ts +15 -0
  49. package/dist/admin/components/ui/dropdown-menu/index.js +1 -0
  50. package/dist/admin/components/ui/input/Input.vue +34 -0
  51. package/dist/admin/components/ui/input/index.d.ts +1 -0
  52. package/dist/admin/components/ui/input/index.js +1 -0
  53. package/dist/admin/components/ui/label/Label.vue +26 -0
  54. package/dist/admin/components/ui/label/index.d.ts +1 -0
  55. package/dist/admin/components/ui/label/index.js +1 -0
  56. package/dist/admin/components/ui/scroll-area/ScrollArea.vue +33 -0
  57. package/dist/admin/components/ui/scroll-area/ScrollBar.vue +32 -0
  58. package/dist/admin/components/ui/scroll-area/index.d.ts +2 -0
  59. package/dist/admin/components/ui/scroll-area/index.js +1 -0
  60. package/dist/admin/components/ui/select/Select.vue +19 -0
  61. package/dist/admin/components/ui/select/SelectContent.vue +51 -0
  62. package/dist/admin/components/ui/select/SelectGroup.vue +15 -0
  63. package/dist/admin/components/ui/select/SelectItem.vue +44 -0
  64. package/dist/admin/components/ui/select/SelectItemText.vue +15 -0
  65. package/dist/admin/components/ui/select/SelectLabel.vue +17 -0
  66. package/dist/admin/components/ui/select/SelectScrollDownButton.vue +26 -0
  67. package/dist/admin/components/ui/select/SelectScrollUpButton.vue +26 -0
  68. package/dist/admin/components/ui/select/SelectSeparator.vue +19 -0
  69. package/dist/admin/components/ui/select/SelectTrigger.vue +33 -0
  70. package/dist/admin/components/ui/select/SelectValue.vue +15 -0
  71. package/dist/admin/components/ui/select/index.d.ts +11 -0
  72. package/dist/admin/components/ui/select/index.js +1 -0
  73. package/dist/admin/components/ui/separator/Separator.vue +29 -0
  74. package/dist/admin/components/ui/separator/index.d.ts +1 -0
  75. package/dist/admin/components/ui/separator/index.js +1 -0
  76. package/dist/admin/components/ui/sheet/Sheet.vue +19 -0
  77. package/dist/admin/components/ui/sheet/SheetClose.vue +15 -0
  78. package/dist/admin/components/ui/sheet/SheetContent.vue +63 -0
  79. package/dist/admin/components/ui/sheet/SheetDescription.vue +21 -0
  80. package/dist/admin/components/ui/sheet/SheetFooter.vue +16 -0
  81. package/dist/admin/components/ui/sheet/SheetHeader.vue +15 -0
  82. package/dist/admin/components/ui/sheet/SheetOverlay.vue +21 -0
  83. package/dist/admin/components/ui/sheet/SheetTitle.vue +21 -0
  84. package/dist/admin/components/ui/sheet/SheetTrigger.vue +15 -0
  85. package/dist/admin/components/ui/sheet/index.d.ts +8 -0
  86. package/dist/admin/components/ui/sheet/index.js +1 -0
  87. package/dist/admin/components/ui/sonner/Toaster.vue +117 -0
  88. package/dist/admin/components/ui/sonner/index.d.ts +1 -0
  89. package/dist/admin/components/ui/sonner/index.js +1 -0
  90. package/dist/admin/components/ui/table/Table.vue +16 -0
  91. package/dist/admin/components/ui/table/TableBody.vue +17 -0
  92. package/dist/admin/components/ui/table/TableCaption.vue +17 -0
  93. package/dist/admin/components/ui/table/TableCell.vue +22 -0
  94. package/dist/admin/components/ui/table/TableEmpty.vue +34 -0
  95. package/dist/admin/components/ui/table/TableFooter.vue +17 -0
  96. package/dist/admin/components/ui/table/TableHead.vue +17 -0
  97. package/dist/admin/components/ui/table/TableHeader.vue +17 -0
  98. package/dist/admin/components/ui/table/TableRow.vue +17 -0
  99. package/dist/admin/components/ui/table/index.d.ts +9 -0
  100. package/dist/admin/components/ui/table/index.js +1 -0
  101. package/dist/admin/components/ui/table/utils.d.ts +3 -0
  102. package/dist/admin/components/ui/table/utils.js +1 -0
  103. package/dist/admin/components/ui/tabs/Tabs.vue +24 -0
  104. package/dist/admin/components/ui/tabs/TabsContent.vue +21 -0
  105. package/dist/admin/components/ui/tabs/TabsList.vue +24 -0
  106. package/dist/admin/components/ui/tabs/TabsTrigger.vue +26 -0
  107. package/dist/admin/components/ui/tabs/index.d.ts +4 -0
  108. package/dist/admin/components/ui/tabs/index.js +1 -0
  109. package/dist/admin/components/ui/tooltip/Tooltip.vue +19 -0
  110. package/dist/admin/components/ui/tooltip/TooltipContent.vue +34 -0
  111. package/dist/admin/components/ui/tooltip/TooltipProvider.vue +14 -0
  112. package/dist/admin/components/ui/tooltip/TooltipTrigger.vue +15 -0
  113. package/dist/admin/components/ui/tooltip/index.d.ts +4 -0
  114. package/dist/admin/components/ui/tooltip/index.js +1 -0
  115. package/dist/admin/crud/components/vm-color-picker.vue +0 -3
  116. package/dist/admin/crud/components/vm-richtext-extensions.js +1 -1
  117. package/dist/admin/crud/config.js +1 -1
  118. package/dist/admin/crud/confirm.d.ts +8 -0
  119. package/dist/admin/crud/confirm.js +1 -1
  120. package/dist/admin/crud/crud-page-request.js +1 -1
  121. package/dist/admin/crud/dict.js +1 -1
  122. package/dist/admin/crud/export-fetch.js +1 -1
  123. package/dist/admin/crud/key.js +1 -1
  124. package/dist/admin/crud/plugins.js +1 -1
  125. package/dist/admin/crud/span.js +1 -1
  126. package/dist/admin/crud/style.d.ts +4 -0
  127. package/dist/admin/crud/style.js +1 -1
  128. package/dist/admin/crud/validate.js +1 -1
  129. package/dist/admin/crud/vm-data-i18n-btn.vue +0 -7
  130. package/dist/admin/directives/perm.js +1 -1
  131. package/dist/admin/env.d.ts +0 -4
  132. package/dist/admin/hooks/useUpload.js +1 -1
  133. package/dist/admin/index.d.ts +11 -4
  134. package/dist/admin/index.js +16 -7
  135. package/dist/admin/lib/browser.js +1 -1
  136. package/dist/admin/lib/cn.js +1 -1
  137. package/dist/admin/lib/dialog-float.js +1 -1
  138. package/dist/admin/lib/document-title.d.ts +6 -0
  139. package/dist/admin/lib/document-title.js +1 -0
  140. package/dist/admin/lib/export-excel.js +1 -1
  141. package/dist/admin/lib/import-excel.js +1 -1
  142. package/dist/admin/lib/json.js +1 -1
  143. package/dist/admin/lib/menu.js +1 -1
  144. package/dist/admin/lib/micro-locale.d.ts +5 -0
  145. package/dist/admin/lib/micro-locale.js +1 -0
  146. package/dist/admin/lib/micro-theme.d.ts +14 -0
  147. package/dist/admin/lib/micro-theme.js +1 -0
  148. package/dist/admin/lib/setup-crud-defaults.d.ts +2 -0
  149. package/dist/admin/lib/setup-crud-defaults.js +1 -0
  150. package/dist/admin/lib/shell.d.ts +13 -0
  151. package/dist/admin/lib/shell.js +1 -0
  152. package/dist/admin/lib/theme-color-options.js +1 -1
  153. package/dist/admin/lib/tree.js +1 -1
  154. package/dist/admin/lib/upload.js +1 -1
  155. package/dist/admin/lib/video-frame.js +1 -1
  156. package/dist/admin/router/create.d.ts +16 -0
  157. package/dist/admin/router/create.js +1 -0
  158. package/dist/admin/router/guards.d.ts +6 -0
  159. package/dist/admin/router/guards.js +1 -0
  160. package/dist/admin/router/index.d.ts +6 -3
  161. package/dist/admin/router/index.js +5 -80
  162. package/dist/admin/router/menu-routes.d.ts +7 -2
  163. package/dist/admin/router/menu-routes.js +10 -12
  164. package/dist/admin/service/index.js +0 -3
  165. package/dist/admin/stores/dict.d.ts +13 -0
  166. package/dist/admin/stores/dict.js +1 -0
  167. package/dist/admin/stores/locale.d.ts +3 -0
  168. package/dist/admin/stores/locale.js +1 -0
  169. package/dist/admin/stores/theme.d.ts +1 -0
  170. package/dist/admin/stores/theme.js +1 -0
  171. package/dist/admin/themes/dark.d.ts +2 -0
  172. package/dist/admin/themes/dark.js +1 -0
  173. package/dist/admin/themes/index.d.ts +9 -0
  174. package/dist/admin/themes/index.js +1 -0
  175. package/dist/admin/themes/light.d.ts +2 -0
  176. package/dist/admin/themes/light.js +1 -0
  177. package/dist/client/configure.d.ts +3 -0
  178. package/dist/client/configure.js +7 -0
  179. package/dist/client/vite-admin.js +2 -4
  180. package/dist/client/vite-auto-import.js +4 -4
  181. package/dist/client/vite-resolve.d.ts +3 -3
  182. package/dist/client/vite-resolve.js +10 -15
  183. package/dist/ext/plugin-market-urls.d.ts +8 -0
  184. package/dist/index.js +1 -1
  185. package/dist/server/index.js +1 -1
  186. package/dist/shared/datetime-picker.js +1 -1
  187. package/dist/shared/excel.js +1 -1
  188. package/dist/shared/index.js +1 -1
  189. package/dist/shared/locale-json.js +1 -1
  190. package/dist/shared/tree.js +1 -1
  191. package/package.json +13 -4
  192. package/typings/admin/theme.ts +10 -0
  193. package/dist/admin/config/plugin-dev.d.ts +0 -52
  194. package/dist/admin/config/plugin-dev.js +0 -1
  195. package/typings/admin/host.d.ts +0 -66
@@ -0,0 +1,490 @@
1
+ <template>
2
+ <div>
3
+ <!-- ===== 迷你侧栏:图标轨 + 多级浮窗 ===== -->
4
+ <template v-if="collapsed">
5
+ <!-- 叶子:直接跳转 -->
6
+ <RouterLink
7
+ v-if="isLeaf && item.router"
8
+ :to="item.router"
9
+ class="vm-nav-item is-collapsed"
10
+ :class="{ 'is-active': shownActive }"
11
+ :title="menuLabel"
12
+ @click="onLeafClick"
13
+ >
14
+ <span class="vm-nav-item__icon-slot">
15
+ <vm-ri-icon v-if="hasIcon(item)" :name="iconName(item)" class="vm-nav-item__icon" />
16
+ </span>
17
+ </RouterLink>
18
+
19
+ <!-- 目录:悬停弹出多级浮窗(无子项仍显示图标) -->
20
+ <div
21
+ v-else-if="isDir"
22
+ ref="miniRef"
23
+ class="vm-nav-mini"
24
+ @mouseenter="openMiniFlyout"
25
+ @mouseleave="scheduleCloseMini"
26
+ >
27
+ <button
28
+ type="button"
29
+ class="vm-nav-item is-collapsed is-dir"
30
+ :class="{ 'is-active': activeBranch, 'is-branch': activeBranch }"
31
+ :title="menuLabel"
32
+ >
33
+ <span class="vm-nav-item__icon-slot">
34
+ <vm-ri-icon v-if="hasIcon(item)" :name="iconName(item)" class="vm-nav-item__icon" />
35
+ </span>
36
+ </button>
37
+
38
+ <Teleport to="body">
39
+ <vm-sidebar-flyout
40
+ v-if="miniOpen && children.length"
41
+ :items="children"
42
+ :parent-id="item.id"
43
+ :anchor="miniAnchor"
44
+ @enter="clearMiniTimer"
45
+ @leave="scheduleCloseMini"
46
+ @navigate="closeMiniNow"
47
+ />
48
+ </Teleport>
49
+ </div>
50
+ </template>
51
+
52
+ <!-- ===== 展开侧栏 ===== -->
53
+ <template v-else>
54
+ <RouterLink
55
+ v-if="isLeaf && item.router"
56
+ :to="item.router"
57
+ class="vm-nav-item"
58
+ :class="{ 'is-active': shownActive, 'is-mobile': mobile }"
59
+ :style="{ paddingLeft: `${pad}px` }"
60
+ @click="onLeafClick"
61
+ >
62
+ <span
63
+ v-if="!mobile"
64
+ class="vm-nav-item__notch vm-nav-item__notch--top"
65
+ aria-hidden="true"
66
+ />
67
+ <span
68
+ v-if="!mobile"
69
+ class="vm-nav-item__notch vm-nav-item__notch--bottom"
70
+ aria-hidden="true"
71
+ />
72
+ <span class="vm-nav-item__icon-slot">
73
+ <vm-ri-icon v-if="hasIcon(item)" :name="iconName(item)" class="vm-nav-item__icon" />
74
+ </span>
75
+ <span class="vm-nav-item__label">{{ menuLabel }}</span>
76
+ </RouterLink>
77
+
78
+ <div
79
+ v-else-if="isDir"
80
+ class="vm-nav-group"
81
+ :class="{ 'is-deep': isDeepGroup }"
82
+ >
83
+ <button
84
+ type="button"
85
+ class="vm-nav-item is-dir"
86
+ :class="{ 'is-branch': activeBranch, 'is-mobile': mobile }"
87
+ :style="{ paddingLeft: `${pad}px` }"
88
+ @click="toggle"
89
+ >
90
+ <span class="vm-nav-item__icon-slot">
91
+ <vm-ri-icon v-if="hasIcon(item)" :name="iconName(item)" class="vm-nav-item__icon" />
92
+ </span>
93
+ <span class="vm-nav-item__label">{{ menuLabel }}</span>
94
+ <i
95
+ class="ri-arrow-down-s-line vm-nav-item__arrow"
96
+ :class="{ 'is-open': open }"
97
+ />
98
+ </button>
99
+ <div v-show="open" class="vm-nav-group__children">
100
+ <vm-sidebar-nav-item
101
+ v-for="child in children"
102
+ :key="child.id"
103
+ :item="child"
104
+ :depth="depth + 1"
105
+ :parent-id="item.id"
106
+ :collapsed="false"
107
+ :mobile="mobile"
108
+ />
109
+ </div>
110
+ </div>
111
+ </template>
112
+ </div>
113
+ </template>
114
+
115
+ <script setup lang="ts">
116
+ import { useLocaleStore } from '../../stores/locale'
117
+
118
+ defineOptions({ name: 'vm-sidebar-nav-item' })
119
+
120
+ const props = defineProps<{
121
+ item: MenuTreeNode
122
+ depth: number
123
+ parentId?: number | null
124
+ collapsed?: boolean
125
+ /** 移动端抽屉:选中用内嵌白底,不用右侧开口 */
126
+ mobile?: boolean
127
+ }>()
128
+
129
+ const locale = useLocaleStore()
130
+ const menuLabel = computed(() => locale.tMenu(props.item))
131
+
132
+ const route = useRoute()
133
+
134
+ const OPEN_KEY = 'sidebar-open-ids'
135
+ const PENDING_KEY = 'sidebar-pending-leaf'
136
+ const DEEP_KEY = 'sidebar-deep-focus'
137
+ const openIds = inject<Ref<Set<number>>>(OPEN_KEY)!
138
+ const pendingLeafId = inject<Ref<number | null>>(PENDING_KEY)!
139
+ const deepFocusId = inject<Ref<number | null>>(DEEP_KEY)!
140
+ if (!openIds || !pendingLeafId || !deepFocusId) {
141
+ throw new Error('[vm-sidebar-nav-item] 缺少 sidebar provide')
142
+ }
143
+
144
+ const miniRef = ref<HTMLElement | null>(null)
145
+ const miniOpen = ref(false)
146
+ const miniAnchor = ref({ top: 0, left: 0 })
147
+ let miniTimer: ReturnType<typeof setTimeout> | null = null
148
+
149
+ function hasIcon(item: MenuTreeNode) {
150
+ return Boolean(item.icon && String(item.icon).trim())
151
+ }
152
+
153
+ function iconName(item: MenuTreeNode) {
154
+ const raw = String(item.icon || '').trim()
155
+ return raw.startsWith('ri-') ? raw : `ri-${raw}`
156
+ }
157
+
158
+ function visibleChildren(item: MenuTreeNode) {
159
+ return (item.children ?? []).filter(
160
+ (c) => c.isShow !== false && Number(c.type) !== 2,
161
+ )
162
+ }
163
+
164
+ const children = computed(() => visibleChildren(props.item))
165
+ const isDir = computed(
166
+ () => Number(props.item.type) === 0 || children.value.length > 0,
167
+ )
168
+ const isLeaf = computed(
169
+ () => Number(props.item.type) === 1 && Boolean(props.item.router),
170
+ )
171
+
172
+ function isActiveLeaf(item: MenuTreeNode) {
173
+ if (!item.router) return false
174
+ return route.path === item.router || route.path.startsWith(`${item.router}/`)
175
+ }
176
+
177
+ function containsActive(item: MenuTreeNode): boolean {
178
+ if (isActiveLeaf(item)) return true
179
+ return visibleChildren(item).some((c) => containsActive(c))
180
+ }
181
+
182
+ function containsId(item: MenuTreeNode, id: number): boolean {
183
+ if (item.id === id) return true
184
+ return visibleChildren(item).some((c) => containsId(c, id))
185
+ }
186
+
187
+ const activeLeaf = computed(() => isActiveLeaf(props.item))
188
+ const shownActive = computed(
189
+ () => activeLeaf.value || pendingLeafId.value === props.item.id,
190
+ )
191
+ const activeBranch = computed(() => containsActive(props.item))
192
+ const open = computed(() => openIds!.value.has(props.item.id))
193
+ const isDeepGroup = computed(
194
+ () => !props.collapsed && open.value && deepFocusId.value === props.item.id,
195
+ )
196
+
197
+ function onLeafClick() {
198
+ pendingLeafId.value = props.item.id
199
+ deepFocusId.value = props.parentId ?? null
200
+ }
201
+
202
+ function ensureOpenAlongActive(item: MenuTreeNode) {
203
+ if (!containsActive(item)) return
204
+ if (isDir.value) {
205
+ openIds!.value = new Set(openIds!.value).add(item.id)
206
+ }
207
+ for (const c of children.value) {
208
+ if (containsActive(c) && (Number(c.type) === 0 || visibleChildren(c).length)) {
209
+ openIds!.value = new Set(openIds!.value).add(c.id)
210
+ }
211
+ }
212
+ }
213
+
214
+ watch(
215
+ () => route.path,
216
+ () => ensureOpenAlongActive(props.item),
217
+ { immediate: true },
218
+ )
219
+
220
+ function toggle() {
221
+ const next = new Set(openIds!.value)
222
+ const willOpen = !next.has(props.item.id)
223
+ if (willOpen) {
224
+ next.add(props.item.id)
225
+ deepFocusId.value = props.item.id
226
+ } else {
227
+ next.delete(props.item.id)
228
+ const focus = deepFocusId.value
229
+ if (focus != null && containsId(props.item, focus)) {
230
+ deepFocusId.value = null
231
+ }
232
+ }
233
+ openIds!.value = next
234
+ }
235
+
236
+ function clearMiniTimer() {
237
+ if (miniTimer) {
238
+ clearTimeout(miniTimer)
239
+ miniTimer = null
240
+ }
241
+ }
242
+
243
+ function openMiniFlyout() {
244
+ clearMiniTimer()
245
+ const el = miniRef.value
246
+ if (!el) return
247
+ const btn = el.querySelector('.vm-nav-item') as HTMLElement | null
248
+ const btnRect = (btn || el).getBoundingClientRect()
249
+ // 按钮在侧栏内居中,必须用侧栏右边缘,否则浮窗会盖住图标
250
+ const sidebar = el.closest('.vm-sidebar') as HTMLElement | null
251
+ const sideRight = sidebar?.getBoundingClientRect().right ?? btnRect.right
252
+ miniAnchor.value = {
253
+ top: Math.round(btnRect.top),
254
+ left: Math.round(sideRight + 12),
255
+ }
256
+ miniOpen.value = true
257
+ }
258
+
259
+ function scheduleCloseMini() {
260
+ clearMiniTimer()
261
+ miniTimer = setTimeout(() => {
262
+ miniOpen.value = false
263
+ }, 140)
264
+ }
265
+
266
+ function closeMiniNow() {
267
+ clearMiniTimer()
268
+ miniOpen.value = false
269
+ }
270
+
271
+ onBeforeUnmount(() => clearMiniTimer())
272
+
273
+ const pad = computed(() => 12 + props.depth * 14)
274
+ </script>
275
+
276
+ <style lang="scss" scoped>
277
+ $open: 14px;
278
+ $gutter: 10px;
279
+
280
+ .vm-nav-mini {
281
+ position: relative;
282
+ }
283
+
284
+ .vm-nav-item {
285
+ position: relative;
286
+ z-index: 0;
287
+ display: flex;
288
+ width: 100%;
289
+ height: 44px;
290
+ align-items: center;
291
+ gap: 10px;
292
+ box-sizing: border-box;
293
+ padding-right: 12px;
294
+ margin: 0;
295
+ border: none;
296
+ border-radius: 12px;
297
+ background: transparent;
298
+ color: rgba(255, 255, 255, 0.95);
299
+ font-size: 14px;
300
+ font-weight: 700 !important;
301
+ line-height: 1;
302
+ text-decoration: none;
303
+ cursor: pointer;
304
+ transition: none;
305
+
306
+ .vm-nav-item__icon-slot,
307
+ .vm-nav-item__icon,
308
+ .vm-nav-item__label,
309
+ .vm-nav-item__arrow {
310
+ position: relative;
311
+ z-index: 1;
312
+ }
313
+
314
+ &:hover:not(.is-active)::before {
315
+ content: '';
316
+ position: absolute;
317
+ inset: 0;
318
+ z-index: 0;
319
+ border-radius: 12px;
320
+ background: rgba(255, 255, 255, 0.16);
321
+ pointer-events: none;
322
+ }
323
+
324
+ &:active:not(.is-active)::before {
325
+ background: rgba(255, 255, 255, 0.24);
326
+ }
327
+
328
+ &.is-collapsed {
329
+ width: 44px;
330
+ height: 44px;
331
+ justify-content: center;
332
+ padding: 0;
333
+ margin: 0 auto;
334
+ border-radius: 12px;
335
+ }
336
+
337
+ /* 桌面展开:右侧开口贴内容区 */
338
+ &.is-active:not(.is-collapsed):not(.is-mobile) {
339
+ z-index: 1;
340
+ width: calc(100% + #{$gutter});
341
+ margin-right: -$gutter;
342
+ border-radius: $open 0 0 $open;
343
+ background: var(--background);
344
+ color: var(--brand);
345
+
346
+ .vm-nav-item__icon,
347
+ .vm-nav-item__label {
348
+ color: var(--brand);
349
+ }
350
+
351
+ .vm-nav-item__notch {
352
+ display: block;
353
+ }
354
+ }
355
+
356
+ /* 移动端抽屉:内嵌白底圆角,避免开口溢出 Sheet */
357
+ &.is-active.is-mobile:not(.is-collapsed) {
358
+ z-index: 1;
359
+ width: 100%;
360
+ margin-right: 0;
361
+ border-radius: 12px;
362
+ background: #fff;
363
+ color: var(--brand);
364
+
365
+ .vm-nav-item__icon,
366
+ .vm-nav-item__label {
367
+ color: var(--brand);
368
+ }
369
+
370
+ .vm-nav-item__notch {
371
+ display: none;
372
+ }
373
+ }
374
+
375
+ &.is-active.is-collapsed,
376
+ &.is-branch.is-collapsed {
377
+ width: 44px;
378
+ margin-right: auto;
379
+ background: #fff;
380
+ color: var(--brand);
381
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
382
+
383
+ .vm-nav-item__notch {
384
+ display: none;
385
+ }
386
+
387
+ .vm-nav-item__icon {
388
+ color: var(--brand);
389
+ }
390
+ }
391
+
392
+ &.is-dir.is-branch:not(.is-active):not(.is-collapsed) {
393
+ color: #fff;
394
+ }
395
+ }
396
+
397
+ .vm-nav-item__notch {
398
+ display: none;
399
+ position: absolute;
400
+ right: 0;
401
+ width: $open;
402
+ height: $open;
403
+ background: transparent;
404
+ pointer-events: none;
405
+
406
+ &--top {
407
+ top: -$open;
408
+ border-bottom-right-radius: $open;
409
+ box-shadow: 5px 5px 0 5px var(--background);
410
+ }
411
+
412
+ &--bottom {
413
+ bottom: -$open;
414
+ border-top-right-radius: $open;
415
+ box-shadow: 5px -5px 0 5px var(--background);
416
+ }
417
+ }
418
+
419
+ .vm-nav-item__icon-slot {
420
+ display: inline-flex;
421
+ width: 18px;
422
+ height: 18px;
423
+ flex-shrink: 0;
424
+ align-items: center;
425
+ justify-content: center;
426
+ }
427
+
428
+ .vm-nav-item__icon {
429
+ display: inline-flex;
430
+ flex-shrink: 0;
431
+ width: 18px;
432
+ height: 18px;
433
+ align-items: center;
434
+ justify-content: center;
435
+ font-size: 18px;
436
+ line-height: 1;
437
+ opacity: 0.95;
438
+ }
439
+
440
+ .vm-nav-item__label {
441
+ display: inline-flex;
442
+ flex: 1;
443
+ min-width: 0;
444
+ align-items: center;
445
+ overflow: hidden;
446
+ text-overflow: ellipsis;
447
+ white-space: nowrap;
448
+ text-align: left;
449
+ font-size: 14px;
450
+ font-weight: 700;
451
+ line-height: 1;
452
+ font-synthesis: weight;
453
+ }
454
+
455
+ .vm-nav-item__arrow {
456
+ display: inline-flex;
457
+ flex-shrink: 0;
458
+ width: 18px;
459
+ height: 18px;
460
+ margin-right: 2px;
461
+ align-items: center;
462
+ justify-content: center;
463
+ font-size: 16px;
464
+ line-height: 1;
465
+ opacity: 0.75;
466
+ transition: transform 0.2s ease;
467
+
468
+ &.is-open {
469
+ transform: rotate(180deg);
470
+ }
471
+ }
472
+
473
+ .vm-nav-group {
474
+ display: flex;
475
+ flex-direction: column;
476
+ gap: 4px;
477
+ padding: 0;
478
+ border-radius: 12px;
479
+
480
+ &.is-deep {
481
+ background: rgba(0, 0, 0, 0.2);
482
+ }
483
+ }
484
+
485
+ .vm-nav-group__children {
486
+ display: flex;
487
+ flex-direction: column;
488
+ gap: 4px;
489
+ }
490
+ </style>
@@ -0,0 +1,104 @@
1
+ <template>
2
+ <nav class="vm-nav" :class="{ 'is-collapsed': collapsed && !mobile }">
3
+ <vm-sidebar-nav-item
4
+ v-for="item in visibleMenus"
5
+ :key="item.id"
6
+ :item="item"
7
+ :depth="0"
8
+ :parent-id="null"
9
+ :collapsed="collapsed && !mobile"
10
+ :mobile="mobile"
11
+ />
12
+ </nav>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ defineOptions({ name: 'vm-sidebar-nav' })
17
+
18
+ const props = defineProps<{
19
+ menus: MenuTreeNode[]
20
+ collapsed?: boolean
21
+ /** 移动端抽屉:禁用右侧开口选中态 */
22
+ mobile?: boolean
23
+ }>()
24
+
25
+ const route = useRoute()
26
+ const openIds = ref(new Set<number>())
27
+ /** 点击瞬间先开口,等路由落地后再清,避免闪烁 */
28
+ const pendingLeafId = ref<number | null>(null)
29
+ /**
30
+ * 加深背景的目录 id:
31
+ * - 展开目录 → 该目录
32
+ * - 切到页面 → 改为该页面的直接父目录
33
+ */
34
+ const deepFocusId = ref<number | null>(null)
35
+
36
+ provide('sidebar-open-ids', openIds)
37
+ provide('sidebar-pending-leaf', pendingLeafId)
38
+ provide('sidebar-deep-focus', deepFocusId)
39
+
40
+ const visibleMenus = computed(() =>
41
+ props.menus.filter((m) => m.isShow !== false && Number(m.type) !== 2),
42
+ )
43
+
44
+ function visibleChildren(item: MenuTreeNode) {
45
+ return (item.children ?? []).filter(
46
+ (c) => c.isShow !== false && Number(c.type) !== 2,
47
+ )
48
+ }
49
+
50
+ function isActivePath(routerPath: string) {
51
+ return route.path === routerPath || route.path.startsWith(`${routerPath}/`)
52
+ }
53
+
54
+ /** 当前激活叶子页的直接父目录 id */
55
+ function findActiveLeafParent(
56
+ items: MenuTreeNode[],
57
+ parentId: number | null = null,
58
+ ): number | null | undefined {
59
+ for (const item of items) {
60
+ const kids = visibleChildren(item)
61
+ if (Number(item.type) === 1 && item.router && isActivePath(item.router)) {
62
+ return parentId
63
+ }
64
+ if (kids.length) {
65
+ const hit = findActiveLeafParent(kids, item.id)
66
+ if (hit !== undefined) return hit
67
+ }
68
+ }
69
+ return undefined
70
+ }
71
+
72
+ watch(
73
+ () => [route.path, props.menus] as const,
74
+ () => {
75
+ pendingLeafId.value = null
76
+ const parentId = findActiveLeafParent(visibleMenus.value)
77
+ // undefined = 未找到叶子;null = 顶级叶子无父级
78
+ if (parentId !== undefined) {
79
+ deepFocusId.value = parentId
80
+ }
81
+ },
82
+ { immediate: true, deep: true },
83
+ )
84
+ </script>
85
+
86
+ <style lang="scss" scoped>
87
+ $gutter: 10px;
88
+
89
+ .vm-nav {
90
+ display: flex;
91
+ flex: 1;
92
+ flex-direction: column;
93
+ gap: 4px;
94
+ overflow-x: hidden;
95
+ overflow-y: auto;
96
+ padding: 8px $gutter 24px;
97
+
98
+ &.is-collapsed {
99
+ align-items: center;
100
+ overflow: visible;
101
+ padding: 8px $gutter 24px;
102
+ }
103
+ }
104
+ </style>