vome-core 0.3.64 → 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 (345) 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/core/index.d.ts +1 -1
  234. package/dist/index.js +1 -1
  235. package/dist/ioc/index.d.ts +1 -1
  236. package/dist/server/index.d.ts +1 -1
  237. package/dist/server/index.js +1 -1
  238. package/dist/shared/datetime-picker.js +1 -1
  239. package/dist/shared/decimal.js +1 -1
  240. package/dist/shared/excel.js +1 -1
  241. package/dist/shared/index.js +1 -1
  242. package/dist/shared/locale-json.js +1 -1
  243. package/dist/shared/tree.js +1 -1
  244. package/package.json +1 -1
  245. package/typings/admin/base/auth.ts +24 -24
  246. package/typings/admin/base/permission.ts +19 -19
  247. package/typings/admin/comm/cascader.ts +7 -7
  248. package/typings/admin/comm/check-tree.ts +7 -7
  249. package/typings/admin/comm/crud.ts +427 -427
  250. package/typings/admin/comm/eps.ts +86 -86
  251. package/typings/admin/comm/request.ts +16 -16
  252. package/typings/admin/comm/service.ts +43 -43
  253. package/typings/admin/comm/upload.ts +39 -39
  254. package/typings/admin/theme.ts +10 -10
  255. package/typings/admin/vome/browser.ts +11 -11
  256. package/typings/admin/vome/module.ts +13 -13
  257. package/typings/admin/vome/tags.ts +9 -9
  258. package/typings/comm/crud.ts +83 -83
  259. package/typings/comm/page.ts +18 -18
  260. package/typings/comm/scan.ts +6 -6
  261. package/typings/common.ts +4 -4
  262. package/typings/context/data.ts +10 -10
  263. package/typings/ext/market-seat.ts +42 -42
  264. package/typings/ext/market.ts +55 -55
  265. package/typings/ext/module.ts +64 -64
  266. package/typings/ext/plugin.ts +38 -38
  267. package/typings/ioc/metadata.ts +34 -34
  268. package/typings/logger/config.ts +6 -6
  269. package/typings/routing/crud.ts +199 -199
  270. package/typings/routing/eps.ts +88 -88
  271. package/typings/routing/meta.ts +63 -63
  272. package/typings/routing/register.ts +21 -21
  273. package/typings/routing/scan-context.ts +7 -7
  274. package/dist/admin/api/client.d.ts +0 -50
  275. package/dist/admin/components/ui/avatar/index.d.ts +0 -3
  276. package/dist/admin/components/ui/badge/index.d.ts +0 -6
  277. package/dist/admin/components/ui/button/index.d.ts +0 -7
  278. package/dist/admin/components/ui/checkbox/index.d.ts +0 -1
  279. package/dist/admin/components/ui/dialog/index.d.ts +0 -10
  280. package/dist/admin/components/ui/dropdown-menu/index.d.ts +0 -15
  281. package/dist/admin/components/ui/input/index.d.ts +0 -1
  282. package/dist/admin/components/ui/label/index.d.ts +0 -1
  283. package/dist/admin/components/ui/scroll-area/index.d.ts +0 -2
  284. package/dist/admin/components/ui/select/index.d.ts +0 -11
  285. package/dist/admin/components/ui/separator/index.d.ts +0 -1
  286. package/dist/admin/components/ui/sheet/index.d.ts +0 -8
  287. package/dist/admin/components/ui/sonner/index.d.ts +0 -1
  288. package/dist/admin/components/ui/table/index.d.ts +0 -9
  289. package/dist/admin/components/ui/tabs/index.d.ts +0 -4
  290. package/dist/admin/components/ui/tooltip/index.d.ts +0 -4
  291. package/dist/admin/crud/auto-fields.d.ts +0 -68
  292. package/dist/admin/crud/config.d.ts +0 -5
  293. package/dist/admin/crud/confirm.d.ts +0 -19
  294. package/dist/admin/crud/crud-page-request.d.ts +0 -21
  295. package/dist/admin/crud/data-i18n.d.ts +0 -62
  296. package/dist/admin/crud/dict.d.ts +0 -4
  297. package/dist/admin/crud/export-fetch.d.ts +0 -22
  298. package/dist/admin/crud/index.d.ts +0 -60
  299. package/dist/admin/crud/json-kv.d.ts +0 -9
  300. package/dist/admin/crud/key.d.ts +0 -22
  301. package/dist/admin/crud/paging.d.ts +0 -2
  302. package/dist/admin/crud/plugins.d.ts +0 -63
  303. package/dist/admin/crud/registry.d.ts +0 -3
  304. package/dist/admin/crud/span.d.ts +0 -17
  305. package/dist/admin/crud/style.d.ts +0 -82
  306. package/dist/admin/crud/useCrud.d.ts +0 -21
  307. package/dist/admin/crud/validate.d.ts +0 -7
  308. package/dist/admin/directives/perm.d.ts +0 -3
  309. package/dist/admin/hooks/useBrowser.d.ts +0 -6
  310. package/dist/admin/hooks/useUpload.d.ts +0 -8
  311. package/dist/admin/hooks/useVome.d.ts +0 -10
  312. package/dist/admin/lib/browser.d.ts +0 -3
  313. package/dist/admin/lib/cn.d.ts +0 -2
  314. package/dist/admin/lib/compress-image.d.ts +0 -12
  315. package/dist/admin/lib/dialog-float.d.ts +0 -8
  316. package/dist/admin/lib/eps.d.ts +0 -2
  317. package/dist/admin/lib/export-excel.d.ts +0 -9
  318. package/dist/admin/lib/file-preview.d.ts +0 -9
  319. package/dist/admin/lib/import-excel.d.ts +0 -21
  320. package/dist/admin/lib/json.d.ts +0 -11
  321. package/dist/admin/lib/menu.d.ts +0 -3
  322. package/dist/admin/lib/micro-locale.d.ts +0 -5
  323. package/dist/admin/lib/micro-theme.d.ts +0 -14
  324. package/dist/admin/lib/module.d.ts +0 -21
  325. package/dist/admin/lib/scarce-assets.d.ts +0 -5
  326. package/dist/admin/lib/shell.d.ts +0 -8
  327. package/dist/admin/lib/theme-color-options.d.ts +0 -15
  328. package/dist/admin/lib/tree.d.ts +0 -2
  329. package/dist/admin/lib/upload.d.ts +0 -17
  330. package/dist/admin/lib/video-frame.d.ts +0 -6
  331. package/dist/admin/router/create.d.ts +0 -16
  332. package/dist/admin/router/guards.d.ts +0 -6
  333. package/dist/admin/router/menu-routes.d.ts +0 -13
  334. package/dist/admin/stores/app.d.ts +0 -13
  335. package/dist/admin/stores/tags.d.ts +0 -14
  336. package/dist/admin/stores/user.d.ts +0 -48
  337. package/dist/client/ai-model-registry.d.ts +0 -16
  338. package/dist/client/base.d.ts +0 -28
  339. package/dist/client/configure.d.ts +0 -33
  340. package/dist/client/dict.d.ts +0 -31
  341. package/dist/client/eps-payload.d.ts +0 -3
  342. package/dist/client/eps.d.ts +0 -17
  343. package/dist/client/locale-change.d.ts +0 -16
  344. package/dist/client/locale-store.d.ts +0 -105
  345. package/dist/client/service.d.ts +0 -29
