vome-core 0.3.65 → 0.3.67

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,632 +1,632 @@
1
- <template>
2
- <Dialog
3
- :open="open"
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">{{ title }}</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, form)"
43
- class="vm-crud-upsert__req"
44
- >*</span>
45
- </Label>
46
-
47
- <vm-radio
48
- v-if="itemType(item) === 'radio'"
49
- :name="`form-dialog-${item.prop}`"
50
- :options="resolveOptions(item)"
51
- :model-value="String(form[item.prop] ?? '')"
52
- :disabled="readonly || 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="form[item.prop]"
62
- :disabled="readonly || 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="readonly || item.disabled"
72
- :aria-invalid="fieldErrors[item.prop] ? true : undefined"
73
- :model-value="
74
- form[item.prop] == null ? '' : (form[item.prop] as string | number)
75
- "
76
- @update:model-value="(v) => setField(item.prop, v)"
77
- />
78
- <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
79
- {{ fieldErrors[item.prop] }}
80
- </p>
81
- </div>
82
- <slot :form="form" />
83
- <div
84
- v-for="item in formItemsAfterSlot"
85
- :key="item.prop"
86
- class="vm-crud-upsert__field"
87
- :class="{ 'is-error': fieldErrors[item.prop] }"
88
- :style="{ gridColumn: `span ${itemSpan(item)}` }"
89
- >
90
- <Label class="vm-crud-upsert__label">
91
- {{ item.label }}
92
- <span
93
- v-if="resolveItemRequired(item, form)"
94
- class="vm-crud-upsert__req"
95
- >*</span>
96
- </Label>
97
-
98
- <div class="vm-crud-upsert__textarea-wrap">
99
- <textarea
100
- class="vm-crud-upsert__textarea"
101
- :placeholder="item.placeholder"
102
- :disabled="readonly || item.disabled"
103
- :rows="textareaRows(item)"
104
- :style="textareaStyle(item)"
105
- :value="String(form[item.prop] ?? '')"
106
- @input="setField(item.prop, ($event.target as HTMLTextAreaElement).value)"
107
- />
108
- <i
109
- v-if="
110
- textareaClearable(item) &&
111
- String(form[item.prop] ?? '') !== '' &&
112
- !readonly &&
113
- !item.disabled
114
- "
115
- class="ri-close-circle-fill vm-crud-upsert__textarea-clear"
116
- role="button"
117
- tabindex="-1"
118
- :aria-label="crudLabel('clear')"
119
- @mousedown.prevent
120
- @click="setField(item.prop, '')"
121
- />
122
- </div>
123
- <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
124
- {{ fieldErrors[item.prop] }}
125
- </p>
126
- </div>
127
- </template>
128
- <div v-else class="vm-crud-upsert__empty">
129
- <vm-empty kind="暂无内容" />
130
- </div>
131
- </div>
132
- <DialogFooter v-if="footer !== false" class="vm-crud-shell__foot">
133
- <slot
134
- v-if="slots.footer"
135
- name="footer"
136
- v-bind="footerSlotProps"
137
- />
138
- <template v-else>
139
- <slot name="footer-prepend" v-bind="footerSlotProps" />
140
- <button
141
- v-if="close !== false"
142
- type="button"
143
- class="vm-crud-shell__btn is-ghost"
144
- :disabled="submitting"
145
- @click="onCloseClick"
146
- >
147
- {{ closeText || crudLabel('close') }}
148
- </button>
149
- <button
150
- v-if="confirm !== false && !readonly"
151
- type="button"
152
- class="vm-crud-shell__btn is-primary"
153
- :disabled="submitting"
154
- @click="onConfirmClick"
155
- >
156
- {{ submitting ? crudLabel('submitting') : confirmText || crudLabel('confirm') }}
157
- </button>
158
- <slot name="footer-append" v-bind="footerSlotProps" />
159
- </template>
160
- </DialogFooter>
161
- </DialogContent>
162
- </Dialog>
163
- </template>
164
-
165
- <script setup lang="ts">
166
- import { crudLabel } from '../crud/style'
167
- import { ref, computed, watch, useSlots, toValue } from 'vue'
168
- import { preventDialogOutsideClose } from '../lib/dialog-float'
169
- import { resolveCrudComponent } from '../crud/registry'
170
- import { resolveItemRequired, validateFormFields } from '../crud/validate'
171
- import { resolveFormSpan, useFormCols } from '../crud/span'
172
- import { mapDictOptions, resolveFormItemValue } from '../crud/auto-fields'
173
- import type { CrudFormItem } from '../../../typings/admin/comm/crud'
174
-
175
- defineOptions({ name: 'vm-form-dialog' })
176
-
177
- export type VmFormDialogSubmitHelpers = {
178
- /** 关闭弹窗并复位 submitting */
179
- close: () => void
180
- /** 仅复位 submitting(弹窗保持打开) */
181
- done: () => void
182
- }
183
-
184
- const props = withDefaults(
185
- defineProps<{
186
- open?: boolean
187
- /** 表单数据(v-model) */
188
- modelValue?: Record<string, unknown>
189
- title?: string
190
- items?: CrudFormItem[]
191
- height?: string | number
192
- confirm?: boolean
193
- close?: boolean
194
- footer?: boolean
195
- confirmText?: string
196
- closeText?: string
197
- layout?: 'form' | 'fill'
198
- /** 只读:隐藏确定、控件禁用 */
199
- readonly?: boolean
200
- }>(),
201
- {
202
- open: false,
203
- modelValue: () => ({}),
204
- title: '',
205
- items: () => [],
206
- confirm: true,
207
- close: true,
208
- footer: true,
209
- layout: 'form',
210
- readonly: false,
211
- },
212
- )
213
-
214
- const emit = defineEmits<{
215
- 'update:open': [v: boolean]
216
- 'update:modelValue': [v: Record<string, unknown>]
217
- close: []
218
- /** 校验通过后触发;业务自行请求,完成后调用 helpers.close() */
219
- submit: [data: Record<string, unknown>, helpers: VmFormDialogSubmitHelpers]
220
- }>()
221
-
222
- const slots = useSlots()
223
- const formCols = useFormCols()
224
- const submitting = ref(false)
225
- const fieldErrors = ref<Record<string, string>>({})
226
-
227
- const form = computed({
228
- get: () => props.modelValue || {},
229
- set: (v) => emit('update:modelValue', v),
230
- })
231
-
232
- function normalizeHeight(h?: string | number) {
233
- if (h == null || h === '') return ''
234
- if (typeof h === 'number') return `${h}px`
235
- return String(h)
236
- }
237
-
238
- const heightCss = computed(() => normalizeHeight(props.height))
239
-
240
- const visibleItems = computed(() =>
241
- (props.items || []).filter((it) => {
242
- if (typeof it.hidden === 'function') return !it.hidden(form.value)
243
- return !it.hidden
244
- }),
245
- )
246
-
247
- const formItemsBeforeSlot = computed(() =>
248
- visibleItems.value.filter((it) => itemType(it) !== 'textarea'),
249
- )
250
- const formItemsAfterSlot = computed(() =>
251
- visibleItems.value.filter((it) => itemType(it) === 'textarea'),
252
- )
253
-
254
- const isBodyEmpty = computed(
255
- () => !visibleItems.value.length && !slots.default,
256
- )
257
-
258
- const footerSlotProps = computed(() => ({
259
- close: onCloseClick,
260
- submit: onConfirmClick,
261
- confirm: onConfirmClick,
262
- submitting: submitting.value,
263
- form: form.value,
264
- }))
265
-
266
- function setField(prop: string, value: unknown) {
267
- if (props.readonly) return
268
- if (fieldErrors.value[prop]) {
269
- const next = { ...fieldErrors.value }
270
- delete next[prop]
271
- fieldErrors.value = next
272
- }
273
- form.value = { ...form.value, [prop]: value }
274
- }
275
-
276
- function inputBind(item: CrudFormItem) {
277
- if (itemType(item) !== 'number') return {}
278
- const raw = toValue(item.component?.props)
279
- if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
280
- return raw as Record<string, unknown>
281
- }
282
- return { type: 'number', controls: false }
283
- }
284
-
285
- function itemType(item: CrudFormItem) {
286
- if (item.type) return item.type
287
- const name = item.component?.name
288
- if (typeof name !== 'string') return 'input'
289
- if (name === 'vm-switch') return 'switch'
290
- if (name === 'vm-select' || name === 'vm-tree-select') return 'select'
291
- return 'input'
292
- }
293
-
294
- function fieldClass(item: CrudFormItem) {
295
- const name = item.component?.name
296
- if (itemType(item) === 'switch') return 'vm-crud-upsert__switch-field'
297
- if (name === 'vm-upload' || name === 'upload') return 'vm-crud-upsert__upload-field'
298
- if (name === 'vm-markdown') return 'vm-crud-upsert__markdown-field'
299
- if (name === 'vm-json-editor') return 'vm-crud-upsert__json-field'
300
- if (name === 'vm-richtext') return 'vm-crud-upsert__richtext-field'
301
- return 'vm-crud-upsert__control'
302
- }
303
-
304
- function resolveField(item: CrudFormItem) {
305
- if (itemType(item) === 'radio' || itemType(item) === 'textarea') return undefined
306
- const name = item.component?.name
307
- if (typeof name === 'string') {
308
- if (name === 'upload') return resolveCrudComponent('vm-upload')
309
- const comp = resolveCrudComponent(name)
310
- if (comp) return comp
311
- } else if (name) {
312
- return name
313
- }
314
- if (item.type === 'switch') return resolveCrudComponent('vm-switch')
315
- if (item.type === 'select') {
316
- if (item.multiple) return resolveCrudComponent('vm-multi-select')
317
- return resolveCrudComponent('vm-select')
318
- }
319
- return undefined
320
- }
321
-
322
- function fieldProps(item: CrudFormItem) {
323
- const raw = item.component?.props
324
- const unwrapped = toValue(raw)
325
- if (Array.isArray(unwrapped)) {
326
- return {
327
- options: unwrapped,
328
- placeholder: item.placeholder,
329
- refreshOnChange: false,
330
- }
331
- }
332
- return {
333
- options: resolveOptions(item),
334
- placeholder: item.placeholder,
335
- refreshOnChange: false,
336
- ...(unwrapped && typeof unwrapped === 'object'
337
- ? (unwrapped as Record<string, unknown>)
338
- : {}),
339
- }
340
- }
341
-
342
- function resolveOptions(item: CrudFormItem | CrudFormItem['options']) {
343
- if (item && typeof item === 'object' && 'prop' in item) {
344
- const formItem = item as CrudFormItem
345
- if (formItem.dict) return mapDictOptions(formItem.dict)
346
- const list = formItem.options == null ? [] : toValue(formItem.options)
347
- return Array.isArray(list) ? list : []
348
- }
349
- const list = item == null ? [] : toValue(item)
350
- return Array.isArray(list) ? list : []
351
- }
352
-
353
- function itemSpan(item: CrudFormItem) {
354
- return resolveFormSpan(item.span, formCols.value)
355
- }
356
-
357
- function textareaProps(item: CrudFormItem) {
358
- const raw = toValue(item.component?.props)
359
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
360
- return raw as Record<string, unknown>
361
- }
362
-
363
- function textareaClearable(item: CrudFormItem) {
364
- return textareaProps(item).clearable !== false
365
- }
366
-
367
- function textareaRows(item: CrudFormItem) {
368
- const rows = textareaProps(item).rows
369
- return typeof rows === 'number' || typeof rows === 'string' ? rows : undefined
370
- }
371
-
372
- function textareaStyle(item: CrudFormItem) {
373
- const p = textareaProps(item)
374
- const style: Record<string, string> = {
375
- ...((p.style && typeof p.style === 'object'
376
- ? (p.style as Record<string, string>)
377
- : {}) as Record<string, string>),
378
- }
379
- if (p.height != null && p.height !== '') {
380
- const h = typeof p.height === 'number' ? `${p.height}px` : String(p.height)
381
- style.height = h
382
- if (!style.minHeight) style.minHeight = h
383
- }
384
- return Object.keys(style).length ? style : undefined
385
- }
386
-
387
- function onCloseClick() {
388
- submitting.value = false
389
- fieldErrors.value = {}
390
- emit('update:open', false)
391
- emit('close')
392
- }
393
-
394
- async function onConfirmClick() {
395
- if (props.readonly || submitting.value) return
396
- const errors = await validateFormFields(visibleItems.value, form.value)
397
- fieldErrors.value = errors
398
- if (Object.keys(errors).length) return
399
- submitting.value = true
400
- const helpers: VmFormDialogSubmitHelpers = {
401
- close: () => {
402
- submitting.value = false
403
- fieldErrors.value = {}
404
- emit('update:open', false)
405
- emit('close')
406
- },
407
- done: () => {
408
- submitting.value = false
409
- },
410
- }
411
- emit('submit', { ...form.value }, helpers)
412
- }
413
-
414
- watch(
415
- () => props.open,
416
- (open) => {
417
- if (!open) {
418
- submitting.value = false
419
- fieldErrors.value = {}
420
- return
421
- }
422
- const data = { ...form.value }
423
- for (const it of props.items || []) {
424
- if (data[it.prop] === undefined && it.value !== undefined) {
425
- const v = resolveFormItemValue(it.value)
426
- if (v !== undefined) data[it.prop] = v
427
- }
428
- }
429
- form.value = data
430
- },
431
- )
432
-
433
- defineExpose({
434
- form,
435
- submit: onConfirmClick,
436
- close: onCloseClick,
437
- })
438
- </script>
439
-
440
- <style lang="scss" scoped>
441
- .vm-crud-upsert__body {
442
- display: grid;
443
- flex: 1 1 auto;
444
- grid-template-columns: repeat(24, minmax(0, 1fr));
445
- gap: 16px;
446
- min-height: 0;
447
- overflow: auto;
448
- overscroll-behavior: contain;
449
- padding: 18px 22px;
450
- align-content: start;
451
-
452
- &.is-fill {
453
- display: flex;
454
- flex-direction: column;
455
- align-items: stretch;
456
- gap: 0;
457
- padding: 0;
458
-
459
- > * {
460
- width: 100%;
461
- min-width: 0;
462
- }
463
- }
464
-
465
- /* slot 内容在宿主 SFC,须 :deep 才能命中 label / control 样式 */
466
- :deep(.vm-crud-upsert__field) {
467
- display: grid;
468
- gap: 6px;
469
- align-content: start;
470
- min-width: 0;
471
- }
472
-
473
- :deep(.vm-crud-upsert__label) {
474
- font-size: 12px;
475
- color: var(--muted-foreground);
476
- }
477
-
478
- :deep(.vm-crud-upsert__req) {
479
- color: var(--destructive);
480
- }
481
-
482
- :deep(.vm-crud-upsert__control) {
483
- width: 100%;
484
- height: var(--vm-control-height) !important;
485
- min-height: var(--vm-control-height);
486
- border-radius: var(--vm-control-radius) !important;
487
- font-size: var(--vm-control-font-size) !important;
488
- line-height: 1.4 !important;
489
- }
490
-
491
- /* Input 外壳:高度交给内部 input,避免 wrap 压扁清除按钮 */
492
- :deep(.vm-input-wrap.vm-crud-upsert__control) {
493
- height: auto !important;
494
- min-height: var(--vm-control-height);
495
- }
496
-
497
- /* 单行 input:白底;select / tree-select 等:淡蓝底 */
498
- :deep(input.vm-crud-upsert__control),
499
- :deep(.vm-crud-upsert__control[data-slot='input']),
500
- :deep(.vm-input-wrap.vm-crud-upsert__control input) {
501
- background: var(--card) !important;
502
- }
503
-
504
- :deep(.vm-crud-select.vm-crud-upsert__control),
505
- :deep(.vm-group-cascader.vm-crud-upsert__control),
506
- :deep(.vm-multi-select.vm-crud-upsert__control),
507
- :deep(.vm-date-picker.vm-crud-upsert__control),
508
- :deep(.vm-icon-picker.vm-crud-upsert__control) {
509
- background: var(--muted) !important;
510
- }
511
-
512
- :deep(.vm-tree-select.vm-crud-upsert__control .vm-tree-select__trigger) {
513
- background: var(--muted) !important;
514
- }
515
-
516
- @media (max-width: 768px) {
517
- grid-template-columns: repeat(12, minmax(0, 1fr));
518
- flex: 1 1 auto;
519
- min-height: 0;
520
- gap: 10px;
521
- padding: 10px 12px;
522
- }
523
- }
524
-
525
- .vm-crud-upsert__empty {
526
- grid-column: 1 / -1;
527
- display: flex;
528
- min-height: 160px;
529
- align-items: center;
530
- justify-content: center;
531
- }
532
-
533
- .vm-crud-upsert__field {
534
- &.is-error :deep(.vm-crud-upsert__control) {
535
- border-color: var(--destructive) !important;
536
- }
537
- }
538
-
539
- .vm-crud-upsert__error {
540
- margin: 0;
541
- font-size: 12px;
542
- line-height: 1.4;
543
- color: var(--destructive);
544
- }
545
-
546
- :deep(.vm-group-cascader.vm-crud-upsert__control) {
547
- height: auto !important;
548
- min-height: var(--vm-control-height);
549
- }
550
-
551
- /* 上传:勿套用单行高度;多图时铺满 span 以便平铺换行 */
552
- :deep(.vm-upload.vm-crud-upsert__upload-field) {
553
- width: 100%;
554
- max-width: 100%;
555
- height: auto !important;
556
- min-height: 0;
557
- background: transparent !important;
558
- }
559
-
560
- /* Markdown:保留组件自身 min-height,勿被 control 行高压扁 */
561
- :deep(.vm-markdown.vm-crud-upsert__markdown-field) {
562
- width: 100%;
563
- height: auto !important;
564
- min-height: 0;
565
- background: transparent !important;
566
- }
567
-
568
- /* JSON:与备注 textarea 同视觉,勿套单行 control */
569
- :deep(.vm-json-editor.vm-crud-upsert__json-field) {
570
- width: 100%;
571
- height: auto !important;
572
- min-height: 0;
573
- background: transparent !important;
574
- }
575
-
576
- /* 富文本:保留组件默认 minHeight,勿被 control 行高压扁 */
577
- :deep(.vm-richtext.vm-crud-upsert__richtext-field) {
578
- width: 100%;
579
- height: auto !important;
580
- min-height: 0;
581
- background: transparent !important;
582
- }
583
-
584
- :deep(.vm-crud-upsert__switch-field.vm-switch) {
585
- width: 40px;
586
- height: 22px;
587
- margin-top: 8px;
588
- }
589
-
590
- .vm-crud-upsert__textarea-wrap {
591
- position: relative;
592
- width: 100%;
593
- min-width: 0;
594
- }
595
-
596
- .vm-crud-upsert__textarea-clear {
597
- position: absolute;
598
- top: 10px;
599
- right: var(--vm-control-padding-x);
600
- z-index: 1;
601
- color: var(--muted-foreground);
602
- font-size: 14px;
603
- line-height: 1;
604
- cursor: pointer;
605
-
606
- &:hover {
607
- color: var(--foreground);
608
- }
609
- }
610
-
611
- .vm-crud-upsert__textarea,
612
- :deep(.vm-crud-upsert__textarea) {
613
- width: 100%;
614
- min-height: 72px;
615
- padding: 8px 28px 8px var(--vm-control-padding-x);
616
- border: 1px solid var(--input);
617
- border-radius: var(--vm-control-radius);
618
- background: var(--muted);
619
- color: var(--foreground);
620
- font-size: 14px;
621
- line-height: 1.5;
622
- resize: vertical;
623
- outline: none;
624
- transition: border-color 0.15s ease;
625
-
626
- &:focus,
627
- &:focus-visible {
628
- border-color: var(--focus-border);
629
- box-shadow: none;
630
- }
631
- }
632
- </style>
1
+ <template>
2
+ <Dialog
3
+ :open="open"
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">{{ title }}</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, form)"
43
+ class="vm-crud-upsert__req"
44
+ >*</span>
45
+ </Label>
46
+
47
+ <vm-radio
48
+ v-if="itemType(item) === 'radio'"
49
+ :name="`form-dialog-${item.prop}`"
50
+ :options="resolveOptions(item)"
51
+ :model-value="String(form[item.prop] ?? '')"
52
+ :disabled="readonly || 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="form[item.prop]"
62
+ :disabled="readonly || 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="readonly || item.disabled"
72
+ :aria-invalid="fieldErrors[item.prop] ? true : undefined"
73
+ :model-value="
74
+ form[item.prop] == null ? '' : (form[item.prop] as string | number)
75
+ "
76
+ @update:model-value="(v) => setField(item.prop, v)"
77
+ />
78
+ <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
79
+ {{ fieldErrors[item.prop] }}
80
+ </p>
81
+ </div>
82
+ <slot :form="form" />
83
+ <div
84
+ v-for="item in formItemsAfterSlot"
85
+ :key="item.prop"
86
+ class="vm-crud-upsert__field"
87
+ :class="{ 'is-error': fieldErrors[item.prop] }"
88
+ :style="{ gridColumn: `span ${itemSpan(item)}` }"
89
+ >
90
+ <Label class="vm-crud-upsert__label">
91
+ {{ item.label }}
92
+ <span
93
+ v-if="resolveItemRequired(item, form)"
94
+ class="vm-crud-upsert__req"
95
+ >*</span>
96
+ </Label>
97
+
98
+ <div class="vm-crud-upsert__textarea-wrap">
99
+ <textarea
100
+ class="vm-crud-upsert__textarea"
101
+ :placeholder="item.placeholder"
102
+ :disabled="readonly || item.disabled"
103
+ :rows="textareaRows(item)"
104
+ :style="textareaStyle(item)"
105
+ :value="String(form[item.prop] ?? '')"
106
+ @input="setField(item.prop, ($event.target as HTMLTextAreaElement).value)"
107
+ />
108
+ <i
109
+ v-if="
110
+ textareaClearable(item) &&
111
+ String(form[item.prop] ?? '') !== '' &&
112
+ !readonly &&
113
+ !item.disabled
114
+ "
115
+ class="ri-close-circle-fill vm-crud-upsert__textarea-clear"
116
+ role="button"
117
+ tabindex="-1"
118
+ :aria-label="crudLabel('clear')"
119
+ @mousedown.prevent
120
+ @click="setField(item.prop, '')"
121
+ />
122
+ </div>
123
+ <p v-if="fieldErrors[item.prop]" class="vm-crud-upsert__error">
124
+ {{ fieldErrors[item.prop] }}
125
+ </p>
126
+ </div>
127
+ </template>
128
+ <div v-else class="vm-crud-upsert__empty">
129
+ <vm-empty kind="暂无内容" />
130
+ </div>
131
+ </div>
132
+ <DialogFooter v-if="footer !== false" class="vm-crud-shell__foot">
133
+ <slot
134
+ v-if="slots.footer"
135
+ name="footer"
136
+ v-bind="footerSlotProps"
137
+ />
138
+ <template v-else>
139
+ <slot name="footer-prepend" v-bind="footerSlotProps" />
140
+ <button
141
+ v-if="close !== false"
142
+ type="button"
143
+ class="vm-crud-shell__btn is-ghost"
144
+ :disabled="submitting"
145
+ @click="onCloseClick"
146
+ >
147
+ {{ closeText || crudLabel('close') }}
148
+ </button>
149
+ <button
150
+ v-if="confirm !== false && !readonly"
151
+ type="button"
152
+ class="vm-crud-shell__btn is-primary"
153
+ :disabled="submitting"
154
+ @click="onConfirmClick"
155
+ >
156
+ {{ submitting ? crudLabel('submitting') : confirmText || crudLabel('confirm') }}
157
+ </button>
158
+ <slot name="footer-append" v-bind="footerSlotProps" />
159
+ </template>
160
+ </DialogFooter>
161
+ </DialogContent>
162
+ </Dialog>
163
+ </template>
164
+
165
+ <script setup lang="ts">
166
+ import { crudLabel } from '../crud/style'
167
+ import { ref, computed, watch, useSlots, toValue } from 'vue'
168
+ import { preventDialogOutsideClose } from '../lib/dialog-float'
169
+ import { resolveCrudComponent } from '../crud/registry'
170
+ import { resolveItemRequired, validateFormFields } from '../crud/validate'
171
+ import { resolveFormSpan, useFormCols } from '../crud/span'
172
+ import { mapDictOptions, resolveFormItemValue } from '../crud/auto-fields'
173
+ import type { CrudFormItem } from '../../../typings/admin/comm/crud'
174
+
175
+ defineOptions({ name: 'vm-form-dialog' })
176
+
177
+ export type VmFormDialogSubmitHelpers = {
178
+ /** 关闭弹窗并复位 submitting */
179
+ close: () => void
180
+ /** 仅复位 submitting(弹窗保持打开) */
181
+ done: () => void
182
+ }
183
+
184
+ const props = withDefaults(
185
+ defineProps<{
186
+ open?: boolean
187
+ /** 表单数据(v-model) */
188
+ modelValue?: Record<string, unknown>
189
+ title?: string
190
+ items?: CrudFormItem[]
191
+ height?: string | number
192
+ confirm?: boolean
193
+ close?: boolean
194
+ footer?: boolean
195
+ confirmText?: string
196
+ closeText?: string
197
+ layout?: 'form' | 'fill'
198
+ /** 只读:隐藏确定、控件禁用 */
199
+ readonly?: boolean
200
+ }>(),
201
+ {
202
+ open: false,
203
+ modelValue: () => ({}),
204
+ title: '',
205
+ items: () => [],
206
+ confirm: true,
207
+ close: true,
208
+ footer: true,
209
+ layout: 'form',
210
+ readonly: false,
211
+ },
212
+ )
213
+
214
+ const emit = defineEmits<{
215
+ 'update:open': [v: boolean]
216
+ 'update:modelValue': [v: Record<string, unknown>]
217
+ close: []
218
+ /** 校验通过后触发;业务自行请求,完成后调用 helpers.close() */
219
+ submit: [data: Record<string, unknown>, helpers: VmFormDialogSubmitHelpers]
220
+ }>()
221
+
222
+ const slots = useSlots()
223
+ const formCols = useFormCols()
224
+ const submitting = ref(false)
225
+ const fieldErrors = ref<Record<string, string>>({})
226
+
227
+ const form = computed({
228
+ get: () => props.modelValue || {},
229
+ set: (v) => emit('update:modelValue', v),
230
+ })
231
+
232
+ function normalizeHeight(h?: string | number) {
233
+ if (h == null || h === '') return ''
234
+ if (typeof h === 'number') return `${h}px`
235
+ return String(h)
236
+ }
237
+
238
+ const heightCss = computed(() => normalizeHeight(props.height))
239
+
240
+ const visibleItems = computed(() =>
241
+ (props.items || []).filter((it) => {
242
+ if (typeof it.hidden === 'function') return !it.hidden(form.value)
243
+ return !it.hidden
244
+ }),
245
+ )
246
+
247
+ const formItemsBeforeSlot = computed(() =>
248
+ visibleItems.value.filter((it) => itemType(it) !== 'textarea'),
249
+ )
250
+ const formItemsAfterSlot = computed(() =>
251
+ visibleItems.value.filter((it) => itemType(it) === 'textarea'),
252
+ )
253
+
254
+ const isBodyEmpty = computed(
255
+ () => !visibleItems.value.length && !slots.default,
256
+ )
257
+
258
+ const footerSlotProps = computed(() => ({
259
+ close: onCloseClick,
260
+ submit: onConfirmClick,
261
+ confirm: onConfirmClick,
262
+ submitting: submitting.value,
263
+ form: form.value,
264
+ }))
265
+
266
+ function setField(prop: string, value: unknown) {
267
+ if (props.readonly) return
268
+ if (fieldErrors.value[prop]) {
269
+ const next = { ...fieldErrors.value }
270
+ delete next[prop]
271
+ fieldErrors.value = next
272
+ }
273
+ form.value = { ...form.value, [prop]: value }
274
+ }
275
+
276
+ function inputBind(item: CrudFormItem) {
277
+ if (itemType(item) !== 'number') return {}
278
+ const raw = toValue(item.component?.props)
279
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
280
+ return raw as Record<string, unknown>
281
+ }
282
+ return { type: 'number', controls: false }
283
+ }
284
+
285
+ function itemType(item: CrudFormItem) {
286
+ if (item.type) return item.type
287
+ const name = item.component?.name
288
+ if (typeof name !== 'string') return 'input'
289
+ if (name === 'vm-switch') return 'switch'
290
+ if (name === 'vm-select' || name === 'vm-tree-select') return 'select'
291
+ return 'input'
292
+ }
293
+
294
+ function fieldClass(item: CrudFormItem) {
295
+ const name = item.component?.name
296
+ if (itemType(item) === 'switch') return 'vm-crud-upsert__switch-field'
297
+ if (name === 'vm-upload' || name === 'upload') return 'vm-crud-upsert__upload-field'
298
+ if (name === 'vm-markdown') return 'vm-crud-upsert__markdown-field'
299
+ if (name === 'vm-json-editor') return 'vm-crud-upsert__json-field'
300
+ if (name === 'vm-richtext') return 'vm-crud-upsert__richtext-field'
301
+ return 'vm-crud-upsert__control'
302
+ }
303
+
304
+ function resolveField(item: CrudFormItem) {
305
+ if (itemType(item) === 'radio' || itemType(item) === 'textarea') return undefined
306
+ const name = item.component?.name
307
+ if (typeof name === 'string') {
308
+ if (name === 'upload') return resolveCrudComponent('vm-upload')
309
+ const comp = resolveCrudComponent(name)
310
+ if (comp) return comp
311
+ } else if (name) {
312
+ return name
313
+ }
314
+ if (item.type === 'switch') return resolveCrudComponent('vm-switch')
315
+ if (item.type === 'select') {
316
+ if (item.multiple) return resolveCrudComponent('vm-multi-select')
317
+ return resolveCrudComponent('vm-select')
318
+ }
319
+ return undefined
320
+ }
321
+
322
+ function fieldProps(item: CrudFormItem) {
323
+ const raw = item.component?.props
324
+ const unwrapped = toValue(raw)
325
+ if (Array.isArray(unwrapped)) {
326
+ return {
327
+ options: unwrapped,
328
+ placeholder: item.placeholder,
329
+ refreshOnChange: false,
330
+ }
331
+ }
332
+ return {
333
+ options: resolveOptions(item),
334
+ placeholder: item.placeholder,
335
+ refreshOnChange: false,
336
+ ...(unwrapped && typeof unwrapped === 'object'
337
+ ? (unwrapped as Record<string, unknown>)
338
+ : {}),
339
+ }
340
+ }
341
+
342
+ function resolveOptions(item: CrudFormItem | CrudFormItem['options']) {
343
+ if (item && typeof item === 'object' && 'prop' in item) {
344
+ const formItem = item as CrudFormItem
345
+ if (formItem.dict) return mapDictOptions(formItem.dict)
346
+ const list = formItem.options == null ? [] : toValue(formItem.options)
347
+ return Array.isArray(list) ? list : []
348
+ }
349
+ const list = item == null ? [] : toValue(item)
350
+ return Array.isArray(list) ? list : []
351
+ }
352
+
353
+ function itemSpan(item: CrudFormItem) {
354
+ return resolveFormSpan(item.span, formCols.value)
355
+ }
356
+
357
+ function textareaProps(item: CrudFormItem) {
358
+ const raw = toValue(item.component?.props)
359
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {}
360
+ return raw as Record<string, unknown>
361
+ }
362
+
363
+ function textareaClearable(item: CrudFormItem) {
364
+ return textareaProps(item).clearable !== false
365
+ }
366
+
367
+ function textareaRows(item: CrudFormItem) {
368
+ const rows = textareaProps(item).rows
369
+ return typeof rows === 'number' || typeof rows === 'string' ? rows : undefined
370
+ }
371
+
372
+ function textareaStyle(item: CrudFormItem) {
373
+ const p = textareaProps(item)
374
+ const style: Record<string, string> = {
375
+ ...((p.style && typeof p.style === 'object'
376
+ ? (p.style as Record<string, string>)
377
+ : {}) as Record<string, string>),
378
+ }
379
+ if (p.height != null && p.height !== '') {
380
+ const h = typeof p.height === 'number' ? `${p.height}px` : String(p.height)
381
+ style.height = h
382
+ if (!style.minHeight) style.minHeight = h
383
+ }
384
+ return Object.keys(style).length ? style : undefined
385
+ }
386
+
387
+ function onCloseClick() {
388
+ submitting.value = false
389
+ fieldErrors.value = {}
390
+ emit('update:open', false)
391
+ emit('close')
392
+ }
393
+
394
+ async function onConfirmClick() {
395
+ if (props.readonly || submitting.value) return
396
+ const errors = await validateFormFields(visibleItems.value, form.value)
397
+ fieldErrors.value = errors
398
+ if (Object.keys(errors).length) return
399
+ submitting.value = true
400
+ const helpers: VmFormDialogSubmitHelpers = {
401
+ close: () => {
402
+ submitting.value = false
403
+ fieldErrors.value = {}
404
+ emit('update:open', false)
405
+ emit('close')
406
+ },
407
+ done: () => {
408
+ submitting.value = false
409
+ },
410
+ }
411
+ emit('submit', { ...form.value }, helpers)
412
+ }
413
+
414
+ watch(
415
+ () => props.open,
416
+ (open) => {
417
+ if (!open) {
418
+ submitting.value = false
419
+ fieldErrors.value = {}
420
+ return
421
+ }
422
+ const data = { ...form.value }
423
+ for (const it of props.items || []) {
424
+ if (data[it.prop] === undefined && it.value !== undefined) {
425
+ const v = resolveFormItemValue(it.value)
426
+ if (v !== undefined) data[it.prop] = v
427
+ }
428
+ }
429
+ form.value = data
430
+ },
431
+ )
432
+
433
+ defineExpose({
434
+ form,
435
+ submit: onConfirmClick,
436
+ close: onCloseClick,
437
+ })
438
+ </script>
439
+
440
+ <style lang="scss" scoped>
441
+ .vm-crud-upsert__body {
442
+ display: grid;
443
+ flex: 1 1 auto;
444
+ grid-template-columns: repeat(24, minmax(0, 1fr));
445
+ gap: 16px;
446
+ min-height: 0;
447
+ overflow: auto;
448
+ overscroll-behavior: contain;
449
+ padding: 18px 22px;
450
+ align-content: start;
451
+
452
+ &.is-fill {
453
+ display: flex;
454
+ flex-direction: column;
455
+ align-items: stretch;
456
+ gap: 0;
457
+ padding: 0;
458
+
459
+ > * {
460
+ width: 100%;
461
+ min-width: 0;
462
+ }
463
+ }
464
+
465
+ /* slot 内容在宿主 SFC,须 :deep 才能命中 label / control 样式 */
466
+ :deep(.vm-crud-upsert__field) {
467
+ display: grid;
468
+ gap: 6px;
469
+ align-content: start;
470
+ min-width: 0;
471
+ }
472
+
473
+ :deep(.vm-crud-upsert__label) {
474
+ font-size: 12px;
475
+ color: var(--muted-foreground);
476
+ }
477
+
478
+ :deep(.vm-crud-upsert__req) {
479
+ color: var(--destructive);
480
+ }
481
+
482
+ :deep(.vm-crud-upsert__control) {
483
+ width: 100%;
484
+ height: var(--vm-control-height) !important;
485
+ min-height: var(--vm-control-height);
486
+ border-radius: var(--vm-control-radius) !important;
487
+ font-size: var(--vm-control-font-size) !important;
488
+ line-height: 1.4 !important;
489
+ }
490
+
491
+ /* Input 外壳:高度交给内部 input,避免 wrap 压扁清除按钮 */
492
+ :deep(.vm-input-wrap.vm-crud-upsert__control) {
493
+ height: auto !important;
494
+ min-height: var(--vm-control-height);
495
+ }
496
+
497
+ /* 单行 input:白底;select / tree-select 等:淡蓝底 */
498
+ :deep(input.vm-crud-upsert__control),
499
+ :deep(.vm-crud-upsert__control[data-slot='input']),
500
+ :deep(.vm-input-wrap.vm-crud-upsert__control input) {
501
+ background: var(--card) !important;
502
+ }
503
+
504
+ :deep(.vm-crud-select.vm-crud-upsert__control),
505
+ :deep(.vm-group-cascader.vm-crud-upsert__control),
506
+ :deep(.vm-multi-select.vm-crud-upsert__control),
507
+ :deep(.vm-date-picker.vm-crud-upsert__control),
508
+ :deep(.vm-icon-picker.vm-crud-upsert__control) {
509
+ background: var(--muted) !important;
510
+ }
511
+
512
+ :deep(.vm-tree-select.vm-crud-upsert__control .vm-tree-select__trigger) {
513
+ background: var(--muted) !important;
514
+ }
515
+
516
+ @media (max-width: 768px) {
517
+ grid-template-columns: repeat(12, minmax(0, 1fr));
518
+ flex: 1 1 auto;
519
+ min-height: 0;
520
+ gap: 10px;
521
+ padding: 10px 12px;
522
+ }
523
+ }
524
+
525
+ .vm-crud-upsert__empty {
526
+ grid-column: 1 / -1;
527
+ display: flex;
528
+ min-height: 160px;
529
+ align-items: center;
530
+ justify-content: center;
531
+ }
532
+
533
+ .vm-crud-upsert__field {
534
+ &.is-error :deep(.vm-crud-upsert__control) {
535
+ border-color: var(--destructive) !important;
536
+ }
537
+ }
538
+
539
+ .vm-crud-upsert__error {
540
+ margin: 0;
541
+ font-size: 12px;
542
+ line-height: 1.4;
543
+ color: var(--destructive);
544
+ }
545
+
546
+ :deep(.vm-group-cascader.vm-crud-upsert__control) {
547
+ height: auto !important;
548
+ min-height: var(--vm-control-height);
549
+ }
550
+
551
+ /* 上传:勿套用单行高度;多图时铺满 span 以便平铺换行 */
552
+ :deep(.vm-upload.vm-crud-upsert__upload-field) {
553
+ width: 100%;
554
+ max-width: 100%;
555
+ height: auto !important;
556
+ min-height: 0;
557
+ background: transparent !important;
558
+ }
559
+
560
+ /* Markdown:保留组件自身 min-height,勿被 control 行高压扁 */
561
+ :deep(.vm-markdown.vm-crud-upsert__markdown-field) {
562
+ width: 100%;
563
+ height: auto !important;
564
+ min-height: 0;
565
+ background: transparent !important;
566
+ }
567
+
568
+ /* JSON:与备注 textarea 同视觉,勿套单行 control */
569
+ :deep(.vm-json-editor.vm-crud-upsert__json-field) {
570
+ width: 100%;
571
+ height: auto !important;
572
+ min-height: 0;
573
+ background: transparent !important;
574
+ }
575
+
576
+ /* 富文本:保留组件默认 minHeight,勿被 control 行高压扁 */
577
+ :deep(.vm-richtext.vm-crud-upsert__richtext-field) {
578
+ width: 100%;
579
+ height: auto !important;
580
+ min-height: 0;
581
+ background: transparent !important;
582
+ }
583
+
584
+ :deep(.vm-crud-upsert__switch-field.vm-switch) {
585
+ width: 40px;
586
+ height: 22px;
587
+ margin-top: 8px;
588
+ }
589
+
590
+ .vm-crud-upsert__textarea-wrap {
591
+ position: relative;
592
+ width: 100%;
593
+ min-width: 0;
594
+ }
595
+
596
+ .vm-crud-upsert__textarea-clear {
597
+ position: absolute;
598
+ top: 10px;
599
+ right: var(--vm-control-padding-x);
600
+ z-index: 1;
601
+ color: var(--muted-foreground);
602
+ font-size: 14px;
603
+ line-height: 1;
604
+ cursor: pointer;
605
+
606
+ &:hover {
607
+ color: var(--foreground);
608
+ }
609
+ }
610
+
611
+ .vm-crud-upsert__textarea,
612
+ :deep(.vm-crud-upsert__textarea) {
613
+ width: 100%;
614
+ min-height: 72px;
615
+ padding: 8px 28px 8px var(--vm-control-padding-x);
616
+ border: 1px solid var(--input);
617
+ border-radius: var(--vm-control-radius);
618
+ background: var(--muted);
619
+ color: var(--foreground);
620
+ font-size: 14px;
621
+ line-height: 1.5;
622
+ resize: vertical;
623
+ outline: none;
624
+ transition: border-color 0.15s ease;
625
+
626
+ &:focus,
627
+ &:focus-visible {
628
+ border-color: var(--focus-border);
629
+ box-shadow: none;
630
+ }
631
+ }
632
+ </style>