vome-core 0.3.65 → 0.3.66

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 (342) hide show
  1. package/dist/admin/api/client.js +7 -2
  2. package/dist/admin/components/ui/avatar/Avatar.vue +18 -18
  3. package/dist/admin/components/ui/avatar/AvatarFallback.vue +21 -21
  4. package/dist/admin/components/ui/avatar/AvatarImage.vue +16 -16
  5. package/dist/admin/components/ui/avatar/index.js +1 -1
  6. package/dist/admin/components/ui/badge/Badge.vue +26 -26
  7. package/dist/admin/components/ui/button/Button.vue +31 -31
  8. package/dist/admin/components/ui/checkbox/Checkbox.vue +35 -35
  9. package/dist/admin/components/ui/checkbox/index.js +1 -1
  10. package/dist/admin/components/ui/dialog/Dialog.vue +19 -19
  11. package/dist/admin/components/ui/dialog/DialogClose.vue +15 -15
  12. package/dist/admin/components/ui/dialog/DialogContent.vue +267 -267
  13. package/dist/admin/components/ui/dialog/DialogDescription.vue +23 -23
  14. package/dist/admin/components/ui/dialog/DialogFooter.vue +27 -27
  15. package/dist/admin/components/ui/dialog/DialogHeader.vue +17 -17
  16. package/dist/admin/components/ui/dialog/DialogOverlay.vue +21 -21
  17. package/dist/admin/components/ui/dialog/DialogScrollContent.vue +60 -60
  18. package/dist/admin/components/ui/dialog/DialogTitle.vue +23 -23
  19. package/dist/admin/components/ui/dialog/DialogTrigger.vue +15 -15
  20. package/dist/admin/components/ui/dialog/index.js +1 -1
  21. package/dist/admin/components/ui/dropdown-menu/DropdownMenu.vue +19 -19
  22. package/dist/admin/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -39
  23. package/dist/admin/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -39
  24. package/dist/admin/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -15
  25. package/dist/admin/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -31
  26. package/dist/admin/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -23
  27. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -21
  28. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -40
  29. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -23
  30. package/dist/admin/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -17
  31. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -18
  32. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -27
  33. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -31
  34. package/dist/admin/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -17
  35. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  36. package/dist/admin/components/ui/input/Input.vue +183 -183
  37. package/dist/admin/components/ui/input/index.js +1 -1
  38. package/dist/admin/components/ui/label/Label.vue +26 -26
  39. package/dist/admin/components/ui/label/index.js +1 -1
  40. package/dist/admin/components/ui/scroll-area/ScrollArea.vue +33 -33
  41. package/dist/admin/components/ui/scroll-area/ScrollBar.vue +32 -32
  42. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  43. package/dist/admin/components/ui/select/Select.vue +19 -19
  44. package/dist/admin/components/ui/select/SelectContent.vue +51 -51
  45. package/dist/admin/components/ui/select/SelectGroup.vue +15 -15
  46. package/dist/admin/components/ui/select/SelectItem.vue +53 -53
  47. package/dist/admin/components/ui/select/SelectItemText.vue +15 -15
  48. package/dist/admin/components/ui/select/SelectLabel.vue +17 -17
  49. package/dist/admin/components/ui/select/SelectScrollDownButton.vue +26 -26
  50. package/dist/admin/components/ui/select/SelectScrollUpButton.vue +26 -26
  51. package/dist/admin/components/ui/select/SelectSeparator.vue +19 -19
  52. package/dist/admin/components/ui/select/SelectTrigger.vue +44 -44
  53. package/dist/admin/components/ui/select/SelectValue.vue +15 -15
  54. package/dist/admin/components/ui/select/index.js +1 -1
  55. package/dist/admin/components/ui/separator/Separator.vue +29 -29
  56. package/dist/admin/components/ui/separator/index.js +1 -1
  57. package/dist/admin/components/ui/sheet/Sheet.vue +19 -19
  58. package/dist/admin/components/ui/sheet/SheetClose.vue +15 -15
  59. package/dist/admin/components/ui/sheet/SheetContent.vue +63 -63
  60. package/dist/admin/components/ui/sheet/SheetDescription.vue +21 -21
  61. package/dist/admin/components/ui/sheet/SheetFooter.vue +16 -16
  62. package/dist/admin/components/ui/sheet/SheetHeader.vue +15 -15
  63. package/dist/admin/components/ui/sheet/SheetOverlay.vue +21 -21
  64. package/dist/admin/components/ui/sheet/SheetTitle.vue +21 -21
  65. package/dist/admin/components/ui/sheet/SheetTrigger.vue +15 -15
  66. package/dist/admin/components/ui/sheet/index.js +1 -1
  67. package/dist/admin/components/ui/sonner/Toaster.vue +117 -117
  68. package/dist/admin/components/ui/sonner/index.js +1 -1
  69. package/dist/admin/components/ui/table/Table.vue +16 -16
  70. package/dist/admin/components/ui/table/TableBody.vue +17 -17
  71. package/dist/admin/components/ui/table/TableCaption.vue +17 -17
  72. package/dist/admin/components/ui/table/TableCell.vue +22 -22
  73. package/dist/admin/components/ui/table/TableEmpty.vue +34 -34
  74. package/dist/admin/components/ui/table/TableFooter.vue +17 -17
  75. package/dist/admin/components/ui/table/TableHead.vue +17 -17
  76. package/dist/admin/components/ui/table/TableHeader.vue +17 -17
  77. package/dist/admin/components/ui/table/TableRow.vue +17 -17
  78. package/dist/admin/components/ui/table/index.js +1 -1
  79. package/dist/admin/components/ui/tabs/Tabs.vue +24 -24
  80. package/dist/admin/components/ui/tabs/TabsContent.vue +21 -21
  81. package/dist/admin/components/ui/tabs/TabsList.vue +24 -24
  82. package/dist/admin/components/ui/tabs/TabsTrigger.vue +26 -26
  83. package/dist/admin/components/ui/tabs/index.js +1 -1
  84. package/dist/admin/components/ui/tooltip/Tooltip.vue +19 -19
  85. package/dist/admin/components/ui/tooltip/TooltipContent.vue +34 -34
  86. package/dist/admin/components/ui/tooltip/TooltipProvider.vue +14 -14
  87. package/dist/admin/components/ui/tooltip/TooltipTrigger.vue +15 -15
  88. package/dist/admin/components/ui/tooltip/index.js +1 -1
  89. package/dist/admin/components/vm-action-btn.vue +136 -136
  90. package/dist/admin/components/vm-aside-list-item.vue +136 -136
  91. package/dist/admin/components/vm-aside.vue +229 -229
  92. package/dist/admin/components/vm-auto-perm-dialog.vue +371 -371
  93. package/dist/admin/components/vm-card-grid.vue +37 -37
  94. package/dist/admin/components/vm-card-head.vue +45 -45
  95. package/dist/admin/components/vm-card-meta.vue +22 -22
  96. package/dist/admin/components/vm-card-title.vue +56 -56
  97. package/dist/admin/components/vm-card.vue +176 -176
  98. package/dist/admin/components/vm-check-tree.vue +340 -340
  99. package/dist/admin/components/vm-clamp-text.vue +39 -39
  100. package/dist/admin/components/vm-dept-tree.vue +497 -497
  101. package/dist/admin/components/vm-empty.vue +68 -68
  102. package/dist/admin/components/vm-eps-perm-picker.vue +123 -123
  103. package/dist/admin/components/vm-form-dialog.vue +632 -632
  104. package/dist/admin/components/vm-group-cascader.vue +560 -560
  105. package/dist/admin/components/vm-icon-btn.vue +88 -88
  106. package/dist/admin/components/vm-icon-picker.vue +647 -647
  107. package/dist/admin/components/vm-inline-search.vue +61 -61
  108. package/dist/admin/components/vm-loading.vue +114 -114
  109. package/dist/admin/components/vm-markdown.vue +405 -405
  110. package/dist/admin/components/vm-marquee.vue +265 -265
  111. package/dist/admin/components/vm-page-tabs.vue +94 -94
  112. package/dist/admin/components/vm-perm-section.vue +57 -57
  113. package/dist/admin/components/vm-ri-icon.vue +23 -23
  114. package/dist/admin/components/vm-role-picker.vue +314 -314
  115. package/dist/admin/components/vm-split-layout.vue +106 -106
  116. package/dist/admin/components/vm-thumb.vue +49 -49
  117. package/dist/admin/components/vm-view-path-picker.vue +59 -59
  118. package/dist/admin/crud/auto-fields.js +1 -1
  119. package/dist/admin/crud/components/vm-avatar.vue +104 -104
  120. package/dist/admin/crud/components/vm-color-picker.vue +1626 -1626
  121. package/dist/admin/crud/components/vm-column-custom.vue +193 -193
  122. package/dist/admin/crud/components/vm-context-menu.vue +105 -105
  123. package/dist/admin/crud/components/vm-copy-btn.vue +102 -102
  124. package/dist/admin/crud/components/vm-date-calendar-panel.vue +537 -537
  125. package/dist/admin/crud/components/vm-date-picker.vue +75 -75
  126. package/dist/admin/crud/components/vm-date-range.vue +88 -88
  127. package/dist/admin/crud/components/vm-date-text.vue +42 -42
  128. package/dist/admin/crud/components/vm-datetime-field.vue +215 -215
  129. package/dist/admin/crud/components/vm-datetime-picker.vue +9 -9
  130. package/dist/admin/crud/components/vm-dict-tag.vue +112 -112
  131. package/dist/admin/crud/components/vm-ellipsis-text.vue +34 -34
  132. package/dist/admin/crud/components/vm-form-hint.vue +81 -81
  133. package/dist/admin/crud/components/vm-json-code.vue +41 -41
  134. package/dist/admin/crud/components/vm-json-editor.vue +161 -161
  135. package/dist/admin/crud/components/vm-json-kv-editor.vue +318 -318
  136. package/dist/admin/crud/components/vm-kv-row.vue +51 -51
  137. package/dist/admin/crud/components/vm-multi-select.vue +279 -279
  138. package/dist/admin/crud/components/vm-number-range.vue +204 -204
  139. package/dist/admin/crud/components/vm-preview-viewer.vue +894 -894
  140. package/dist/admin/crud/components/vm-radio.vue +87 -87
  141. package/dist/admin/crud/components/vm-richtext.vue +313 -313
  142. package/dist/admin/crud/components/vm-select.vue +570 -570
  143. package/dist/admin/crud/components/vm-status-tag.vue +158 -158
  144. package/dist/admin/crud/components/vm-switch.vue +158 -158
  145. package/dist/admin/crud/components/vm-tag-list.vue +108 -108
  146. package/dist/admin/crud/components/vm-text-link.vue +52 -52
  147. package/dist/admin/crud/components/vm-toggle-switch.vue +84 -84
  148. package/dist/admin/crud/components/vm-tree-select.vue +450 -450
  149. package/dist/admin/crud/components/vm-upload-item.vue +298 -298
  150. package/dist/admin/crud/components/vm-upload.vue +436 -436
  151. package/dist/admin/crud/config.js +1 -1
  152. package/dist/admin/crud/confirm.js +1 -1
  153. package/dist/admin/crud/crud-page-request.js +1 -1
  154. package/dist/admin/crud/data-i18n.js +1 -1
  155. package/dist/admin/crud/dict.js +1 -1
  156. package/dist/admin/crud/export-fetch.js +1 -1
  157. package/dist/admin/crud/index.js +1 -0
  158. package/dist/admin/crud/key.js +1 -1
  159. package/dist/admin/crud/plugins.js +1 -1
  160. package/dist/admin/crud/span.js +1 -1
  161. package/dist/admin/crud/style.js +1 -1
  162. package/dist/admin/crud/useCrud.js +3 -0
  163. package/dist/admin/crud/validate.js +1 -1
  164. package/dist/admin/crud/vm-add-btn.vue +70 -70
  165. package/dist/admin/crud/vm-adv-search.vue +83 -83
  166. package/dist/admin/crud/vm-crud.vue +711 -711
  167. package/dist/admin/crud/vm-data-i18n-btn.vue +456 -456
  168. package/dist/admin/crud/vm-data-i18n-lang.vue +183 -183
  169. package/dist/admin/crud/vm-export-btn.vue +183 -183
  170. package/dist/admin/crud/vm-flex.vue +14 -14
  171. package/dist/admin/crud/vm-form.vue +33 -15
  172. package/dist/admin/crud/vm-import-btn.vue +747 -747
  173. package/dist/admin/crud/vm-multi-delete-btn.vue +49 -49
  174. package/dist/admin/crud/vm-pagination.vue +212 -212
  175. package/dist/admin/crud/vm-refresh-btn.vue +35 -35
  176. package/dist/admin/crud/vm-row.vue +55 -55
  177. package/dist/admin/crud/vm-search-key.vue +78 -78
  178. package/dist/admin/crud/vm-search.vue +466 -466
  179. package/dist/admin/crud/vm-table.vue +1390 -1390
  180. package/dist/admin/crud/vm-tabs.vue +94 -94
  181. package/dist/admin/crud/vm-toolbar.vue +357 -357
  182. package/dist/admin/crud/vm-upsert.vue +765 -765
  183. package/dist/admin/directives/perm.js +1 -1
  184. package/dist/admin/env.d.ts +1 -1
  185. package/dist/admin/hooks/useUpload.js +1 -1
  186. package/dist/admin/index.d.ts +1 -31
  187. package/dist/admin/lib/browser.js +1 -1
  188. package/dist/admin/lib/cn.js +1 -1
  189. package/dist/admin/lib/dialog-float.js +1 -1
  190. package/dist/admin/lib/export-excel.js +1 -1
  191. package/dist/admin/lib/import-excel.js +1 -1
  192. package/dist/admin/lib/json.js +1 -1
  193. package/dist/admin/lib/menu.js +1 -1
  194. package/dist/admin/lib/micro-locale.js +1 -1
  195. package/dist/admin/lib/micro-theme.js +1 -1
  196. package/dist/admin/lib/shell.js +1 -1
  197. package/dist/admin/lib/theme-color-options.js +1 -1
  198. package/dist/admin/lib/tree.js +1 -1
  199. package/dist/admin/lib/upload.js +1 -1
  200. package/dist/admin/lib/video-frame.js +1 -1
  201. package/dist/admin/router/create.js +1 -1
  202. package/dist/admin/router/guards.js +1 -1
  203. package/dist/admin/router/index.d.ts +1 -5
  204. package/dist/admin/service/index.d.ts +1 -11
  205. package/dist/admin/styles/base.css +121 -121
  206. package/dist/admin/vendor/browser-image-compression.js +1144 -1144
  207. package/dist/admin/vendor/decimal-js.js +12 -12
  208. package/dist/admin/vendor/xlsx.js +263 -263
  209. package/dist/ai/index.js +1 -1
  210. package/dist/client/admin-vendor.d.ts +1 -13
  211. package/dist/client/ai-model-registry.js +1 -1
  212. package/dist/client/base.js +1 -1
  213. package/dist/client/configure.js +1 -1
  214. package/dist/client/dict.js +1 -1
  215. package/dist/client/eps-payload.js +1 -1
  216. package/dist/client/eps.js +1 -1
  217. package/dist/client/index.d.ts +1 -7
  218. package/dist/client/index.js +1 -1
  219. package/dist/client/locale-change.js +1 -1
  220. package/dist/client/locale-store.js +1 -1
  221. package/dist/client/service.js +1 -1
  222. package/dist/client/vite-admin.d.ts +1 -40
  223. package/dist/client/vite-admin.js +1 -1
  224. package/dist/client/vite-auto-import.d.ts +1 -2
  225. package/dist/client/vite-auto-import.js +1 -1
  226. package/dist/client/vite-micro-proxy.d.ts +1 -20
  227. package/dist/client/vite-micro-proxy.js +1 -1
  228. package/dist/client/vite-plugin-eps.d.ts +1 -20
  229. package/dist/client/vite-plugin-eps.js +1 -1
  230. package/dist/client/vite-resolve.d.ts +1 -16
  231. package/dist/client/vite-resolve.js +1 -1
  232. package/dist/config/index.js +1 -1
  233. package/dist/index.js +1 -1
  234. package/dist/server/index.js +1 -1
  235. package/dist/shared/datetime-picker.js +1 -1
  236. package/dist/shared/decimal.js +1 -1
  237. package/dist/shared/excel.js +1 -1
  238. package/dist/shared/index.js +1 -1
  239. package/dist/shared/locale-json.js +1 -1
  240. package/dist/shared/tree.js +1 -1
  241. package/package.json +1 -1
  242. package/typings/admin/base/auth.ts +24 -24
  243. package/typings/admin/base/permission.ts +19 -19
  244. package/typings/admin/comm/cascader.ts +7 -7
  245. package/typings/admin/comm/check-tree.ts +7 -7
  246. package/typings/admin/comm/crud.ts +427 -427
  247. package/typings/admin/comm/eps.ts +86 -86
  248. package/typings/admin/comm/request.ts +16 -16
  249. package/typings/admin/comm/service.ts +43 -43
  250. package/typings/admin/comm/upload.ts +39 -39
  251. package/typings/admin/theme.ts +10 -10
  252. package/typings/admin/vome/browser.ts +11 -11
  253. package/typings/admin/vome/module.ts +13 -13
  254. package/typings/admin/vome/tags.ts +9 -9
  255. package/typings/comm/crud.ts +83 -83
  256. package/typings/comm/page.ts +18 -18
  257. package/typings/comm/scan.ts +6 -6
  258. package/typings/common.ts +4 -4
  259. package/typings/context/data.ts +10 -10
  260. package/typings/ext/market-seat.ts +42 -42
  261. package/typings/ext/market.ts +55 -55
  262. package/typings/ext/module.ts +64 -64
  263. package/typings/ext/plugin.ts +38 -38
  264. package/typings/ioc/metadata.ts +34 -34
  265. package/typings/logger/config.ts +6 -6
  266. package/typings/routing/crud.ts +199 -199
  267. package/typings/routing/eps.ts +88 -88
  268. package/typings/routing/meta.ts +63 -63
  269. package/typings/routing/register.ts +21 -21
  270. package/typings/routing/scan-context.ts +7 -7
  271. package/dist/admin/api/client.d.ts +0 -50
  272. package/dist/admin/components/ui/avatar/index.d.ts +0 -3
  273. package/dist/admin/components/ui/badge/index.d.ts +0 -6
  274. package/dist/admin/components/ui/button/index.d.ts +0 -7
  275. package/dist/admin/components/ui/checkbox/index.d.ts +0 -1
  276. package/dist/admin/components/ui/dialog/index.d.ts +0 -10
  277. package/dist/admin/components/ui/dropdown-menu/index.d.ts +0 -15
  278. package/dist/admin/components/ui/input/index.d.ts +0 -1
  279. package/dist/admin/components/ui/label/index.d.ts +0 -1
  280. package/dist/admin/components/ui/scroll-area/index.d.ts +0 -2
  281. package/dist/admin/components/ui/select/index.d.ts +0 -11
  282. package/dist/admin/components/ui/separator/index.d.ts +0 -1
  283. package/dist/admin/components/ui/sheet/index.d.ts +0 -8
  284. package/dist/admin/components/ui/sonner/index.d.ts +0 -1
  285. package/dist/admin/components/ui/table/index.d.ts +0 -9
  286. package/dist/admin/components/ui/tabs/index.d.ts +0 -4
  287. package/dist/admin/components/ui/tooltip/index.d.ts +0 -4
  288. package/dist/admin/crud/auto-fields.d.ts +0 -68
  289. package/dist/admin/crud/config.d.ts +0 -5
  290. package/dist/admin/crud/confirm.d.ts +0 -19
  291. package/dist/admin/crud/crud-page-request.d.ts +0 -21
  292. package/dist/admin/crud/data-i18n.d.ts +0 -62
  293. package/dist/admin/crud/dict.d.ts +0 -4
  294. package/dist/admin/crud/export-fetch.d.ts +0 -22
  295. package/dist/admin/crud/index.d.ts +0 -60
  296. package/dist/admin/crud/json-kv.d.ts +0 -9
  297. package/dist/admin/crud/key.d.ts +0 -22
  298. package/dist/admin/crud/paging.d.ts +0 -2
  299. package/dist/admin/crud/plugins.d.ts +0 -63
  300. package/dist/admin/crud/registry.d.ts +0 -3
  301. package/dist/admin/crud/span.d.ts +0 -17
  302. package/dist/admin/crud/style.d.ts +0 -82
  303. package/dist/admin/crud/useCrud.d.ts +0 -21
  304. package/dist/admin/crud/validate.d.ts +0 -7
  305. package/dist/admin/directives/perm.d.ts +0 -3
  306. package/dist/admin/hooks/useBrowser.d.ts +0 -6
  307. package/dist/admin/hooks/useUpload.d.ts +0 -8
  308. package/dist/admin/hooks/useVome.d.ts +0 -10
  309. package/dist/admin/lib/browser.d.ts +0 -3
  310. package/dist/admin/lib/cn.d.ts +0 -2
  311. package/dist/admin/lib/compress-image.d.ts +0 -12
  312. package/dist/admin/lib/dialog-float.d.ts +0 -8
  313. package/dist/admin/lib/eps.d.ts +0 -2
  314. package/dist/admin/lib/export-excel.d.ts +0 -9
  315. package/dist/admin/lib/file-preview.d.ts +0 -9
  316. package/dist/admin/lib/import-excel.d.ts +0 -21
  317. package/dist/admin/lib/json.d.ts +0 -11
  318. package/dist/admin/lib/menu.d.ts +0 -3
  319. package/dist/admin/lib/micro-locale.d.ts +0 -5
  320. package/dist/admin/lib/micro-theme.d.ts +0 -14
  321. package/dist/admin/lib/module.d.ts +0 -21
  322. package/dist/admin/lib/scarce-assets.d.ts +0 -5
  323. package/dist/admin/lib/shell.d.ts +0 -8
  324. package/dist/admin/lib/theme-color-options.d.ts +0 -15
  325. package/dist/admin/lib/tree.d.ts +0 -2
  326. package/dist/admin/lib/upload.d.ts +0 -17
  327. package/dist/admin/lib/video-frame.d.ts +0 -6
  328. package/dist/admin/router/create.d.ts +0 -16
  329. package/dist/admin/router/guards.d.ts +0 -6
  330. package/dist/admin/router/menu-routes.d.ts +0 -13
  331. package/dist/admin/stores/app.d.ts +0 -13
  332. package/dist/admin/stores/tags.d.ts +0 -14
  333. package/dist/admin/stores/user.d.ts +0 -48
  334. package/dist/client/ai-model-registry.d.ts +0 -16
  335. package/dist/client/base.d.ts +0 -28
  336. package/dist/client/configure.d.ts +0 -33
  337. package/dist/client/dict.d.ts +0 -31
  338. package/dist/client/eps-payload.d.ts +0 -3
  339. package/dist/client/eps.d.ts +0 -17
  340. package/dist/client/locale-change.d.ts +0 -16
  341. package/dist/client/locale-store.d.ts +0 -105
  342. package/dist/client/service.d.ts +0 -29
