vome-core 0.0.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 (209) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +1 -0
  3. package/dist/server/index.d.ts +1 -0
  4. package/dist/server/index.js +1 -0
  5. package/dist/src/auth/define-macro.d.ts +8 -0
  6. package/dist/src/auth/index.d.ts +2 -0
  7. package/dist/src/comm/daily-log.d.ts +8 -0
  8. package/dist/src/comm/env.d.ts +4 -0
  9. package/dist/src/comm/format-time.d.ts +10 -0
  10. package/dist/src/comm/merge.d.ts +2 -0
  11. package/dist/src/comm/scan.d.ts +4 -0
  12. package/dist/src/comm/startBanner.d.ts +1 -0
  13. package/dist/src/core/app.d.ts +37 -0
  14. package/dist/src/core/config.d.ts +2 -0
  15. package/dist/src/core/context/index.d.ts +22 -0
  16. package/dist/src/core/host-infra.d.ts +20 -0
  17. package/dist/src/core/index.d.ts +20 -0
  18. package/dist/src/core/plugins/async-context.d.ts +3 -0
  19. package/dist/src/core/plugins/error.d.ts +5 -0
  20. package/dist/src/core/plugins/logging.d.ts +3 -0
  21. package/dist/src/core/plugins/openapi.d.ts +3 -0
  22. package/dist/src/core/vome-config.d.ts +2 -0
  23. package/dist/src/ext/base-plugin.d.ts +13 -0
  24. package/dist/src/ext/index.d.ts +15 -0
  25. package/dist/src/ext/load-plugin.d.ts +12 -0
  26. package/dist/src/ext/micro-apps.d.ts +60 -0
  27. package/dist/src/ext/module-gateway.d.ts +10 -0
  28. package/dist/src/ext/module-registry.d.ts +45 -0
  29. package/dist/src/ext/path.d.ts +8 -0
  30. package/dist/src/index.d.ts +10 -0
  31. package/dist/src/ioc/container.d.ts +29 -0
  32. package/dist/src/ioc/decorators.d.ts +35 -0
  33. package/dist/src/ioc/host.d.ts +2 -0
  34. package/dist/src/ioc/index.d.ts +6 -0
  35. package/dist/src/ioc/metadata.d.ts +12 -0
  36. package/dist/src/ioc/scope.d.ts +9 -0
  37. package/dist/src/logger/bootstrap.d.ts +2 -0
  38. package/dist/src/logger/exception.d.ts +22 -0
  39. package/dist/src/logger/index.d.ts +5 -0
  40. package/dist/src/logger/logger.d.ts +18 -0
  41. package/dist/src/logger/pino.d.ts +8 -0
  42. package/dist/src/orm/base-columns.d.ts +11 -0
  43. package/dist/src/orm/column-comments.d.ts +25 -0
  44. package/dist/src/orm/crud-config.d.ts +7 -0
  45. package/dist/src/orm/crud-register.d.ts +5 -0
  46. package/dist/src/orm/data-scope.d.ts +24 -0
  47. package/dist/src/orm/db-store.d.ts +12 -0
  48. package/dist/src/orm/entity-schema.d.ts +7 -0
  49. package/dist/src/orm/index.d.ts +22 -0
  50. package/dist/src/orm/query-builder.d.ts +12 -0
  51. package/dist/src/orm/query-op.d.ts +27 -0
  52. package/dist/src/orm/repository.d.ts +74 -0
  53. package/dist/src/orm/service.d.ts +71 -0
  54. package/dist/src/orm/tenant.d.ts +24 -0
  55. package/dist/src/routing/base.d.ts +27 -0
  56. package/dist/src/routing/context.d.ts +14 -0
  57. package/dist/src/routing/crud-summary.d.ts +2 -0
  58. package/dist/src/routing/decorators.d.ts +48 -0
  59. package/dist/src/routing/eps.d.ts +16 -0
  60. package/dist/src/routing/index.d.ts +8 -0
  61. package/dist/src/routing/metadata.d.ts +28 -0
  62. package/dist/src/routing/openapi-tags.d.ts +23 -0
  63. package/dist/src/routing/path-utils.d.ts +5 -0
  64. package/dist/src/routing/perm.d.ts +18 -0
  65. package/dist/src/routing/register.d.ts +7 -0
  66. package/dist/src/server/index.d.ts +10 -0
  67. package/dist/src/shared/index.d.ts +8 -0
  68. package/dist/src/shared/tree.d.ts +18 -0
  69. package/dist/typings/comm/crud.d.ts +24 -0
  70. package/dist/typings/comm/page.d.ts +16 -0
  71. package/dist/typings/comm/scan.d.ts +6 -0
  72. package/dist/typings/common.d.ts +6 -0
  73. package/dist/typings/context/data.d.ts +10 -0
  74. package/dist/typings/ext/module.d.ts +45 -0
  75. package/dist/typings/ext/plugin.d.ts +40 -0
  76. package/dist/typings/ioc/metadata.d.ts +31 -0
  77. package/dist/typings/logger/config.d.ts +6 -0
  78. package/dist/typings/routing/crud.d.ts +99 -0
  79. package/dist/typings/routing/eps.d.ts +53 -0
  80. package/dist/typings/routing/meta.d.ts +56 -0
  81. package/dist/typings/routing/register.d.ts +15 -0
  82. package/dist/typings/routing/scan-context.d.ts +6 -0
  83. package/package.json +96 -0
  84. package/src/admin/api/client.ts +184 -0
  85. package/src/admin/components/vm-aside.vue +227 -0
  86. package/src/admin/components/vm-auto-perm-dialog.vue +361 -0
  87. package/src/admin/components/vm-check-tree.vue +338 -0
  88. package/src/admin/components/vm-dept-tree.vue +549 -0
  89. package/src/admin/components/vm-empty.vue +62 -0
  90. package/src/admin/components/vm-eps-perm-picker.vue +114 -0
  91. package/src/admin/components/vm-group-cascader.vue +563 -0
  92. package/src/admin/components/vm-icon-picker.vue +616 -0
  93. package/src/admin/components/vm-markdown.vue +141 -0
  94. package/src/admin/components/vm-ri-icon.vue +21 -0
  95. package/src/admin/components/vm-role-picker.vue +314 -0
  96. package/src/admin/components/vm-view-path-picker.vue +56 -0
  97. package/src/admin/config/plugin-dev.ts +31 -0
  98. package/src/admin/crud/components/vm-column-custom.vue +196 -0
  99. package/src/admin/crud/components/vm-context-menu.vue +99 -0
  100. package/src/admin/crud/components/vm-date-picker.vue +74 -0
  101. package/src/admin/crud/components/vm-date-range.vue +291 -0
  102. package/src/admin/crud/components/vm-date-text.vue +41 -0
  103. package/src/admin/crud/components/vm-dict-tag.vue +47 -0
  104. package/src/admin/crud/components/vm-file-icon.vue +54 -0
  105. package/src/admin/crud/components/vm-multi-select.vue +181 -0
  106. package/src/admin/crud/components/vm-number-range.vue +200 -0
  107. package/src/admin/crud/components/vm-radio.vue +87 -0
  108. package/src/admin/crud/components/vm-select.vue +131 -0
  109. package/src/admin/crud/components/vm-switch.vue +128 -0
  110. package/src/admin/crud/components/vm-tree-select.vue +456 -0
  111. package/src/admin/crud/components/vm-upload-item.vue +215 -0
  112. package/src/admin/crud/components/vm-upload.vue +323 -0
  113. package/src/admin/crud/components/vm-user-select.vue +234 -0
  114. package/src/admin/crud/config.ts +38 -0
  115. package/src/admin/crud/confirm.ts +135 -0
  116. package/src/admin/crud/dict.ts +49 -0
  117. package/src/admin/crud/index.ts +79 -0
  118. package/src/admin/crud/key.ts +47 -0
  119. package/src/admin/crud/mitt.ts +27 -0
  120. package/src/admin/crud/plugins.ts +298 -0
  121. package/src/admin/crud/registry.ts +45 -0
  122. package/src/admin/crud/span.ts +48 -0
  123. package/src/admin/crud/style.ts +112 -0
  124. package/src/admin/crud/useCrud.ts +81 -0
  125. package/src/admin/crud/validate.ts +109 -0
  126. package/src/admin/crud/vm-add-btn.vue +68 -0
  127. package/src/admin/crud/vm-adv-search.vue +73 -0
  128. package/src/admin/crud/vm-crud.vue +517 -0
  129. package/src/admin/crud/vm-dialog.vue +234 -0
  130. package/src/admin/crud/vm-flex1.vue +14 -0
  131. package/src/admin/crud/vm-form.vue +239 -0
  132. package/src/admin/crud/vm-multi-delete-btn.vue +48 -0
  133. package/src/admin/crud/vm-pagination.vue +125 -0
  134. package/src/admin/crud/vm-refresh-btn.vue +34 -0
  135. package/src/admin/crud/vm-row.vue +42 -0
  136. package/src/admin/crud/vm-search-key.vue +76 -0
  137. package/src/admin/crud/vm-search.vue +367 -0
  138. package/src/admin/crud/vm-table.vue +852 -0
  139. package/src/admin/crud/vm-toolbar.vue +245 -0
  140. package/src/admin/crud/vm-upsert.vue +530 -0
  141. package/src/admin/data/remixicon-all.json +1 -0
  142. package/src/admin/directives/perm.ts +18 -0
  143. package/src/admin/env.d.ts +5 -0
  144. package/src/admin/hooks/useBrowser.ts +27 -0
  145. package/src/admin/hooks/useUpload.ts +82 -0
  146. package/src/admin/hooks/useVome.ts +13 -0
  147. package/src/admin/index.ts +59 -0
  148. package/src/admin/lib/browser.ts +28 -0
  149. package/src/admin/lib/cn.ts +7 -0
  150. package/src/admin/lib/dialog-float.ts +14 -0
  151. package/src/admin/lib/eps.ts +43 -0
  152. package/src/admin/lib/menu.ts +15 -0
  153. package/src/admin/lib/module.ts +112 -0
  154. package/src/admin/lib/tree.ts +2 -0
  155. package/src/admin/lib/upload.ts +102 -0
  156. package/src/admin/router/index.ts +61 -0
  157. package/src/admin/router/menu-routes.ts +126 -0
  158. package/src/admin/service/base.ts +96 -0
  159. package/src/admin/service/index.ts +281 -0
  160. package/src/admin/static/fileicon/demo.css +539 -0
  161. package/src/admin/static/fileicon/demo_index.html +901 -0
  162. package/src/admin/static/fileicon/iconfont.css +139 -0
  163. package/src/admin/static/fileicon/iconfont.js +1 -0
  164. package/src/admin/static/fileicon/iconfont.json +226 -0
  165. package/src/admin/static/fileicon/iconfont.ttf +0 -0
  166. package/src/admin/static/fileicon/iconfont.woff +0 -0
  167. package/src/admin/static/fileicon/iconfont.woff2 +0 -0
  168. package/src/admin/static/scarce//344/270/213/350/275/275/345/244/261/350/264/245.svg +1 -0
  169. package/src/admin/static/scarce//346/224/257/344/273/230/346/210/220/345/212/237.svg +1 -0
  170. package/src/admin/static/scarce//346/232/202/346/227/240/344/274/230/346/203/240/345/210/270.svg +1 -0
  171. package/src/admin/static/scarce//346/232/202/346/227/240/345/206/205/345/256/271.svg +1 -0
  172. package/src/admin/static/scarce//346/232/202/346/227/240/345/217/221/347/245/250.svg +1 -0
  173. package/src/admin/static/scarce//346/232/202/346/227/240/345/233/276/347/211/207.svg +1 -0
  174. package/src/admin/static/scarce//346/232/202/346/227/240/345/234/260/345/235/200.svg +1 -0
  175. package/src/admin/static/scarce//346/232/202/346/227/240/345/272/227/351/223/272.svg +1 -0
  176. package/src/admin/static/scarce//346/232/202/346/227/240/346/216/250/351/200/201.svg +1 -0
  177. package/src/admin/static/scarce//346/232/202/346/227/240/346/220/234/347/264/242/347/273/223/346/236/234.svg +1 -0
  178. package/src/admin/static/scarce//346/232/202/346/227/240/346/224/266/350/227/217.svg +1 -0
  179. package/src/admin/static/scarce//346/232/202/346/227/240/346/225/260/346/215/256.svg +1 -0
  180. package/src/admin/static/scarce//346/232/202/346/227/240/346/235/203/351/231/220.svg +1 -0
  181. package/src/admin/static/scarce//346/232/202/346/227/240/346/266/210/346/201/257.svg +1 -0
  182. package/src/admin/static/scarce//346/232/202/346/227/240/347/244/274/347/211/251.svg +1 -0
  183. package/src/admin/static/scarce//346/232/202/346/227/240/347/247/257/345/210/206.svg +1 -0
  184. package/src/admin/static/scarce//346/232/202/346/227/240/347/275/221/347/273/234.svg +1 -0
  185. package/src/admin/static/scarce//346/232/202/346/227/240/350/247/206/351/242/221.svg +1 -0
  186. package/src/admin/static/scarce//346/232/202/346/227/240/350/256/242/345/215/225.svg +1 -0
  187. package/src/admin/static/scarce//346/232/202/346/227/240/350/257/204/350/256/272.svg +1 -0
  188. package/src/admin/static/scarce//346/232/202/346/227/240/351/205/215/351/200/201.svg +1 -0
  189. package/src/admin/static/scarce//346/232/202/346/227/240/351/223/266/350/241/214/345/215/241.svg +1 -0
  190. package/src/admin/static/scarce//346/232/202/346/227/240/351/237/263/344/271/220.svg +1 -0
  191. package/src/admin/static/scarce//351/241/265/351/235/242/344/270/215/345/255/230/345/234/250.svg +1 -0
  192. package/src/admin/stores/app.ts +30 -0
  193. package/src/admin/stores/tags.ts +73 -0
  194. package/src/admin/stores/user.ts +52 -0
  195. package/src/admin/styles/base.css +137 -0
  196. package/src/admin/tsconfig.json +19 -0
  197. package/typings/admin/base/auth.ts +23 -0
  198. package/typings/admin/base/permission.ts +19 -0
  199. package/typings/admin/comm/cascader.ts +7 -0
  200. package/typings/admin/comm/check-tree.ts +7 -0
  201. package/typings/admin/comm/crud.ts +294 -0
  202. package/typings/admin/comm/eps.ts +52 -0
  203. package/typings/admin/comm/request.ts +12 -0
  204. package/typings/admin/comm/service.ts +35 -0
  205. package/typings/admin/comm/upload.ts +39 -0
  206. package/typings/admin/host.d.ts +61 -0
  207. package/typings/admin/vome/browser.ts +11 -0
  208. package/typings/admin/vome/module.ts +13 -0
  209. package/typings/admin/vome/tags.ts +9 -0