@@ -1,765 +1,765 @@
1
- <template>
2
- <Dialog
3
- :open="crud.upsertVisible.value"
4
- @update:open="(v) => (!v ? onCloseClick() : undefined)"
5
- >
6
- <DialogContent
7
- shell="crud"
8
- :show-close-button="false"
9
- class="vm-crud-upsert"
10
- :class="{
11
- 'is-empty': isBodyEmpty,
12
- 'is-height': !!heightCss,
13
- }"
14
- :style="heightCss ? { '--vm-dialog-height': heightCss } : undefined"
15
- @pointer-down-outside="preventDialogOutsideClose"
16
- @focus-outside="preventDialogOutsideClose"
17
- @interact-outside="preventDialogOutsideClose"
18
- >
19
- <DialogHeader class="vm-crud-shell__head">
20
- <DialogTitle class="vm-crud-shell__title">{{ titleText }}</DialogTitle>
21
- <slot name="title-extra" />
22
- <DialogClose class="vm-crud-shell__close">
23
- <i class="ri-close-line" />
24
- <span class="sr-only">Close</span>
25
- </DialogClose>
26
- </DialogHeader>
27
- <div
28
- class="vm-crud-upsert__body"
29
- :class="{ 'is-fill': layout === 'fill' }"
30
- >
31
- <template v-if="!isBodyEmpty">
32
- <div
33
- v-for="item in formItemsBeforeSlot"
34
- :key="item.prop"
35
- class="vm-crud-upsert__field"
36
- :class="{ 'is-error': fieldErrors[item.prop] }"
37
- :style="{ gridColumn: `span ${itemSpan(item)}` }"
38
- >
39
- <Label class="vm-crud-upsert__label">
40
- {{ item.label }}
41
- <span
42
- v-if="resolveItemRequired(item, crud.upsertForm.value)"
43
- class="vm-crud-upsert__req"
44
- >*</span>
45
- </Label>
46
-
47
- <vm-radio
48
- v-if="itemType(item) === 'radio'"
49
- :name="`upsert-${item.prop}`"
50
- :options="resolveOptions(item)"
51
- :model-value="String(crud.upsertForm.value[item.prop] ?? '')"
52
- :disabled="isInfo || item.disabled"
53
- @update:model-value="(v) => setField(item.prop, v)"
54
- />
55
-
56
- <component
57
- :is="resolveField(item)"
58
- v-else-if="resolveField(item)"
59
- :class="fieldClass(item)"
60
- v-bind="fieldProps(item)"
61
- :model-value="crud.upsertForm.value[item.prop]"
62
- :disabled="isInfo || item.disabled"
63
- @update:model-value="(v: unknown) => setField(item.prop, v)"
64
- />
65
-
66
- <Input
67
- v-else
68
- class="vm-crud-upsert__control"
69
- v-bind="inputBind(item)"
70
- :placeholder="item.placeholder"
71
- :disabled="isInfo || item.disabled"
72
- :aria-invalid="fieldErrors[item.prop] ? true : undefined"
73
- :model-value="
74
- crud.upsertForm.value[item.prop] == null
75
- ? ''
76
- : (crud.upsertForm.value[item.prop] as string | number)
77
- "
78
- @update:model-value="(v) => setField(item.prop, v)"
79
- />
80
- <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
81
- {{ fieldErrors[item.prop] }}
82
- </p>
83
- </div>
84
- <slot :form="crud.upsertForm.value" />
85
- <div
86
- v-for="item in formItemsAfterSlot"
87
- :key="item.prop"
88
- class="vm-crud-upsert__field"
89
- :class="{ 'is-error': fieldErrors[item.prop] }"
90
- :style="{ gridColumn: `span ${itemSpan(item)}` }"
91
- >
92
- <Label class="vm-crud-upsert__label">
93
- {{ item.label }}
94
- <span
95
- v-if="resolveItemRequired(item, crud.upsertForm.value)"
96
- class="vm-crud-upsert__req"
97
- >*</span>
98
- </Label>
99
-
100
- <div class="vm-crud-upsert__textarea-wrap">
101
- <textarea
102
- class="vm-crud-upsert__textarea"
103
- :placeholder="item.placeholder"
104
- :disabled="isInfo || item.disabled"
105
- :rows="textareaRows(item)"
106
- :style="textareaStyle(item)"
107
- :value="String(crud.upsertForm.value[item.prop] ?? '')"
108
- @input="setField(item.prop, ($event.target as HTMLTextAreaElement).value)"
109
- />
110
- <i
111
- v-if="textareaClearable(item) && String(crud.upsertForm.value[item.prop] ?? '') !== '' && !isInfo && !item.disabled"
112
- class="ri-close-circle-fill vm-crud-upsert__textarea-clear"
113
- role="button"
114
- tabindex="-1"
115
- aria-label="清除"
116
- @mousedown.prevent
117
- @click="setField(item.prop, '')"
118
- />
119
- </div>
120
- <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
121
- {{ fieldErrors[item.prop] }}
122
- </p>
123
- </div>
124
- </template>
125
- <div v-else class="vm-crud-upsert__empty">
126
- <vm-empty kind="暂无内容" />
127
- </div>
128
- </div>
129
- <DialogFooter v-if="showFooter" class="vm-crud-shell__foot">
130
- <slot
131
- v-if="slots.footer"
132
- name="footer"
133
- v-bind="footerSlotProps"
134
- />
135
- <template v-else>
136
- <slot name="footer-prepend" v-bind="footerSlotProps" />
137
- <button
138
- v-if="showClose"
139
- type="button"
140
- class="vm-crud-shell__btn is-ghost"
141
- :disabled="submitting"
142
- @click="onCloseClick"
143
- >
144
- {{ closeLabel }}
145
- </button>
146
- <button
147
- v-if="showConfirm"
148
- type="button"
149
- class="vm-crud-shell__btn is-primary"
150
- :disabled="submitting"
151
- @click="onConfirmClick"
152
- >
153
- {{ submitting ? '提交中…' : confirmLabel }}
154
- </button>
155
- <slot name="footer-append" v-bind="footerSlotProps" />
156
- </template>
157
- </DialogFooter>
158
- </DialogContent>
159
- </Dialog>
160
- </template>
161
-
162
- <script setup lang="ts">
163
- import { ref, computed, watch, useSlots, toValue } from 'vue'
164
- import { toast } from 'vue-sonner'
165
- import { useCrud } from './useCrud'
166
- import { injectUpsertOptions } from './key'
167
- import { getCrudStyle, crudLabel } from './style'
168
- import { resolveCrudComponent } from './registry'
169
- import { applySetRules, resolveItemRequired, validateFormFields } from './validate'
170
- import { resolveFormSpan, useFormCols } from './span'
171
- import { preventDialogOutsideClose } from '../lib/dialog-float'
172
- import {
173
- buildAutoUpsertItems,
174
- mapDictOptions,
175
- mergeByProp,
176
- resolveFormItemValue,
177
- sortRequiredFirst,
178
- } from './auto-fields'
179
- import type { CrudFormItem } from '../../../typings/admin/comm/crud'
180
- defineOptions({ name: 'vm-upsert' })
181
-
182
- const props = withDefaults(
183
- defineProps<{
184
- items?: CrudFormItem[]
185
- title?: string
186
- /** 弹窗高度,如 800 / '800px';最高受壳层 800px 约束 */
187
- height?: string | number
188
- /** 是否显示确定;未设时 info 模式自动隐藏 */
189
- confirm?: boolean
190
- /** 是否显示关闭,默认 true */
191
- close?: boolean
192
- /** false 时隐藏整个底栏 */
193
- footer?: boolean
194
- confirmText?: string
195
- closeText?: string
196
- /** form=栅格表单;fill=纵向铺满(只读列表/文档) */
197
- layout?: 'form' | 'fill'
198
- }>(),
199
- {
200
- close: undefined,
201
- confirm: undefined,
202
- footer: true,
203
- layout: 'form',
204
- },
205
- )
206
-
207
- const emit = defineEmits<{
208
- close: []
209
- confirm: []
210
- }>()
211
-
212
- const slots = useSlots()
213
- const crud = useCrud()
214
- const upsertOpts = injectUpsertOptions()
215
- const formCols = useFormCols()
216
- const submitting = ref(false)
217
- const fieldErrors = ref<Record<string, string>>({})
218
-
219
- function normalizeHeight(h?: string | number) {
220
- if (h == null || h === '') return ''
221
- if (typeof h === 'number') return `${h}px`
222
- return String(h)
223
- }
224
-
225
- const heightCss = computed(() =>
226
- normalizeHeight(props.height ?? upsertOpts?.dialog?.height),
227
- )
228
-
229
- const mergedItems = computed(() => {
230
- const manual = props.items?.length ? props.items : upsertOpts?.items || []
231
- const formPlugins = getCrudStyle().form.plugins as Array<{
232
- __plugin?: string
233
- ignoreFields?: string[]
234
- }>
235
- const autoPlugin = formPlugins.find((p) => p?.__plugin === 'setAuto')
236
- const autoEnabled = upsertOpts?.auto !== false && Boolean(autoPlugin)
237
- let raw = manual
238
- if (autoEnabled) {
239
- const pluginIgnore = (autoPlugin?.ignoreFields as string[] | undefined) || []
240
- const pageIgnore = upsertOpts?.ignoreFields || []
241
- const auto = buildAutoUpsertItems(crud.service, {
242
- ignoreFields: [...pluginIgnore, ...pageIgnore],
243
- })
244
- raw = mergeByProp(auto, manual)
245
- }
246
- raw = sortRequiredFirst(raw)
247
- const useRules = formPlugins.some((p) => p?.__plugin === 'setRules')
248
- return useRules ? applySetRules(raw) : raw
249
- })
250
-
251
- const titleText = computed(() => {
252
- if (props.title != null && props.title !== '') return props.title
253
- if (upsertOpts?.dialog?.title) return upsertOpts.dialog.title
254
- if (crud.upsertMode.value === 'add') return crudLabel('add', crud.dict.label)
255
- if (crud.upsertMode.value === 'info') return crudLabel('info', crud.dict.label)
256
- return crudLabel('update', crud.dict.label)
257
- })
258
-
259
- const visibleItems = computed(() =>
260
- mergedItems.value.filter((it) => {
261
- if (typeof it.hidden === 'function') return !it.hidden(crud.upsertForm.value)
262
- return !it.hidden
263
- }),
264
- )
265
-
266
- /** 备注等 textarea 固定渲染在 slot 之后 */
267
- const formItemsBeforeSlot = computed(() =>
268
- visibleItems.value.filter((it) => itemType(it) !== 'textarea'),
269
- )
270
- const formItemsAfterSlot = computed(() =>
271
- visibleItems.value.filter((it) => itemType(it) === 'textarea'),
272
- )
273
-
274
- const isInfo = computed(() => crud.upsertMode.value === 'info')
275
- /** prop > dialog.confirm > info 自动隐藏 */
276
- const showConfirm = computed(() => {
277
- if (typeof props.confirm === 'boolean') return props.confirm
278
- const flag = upsertOpts?.dialog?.confirm
279
- if (typeof flag === 'boolean') return flag
280
- return !isInfo.value
281
- })
282
- const showClose = computed(() => {
283
- if (typeof props.close === 'boolean') return props.close
284
- const flag = upsertOpts?.dialog?.close
285
- if (typeof flag === 'boolean') return flag
286
- return true
287
- })
288
- const showFooter = computed(() => props.footer !== false)
289
- const confirmLabel = computed(
290
- () =>
291
- props.confirmText ||
292
- upsertOpts?.dialog?.confirmText ||
293
- crudLabel('confirm', crud.dict.label),
294
- )
295
- const closeLabel = computed(
296
- () =>
297
- props.closeText ||
298
- upsertOpts?.dialog?.closeText ||
299
- crudLabel('close', crud.dict.label),
300
- )
301
- const isBodyEmpty = computed(
302
- () => !visibleItems.value.length && !slots.default,
303
- )
304
-
305
- const footerSlotProps = computed(() => ({
306
- close: onCloseClick,
307
- submit: onSubmit,
308
- confirm: onConfirmClick,
309
- submitting: submitting.value,
310
- showConfirm: showConfirm.value,
311
- showClose: showClose.value,
312
- form: crud.upsertForm.value,
313
- }))
314
-
315
- function setField(prop: string, value: unknown) {
316
- if (isInfo.value) return
317
- if (fieldErrors.value[prop]) {
318
- const next = { ...fieldErrors.value }
319
- delete next[prop]
320
- fieldErrors.value = next
321
- }
322
- crud.upsertForm.value = { ...crud.upsertForm.value, [prop]: value }
323
- }
324
-
325
- /** number:把 component.props(type / precision / controls)落到 Input */
326
- function inputBind(item: CrudFormItem) {
327
- if (itemType(item) !== 'number') return {}
328
- const raw = toValue(item.component?.props)
329
- if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
330
- return raw as Record<string, unknown>
331
- }
332
- return { type: 'number', controls: false }
333
- }
334
-
335
- function itemType(item: CrudFormItem) {
336
- if (item.type) return item.type
337
- const name = item.component?.name
338
- if (typeof name !== 'string') return 'input'
339
- if (name === 'vm-switch') return 'switch'
340
- if (name === 'vm-select' || name === 'vm-tree-select') {
341
- return 'select'
342
- }
343
- return 'input'
344
- }
345
-
346
- function isSwitchField(item: CrudFormItem) {
347
- return itemType(item) === 'switch'
348
- }
349
-
350
- function isUploadField(item: CrudFormItem) {
351
- const name = item.component?.name
352
- return name === 'vm-upload' || name === 'upload'
353
- }
354
-
355
- function isMarkdownField(item: CrudFormItem) {
356
- return item.component?.name === 'vm-markdown'
357
- }
358
-
359
- function isJsonEditorField(item: CrudFormItem) {
360
- return item.component?.name === 'vm-json-editor'
361
- }
362
-
363
- function isRichtextField(item: CrudFormItem) {
364
- return item.component?.name === 'vm-richtext'
365
- }
366
-
367
- function fieldClass(item: CrudFormItem) {
368
- if (isSwitchField(item)) return 'vm-crud-upsert__switch-field'
369
- if (isUploadField(item)) return 'vm-crud-upsert__upload-field'
370
- // markdown / json-editor / richtext 自带高度,禁止套用单行 control 高度
371
- if (isMarkdownField(item)) return 'vm-crud-upsert__markdown-field'
372
- if (isJsonEditorField(item)) return 'vm-crud-upsert__json-field'
373
- if (isRichtextField(item)) return 'vm-crud-upsert__richtext-field'
374
- return 'vm-crud-upsert__control'
375
- }
376
-
377
- function resolveField(item: CrudFormItem) {
378
- if (itemType(item) === 'radio' || itemType(item) === 'textarea') return undefined
379
- const name = item.component?.name
380
- if (typeof name === 'string') {
381
- if (name === 'upload') return resolveCrudComponent('vm-upload')
382
- const comp = resolveCrudComponent(name)
383
- if (comp) return comp
384
- } else if (name) {
385
- return name
386
- }
387
- if (item.type === 'switch') return resolveCrudComponent('vm-switch')
388
- // multiple 须走多选组件;单选 Select 无法展示数组 tagIds
389
- if (item.type === 'select') {
390
- if (item.multiple) return resolveCrudComponent('vm-multi-select')
391
- return resolveCrudComponent('vm-select')
392
- }
393
- return undefined
394
- }
395
-
396
- function fieldProps(item: CrudFormItem) {
397
- const raw = item.component?.props
398
- const unwrapped = toValue(raw)
399
- // props: dict.get('status') → 直接作 options(switch 等)
400
- if (Array.isArray(unwrapped)) {
401
- return {
402
- options: unwrapped,
403
- placeholder: item.placeholder,
404
- refreshOnChange: false,
405
- }
406
- }
407
- return {
408
- options: resolveOptions(item),
409
- placeholder: item.placeholder,
410
- refreshOnChange: false,
411
- ...(unwrapped && typeof unwrapped === 'object'
412
- ? (unwrapped as Record<string, unknown>)
413
- : {}),
414
- }
415
- }
416
-
417
- /** options / dict → 渲染时解析 */
418
- function resolveOptions(item: CrudFormItem | CrudFormItem['options']) {
419
- if (item && typeof item === 'object' && 'prop' in item) {
420
- const formItem = item as CrudFormItem
421
- if (formItem.dict) return mapDictOptions(formItem.dict)
422
- const list = formItem.options == null ? [] : toValue(formItem.options)
423
- return Array.isArray(list) ? list : []
424
- }
425
- const list = item == null ? [] : toValue(item)
426
- return Array.isArray(list) ? list : []
427
- }
428
-
429
- function itemSpan(item: CrudFormItem) {
430
- return resolveFormSpan(item.span, formCols.value)
431
- }
432
-
433
- /** textarea:component.props.rows / height / style */
434
- function textareaProps(item: CrudFormItem) {
435
- const raw = toValue(item.component?.props)
436
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
437
- return raw as Record<string, unknown>
438
- }
439
-
440
- /** textarea 默认可清除;component.props.clearable === false 关闭 */
441
- function textareaClearable(item: CrudFormItem) {
442
- return textareaProps(item).clearable !== false
443
- }
444
-
445
- function textareaRows(item: CrudFormItem) {
446
- const rows = textareaProps(item).rows
447
- return typeof rows === 'number' || typeof rows === 'string' ? rows : undefined
448
- }
449
-
450
- function textareaStyle(item: CrudFormItem) {
451
- const p = textareaProps(item)
452
- const style: Record<string, string> = {
453
- ...((p.style && typeof p.style === 'object'
454
- ? (p.style as Record<string, string>)
455
- : {}) as Record<string, string>),
456
- }
457
- if (p.height != null && p.height !== '') {
458
- const h = typeof p.height === 'number' ? `${p.height}px` : String(p.height)
459
- style.height = h
460
- if (!style.minHeight) style.minHeight = h
461
- }
462
- return Object.keys(style).length ? style : undefined
463
- }
464
-
465
- function onCloseClick() {
466
- const done = () => crud.upsertClose()
467
- if (upsertOpts?.onClose) {
468
- upsertOpts.onClose('close', done)
469
- emit('close')
470
- return
471
- }
472
- done()
473
- emit('close')
474
- }
475
-
476
- function onConfirmClick() {
477
- emit('confirm')
478
- void onSubmit()
479
- }
480
-
481
- async function onSubmit() {
482
- if (isInfo.value) {
483
- onCloseClick()
484
- return
485
- }
486
- if (submitting.value) return
487
- const errors = await validateFormFields(
488
- visibleItems.value,
489
- crud.upsertForm.value,
490
- )
491
- fieldErrors.value = errors
492
- if (Object.keys(errors).length) return
493
- submitting.value = true
494
- try {
495
- if (upsertOpts?.onSubmit) {
496
- await upsertOpts.onSubmit(
497
- { ...crud.upsertForm.value },
498
- {
499
- next: async (data) => {
500
- crud.upsertForm.value = { ...crud.upsertForm.value, ...data }
501
- await crud.upsertSubmit()
502
- },
503
- done: () => {},
504
- close: () => crud.upsertClose(),
505
- },
506
- )
507
- return
508
- }
509
- await crud.upsertSubmit()
510
- } catch (e) {
511
- console.error(e)
512
- const errObj = e as Error & { toasted?: boolean }
513
- const msg = e instanceof Error && e.message ? e.message : '操作失败'
514
- if (!errObj.toasted) toast.error(msg.slice(0, 200))
515
- } finally {
516
- submitting.value = false
517
- }
518
- }
519
-
520
- watch(
521
- () => crud.upsertVisible.value,
522
- (open) => {
523
- if (!open) {
524
- submitting.value = false
525
- fieldErrors.value = {}
526
- return
527
- }
528
- const data = { ...crud.upsertForm.value }
529
- upsertOpts?.onOpen?.(data)
530
- for (const it of mergedItems.value) {
531
- if (data[it.prop] === undefined && it.value !== undefined) {
532
- const v = resolveFormItemValue(it.value)
533
- if (v !== undefined) data[it.prop] = v
534
- }
535
- }
536
- crud.upsertForm.value = data
537
- upsertOpts?.onOpened?.({ ...crud.upsertForm.value })
538
- const focusPlugin = getCrudStyle().form.plugins.find(
539
- (p) => (p as { __plugin?: string })?.__plugin === 'setFocus',
540
- ) as { prop?: string } | undefined
541
- const prop =
542
- focusPlugin?.prop ||
543
- mergedItems.value.find((i) => !i.hidden)?.prop
544
- requestAnimationFrame(() => {
545
- const el = document.querySelector(
546
- `.vm-crud-upsert__field input, .vm-crud-upsert__field textarea`,
547
- ) as HTMLInputElement | null
548
- if (prop) {
549
- const named = document.querySelector(
550
- `[name="upsert-${prop}"], .vm-crud-upsert__field:has(input) input`,
551
- ) as HTMLInputElement | null
552
- ;(named || el)?.focus?.()
553
- } else {
554
- el?.focus?.()
555
- }
556
- })
557
- },
558
- )
559
-
560
- defineExpose({
561
- form: crud.upsertForm,
562
- mode: crud.upsertMode,
563
- submit: onSubmit,
564
- close: onCloseClick,
565
- add: () => crud.rowAdd(),
566
- append: (data?: Record<string, unknown>) => crud.rowAppend(data),
567
- edit: (data: Record<string, unknown>) => crud.rowEdit(data),
568
- info: (data: Record<string, unknown>) => crud.rowInfo(data),
569
- })
570
- </script>
571
-
572
- <style lang="scss" scoped>
573
- .vm-crud-upsert__body {
574
- display: grid;
575
- flex: 1 1 auto;
576
- grid-template-columns: repeat(24, minmax(0, 1fr));
577
- gap: 16px;
578
- min-height: 0;
579
- overflow: auto;
580
- overscroll-behavior: contain;
581
- padding: 18px 22px;
582
- align-content: start;
583
-
584
- &.is-fill {
585
- display: flex;
586
- flex-direction: column;
587
- align-items: stretch;
588
- gap: 0;
589
- padding: 0;
590
-
591
- > * {
592
- width: 100%;
593
- min-width: 0;
594
- }
595
- }
596
-
597
- /* slot 内容在宿主 SFC,须 :deep 才能命中 label / control 样式 */
598
- :deep(.vm-crud-upsert__field) {
599
- display: grid;
600
- gap: 6px;
601
- align-content: start;
602
- min-width: 0;
603
- }
604
-
605
- :deep(.vm-crud-upsert__label) {
606
- font-size: 12px;
607
- color: var(--muted-foreground);
608
- }
609
-
610
- :deep(.vm-crud-upsert__req) {
611
- color: var(--destructive);
612
- }
613
-
614
- :deep(.vm-crud-upsert__control) {
615
- width: 100%;
616
- height: var(--vm-control-height) !important;
617
- min-height: var(--vm-control-height);
618
- border-radius: var(--vm-control-radius) !important;
619
- font-size: var(--vm-control-font-size) !important;
620
- line-height: 1.4 !important;
621
- }
622
-
623
- /* Input 外壳:高度交给内部 input,避免 wrap 压扁清除按钮 */
624
- :deep(.vm-input-wrap.vm-crud-upsert__control) {
625
- height: auto !important;
626
- min-height: var(--vm-control-height);
627
- }
628
-
629
- /* 单行 input:白底;select / tree-select 等:淡蓝底 */
630
- :deep(input.vm-crud-upsert__control),
631
- :deep(.vm-crud-upsert__control[data-slot='input']),
632
- :deep(.vm-input-wrap.vm-crud-upsert__control input) {
633
- background: var(--card) !important;
634
- }
635
-
636
- :deep(.vm-crud-select.vm-crud-upsert__control),
637
- :deep(.vm-group-cascader.vm-crud-upsert__control),
638
- :deep(.vm-multi-select.vm-crud-upsert__control),
639
- :deep(.vm-user-select.vm-crud-upsert__control),
640
- :deep(.vm-date-picker.vm-crud-upsert__control),
641
- :deep(.vm-icon-picker.vm-crud-upsert__control) {
642
- background: var(--muted) !important;
643
- }
644
-
645
- :deep(.vm-tree-select.vm-crud-upsert__control .vm-tree-select__trigger) {
646
- background: var(--muted) !important;
647
- }
648
-
649
- @media (max-width: 768px) {
650
- grid-template-columns: repeat(12, minmax(0, 1fr));
651
- flex: 1 1 auto;
652
- min-height: 0;
653
- gap: 10px;
654
- padding: 10px 12px;
655
- }
656
- }
657
-
658
- .vm-crud-upsert__empty {
659
- grid-column: 1 / -1;
660
- display: flex;
661
- min-height: 160px;
662
- align-items: center;
663
- justify-content: center;
664
- }
665
-
666
- .vm-crud-upsert__field {
667
- &.is-error :deep(.vm-crud-upsert__control) {
668
- border-color: var(--destructive) !important;
669
- }
670
- }
671
-
672
- .vm-crud-upsert__error {
673
- margin: 0;
674
- font-size: 12px;
675
- line-height: 1.4;
676
- color: var(--destructive);
677
- }
678
-
679
- :deep(.vm-group-cascader.vm-crud-upsert__control) {
680
- height: auto !important;
681
- min-height: var(--vm-control-height);
682
- }
683
-
684
- /* 上传:勿套用单行高度;多图时铺满 span 以便平铺换行 */
685
- :deep(.vm-upload.vm-crud-upsert__upload-field) {
686
- width: 100%;
687
- max-width: 100%;
688
- height: auto !important;
689
- min-height: 0;
690
- background: transparent !important;
691
- }
692
-
693
- /* Markdown:保留组件自身 min-height,勿被 control 行高压扁 */
694
- :deep(.vm-markdown.vm-crud-upsert__markdown-field) {
695
- width: 100%;
696
- height: auto !important;
697
- min-height: 0;
698
- background: transparent !important;
699
- }
700
-
701
- /* JSON:与备注 textarea 同视觉,勿套单行 control */
702
- :deep(.vm-json-editor.vm-crud-upsert__json-field) {
703
- width: 100%;
704
- height: auto !important;
705
- min-height: 0;
706
- background: transparent !important;
707
- }
708
-
709
- /* 富文本:保留组件默认 minHeight,勿被 control 行高压扁 */
710
- :deep(.vm-richtext.vm-crud-upsert__richtext-field) {
711
- width: 100%;
712
- height: auto !important;
713
- min-height: 0;
714
- background: transparent !important;
715
- }
716
-
717
- :deep(.vm-crud-upsert__switch-field.vm-switch) {
718
- width: 40px;
719
- height: 22px;
720
- margin-top: 8px;
721
- }
722
-
723
- .vm-crud-upsert__textarea-wrap {
724
- position: relative;
725
- width: 100%;
726
- min-width: 0;
727
- }
728
-
729
- .vm-crud-upsert__textarea-clear {
730
- position: absolute;
731
- top: 10px;
732
- right: var(--vm-control-padding-x);
733
- z-index: 1;
734
- color: var(--muted-foreground);
735
- font-size: 14px;
736
- line-height: 1;
737
- cursor: pointer;
738
-
739
- &:hover {
740
- color: var(--foreground);
741
- }
742
- }
743
-
744
- .vm-crud-upsert__textarea,
745
- :deep(.vm-crud-upsert__textarea) {
746
- width: 100%;
747
- min-height: 72px;
748
- padding: 8px 28px 8px var(--vm-control-padding-x);
749
- border: 1px solid var(--input);
750
- border-radius: var(--vm-control-radius);
751
- background: var(--muted);
752
- color: var(--foreground);
753
- font-size: 14px;
754
- line-height: 1.5;
755
- resize: vertical;
756
- outline: none;
757
- transition: border-color 0.15s ease;
758
-
759
- &:focus,
760
- &:focus-visible {
761
- border-color: var(--focus-border);
762
- box-shadow: none;
763
- }
764
- }
765
- </style>
1
+ <template>
2
+ <Dialog
3
+ :open="crud.upsertVisible.value"
4
+ @update:open="(v) => (!v ? onCloseClick() : undefined)"
5
+ >
6
+ <DialogContent
7
+ shell="crud"
8
+ :show-close-button="false"
9
+ class="vm-crud-upsert"
10
+ :class="{
11
+ 'is-empty': isBodyEmpty,
12
+ 'is-height': !!heightCss,
13
+ }"
14
+ :style="heightCss ? { '--vm-dialog-height': heightCss } : undefined"
15
+ @pointer-down-outside="preventDialogOutsideClose"
16
+ @focus-outside="preventDialogOutsideClose"
17
+ @interact-outside="preventDialogOutsideClose"
18
+ >
19
+ <DialogHeader class="vm-crud-shell__head">
20
+ <DialogTitle class="vm-crud-shell__title">{{ titleText }}</DialogTitle>
21
+ <slot name="title-extra" />
22
+ <DialogClose class="vm-crud-shell__close">
23
+ <i class="ri-close-line" />
24
+ <span class="sr-only">Close</span>
25
+ </DialogClose>
26
+ </DialogHeader>
27
+ <div
28
+ class="vm-crud-upsert__body"
29
+ :class="{ 'is-fill': layout === 'fill' }"
30
+ >
31
+ <template v-if="!isBodyEmpty">
32
+ <div
33
+ v-for="item in formItemsBeforeSlot"
34
+ :key="item.prop"
35
+ class="vm-crud-upsert__field"
36
+ :class="{ 'is-error': fieldErrors[item.prop] }"
37
+ :style="{ gridColumn: `span ${itemSpan(item)}` }"
38
+ >
39
+ <Label class="vm-crud-upsert__label">
40
+ {{ item.label }}
41
+ <span
42
+ v-if="resolveItemRequired(item, crud.upsertForm.value)"
43
+ class="vm-crud-upsert__req"
44
+ >*</span>
45
+ </Label>
46
+
47
+ <vm-radio
48
+ v-if="itemType(item) === 'radio'"
49
+ :name="`upsert-${item.prop}`"
50
+ :options="resolveOptions(item)"
51
+ :model-value="String(crud.upsertForm.value[item.prop] ?? '')"
52
+ :disabled="isInfo || item.disabled"
53
+ @update:model-value="(v) => setField(item.prop, v)"
54
+ />
55
+
56
+ <component
57
+ :is="resolveField(item)"
58
+ v-else-if="resolveField(item)"
59
+ :class="fieldClass(item)"
60
+ v-bind="fieldProps(item)"
61
+ :model-value="crud.upsertForm.value[item.prop]"
62
+ :disabled="isInfo || item.disabled"
63
+ @update:model-value="(v: unknown) => setField(item.prop, v)"
64
+ />
65
+
66
+ <Input
67
+ v-else
68
+ class="vm-crud-upsert__control"
69
+ v-bind="inputBind(item)"
70
+ :placeholder="item.placeholder"
71
+ :disabled="isInfo || item.disabled"
72
+ :aria-invalid="fieldErrors[item.prop] ? true : undefined"
73
+ :model-value="
74
+ crud.upsertForm.value[item.prop] == null
75
+ ? ''
76
+ : (crud.upsertForm.value[item.prop] as string | number)
77
+ "
78
+ @update:model-value="(v) => setField(item.prop, v)"
79
+ />
80
+ <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
81
+ {{ fieldErrors[item.prop] }}
82
+ </p>
83
+ </div>
84
+ <slot :form="crud.upsertForm.value" />
85
+ <div
86
+ v-for="item in formItemsAfterSlot"
87
+ :key="item.prop"
88
+ class="vm-crud-upsert__field"
89
+ :class="{ 'is-error': fieldErrors[item.prop] }"
90
+ :style="{ gridColumn: `span ${itemSpan(item)}` }"
91
+ >
92
+ <Label class="vm-crud-upsert__label">
93
+ {{ item.label }}
94
+ <span
95
+ v-if="resolveItemRequired(item, crud.upsertForm.value)"
96
+ class="vm-crud-upsert__req"
97
+ >*</span>
98
+ </Label>
99
+
100
+ <div class="vm-crud-upsert__textarea-wrap">
101
+ <textarea
102
+ class="vm-crud-upsert__textarea"
103
+ :placeholder="item.placeholder"
104
+ :disabled="isInfo || item.disabled"
105
+ :rows="textareaRows(item)"
106
+ :style="textareaStyle(item)"
107
+ :value="String(crud.upsertForm.value[item.prop] ?? '')"
108
+ @input="setField(item.prop, ($event.target as HTMLTextAreaElement).value)"
109
+ />
110
+ <i
111
+ v-if="textareaClearable(item) && String(crud.upsertForm.value[item.prop] ?? '') !== '' && !isInfo && !item.disabled"
112
+ class="ri-close-circle-fill vm-crud-upsert__textarea-clear"
113
+ role="button"
114
+ tabindex="-1"
115
+ aria-label="清除"
116
+ @mousedown.prevent
117
+ @click="setField(item.prop, '')"
118
+ />
119
+ </div>
120
+ <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
121
+ {{ fieldErrors[item.prop] }}
122
+ </p>
123
+ </div>
124
+ </template>
125
+ <div v-else class="vm-crud-upsert__empty">
126
+ <vm-empty kind="暂无内容" />
127
+ </div>
128
+ </div>
129
+ <DialogFooter v-if="showFooter" class="vm-crud-shell__foot">
130
+ <slot
131
+ v-if="slots.footer"
132
+ name="footer"
133
+ v-bind="footerSlotProps"
134
+ />
135
+ <template v-else>
136
+ <slot name="footer-prepend" v-bind="footerSlotProps" />
137
+ <button
138
+ v-if="showClose"
139
+ type="button"
140
+ class="vm-crud-shell__btn is-ghost"
141
+ :disabled="submitting"
142
+ @click="onCloseClick"
143
+ >
144
+ {{ closeLabel }}
145
+ </button>
146
+ <button
147
+ v-if="showConfirm"
148
+ type="button"
149
+ class="vm-crud-shell__btn is-primary"
150
+ :disabled="submitting"
151
+ @click="onConfirmClick"
152
+ >
153
+ {{ submitting ? '提交中…' : confirmLabel }}
154
+ </button>
155
+ <slot name="footer-append" v-bind="footerSlotProps" />
156
+ </template>
157
+ </DialogFooter>
158
+ </DialogContent>
159
+ </Dialog>
160
+ </template>
161
+
162
+ <script setup lang="ts">
163
+ import { ref, computed, watch, useSlots, toValue } from 'vue'
164
+ import { toast } from 'vue-sonner'
165
+ import { useCrud } from './useCrud'
166
+ import { injectUpsertOptions } from './key'
167
+ import { getCrudStyle, crudLabel } from './style'
168
+ import { resolveCrudComponent } from './registry'
169
+ import { applySetRules, resolveItemRequired, validateFormFields } from './validate'
170
+ import { resolveFormSpan, useFormCols } from './span'
171
+ import { preventDialogOutsideClose } from '../lib/dialog-float'
172
+ import {
173
+ buildAutoUpsertItems,
174
+ mapDictOptions,
175
+ mergeByProp,
176
+ resolveFormItemValue,
177
+ sortRequiredFirst,
178
+ } from './auto-fields'
179
+ import type { CrudFormItem } from '../../../typings/admin/comm/crud'
180
+ defineOptions({ name: 'vm-upsert' })
181
+
182
+ const props = withDefaults(
183
+ defineProps<{
184
+ items?: CrudFormItem[]
185
+ title?: string
186
+ /** 弹窗高度,如 800 / '800px';最高受壳层 800px 约束 */
187
+ height?: string | number
188
+ /** 是否显示确定;未设时 info 模式自动隐藏 */
189
+ confirm?: boolean
190
+ /** 是否显示关闭,默认 true */
191
+ close?: boolean
192
+ /** false 时隐藏整个底栏 */
193
+ footer?: boolean
194
+ confirmText?: string
195
+ closeText?: string
196
+ /** form=栅格表单;fill=纵向铺满(只读列表/文档) */
197
+ layout?: 'form' | 'fill'
198
+ }>(),
199
+ {
200
+ close: undefined,
201
+ confirm: undefined,
202
+ footer: true,
203
+ layout: 'form',
204
+ },
205
+ )
206
+
207
+ const emit = defineEmits<{
208
+ close: []
209
+ confirm: []
210
+ }>()
211
+
212
+ const slots = useSlots()
213
+ const crud = useCrud()
214
+ const upsertOpts = injectUpsertOptions()
215
+ const formCols = useFormCols()
216
+ const submitting = ref(false)
217
+ const fieldErrors = ref<Record<string, string>>({})
218
+
219
+ function normalizeHeight(h?: string | number) {
220
+ if (h == null || h === '') return ''
221
+ if (typeof h === 'number') return `${h}px`
222
+ return String(h)
223
+ }
224
+
225
+ const heightCss = computed(() =>
226
+ normalizeHeight(props.height ?? upsertOpts?.dialog?.height),
227
+ )
228
+
229
+ const mergedItems = computed(() => {
230
+ const manual = props.items?.length ? props.items : upsertOpts?.items || []
231
+ const formPlugins = getCrudStyle().form.plugins as Array<{
232
+ __plugin?: string
233
+ ignoreFields?: string[]
234
+ }>
235
+ const autoPlugin = formPlugins.find((p) => p?.__plugin === 'setAuto')
236
+ const autoEnabled = upsertOpts?.auto !== false && Boolean(autoPlugin)
237
+ let raw = manual
238
+ if (autoEnabled) {
239
+ const pluginIgnore = (autoPlugin?.ignoreFields as string[] | undefined) || []
240
+ const pageIgnore = upsertOpts?.ignoreFields || []
241
+ const auto = buildAutoUpsertItems(crud.service, {
242
+ ignoreFields: [...pluginIgnore, ...pageIgnore],
243
+ })
244
+ raw = mergeByProp(auto, manual)
245
+ }
246
+ raw = sortRequiredFirst(raw)
247
+ const useRules = formPlugins.some((p) => p?.__plugin === 'setRules')
248
+ return useRules ? applySetRules(raw) : raw
249
+ })
250
+
251
+ const titleText = computed(() => {
252
+ if (props.title != null && props.title !== '') return props.title
253
+ if (upsertOpts?.dialog?.title) return upsertOpts.dialog.title
254
+ if (crud.upsertMode.value === 'add') return crudLabel('add', crud.dict.label)
255
+ if (crud.upsertMode.value === 'info') return crudLabel('info', crud.dict.label)
256
+ return crudLabel('update', crud.dict.label)
257
+ })
258
+
259
+ const visibleItems = computed(() =>
260
+ mergedItems.value.filter((it) => {
261
+ if (typeof it.hidden === 'function') return !it.hidden(crud.upsertForm.value)
262
+ return !it.hidden
263
+ }),
264
+ )
265
+
266
+ /** 备注等 textarea 固定渲染在 slot 之后 */
267
+ const formItemsBeforeSlot = computed(() =>
268
+ visibleItems.value.filter((it) => itemType(it) !== 'textarea'),
269
+ )
270
+ const formItemsAfterSlot = computed(() =>
271
+ visibleItems.value.filter((it) => itemType(it) === 'textarea'),
272
+ )
273
+
274
+ const isInfo = computed(() => crud.upsertMode.value === 'info')
275
+ /** prop > dialog.confirm > info 自动隐藏 */
276
+ const showConfirm = computed(() => {
277
+ if (typeof props.confirm === 'boolean') return props.confirm
278
+ const flag = upsertOpts?.dialog?.confirm
279
+ if (typeof flag === 'boolean') return flag
280
+ return !isInfo.value
281
+ })
282
+ const showClose = computed(() => {
283
+ if (typeof props.close === 'boolean') return props.close
284
+ const flag = upsertOpts?.dialog?.close
285
+ if (typeof flag === 'boolean') return flag
286
+ return true
287
+ })
288
+ const showFooter = computed(() => props.footer !== false)
289
+ const confirmLabel = computed(
290
+ () =>
291
+ props.confirmText ||
292
+ upsertOpts?.dialog?.confirmText ||
293
+ crudLabel('confirm', crud.dict.label),
294
+ )
295
+ const closeLabel = computed(
296
+ () =>
297
+ props.closeText ||
298
+ upsertOpts?.dialog?.closeText ||
299
+ crudLabel('close', crud.dict.label),
300
+ )
301
+ const isBodyEmpty = computed(
302
+ () => !visibleItems.value.length && !slots.default,
303
+ )
304
+
305
+ const footerSlotProps = computed(() => ({
306
+ close: onCloseClick,
307
+ submit: onSubmit,
308
+ confirm: onConfirmClick,
309
+ submitting: submitting.value,
310
+ showConfirm: showConfirm.value,
311
+ showClose: showClose.value,
312
+ form: crud.upsertForm.value,
313
+ }))
314
+
315
+ function setField(prop: string, value: unknown) {
316
+ if (isInfo.value) return
317
+ if (fieldErrors.value[prop]) {
318
+ const next = { ...fieldErrors.value }
319
+ delete next[prop]
320
+ fieldErrors.value = next
321
+ }
322
+ crud.upsertForm.value = { ...crud.upsertForm.value, [prop]: value }
323
+ }
324
+
325
+ /** number:把 component.props(type / precision / controls)落到 Input */
326
+ function inputBind(item: CrudFormItem) {
327
+ if (itemType(item) !== 'number') return {}
328
+ const raw = toValue(item.component?.props)
329
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
330
+ return raw as Record<string, unknown>
331
+ }
332
+ return { type: 'number', controls: false }
333
+ }
334
+
335
+ function itemType(item: CrudFormItem) {
336
+ if (item.type) return item.type
337
+ const name = item.component?.name
338
+ if (typeof name !== 'string') return 'input'
339
+ if (name === 'vm-switch') return 'switch'
340
+ if (name === 'vm-select' || name === 'vm-tree-select') {
341
+ return 'select'
342
+ }
343
+ return 'input'
344
+ }
345
+
346
+ function isSwitchField(item: CrudFormItem) {
347
+ return itemType(item) === 'switch'
348
+ }
349
+
350
+ function isUploadField(item: CrudFormItem) {
351
+ const name = item.component?.name
352
+ return name === 'vm-upload' || name === 'upload'
353
+ }
354
+
355
+ function isMarkdownField(item: CrudFormItem) {
356
+ return item.component?.name === 'vm-markdown'
357
+ }
358
+
359
+ function isJsonEditorField(item: CrudFormItem) {
360
+ return item.component?.name === 'vm-json-editor'
361
+ }
362
+
363
+ function isRichtextField(item: CrudFormItem) {
364
+ return item.component?.name === 'vm-richtext'
365
+ }
366
+
367
+ function fieldClass(item: CrudFormItem) {
368
+ if (isSwitchField(item)) return 'vm-crud-upsert__switch-field'
369
+ if (isUploadField(item)) return 'vm-crud-upsert__upload-field'
370
+ // markdown / json-editor / richtext 自带高度,禁止套用单行 control 高度
371
+ if (isMarkdownField(item)) return 'vm-crud-upsert__markdown-field'
372
+ if (isJsonEditorField(item)) return 'vm-crud-upsert__json-field'
373
+ if (isRichtextField(item)) return 'vm-crud-upsert__richtext-field'
374
+ return 'vm-crud-upsert__control'
375
+ }
376
+
377
+ function resolveField(item: CrudFormItem) {
378
+ if (itemType(item) === 'radio' || itemType(item) === 'textarea') return undefined
379
+ const name = item.component?.name
380
+ if (typeof name === 'string') {
381
+ if (name === 'upload') return resolveCrudComponent('vm-upload')
382
+ const comp = resolveCrudComponent(name)
383
+ if (comp) return comp
384
+ } else if (name) {
385
+ return name
386
+ }
387
+ if (item.type === 'switch') return resolveCrudComponent('vm-switch')
388
+ // multiple 须走多选组件;单选 Select 无法展示数组 tagIds
389
+ if (item.type === 'select') {
390
+ if (item.multiple) return resolveCrudComponent('vm-multi-select')
391
+ return resolveCrudComponent('vm-select')
392
+ }
393
+ return undefined
394
+ }
395
+
396
+ function fieldProps(item: CrudFormItem) {
397
+ const raw = item.component?.props
398
+ const unwrapped = toValue(raw)
399
+ // props: dict.get('status') → 直接作 options(switch 等)
400
+ if (Array.isArray(unwrapped)) {
401
+ return {
402
+ options: unwrapped,
403
+ placeholder: item.placeholder,
404
+ refreshOnChange: false,
405
+ }
406
+ }
407
+ return {
408
+ options: resolveOptions(item),
409
+ placeholder: item.placeholder,
410
+ refreshOnChange: false,
411
+ ...(unwrapped && typeof unwrapped === 'object'
412
+ ? (unwrapped as Record<string, unknown>)
413
+ : {}),
414
+ }
415
+ }
416
+
417
+ /** options / dict → 渲染时解析 */
418
+ function resolveOptions(item: CrudFormItem | CrudFormItem['options']) {
419
+ if (item && typeof item === 'object' && 'prop' in item) {
420
+ const formItem = item as CrudFormItem
421
+ if (formItem.dict) return mapDictOptions(formItem.dict)
422
+ const list = formItem.options == null ? [] : toValue(formItem.options)
423
+ return Array.isArray(list) ? list : []
424
+ }
425
+ const list = item == null ? [] : toValue(item)
426
+ return Array.isArray(list) ? list : []
427
+ }
428
+
429
+ function itemSpan(item: CrudFormItem) {
430
+ return resolveFormSpan(item.span, formCols.value)
431
+ }
432
+
433
+ /** textarea:component.props.rows / height / style */
434
+ function textareaProps(item: CrudFormItem) {
435
+ const raw = toValue(item.component?.props)
436
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
437
+ return raw as Record<string, unknown>
438
+ }
439
+
440
+ /** textarea 默认可清除;component.props.clearable === false 关闭 */
441
+ function textareaClearable(item: CrudFormItem) {
442
+ return textareaProps(item).clearable !== false
443
+ }
444
+
445
+ function textareaRows(item: CrudFormItem) {
446
+ const rows = textareaProps(item).rows
447
+ return typeof rows === 'number' || typeof rows === 'string' ? rows : undefined
448
+ }
449
+
450
+ function textareaStyle(item: CrudFormItem) {
451
+ const p = textareaProps(item)
452
+ const style: Record<string, string> = {
453
+ ...((p.style && typeof p.style === 'object'
454
+ ? (p.style as Record<string, string>)
455
+ : {}) as Record<string, string>),
456
+ }
457
+ if (p.height != null && p.height !== '') {
458
+ const h = typeof p.height === 'number' ? `${p.height}px` : String(p.height)
459
+ style.height = h
460
+ if (!style.minHeight) style.minHeight = h
461
+ }
462
+ return Object.keys(style).length ? style : undefined
463
+ }
464
+
465
+ function onCloseClick() {
466
+ const done = () => crud.upsertClose()
467
+ if (upsertOpts?.onClose) {
468
+ upsertOpts.onClose('close', done)
469
+ emit('close')
470
+ return
471
+ }
472
+ done()
473
+ emit('close')
474
+ }
475
+
476
+ function onConfirmClick() {
477
+ emit('confirm')
478
+ void onSubmit()
479
+ }
480
+
481
+ async function onSubmit() {
482
+ if (isInfo.value) {
483
+ onCloseClick()
484
+ return
485
+ }
486
+ if (submitting.value) return
487
+ const errors = await validateFormFields(
488
+ visibleItems.value,
489
+ crud.upsertForm.value,
490
+ )
491
+ fieldErrors.value = errors
492
+ if (Object.keys(errors).length) return
493
+ submitting.value = true
494
+ try {
495
+ if (upsertOpts?.onSubmit) {
496
+ await upsertOpts.onSubmit(
497
+ { ...crud.upsertForm.value },
498
+ {
499
+ next: async (data) => {
500
+ crud.upsertForm.value = { ...crud.upsertForm.value, ...data }
501
+ await crud.upsertSubmit()
502
+ },
503
+ done: () => {},
504
+ close: () => crud.upsertClose(),
505
+ },
506
+ )
507
+ return
508
+ }
509
+ await crud.upsertSubmit()
510
+ } catch (e) {
511
+ console.error(e)
512
+ const errObj = e as Error & { toasted?: boolean }
513
+ const msg = e instanceof Error && e.message ? e.message : '操作失败'
514
+ if (!errObj.toasted) toast.error(msg.slice(0, 200))
515
+ } finally {
516
+ submitting.value = false
517
+ }
518
+ }
519
+
520
+ watch(
521
+ () => crud.upsertVisible.value,
522
+ (open) => {
523
+ if (!open) {
524
+ submitting.value = false
525
+ fieldErrors.value = {}
526
+ return
527
+ }
528
+ const data = { ...crud.upsertForm.value }
529
+ upsertOpts?.onOpen?.(data)
530
+ for (const it of mergedItems.value) {
531
+ if (data[it.prop] === undefined && it.value !== undefined) {
532
+ const v = resolveFormItemValue(it.value)
533
+ if (v !== undefined) data[it.prop] = v
534
+ }
535
+ }
536
+ crud.upsertForm.value = data
537
+ upsertOpts?.onOpened?.({ ...crud.upsertForm.value })
538
+ const focusPlugin = getCrudStyle().form.plugins.find(
539
+ (p) => (p as { __plugin?: string })?.__plugin === 'setFocus',
540
+ ) as { prop?: string } | undefined
541
+ const prop =
542
+ focusPlugin?.prop ||
543
+ mergedItems.value.find((i) => !i.hidden)?.prop
544
+ requestAnimationFrame(() => {
545
+ const el = document.querySelector(
546
+ `.vm-crud-upsert__field input, .vm-crud-upsert__field textarea`,
547
+ ) as HTMLInputElement | null
548
+ if (prop) {
549
+ const named = document.querySelector(
550
+ `[name="upsert-${prop}"], .vm-crud-upsert__field:has(input) input`,
551
+ ) as HTMLInputElement | null
552
+ ;(named || el)?.focus?.()
553
+ } else {
554
+ el?.focus?.()
555
+ }
556
+ })
557
+ },
558
+ )
559
+
560
+ defineExpose({
561
+ form: crud.upsertForm,
562
+ mode: crud.upsertMode,
563
+ submit: onSubmit,
564
+ close: onCloseClick,
565
+ add: () => crud.rowAdd(),
566
+ append: (data?: Record<string, unknown>) => crud.rowAppend(data),
567
+ edit: (data: Record<string, unknown>) => crud.rowEdit(data),
568
+ info: (data: Record<string, unknown>) => crud.rowInfo(data),
569
+ })
570
+ </script>
571
+
572
+ <style lang="scss" scoped>
573
+ .vm-crud-upsert__body {
574
+ display: grid;
575
+ flex: 1 1 auto;
576
+ grid-template-columns: repeat(24, minmax(0, 1fr));
577
+ gap: 16px;
578
+ min-height: 0;
579
+ overflow: auto;
580
+ overscroll-behavior: contain;
581
+ padding: 18px 22px;
582
+ align-content: start;
583
+
584
+ &.is-fill {
585
+ display: flex;
586
+ flex-direction: column;
587
+ align-items: stretch;
588
+ gap: 0;
589
+ padding: 0;
590
+
591
+ > * {
592
+ width: 100%;
593
+ min-width: 0;
594
+ }
595
+ }
596
+
597
+ /* slot 内容在宿主 SFC,须 :deep 才能命中 label / control 样式 */
598
+ :deep(.vm-crud-upsert__field) {
599
+ display: grid;
600
+ gap: 6px;
601
+ align-content: start;
602
+ min-width: 0;
603
+ }
604
+
605
+ :deep(.vm-crud-upsert__label) {
606
+ font-size: 12px;
607
+ color: var(--muted-foreground);
608
+ }
609
+
610
+ :deep(.vm-crud-upsert__req) {
611
+ color: var(--destructive);
612
+ }
613
+
614
+ :deep(.vm-crud-upsert__control) {
615
+ width: 100%;
616
+ height: var(--vm-control-height) !important;
617
+ min-height: var(--vm-control-height);
618
+ border-radius: var(--vm-control-radius) !important;
619
+ font-size: var(--vm-control-font-size) !important;
620
+ line-height: 1.4 !important;
621
+ }
622
+
623
+ /* Input 外壳:高度交给内部 input,避免 wrap 压扁清除按钮 */
624
+ :deep(.vm-input-wrap.vm-crud-upsert__control) {
625
+ height: auto !important;
626
+ min-height: var(--vm-control-height);
627
+ }
628
+
629
+ /* 单行 input:白底;select / tree-select 等:淡蓝底 */
630
+ :deep(input.vm-crud-upsert__control),
631
+ :deep(.vm-crud-upsert__control[data-slot='input']),
632
+ :deep(.vm-input-wrap.vm-crud-upsert__control input) {
633
+ background: var(--card) !important;
634
+ }
635
+
636
+ :deep(.vm-crud-select.vm-crud-upsert__control),
637
+ :deep(.vm-group-cascader.vm-crud-upsert__control),
638
+ :deep(.vm-multi-select.vm-crud-upsert__control),
639
+ :deep(.vm-user-select.vm-crud-upsert__control),
640
+ :deep(.vm-date-picker.vm-crud-upsert__control),
641
+ :deep(.vm-icon-picker.vm-crud-upsert__control) {
642
+ background: var(--muted) !important;
643
+ }
644
+
645
+ :deep(.vm-tree-select.vm-crud-upsert__control .vm-tree-select__trigger) {
646
+ background: var(--muted) !important;
647
+ }
648
+
649
+ @media (max-width: 768px) {
650
+ grid-template-columns: repeat(12, minmax(0, 1fr));
651
+ flex: 1 1 auto;
652
+ min-height: 0;
653
+ gap: 10px;
654
+ padding: 10px 12px;
655
+ }
656
+ }
657
+
658
+ .vm-crud-upsert__empty {
659
+ grid-column: 1 / -1;
660
+ display: flex;
661
+ min-height: 160px;
662
+ align-items: center;
663
+ justify-content: center;
664
+ }
665
+
666
+ .vm-crud-upsert__field {
667
+ &.is-error :deep(.vm-crud-upsert__control) {
668
+ border-color: var(--destructive) !important;
669
+ }
670
+ }
671
+
672
+ .vm-crud-upsert__error {
673
+ margin: 0;
674
+ font-size: 12px;
675
+ line-height: 1.4;
676
+ color: var(--destructive);
677
+ }
678
+
679
+ :deep(.vm-group-cascader.vm-crud-upsert__control) {
680
+ height: auto !important;
681
+ min-height: var(--vm-control-height);
682
+ }
683
+
684
+ /* 上传:勿套用单行高度;多图时铺满 span 以便平铺换行 */
685
+ :deep(.vm-upload.vm-crud-upsert__upload-field) {
686
+ width: 100%;
687
+ max-width: 100%;
688
+ height: auto !important;
689
+ min-height: 0;
690
+ background: transparent !important;
691
+ }
692
+
693
+ /* Markdown:保留组件自身 min-height,勿被 control 行高压扁 */
694
+ :deep(.vm-markdown.vm-crud-upsert__markdown-field) {
695
+ width: 100%;
696
+ height: auto !important;
697
+ min-height: 0;
698
+ background: transparent !important;
699
+ }
700
+
701
+ /* JSON:与备注 textarea 同视觉,勿套单行 control */
702
+ :deep(.vm-json-editor.vm-crud-upsert__json-field) {
703
+ width: 100%;
704
+ height: auto !important;
705
+ min-height: 0;
706
+ background: transparent !important;
707
+ }
708
+
709
+ /* 富文本:保留组件默认 minHeight,勿被 control 行高压扁 */
710
+ :deep(.vm-richtext.vm-crud-upsert__richtext-field) {
711
+ width: 100%;
712
+ height: auto !important;
713
+ min-height: 0;
714
+ background: transparent !important;
715
+ }
716
+
717
+ :deep(.vm-crud-upsert__switch-field.vm-switch) {
718
+ width: 40px;
719
+ height: 22px;
720
+ margin-top: 8px;
721
+ }
722
+
723
+ .vm-crud-upsert__textarea-wrap {
724
+ position: relative;
725
+ width: 100%;
726
+ min-width: 0;
727
+ }
728
+
729
+ .vm-crud-upsert__textarea-clear {
730
+ position: absolute;
731
+ top: 10px;
732
+ right: var(--vm-control-padding-x);
733
+ z-index: 1;
734
+ color: var(--muted-foreground);
735
+ font-size: 14px;
736
+ line-height: 1;
737
+ cursor: pointer;
738
+
739
+ &:hover {
740
+ color: var(--foreground);
741
+ }
742
+ }
743
+
744
+ .vm-crud-upsert__textarea,
745
+ :deep(.vm-crud-upsert__textarea) {
746
+ width: 100%;
747
+ min-height: 72px;
748
+ padding: 8px 28px 8px var(--vm-control-padding-x);
749
+ border: 1px solid var(--input);
750
+ border-radius: var(--vm-control-radius);
751
+ background: var(--muted);
752
+ color: var(--foreground);
753
+ font-size: 14px;
754
+ line-height: 1.5;
755
+ resize: vertical;
756
+ outline: none;
757
+ transition: border-color 0.15s ease;
758
+
759
+ &:focus,
760
+ &:focus-visible {
761
+ border-color: var(--focus-border);
762
+ box-shadow: none;
763
+ }
764
+ }
765
+ </style>