@@ -1,570 +1,570 @@
1
- <template>
2
- <div
3
- ref="rootRef"
4
- class="vm-select"
5
- :class="{ 'is-full': isFullWidth, 'is-open': open, 'is-disabled': disabled }"
6
- :style="triggerStyle"
7
- >
8
- <button
9
- ref="triggerRef"
10
- type="button"
11
- class="vm-crud-select vm-select__trigger"
12
- :class="{ 'is-full': isFullWidth }"
13
- :disabled="disabled"
14
- @click="toggle"
15
- >
16
- <span
17
- class="vm-select__label"
18
- :class="{ 'is-placeholder': !displayLabel }"
19
- >
20
- {{ displayLabel || placeholder || crudLabel('pleaseSelect') }}
21
- </span>
22
- <i
23
- v-if="clearable && hasValue"
24
- class="ri-close-circle-fill vm-select__clear"
25
- @click.stop="clear"
26
- />
27
- <i class="ri-arrow-down-s-line vm-select__caret" aria-hidden="true" />
28
- </button>
29
-
30
- <Teleport to="body">
31
- <FocusScope
32
- v-if="open"
33
- as-child
34
- loop
35
- :trapped="true"
36
- @unmount-auto-focus="onPanelUnmountAutoFocus"
37
- >
38
- <div
39
- ref="panelRef"
40
- class="vm-select__panel vm-dialog-float"
41
- :style="panelStyle"
42
- @pointerdown.stop
43
- @mousedown.stop
44
- >
45
- <div v-if="filterable" class="vm-select__search">
46
- <i class="ri-search-line vm-select__search-ico" aria-hidden="true" />
47
- <input
48
- ref="searchRef"
49
- class="vm-select__search-input"
50
- :value="search"
51
- :placeholder="crudLabel('searchEllipsis')"
52
- @input="onSearchInput"
53
- @keydown.esc.stop="closePanel"
54
- />
55
- <i
56
- v-if="search"
57
- class="ri-close-circle-fill vm-select__search-clear"
58
- @click.stop="clearSearch"
59
- />
60
- </div>
61
- <div class="vm-select__list vm-scroll" @scroll.passive="onListScroll">
62
- <button
63
- v-for="opt in filtered"
64
- :key="String(opt[valueKey])"
65
- type="button"
66
- class="vm-select__option"
67
- :class="{ 'is-active': String(opt[valueKey]) === inner }"
68
- @click="pick(opt)"
69
- >
70
- {{ opt[labelKey] }}
71
- </button>
72
- <vm-loading
73
- v-if="lazy && loading"
74
- mode="footer"
75
- status="loading"
76
- />
77
- <p v-else-if="!filtered.length" class="vm-select__empty">{{ crudLabel('noMatch') }}</p>
78
- <vm-loading
79
- v-else-if="lazy && !hasMore"
80
- mode="footer"
81
- status="end"
82
- />
83
- </div>
84
- </div>
85
- </FocusScope>
86
- </Teleport>
87
- </div>
88
- </template>
89
-
90
- <script setup lang="ts">
91
- import { crudLabel } from '../style'
92
- import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
93
- import { FocusScope } from 'reka-ui'
94
- import { useCrud } from '../useCrud'
95
- import { POPUP_TRIGGER_GAP } from '../../lib/dialog-float'
96
-
97
- defineOptions({ name: 'vm-select' })
98
-
99
- export type VmSelectLoadQuery = {
100
- page: number
101
- size: number
102
- keyWord: string
103
- /** true:业务页应追加到现有 options;false:整表替换 */
104
- append: boolean
105
- }
106
-
107
- const props = withDefaults(
108
- defineProps<{
109
- modelValue?: string | number | boolean | null
110
- options?: Array<Record<string, unknown>>
111
- prop?: string
112
- labelKey?: string
113
- valueKey?: string
114
- placeholder?: string
115
- /** 默认 100% 铺满;搜索栏可传 150px / 180px */
116
- width?: string
117
- disabled?: boolean
118
- refreshOnChange?: boolean
119
- api?: string | (() => Promise<unknown>)
120
- /**
121
- * 是否显示面板顶部搜索框;默认 true。
122
- * false 时 UI 与默认同构,仅隐藏搜索(不再走另一套原生 Select)。
123
- */
124
- filterable?: boolean
125
- /** 有值时展示清除图标,点击置空 */
126
- clearable?: boolean
127
- /**
128
- * 懒加载 / 翻页:打开、搜索、触底时 emit `load`,由页面请求后回推 `:options`。
129
- * 与静态 options、`api` 并存;开启后本地不做关键字过滤。
130
- */
131
- lazy?: boolean
132
- loading?: boolean
133
- hasMore?: boolean
134
- pageSize?: number
135
- }>(),
136
- {
137
- labelKey: 'label',
138
- valueKey: 'value',
139
- refreshOnChange: true,
140
- width: '100%',
141
- filterable: true,
142
- clearable: true,
143
- lazy: false,
144
- loading: false,
145
- hasMore: false,
146
- pageSize: 20,
147
- },
148
- )
149
-
150
- const emit = defineEmits<{
151
- 'update:modelValue': [v: string | number | boolean | '']
152
- change: [v: string | number | boolean | '']
153
- load: [q: VmSelectLoadQuery]
154
- }>()
155
-
156
- const crud = useCrud()
157
- const remote = ref<Array<Record<string, unknown>>>([])
158
- const rootRef = ref<HTMLElement | null>(null)
159
- const triggerRef = ref<HTMLElement | null>(null)
160
- const panelRef = ref<HTMLElement | null>(null)
161
- const searchRef = ref<HTMLInputElement | null>(null)
162
- const open = ref(false)
163
- const search = ref('')
164
- const panelStyle = ref<Record<string, string>>({})
165
- const suppressOpen = ref(false)
166
- /** lazy 模式下已请求到的页码(下一次触底 = page + 1) */
167
- const loadedPage = ref(0)
168
- /** 已 emit load、等待业务回推;防触底连发 */
169
- const awaitingLoad = ref(false)
170
- let searchTimer: ReturnType<typeof setTimeout> | null = null
171
-
172
- const inner = computed(() =>
173
- props.modelValue == null || props.modelValue === ''
174
- ? ''
175
- : String(props.modelValue),
176
- )
177
-
178
- const hasValue = computed(() => inner.value !== '')
179
-
180
- const isFullWidth = computed(
181
- () => !props.width || props.width === '100%' || props.width === '100',
182
- )
183
-
184
- const triggerStyle = computed(() =>
185
- isFullWidth.value ? undefined : { width: props.width },
186
- )
187
-
188
- const normalized = computed(() => {
189
- const list = (props.options?.length ? props.options : remote.value) || []
190
- return list as Array<Record<string, unknown>>
191
- })
192
-
193
- const displayLabel = computed(() => {
194
- if (!inner.value) return ''
195
- const hit = normalized.value.find(
196
- (o) => String(o[props.valueKey]) === inner.value,
197
- )
198
- return hit ? String(hit[props.labelKey] ?? '') : inner.value
199
- })
200
-
201
- const filtered = computed(() => {
202
- // lazy:关键字交给业务页,列表即 options 全量
203
- if (props.lazy || !props.filterable) return normalized.value
204
- const q = search.value.trim().toLowerCase()
205
- if (!q) return normalized.value
206
- return normalized.value.filter((o) => {
207
- const label = String(o[props.labelKey] ?? '').toLowerCase()
208
- const value = String(o[props.valueKey] ?? '').toLowerCase()
209
- return label.includes(q) || value.includes(q)
210
- })
211
- })
212
-
213
- watch(
214
- () => props.api,
215
- async (api) => {
216
- if (!api || props.lazy) return
217
- try {
218
- const req =
219
- typeof api === 'function'
220
- ? api()
221
- : (crud.service[api] as (() => Promise<unknown>) | undefined)?.()
222
- const res = await req
223
- remote.value = Array.isArray(res) ? res : []
224
- } catch {
225
- remote.value = []
226
- }
227
- },
228
- { immediate: true },
229
- )
230
-
231
- watch(open, (v) => {
232
- if (!v) {
233
- search.value = ''
234
- if (searchTimer) {
235
- clearTimeout(searchTimer)
236
- searchTimer = null
237
- }
238
- }
239
- })
240
-
241
- watch(
242
- () => [props.loading, props.options] as const,
243
- () => {
244
- if (props.loading) return
245
- awaitingLoad.value = false
246
- },
247
- { deep: true },
248
- )
249
-
250
- function emitLoad(page: number, append: boolean) {
251
- if (awaitingLoad.value || props.loading) return
252
- awaitingLoad.value = true
253
- emit('load', {
254
- page,
255
- size: props.pageSize,
256
- keyWord: search.value.trim(),
257
- append,
258
- })
259
- loadedPage.value = page
260
- }
261
-
262
- function requestFirstPage() {
263
- if (!props.lazy) return
264
- emitLoad(1, false)
265
- }
266
-
267
- function requestNextPage() {
268
- if (!props.lazy || !props.hasMore) return
269
- emitLoad(loadedPage.value + 1, true)
270
- }
271
-
272
- /** 提交时还原 options 里的原始类型 */
273
- function resolveOptionValue(v: unknown): string | number | boolean {
274
- const s = v == null ? '' : String(v)
275
- if (!s) return ''
276
- const hit = normalized.value.find(
277
- (o) => String(o[props.valueKey]) === s,
278
- )
279
- if (!hit) return s
280
- const raw = hit[props.valueKey]
281
- if (typeof raw === 'number' || typeof raw === 'boolean') return raw
282
- return s
283
- }
284
-
285
- function onChange(v: unknown) {
286
- const val = resolveOptionValue(v)
287
- emit('update:modelValue', val)
288
- emit('change', val)
289
- if (props.prop && props.refreshOnChange) {
290
- crud.refresh({ page: 1, [props.prop]: val === '' ? undefined : val })
291
- }
292
- }
293
-
294
- function pick(opt: Record<string, unknown>) {
295
- onChange(opt[props.valueKey])
296
- closePanel()
297
- }
298
-
299
- function onSearchInput(e: Event) {
300
- search.value = (e.target as HTMLInputElement).value
301
- if (!props.lazy) return
302
- if (searchTimer) clearTimeout(searchTimer)
303
- searchTimer = setTimeout(() => {
304
- searchTimer = null
305
- if (!open.value) return
306
- emitLoad(1, false)
307
- }, 300)
308
- }
309
-
310
- function clearSearch() {
311
- search.value = ''
312
- searchRef.value?.focus()
313
- if (props.lazy) emitLoad(1, false)
314
- }
315
-
316
- function clear() {
317
- closePanel()
318
- onChange('')
319
- }
320
-
321
- function onListScroll(e: Event) {
322
- if (!props.lazy || !props.hasMore) return
323
- const el = e.target as HTMLElement
324
- if (el.scrollHeight - el.scrollTop - el.clientHeight > 48) return
325
- requestNextPage()
326
- }
327
-
328
- function syncPanelPos() {
329
- const el = triggerRef.value
330
- if (!el) return
331
- const r = el.getBoundingClientRect()
332
- const width = Math.max(r.width, 200)
333
- panelStyle.value = {
334
- position: 'fixed',
335
- top: `${r.bottom + POPUP_TRIGGER_GAP}px`,
336
- left: `${r.left}px`,
337
- width: `${width}px`,
338
- zIndex: '200',
339
- }
340
- }
341
-
342
- function onPanelUnmountAutoFocus(event: Event) {
343
- event.preventDefault()
344
- }
345
-
346
- function closePanel() {
347
- if (!open.value) return
348
- suppressOpen.value = true
349
- open.value = false
350
- search.value = ''
351
- nextTick(() => {
352
- requestAnimationFrame(() => {
353
- suppressOpen.value = false
354
- })
355
- })
356
- }
357
-
358
- async function openPanel() {
359
- if (props.disabled || suppressOpen.value) return
360
- open.value = true
361
- await nextTick()
362
- syncPanelPos()
363
- if (props.filterable) searchRef.value?.focus()
364
- requestFirstPage()
365
- }
366
-
367
- async function toggle() {
368
- if (props.disabled) return
369
- if (open.value) {
370
- closePanel()
371
- return
372
- }
373
- await openPanel()
374
- }
375
-
376
- function isInside(el: Node | null) {
377
- if (!el) return false
378
- if (rootRef.value?.contains(el)) return true
379
- if (panelRef.value?.contains(el)) return true
380
- return el instanceof Element && Boolean(el.closest('.vm-select__panel'))
381
- }
382
-
383
- function onDocPointer(e: Event) {
384
- if (!open.value) return
385
- const t = e.target as Node | null
386
- if (!isInside(t)) closePanel()
387
- }
388
-
389
- function onWinChange() {
390
- if (open.value) syncPanelPos()
391
- }
392
-
393
- onMounted(() => {
394
- document.addEventListener('pointerdown', onDocPointer, true)
395
- window.addEventListener('resize', onWinChange)
396
- window.addEventListener('scroll', onWinChange, true)
397
- })
398
- onUnmounted(() => {
399
- if (searchTimer) clearTimeout(searchTimer)
400
- document.removeEventListener('pointerdown', onDocPointer, true)
401
- window.removeEventListener('resize', onWinChange)
402
- window.removeEventListener('scroll', onWinChange, true)
403
- })
404
- </script>
405
-
406
- <style lang="scss" scoped>
407
- .vm-crud-select {
408
- height: var(--vm-control-height);
409
- border-radius: var(--vm-control-radius);
410
- font-size: var(--vm-control-font-size);
411
-
412
- &.is-full {
413
- width: 100% !important;
414
- max-width: 100%;
415
- }
416
- }
417
-
418
- .vm-select {
419
- position: relative;
420
- box-sizing: border-box;
421
- min-width: 0;
422
-
423
- &.is-full {
424
- width: 100%;
425
- }
426
-
427
- &.is-disabled {
428
- opacity: 0.55;
429
- pointer-events: none;
430
- }
431
- }
432
-
433
- .vm-select__trigger {
434
- display: flex;
435
- box-sizing: border-box;
436
- width: 100%;
437
- align-items: center;
438
- justify-content: flex-start;
439
- gap: 6px;
440
- padding: 0 var(--vm-control-padding-x);
441
- border: 1px solid var(--input);
442
- background: var(--muted);
443
- color: var(--foreground);
444
- cursor: pointer;
445
- outline: none;
446
- }
447
-
448
- .vm-select__label {
449
- overflow: hidden;
450
- flex: 1 1 auto;
451
- min-width: 0;
452
- text-align: left;
453
- text-overflow: ellipsis;
454
- white-space: nowrap;
455
-
456
- &.is-placeholder {
457
- color: var(--muted-foreground);
458
- }
459
- }
460
-
461
- .vm-select__caret {
462
- flex: 0 0 auto;
463
- margin-left: auto;
464
- color: var(--muted-foreground);
465
- font-size: 14px;
466
- }
467
-
468
- .vm-select__clear {
469
- flex: 0 0 auto;
470
- margin-left: auto;
471
- color: var(--muted-foreground);
472
- font-size: 14px;
473
- cursor: pointer;
474
-
475
- &:hover {
476
- color: var(--foreground);
477
- }
478
- }
479
-
480
- .vm-select__clear + .vm-select__caret {
481
- margin-left: 0;
482
- }
483
-
484
- .vm-select__panel {
485
- display: flex;
486
- max-height: min(280px, 45vh);
487
- flex-direction: column;
488
- overflow: hidden;
489
- border: 1px solid var(--border);
490
- border-radius: var(--vm-control-radius);
491
- background: var(--card);
492
- box-shadow: var(--shadow-card);
493
- }
494
-
495
- .vm-select__search {
496
- display: flex;
497
- flex: 0 0 auto;
498
- align-items: center;
499
- gap: 6px;
500
- margin: 8px 8px 0;
501
- padding: 0 8px;
502
- border: 1px solid var(--input);
503
- border-radius: 8px;
504
- background: var(--muted);
505
- }
506
-
507
- .vm-select__search-ico {
508
- flex: 0 0 auto;
509
- color: var(--muted-foreground);
510
- font-size: 14px;
511
- }
512
-
513
- .vm-select__search-input {
514
- flex: 1 1 auto;
515
- min-width: 0;
516
- height: 28px;
517
- border: none;
518
- background: transparent;
519
- color: var(--foreground);
520
- font-size: 13px;
521
- outline: none;
522
- }
523
-
524
- .vm-select__search-clear {
525
- flex: 0 0 auto;
526
- color: var(--muted-foreground);
527
- font-size: 14px;
528
- cursor: pointer;
529
- }
530
-
531
- .vm-select__list {
532
- flex: 1 1 auto;
533
- min-height: 0;
534
- margin: 6px 0;
535
- padding: 0 6px 6px;
536
- overflow: auto;
537
- }
538
-
539
- .vm-select__option {
540
- display: block;
541
- box-sizing: border-box;
542
- width: 100%;
543
- padding: 7px 8px;
544
- border: none;
545
- border-radius: 8px;
546
- background: transparent;
547
- color: var(--foreground);
548
- font-size: 13px;
549
- text-align: left;
550
- cursor: pointer;
551
-
552
- &:hover {
553
- background: var(--muted);
554
- }
555
-
556
- &.is-active {
557
- background: color-mix(in srgb, var(--brand) 12%, var(--card));
558
- color: var(--brand);
559
- font-weight: 550;
560
- }
561
- }
562
-
563
- .vm-select__empty {
564
- margin: 0;
565
- padding: 12px 8px;
566
- color: var(--muted-foreground);
567
- font-size: 12px;
568
- text-align: center;
569
- }
570
- </style>
1
+ <template>
2
+ <div
3
+ ref="rootRef"
4
+ class="vm-select"
5
+ :class="{ 'is-full': isFullWidth, 'is-open': open, 'is-disabled': disabled }"
6
+ :style="triggerStyle"
7
+ >
8
+ <button
9
+ ref="triggerRef"
10
+ type="button"
11
+ class="vm-crud-select vm-select__trigger"
12
+ :class="{ 'is-full': isFullWidth }"
13
+ :disabled="disabled"
14
+ @click="toggle"
15
+ >
16
+ <span
17
+ class="vm-select__label"
18
+ :class="{ 'is-placeholder': !displayLabel }"
19
+ >
20
+ {{ displayLabel || placeholder || crudLabel('pleaseSelect') }}
21
+ </span>
22
+ <i
23
+ v-if="clearable && hasValue"
24
+ class="ri-close-circle-fill vm-select__clear"
25
+ @click.stop="clear"
26
+ />
27
+ <i class="ri-arrow-down-s-line vm-select__caret" aria-hidden="true" />
28
+ </button>
29
+
30
+ <Teleport to="body">
31
+ <FocusScope
32
+ v-if="open"
33
+ as-child
34
+ loop
35
+ :trapped="true"
36
+ @unmount-auto-focus="onPanelUnmountAutoFocus"
37
+ >
38
+ <div
39
+ ref="panelRef"
40
+ class="vm-select__panel vm-dialog-float"
41
+ :style="panelStyle"
42
+ @pointerdown.stop
43
+ @mousedown.stop
44
+ >
45
+ <div v-if="filterable" class="vm-select__search">
46
+ <i class="ri-search-line vm-select__search-ico" aria-hidden="true" />
47
+ <input
48
+ ref="searchRef"
49
+ class="vm-select__search-input"
50
+ :value="search"
51
+ :placeholder="crudLabel('searchEllipsis')"
52
+ @input="onSearchInput"
53
+ @keydown.esc.stop="closePanel"
54
+ />
55
+ <i
56
+ v-if="search"
57
+ class="ri-close-circle-fill vm-select__search-clear"
58
+ @click.stop="clearSearch"
59
+ />
60
+ </div>
61
+ <div class="vm-select__list vm-scroll" @scroll.passive="onListScroll">
62
+ <button
63
+ v-for="opt in filtered"
64
+ :key="String(opt[valueKey])"
65
+ type="button"
66
+ class="vm-select__option"
67
+ :class="{ 'is-active': String(opt[valueKey]) === inner }"
68
+ @click="pick(opt)"
69
+ >
70
+ {{ opt[labelKey] }}
71
+ </button>
72
+ <vm-loading
73
+ v-if="lazy && loading"
74
+ mode="footer"
75
+ status="loading"
76
+ />
77
+ <p v-else-if="!filtered.length" class="vm-select__empty">{{ crudLabel('noMatch') }}</p>
78
+ <vm-loading
79
+ v-else-if="lazy && !hasMore"
80
+ mode="footer"
81
+ status="end"
82
+ />
83
+ </div>
84
+ </div>
85
+ </FocusScope>
86
+ </Teleport>
87
+ </div>
88
+ </template>
89
+
90
+ <script setup lang="ts">
91
+ import { crudLabel } from '../style'
92
+ import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
93
+ import { FocusScope } from 'reka-ui'
94
+ import { useCrudOptional } from '../useCrud'
95
+ import { POPUP_TRIGGER_GAP } from '../../lib/dialog-float'
96
+
97
+ defineOptions({ name: 'vm-select' })
98
+
99
+ export type VmSelectLoadQuery = {
100
+ page: number
101
+ size: number
102
+ keyWord: string
103
+ /** true:业务页应追加到现有 options;false:整表替换 */
104
+ append: boolean
105
+ }
106
+
107
+ const props = withDefaults(
108
+ defineProps<{
109
+ modelValue?: string | number | boolean | null
110
+ options?: Array<Record<string, unknown>>
111
+ prop?: string
112
+ labelKey?: string
113
+ valueKey?: string
114
+ placeholder?: string
115
+ /** 默认 100% 铺满;搜索栏可传 150px / 180px */
116
+ width?: string
117
+ disabled?: boolean
118
+ refreshOnChange?: boolean
119
+ api?: string | (() => Promise<unknown>)
120
+ /**
121
+ * 是否显示面板顶部搜索框;默认 true。
122
+ * false 时 UI 与默认同构,仅隐藏搜索(不再走另一套原生 Select)。
123
+ */
124
+ filterable?: boolean
125
+ /** 有值时展示清除图标,点击置空 */
126
+ clearable?: boolean
127
+ /**
128
+ * 懒加载 / 翻页:打开、搜索、触底时 emit `load`,由页面请求后回推 `:options`。
129
+ * 与静态 options、`api` 并存;开启后本地不做关键字过滤。
130
+ */
131
+ lazy?: boolean
132
+ loading?: boolean
133
+ hasMore?: boolean
134
+ pageSize?: number
135
+ }>(),
136
+ {
137
+ labelKey: 'label',
138
+ valueKey: 'value',
139
+ refreshOnChange: true,
140
+ width: '100%',
141
+ filterable: true,
142
+ clearable: true,
143
+ lazy: false,
144
+ loading: false,
145
+ hasMore: false,
146
+ pageSize: 20,
147
+ },
148
+ )
149
+
150
+ const emit = defineEmits<{
151
+ 'update:modelValue': [v: string | number | boolean | '']
152
+ change: [v: string | number | boolean | '']
153
+ load: [q: VmSelectLoadQuery]
154
+ }>()
155
+
156
+ const crud = useCrudOptional()
157
+ const remote = ref<Array<Record<string, unknown>>>([])
158
+ const rootRef = ref<HTMLElement | null>(null)
159
+ const triggerRef = ref<HTMLElement | null>(null)
160
+ const panelRef = ref<HTMLElement | null>(null)
161
+ const searchRef = ref<HTMLInputElement | null>(null)
162
+ const open = ref(false)
163
+ const search = ref('')
164
+ const panelStyle = ref<Record<string, string>>({})
165
+ const suppressOpen = ref(false)
166
+ /** lazy 模式下已请求到的页码(下一次触底 = page + 1) */
167
+ const loadedPage = ref(0)
168
+ /** 已 emit load、等待业务回推;防触底连发 */
169
+ const awaitingLoad = ref(false)
170
+ let searchTimer: ReturnType<typeof setTimeout> | null = null
171
+
172
+ const inner = computed(() =>
173
+ props.modelValue == null || props.modelValue === ''
174
+ ? ''
175
+ : String(props.modelValue),
176
+ )
177
+
178
+ const hasValue = computed(() => inner.value !== '')
179
+
180
+ const isFullWidth = computed(
181
+ () => !props.width || props.width === '100%' || props.width === '100',
182
+ )
183
+
184
+ const triggerStyle = computed(() =>
185
+ isFullWidth.value ? undefined : { width: props.width },
186
+ )
187
+
188
+ const normalized = computed(() => {
189
+ const list = (props.options?.length ? props.options : remote.value) || []
190
+ return list as Array<Record<string, unknown>>
191
+ })
192
+
193
+ const displayLabel = computed(() => {
194
+ if (!inner.value) return ''
195
+ const hit = normalized.value.find(
196
+ (o) => String(o[props.valueKey]) === inner.value,
197
+ )
198
+ return hit ? String(hit[props.labelKey] ?? '') : inner.value
199
+ })
200
+
201
+ const filtered = computed(() => {
202
+ // lazy:关键字交给业务页,列表即 options 全量
203
+ if (props.lazy || !props.filterable) return normalized.value
204
+ const q = search.value.trim().toLowerCase()
205
+ if (!q) return normalized.value
206
+ return normalized.value.filter((o) => {
207
+ const label = String(o[props.labelKey] ?? '').toLowerCase()
208
+ const value = String(o[props.valueKey] ?? '').toLowerCase()
209
+ return label.includes(q) || value.includes(q)
210
+ })
211
+ })
212
+
213
+ watch(
214
+ () => props.api,
215
+ async (api) => {
216
+ if (!api || props.lazy) return
217
+ try {
218
+ const req =
219
+ typeof api === 'function'
220
+ ? api()
221
+ : (crud?.service[api] as (() => Promise<unknown>) | undefined)?.()
222
+ const res = await req
223
+ remote.value = Array.isArray(res) ? res : []
224
+ } catch {
225
+ remote.value = []
226
+ }
227
+ },
228
+ { immediate: true },
229
+ )
230
+
231
+ watch(open, (v) => {
232
+ if (!v) {
233
+ search.value = ''
234
+ if (searchTimer) {
235
+ clearTimeout(searchTimer)
236
+ searchTimer = null
237
+ }
238
+ }
239
+ })
240
+
241
+ watch(
242
+ () => [props.loading, props.options] as const,
243
+ () => {
244
+ if (props.loading) return
245
+ awaitingLoad.value = false
246
+ },
247
+ { deep: true },
248
+ )
249
+
250
+ function emitLoad(page: number, append: boolean) {
251
+ if (awaitingLoad.value || props.loading) return
252
+ awaitingLoad.value = true
253
+ emit('load', {
254
+ page,
255
+ size: props.pageSize,
256
+ keyWord: search.value.trim(),
257
+ append,
258
+ })
259
+ loadedPage.value = page
260
+ }
261
+
262
+ function requestFirstPage() {
263
+ if (!props.lazy) return
264
+ emitLoad(1, false)
265
+ }
266
+
267
+ function requestNextPage() {
268
+ if (!props.lazy || !props.hasMore) return
269
+ emitLoad(loadedPage.value + 1, true)
270
+ }
271
+
272
+ /** 提交时还原 options 里的原始类型 */
273
+ function resolveOptionValue(v: unknown): string | number | boolean {
274
+ const s = v == null ? '' : String(v)
275
+ if (!s) return ''
276
+ const hit = normalized.value.find(
277
+ (o) => String(o[props.valueKey]) === s,
278
+ )
279
+ if (!hit) return s
280
+ const raw = hit[props.valueKey]
281
+ if (typeof raw === 'number' || typeof raw === 'boolean') return raw
282
+ return s
283
+ }
284
+
285
+ function onChange(v: unknown) {
286
+ const val = resolveOptionValue(v)
287
+ emit('update:modelValue', val)
288
+ emit('change', val)
289
+ if (props.prop && props.refreshOnChange) {
290
+ void crud?.refresh({ page: 1, [props.prop]: val === '' ? undefined : val })
291
+ }
292
+ }
293
+
294
+ function pick(opt: Record<string, unknown>) {
295
+ onChange(opt[props.valueKey])
296
+ closePanel()
297
+ }
298
+
299
+ function onSearchInput(e: Event) {
300
+ search.value = (e.target as HTMLInputElement).value
301
+ if (!props.lazy) return
302
+ if (searchTimer) clearTimeout(searchTimer)
303
+ searchTimer = setTimeout(() => {
304
+ searchTimer = null
305
+ if (!open.value) return
306
+ emitLoad(1, false)
307
+ }, 300)
308
+ }
309
+
310
+ function clearSearch() {
311
+ search.value = ''
312
+ searchRef.value?.focus()
313
+ if (props.lazy) emitLoad(1, false)
314
+ }
315
+
316
+ function clear() {
317
+ closePanel()
318
+ onChange('')
319
+ }
320
+
321
+ function onListScroll(e: Event) {
322
+ if (!props.lazy || !props.hasMore) return
323
+ const el = e.target as HTMLElement
324
+ if (el.scrollHeight - el.scrollTop - el.clientHeight > 48) return
325
+ requestNextPage()
326
+ }
327
+
328
+ function syncPanelPos() {
329
+ const el = triggerRef.value
330
+ if (!el) return
331
+ const r = el.getBoundingClientRect()
332
+ const width = Math.max(r.width, 200)
333
+ panelStyle.value = {
334
+ position: 'fixed',
335
+ top: `${r.bottom + POPUP_TRIGGER_GAP}px`,
336
+ left: `${r.left}px`,
337
+ width: `${width}px`,
338
+ zIndex: '200',
339
+ }
340
+ }
341
+
342
+ function onPanelUnmountAutoFocus(event: Event) {
343
+ event.preventDefault()
344
+ }
345
+
346
+ function closePanel() {
347
+ if (!open.value) return
348
+ suppressOpen.value = true
349
+ open.value = false
350
+ search.value = ''
351
+ nextTick(() => {
352
+ requestAnimationFrame(() => {
353
+ suppressOpen.value = false
354
+ })
355
+ })
356
+ }
357
+
358
+ async function openPanel() {
359
+ if (props.disabled || suppressOpen.value) return
360
+ open.value = true
361
+ await nextTick()
362
+ syncPanelPos()
363
+ if (props.filterable) searchRef.value?.focus()
364
+ requestFirstPage()
365
+ }
366
+
367
+ async function toggle() {
368
+ if (props.disabled) return
369
+ if (open.value) {
370
+ closePanel()
371
+ return
372
+ }
373
+ await openPanel()
374
+ }
375
+
376
+ function isInside(el: Node | null) {
377
+ if (!el) return false
378
+ if (rootRef.value?.contains(el)) return true
379
+ if (panelRef.value?.contains(el)) return true
380
+ return el instanceof Element && Boolean(el.closest('.vm-select__panel'))
381
+ }
382
+
383
+ function onDocPointer(e: Event) {
384
+ if (!open.value) return
385
+ const t = e.target as Node | null
386
+ if (!isInside(t)) closePanel()
387
+ }
388
+
389
+ function onWinChange() {
390
+ if (open.value) syncPanelPos()
391
+ }
392
+
393
+ onMounted(() => {
394
+ document.addEventListener('pointerdown', onDocPointer, true)
395
+ window.addEventListener('resize', onWinChange)
396
+ window.addEventListener('scroll', onWinChange, true)
397
+ })
398
+ onUnmounted(() => {
399
+ if (searchTimer) clearTimeout(searchTimer)
400
+ document.removeEventListener('pointerdown', onDocPointer, true)
401
+ window.removeEventListener('resize', onWinChange)
402
+ window.removeEventListener('scroll', onWinChange, true)
403
+ })
404
+ </script>
405
+
406
+ <style lang="scss" scoped>
407
+ .vm-crud-select {
408
+ height: var(--vm-control-height);
409
+ border-radius: var(--vm-control-radius);
410
+ font-size: var(--vm-control-font-size);
411
+
412
+ &.is-full {
413
+ width: 100% !important;
414
+ max-width: 100%;
415
+ }
416
+ }
417
+
418
+ .vm-select {
419
+ position: relative;
420
+ box-sizing: border-box;
421
+ min-width: 0;
422
+
423
+ &.is-full {
424
+ width: 100%;
425
+ }
426
+
427
+ &.is-disabled {
428
+ opacity: 0.55;
429
+ pointer-events: none;
430
+ }
431
+ }
432
+
433
+ .vm-select__trigger {
434
+ display: flex;
435
+ box-sizing: border-box;
436
+ width: 100%;
437
+ align-items: center;
438
+ justify-content: flex-start;
439
+ gap: 6px;
440
+ padding: 0 var(--vm-control-padding-x);
441
+ border: 1px solid var(--input);
442
+ background: var(--muted);
443
+ color: var(--foreground);
444
+ cursor: pointer;
445
+ outline: none;
446
+ }
447
+
448
+ .vm-select__label {
449
+ overflow: hidden;
450
+ flex: 1 1 auto;
451
+ min-width: 0;
452
+ text-align: left;
453
+ text-overflow: ellipsis;
454
+ white-space: nowrap;
455
+
456
+ &.is-placeholder {
457
+ color: var(--muted-foreground);
458
+ }
459
+ }
460
+
461
+ .vm-select__caret {
462
+ flex: 0 0 auto;
463
+ margin-left: auto;
464
+ color: var(--muted-foreground);
465
+ font-size: 14px;
466
+ }
467
+
468
+ .vm-select__clear {
469
+ flex: 0 0 auto;
470
+ margin-left: auto;
471
+ color: var(--muted-foreground);
472
+ font-size: 14px;
473
+ cursor: pointer;
474
+
475
+ &:hover {
476
+ color: var(--foreground);
477
+ }
478
+ }
479
+
480
+ .vm-select__clear + .vm-select__caret {
481
+ margin-left: 0;
482
+ }
483
+
484
+ .vm-select__panel {
485
+ display: flex;
486
+ max-height: min(280px, 45vh);
487
+ flex-direction: column;
488
+ overflow: hidden;
489
+ border: 1px solid var(--border);
490
+ border-radius: var(--vm-control-radius);
491
+ background: var(--card);
492
+ box-shadow: var(--shadow-card);
493
+ }
494
+
495
+ .vm-select__search {
496
+ display: flex;
497
+ flex: 0 0 auto;
498
+ align-items: center;
499
+ gap: 6px;
500
+ margin: 8px 8px 0;
501
+ padding: 0 8px;
502
+ border: 1px solid var(--input);
503
+ border-radius: 8px;
504
+ background: var(--muted);
505
+ }
506
+
507
+ .vm-select__search-ico {
508
+ flex: 0 0 auto;
509
+ color: var(--muted-foreground);
510
+ font-size: 14px;
511
+ }
512
+
513
+ .vm-select__search-input {
514
+ flex: 1 1 auto;
515
+ min-width: 0;
516
+ height: 28px;
517
+ border: none;
518
+ background: transparent;
519
+ color: var(--foreground);
520
+ font-size: 13px;
521
+ outline: none;
522
+ }
523
+
524
+ .vm-select__search-clear {
525
+ flex: 0 0 auto;
526
+ color: var(--muted-foreground);
527
+ font-size: 14px;
528
+ cursor: pointer;
529
+ }
530
+
531
+ .vm-select__list {
532
+ flex: 1 1 auto;
533
+ min-height: 0;
534
+ margin: 6px 0;
535
+ padding: 0 6px 6px;
536
+ overflow: auto;
537
+ }
538
+
539
+ .vm-select__option {
540
+ display: block;
541
+ box-sizing: border-box;
542
+ width: 100%;
543
+ padding: 7px 8px;
544
+ border: none;
545
+ border-radius: 8px;
546
+ background: transparent;
547
+ color: var(--foreground);
548
+ font-size: 13px;
549
+ text-align: left;
550
+ cursor: pointer;
551
+
552
+ &:hover {
553
+ background: var(--muted);
554
+ }
555
+
556
+ &.is-active {
557
+ background: color-mix(in srgb, var(--brand) 12%, var(--card));
558
+ color: var(--brand);
559
+ font-weight: 550;
560
+ }
561
+ }
562
+
563
+ .vm-select__empty {
564
+ margin: 0;
565
+ padding: 12px 8px;
566
+ color: var(--muted-foreground);
567
+ font-size: 12px;
568
+ text-align: center;
569
+ }
570
+ </style>