@@ -0,0 +1,616 @@
1
+ <template>
2
+ <div ref="rootRef" class="vm-icon-picker">
3
+ <div ref="triggerRef" class="vm-icon-picker__row">
4
+ <button
5
+ type="button"
6
+ class="vm-icon-picker__preview"
7
+ :class="{ 'is-open': open }"
8
+ :disabled="disabled"
9
+ :title="normalized || '选择图标'"
10
+ @click="toggle"
11
+ >
12
+ <i v-if="normalized" :class="normalized" />
13
+ <i v-else class="ri-apps-fill vm-icon-picker__preview-ph" />
14
+ </button>
15
+ <div class="vm-icon-picker__value">
16
+ <input
17
+ class="vm-icon-picker__input"
18
+ :value="displayName"
19
+ :placeholder="placeholder"
20
+ :disabled="disabled"
21
+ @focus="openPanel"
22
+ @input="onNameInput"
23
+ @keydown.esc.stop="closePanel"
24
+ />
25
+ <i
26
+ v-if="displayName && clearable && !disabled"
27
+ class="ri-close-circle-fill vm-icon-picker__clear"
28
+ @click.stop="clearValue"
29
+ />
30
+ </div>
31
+ </div>
32
+
33
+ <Teleport to="body">
34
+ <!-- 嵌套 FocusScope:暂停 Dialog 焦点陷阱,否则 Teleport 内 input 无法聚焦 -->
35
+ <FocusScope
36
+ v-if="open"
37
+ as-child
38
+ loop
39
+ :trapped="true"
40
+ @unmount-auto-focus="onPanelUnmountAutoFocus"
41
+ >
42
+ <div
43
+ ref="panelRef"
44
+ class="vm-icon-picker__panel vm-dialog-float"
45
+ :style="panelStyle"
46
+ @pointerdown.stop
47
+ @mousedown.stop
48
+ >
49
+ <div class="vm-icon-picker__toolbar">
50
+ <div class="vm-icon-picker__search">
51
+ <i class="ri-search-line vm-icon-picker__search-ico" />
52
+ <input
53
+ ref="searchRef"
54
+ class="vm-icon-picker__search-input"
55
+ :value="search"
56
+ placeholder="模糊搜索,如 home / shield"
57
+ @input="onSearchInput"
58
+ @keydown.esc.stop="closePanel"
59
+ />
60
+ <i
61
+ v-if="search"
62
+ class="ri-close-circle-fill vm-icon-picker__search-clear"
63
+ @click.stop="clearSearch"
64
+ />
65
+ </div>
66
+ <div class="vm-icon-picker__styles" role="group" aria-label="图标类型">
67
+ <button
68
+ v-for="opt in STYLE_OPTS"
69
+ :key="opt.value"
70
+ type="button"
71
+ class="vm-icon-picker__style-btn"
72
+ :class="{ 'is-active': styleFilter === opt.value }"
73
+ :title="opt.title"
74
+ @click.stop="setStyleFilter(opt.value)"
75
+ >
76
+ {{ opt.label }}
77
+ </button>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="vm-icon-picker__grid">
82
+ <button
83
+ v-for="name in pageIcons"
84
+ :key="name"
85
+ type="button"
86
+ class="vm-icon-picker__cell"
87
+ :class="{ 'is-active': name === normalized }"
88
+ :title="name"
89
+ @click="pick(name)"
90
+ >
91
+ <i :class="name" />
92
+ </button>
93
+ <button
94
+ v-if="showRefresh"
95
+ type="button"
96
+ class="vm-icon-picker__more"
97
+ @click.prevent.stop="nextBatch"
98
+ >
99
+ 换一批
100
+ </button>
101
+ <p v-if="!filtered.length" class="vm-icon-picker__empty">无匹配图标</p>
102
+ </div>
103
+ </div>
104
+ </FocusScope>
105
+ </Teleport>
106
+ </div>
107
+ </template>
108
+
109
+ <script setup lang="ts">
110
+ import { FocusScope } from 'reka-ui'
111
+ import allIcons from '@vome-core/admin/data/remixicon-all.json'
112
+
113
+ defineOptions({ name: 'vm-icon-picker' })
114
+
115
+ type IconStyle = 'fill' | 'line' | 'other'
116
+
117
+ const STYLE_OPTS: { value: IconStyle; label: string; title: string }[] = [
118
+ { value: 'fill', label: '实心', title: 'Fill 实心图标' },
119
+ { value: 'line', label: '线形', title: 'Line 线形图标' },
120
+ { value: 'other', label: '其它', title: '无 fill/line 后缀(多为编辑类)' },
121
+ ]
122
+
123
+ const props = withDefaults(
124
+ defineProps<{
125
+ modelValue?: string | null
126
+ placeholder?: string
127
+ disabled?: boolean
128
+ clearable?: boolean
129
+ }>(),
130
+ {
131
+ placeholder: '如 ri-home-fill',
132
+ clearable: true,
133
+ },
134
+ )
135
+
136
+ const emit = defineEmits<{
137
+ 'update:modelValue': [v: string]
138
+ }>()
139
+
140
+ /** 10 列 × 5 行 = 50 格,末尾 2 格给「换一批」 */
141
+ const PAGE_SIZE = 48
142
+ const ALL = allIcons as string[]
143
+
144
+ const rootRef = ref<HTMLElement | null>(null)
145
+ const triggerRef = ref<HTMLElement | null>(null)
146
+ const panelRef = ref<HTMLElement | null>(null)
147
+ const searchRef = ref<HTMLInputElement | null>(null)
148
+ const open = ref(false)
149
+ const search = ref('')
150
+ const page = ref(0)
151
+ const styleFilter = ref<IconStyle>('fill')
152
+ const panelStyle = ref<Record<string, string>>({})
153
+ /** 关闭后面板卸载会回焦外层 input,避免再次触发 openPanel */
154
+ const suppressOpen = ref(false)
155
+
156
+ /** 仅补齐 ri- 前缀,保留 fill / line / 其它原样 */
157
+ function normalizeIcon(raw: string | null | undefined): string {
158
+ if (!raw) return ''
159
+ let n = String(raw).trim()
160
+ if (!n) return ''
161
+ if (!n.startsWith('ri-')) n = `ri-${n}`
162
+ return n
163
+ }
164
+
165
+ function styleOf(name: string): IconStyle {
166
+ if (name.endsWith('-fill')) return 'fill'
167
+ if (name.endsWith('-line')) return 'line'
168
+ return 'other'
169
+ }
170
+
171
+ const normalized = computed(() => normalizeIcon(props.modelValue))
172
+
173
+ /** 外层输入框展示当前 icon 名称(与 v-model 同步) */
174
+ const displayName = computed(() => {
175
+ const raw = props.modelValue
176
+ if (raw == null || raw === '') return ''
177
+ return String(raw)
178
+ })
179
+
180
+ function fuzzyMatch(hay: string, needle: string): boolean {
181
+ const h = hay.toLowerCase()
182
+ const n = needle.toLowerCase().replace(/\s+/g, '')
183
+ if (!n) return true
184
+ if (h.includes(n)) return true
185
+ let j = 0
186
+ for (let i = 0; i < h.length && j < n.length; i++) {
187
+ if (h[i] === n[j]) j++
188
+ }
189
+ return j === n.length
190
+ }
191
+
192
+ function iconSearchKey(name: string) {
193
+ return name
194
+ .replace(/^ri-/, '')
195
+ .replace(/-fill$/, '')
196
+ .replace(/-line$/, '')
197
+ .replace(/-/g, '')
198
+ }
199
+
200
+ const filtered = computed(() => {
201
+ const byStyle = ALL.filter((name) => styleOf(name) === styleFilter.value)
202
+ const q = search.value.trim()
203
+ if (!q) return byStyle
204
+ const qKey = q
205
+ .replace(/^ri-/, '')
206
+ .replace(/-fill$/, '')
207
+ .replace(/-line$/, '')
208
+ return byStyle.filter((name) => {
209
+ if (fuzzyMatch(name, q)) return true
210
+ if (fuzzyMatch(iconSearchKey(name), qKey.replace(/-/g, ''))) return true
211
+ return fuzzyMatch(
212
+ name.replace(/^ri-/, '').replace(/-fill$/, '').replace(/-line$/, ''),
213
+ qKey,
214
+ )
215
+ })
216
+ })
217
+
218
+ const pageCount = computed(() =>
219
+ Math.max(1, Math.ceil(filtered.value.length / PAGE_SIZE)),
220
+ )
221
+
222
+ const pageIcons = computed(() => {
223
+ const start = (page.value % pageCount.value) * PAGE_SIZE
224
+ return filtered.value.slice(start, start + PAGE_SIZE)
225
+ })
226
+
227
+ const showRefresh = computed(() => filtered.value.length > PAGE_SIZE)
228
+
229
+ watch(filtered, () => {
230
+ page.value = 0
231
+ })
232
+
233
+ function setStyleFilter(v: IconStyle) {
234
+ styleFilter.value = v
235
+ page.value = 0
236
+ }
237
+
238
+ function syncPanelPos() {
239
+ const el = triggerRef.value
240
+ if (!el) return
241
+ const r = el.getBoundingClientRect()
242
+ const width = Math.min(480, window.innerWidth - 16)
243
+ let left = r.left
244
+ if (left + width > window.innerWidth - 8) {
245
+ left = Math.max(8, window.innerWidth - width - 8)
246
+ }
247
+ let top = r.bottom + 6
248
+ const estH = 320
249
+ if (top + estH > window.innerHeight - 8) {
250
+ top = Math.max(8, r.top - estH - 6)
251
+ }
252
+ panelStyle.value = {
253
+ position: 'fixed',
254
+ top: `${top}px`,
255
+ left: `${left}px`,
256
+ width: `${width}px`,
257
+ zIndex: '100',
258
+ }
259
+ }
260
+
261
+ async function openPanel() {
262
+ if (props.disabled || suppressOpen.value) return
263
+ styleFilter.value = 'fill'
264
+ open.value = true
265
+ await nextTick()
266
+ syncPanelPos()
267
+ }
268
+
269
+ /** 禁止卸载时把焦点塞回外层名称框(否则 @focus 会立刻再次打开) */
270
+ function onPanelUnmountAutoFocus(event: Event) {
271
+ event.preventDefault()
272
+ }
273
+
274
+ function closePanel() {
275
+ if (!open.value) return
276
+ suppressOpen.value = true
277
+ open.value = false
278
+ search.value = ''
279
+ page.value = 0
280
+ styleFilter.value = 'fill'
281
+ // 等 FocusScope 卸载与可能的回焦完成后再允许打开
282
+ nextTick(() => {
283
+ requestAnimationFrame(() => {
284
+ suppressOpen.value = false
285
+ })
286
+ })
287
+ }
288
+
289
+ async function toggle() {
290
+ if (props.disabled) return
291
+ if (open.value) {
292
+ closePanel()
293
+ return
294
+ }
295
+ await openPanel()
296
+ }
297
+
298
+ /** 外层输入:直接编辑 / 存储 icon 名称 */
299
+ function onNameInput(e: Event) {
300
+ const v = (e.target as HTMLInputElement).value.trim()
301
+ emit('update:modelValue', v)
302
+ if (!open.value) openPanel()
303
+ }
304
+
305
+ function clearValue() {
306
+ emit('update:modelValue', '')
307
+ }
308
+
309
+ function onSearchInput(e: Event) {
310
+ search.value = (e.target as HTMLInputElement).value
311
+ }
312
+
313
+ function clearSearch() {
314
+ search.value = ''
315
+ searchRef.value?.focus()
316
+ }
317
+
318
+ function pick(name: string) {
319
+ emit('update:modelValue', name)
320
+ closePanel()
321
+ }
322
+
323
+ function nextBatch() {
324
+ page.value = (page.value + 1) % pageCount.value
325
+ }
326
+
327
+ function onDocPointer(e: Event) {
328
+ if (!open.value) return
329
+ const t = e.target as Node | null
330
+ if (!t) return
331
+ if (rootRef.value?.contains(t)) return
332
+ if (panelRef.value?.contains(t)) return
333
+ closePanel()
334
+ }
335
+
336
+ function onWinChange() {
337
+ if (open.value) syncPanelPos()
338
+ }
339
+
340
+ onMounted(() => {
341
+ document.addEventListener('pointerdown', onDocPointer, true)
342
+ window.addEventListener('resize', onWinChange)
343
+ window.addEventListener('scroll', onWinChange, true)
344
+ })
345
+
346
+ onUnmounted(() => {
347
+ document.removeEventListener('pointerdown', onDocPointer, true)
348
+ window.removeEventListener('resize', onWinChange)
349
+ window.removeEventListener('scroll', onWinChange, true)
350
+ })
351
+ </script>
352
+
353
+ <style lang="scss" scoped>
354
+ .vm-icon-picker {
355
+ width: 100%;
356
+ }
357
+
358
+ .vm-icon-picker__row {
359
+ display: flex;
360
+ align-items: center;
361
+ gap: 8px;
362
+ width: 100%;
363
+ }
364
+
365
+ .vm-icon-picker__preview {
366
+ flex: 0 0 var(--vm-control-height);
367
+ width: var(--vm-control-height);
368
+ height: var(--vm-control-height);
369
+ display: inline-flex;
370
+ align-items: center;
371
+ justify-content: center;
372
+ border: 1px solid var(--border);
373
+ border-radius: var(--vm-control-radius);
374
+ background: var(--muted);
375
+ color: var(--foreground);
376
+ font-size: 16px;
377
+ cursor: pointer;
378
+ transition:
379
+ border-color 0.15s,
380
+ background 0.15s,
381
+ color 0.15s;
382
+
383
+ &:hover:not(:disabled),
384
+ &.is-open {
385
+ border-color: var(--brand);
386
+ background: var(--brand-soft);
387
+ color: var(--brand);
388
+ }
389
+
390
+ &:disabled {
391
+ opacity: 0.6;
392
+ cursor: not-allowed;
393
+ }
394
+ }
395
+
396
+ .vm-icon-picker__preview-ph {
397
+ opacity: 0.35;
398
+ }
399
+
400
+ .vm-icon-picker__value {
401
+ position: relative;
402
+ flex: 1;
403
+ min-width: 0;
404
+ }
405
+
406
+ .vm-icon-picker__input {
407
+ width: 100%;
408
+ height: var(--vm-control-height);
409
+ padding: 0 26px 0 var(--vm-control-padding-x);
410
+ border: 1px solid var(--border);
411
+ border-radius: var(--vm-control-radius);
412
+ background: var(--muted);
413
+ color: var(--foreground);
414
+ font-size: var(--vm-control-font-size);
415
+ outline: none;
416
+ transition:
417
+ border-color 0.15s,
418
+ box-shadow 0.15s;
419
+
420
+ &::placeholder {
421
+ color: var(--muted-foreground);
422
+ }
423
+
424
+ &:focus {
425
+ border-color: var(--brand);
426
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand) 18%, transparent);
427
+ background: #fff;
428
+ }
429
+
430
+ &:disabled {
431
+ opacity: 0.6;
432
+ cursor: not-allowed;
433
+ }
434
+ }
435
+
436
+ .vm-icon-picker__clear {
437
+ position: absolute;
438
+ right: 8px;
439
+ top: 50%;
440
+ transform: translateY(-50%);
441
+ font-size: 14px;
442
+ color: var(--muted-foreground);
443
+ cursor: pointer;
444
+
445
+ &:hover {
446
+ color: var(--foreground);
447
+ }
448
+ }
449
+ </style>
450
+
451
+ <style lang="scss">
452
+ /* Teleport 到 body,不可 scoped */
453
+ .vm-icon-picker__panel {
454
+ /* Dialog modal 会给 body 设 pointer-events:none,Teleport 面板必须显式恢复 */
455
+ pointer-events: auto;
456
+ padding: 10px;
457
+ border: 1px solid var(--border);
458
+ border-radius: 12px;
459
+ background: #fff;
460
+ box-shadow: var(--shadow-soft);
461
+ }
462
+
463
+ .vm-icon-picker__toolbar {
464
+ display: flex;
465
+ align-items: center;
466
+ gap: 8px;
467
+ margin-bottom: 8px;
468
+ }
469
+
470
+ .vm-icon-picker__search {
471
+ position: relative;
472
+ flex: 1;
473
+ min-width: 0;
474
+ }
475
+
476
+ .vm-icon-picker__search-ico {
477
+ position: absolute;
478
+ left: 10px;
479
+ top: 50%;
480
+ transform: translateY(-50%);
481
+ font-size: 14px;
482
+ color: var(--muted-foreground);
483
+ pointer-events: none;
484
+ }
485
+
486
+ .vm-icon-picker__search-input {
487
+ width: 100%;
488
+ height: var(--vm-control-height);
489
+ padding: 0 26px 0 28px;
490
+ border: 1px solid var(--border);
491
+ border-radius: var(--vm-control-radius);
492
+ background: var(--muted);
493
+ color: var(--foreground);
494
+ font-size: var(--vm-control-font-size);
495
+ outline: none;
496
+
497
+ &::placeholder {
498
+ color: var(--muted-foreground);
499
+ }
500
+
501
+ &:focus {
502
+ border-color: var(--brand);
503
+ background: #fff;
504
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand) 18%, transparent);
505
+ }
506
+ }
507
+
508
+ .vm-icon-picker__search-clear {
509
+ position: absolute;
510
+ right: 8px;
511
+ top: 50%;
512
+ transform: translateY(-50%);
513
+ font-size: 14px;
514
+ color: var(--muted-foreground);
515
+ cursor: pointer;
516
+
517
+ &:hover {
518
+ color: var(--foreground);
519
+ }
520
+ }
521
+
522
+ .vm-icon-picker__styles {
523
+ display: inline-flex;
524
+ flex-shrink: 0;
525
+ gap: 4px;
526
+ }
527
+
528
+ .vm-icon-picker__style-btn {
529
+ height: var(--vm-control-height);
530
+ padding: 0 var(--vm-control-padding-x);
531
+ border: 1px solid var(--border);
532
+ border-radius: var(--vm-control-radius);
533
+ background: var(--muted);
534
+ color: var(--muted-foreground);
535
+ font-size: 12px;
536
+ font-weight: 500;
537
+ white-space: nowrap;
538
+ cursor: pointer;
539
+ transition:
540
+ background 0.12s,
541
+ color 0.12s,
542
+ border-color 0.12s;
543
+
544
+ &:hover {
545
+ border-color: color-mix(in srgb, var(--brand) 40%, var(--border));
546
+ color: var(--brand);
547
+ }
548
+
549
+ &.is-active {
550
+ border-color: var(--brand);
551
+ background: var(--brand-soft);
552
+ color: var(--brand);
553
+ }
554
+ }
555
+
556
+ .vm-icon-picker__grid {
557
+ display: grid;
558
+ grid-template-columns: repeat(10, minmax(0, 1fr));
559
+ gap: 4px;
560
+ }
561
+
562
+ .vm-icon-picker__cell {
563
+ aspect-ratio: 1;
564
+ display: inline-flex;
565
+ align-items: center;
566
+ justify-content: center;
567
+ border: none;
568
+ border-radius: 8px;
569
+ background: transparent;
570
+ color: #50566b;
571
+ font-size: 18px;
572
+ cursor: pointer;
573
+ transition:
574
+ background 0.12s,
575
+ color 0.12s;
576
+
577
+ &:hover {
578
+ background: var(--brand-soft);
579
+ color: var(--brand);
580
+ }
581
+
582
+ &.is-active {
583
+ background: var(--brand-soft);
584
+ color: var(--brand);
585
+ }
586
+ }
587
+
588
+ .vm-icon-picker__more {
589
+ grid-column: span 2;
590
+ height: 100%;
591
+ min-height: 32px;
592
+ display: inline-flex;
593
+ align-items: center;
594
+ justify-content: center;
595
+ border: 1px dashed color-mix(in srgb, var(--brand) 35%, var(--border));
596
+ border-radius: 8px;
597
+ background: var(--brand-soft);
598
+ color: var(--brand);
599
+ font-size: 12px;
600
+ font-weight: 500;
601
+ cursor: pointer;
602
+ transition: background 0.12s;
603
+
604
+ &:hover {
605
+ background: color-mix(in srgb, var(--brand) 16%, #fff);
606
+ }
607
+ }
608
+
609
+ .vm-icon-picker__empty {
610
+ grid-column: 1 / -1;
611
+ margin: 12px 0;
612
+ text-align: center;
613
+ font-size: 12px;
614
+ color: var(--muted-foreground);
615
+ }
616
+ </style>