vome-core 0.3.65 → 0.3.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/dist/admin/api/client.js +7 -2
  2. package/dist/admin/components/ui/avatar/Avatar.vue +18 -18
  3. package/dist/admin/components/ui/avatar/AvatarFallback.vue +21 -21
  4. package/dist/admin/components/ui/avatar/AvatarImage.vue +16 -16
  5. package/dist/admin/components/ui/avatar/index.js +1 -1
  6. package/dist/admin/components/ui/badge/Badge.vue +26 -26
  7. package/dist/admin/components/ui/button/Button.vue +31 -31
  8. package/dist/admin/components/ui/checkbox/Checkbox.vue +35 -35
  9. package/dist/admin/components/ui/checkbox/index.js +1 -1
  10. package/dist/admin/components/ui/dialog/Dialog.vue +19 -19
  11. package/dist/admin/components/ui/dialog/DialogClose.vue +15 -15
  12. package/dist/admin/components/ui/dialog/DialogContent.vue +267 -267
  13. package/dist/admin/components/ui/dialog/DialogDescription.vue +23 -23
  14. package/dist/admin/components/ui/dialog/DialogFooter.vue +27 -27
  15. package/dist/admin/components/ui/dialog/DialogHeader.vue +17 -17
  16. package/dist/admin/components/ui/dialog/DialogOverlay.vue +21 -21
  17. package/dist/admin/components/ui/dialog/DialogScrollContent.vue +60 -60
  18. package/dist/admin/components/ui/dialog/DialogTitle.vue +23 -23
  19. package/dist/admin/components/ui/dialog/DialogTrigger.vue +15 -15
  20. package/dist/admin/components/ui/dialog/index.js +1 -1
  21. package/dist/admin/components/ui/dropdown-menu/DropdownMenu.vue +19 -19
  22. package/dist/admin/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -39
  23. package/dist/admin/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -39
  24. package/dist/admin/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -15
  25. package/dist/admin/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -31
  26. package/dist/admin/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -23
  27. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -21
  28. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -40
  29. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -23
  30. package/dist/admin/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -17
  31. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -18
  32. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -27
  33. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -31
  34. package/dist/admin/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -17
  35. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  36. package/dist/admin/components/ui/input/Input.vue +183 -183
  37. package/dist/admin/components/ui/input/index.js +1 -1
  38. package/dist/admin/components/ui/label/Label.vue +26 -26
  39. package/dist/admin/components/ui/label/index.js +1 -1
  40. package/dist/admin/components/ui/scroll-area/ScrollArea.vue +33 -33
  41. package/dist/admin/components/ui/scroll-area/ScrollBar.vue +32 -32
  42. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  43. package/dist/admin/components/ui/select/Select.vue +19 -19
  44. package/dist/admin/components/ui/select/SelectContent.vue +51 -51
  45. package/dist/admin/components/ui/select/SelectGroup.vue +15 -15
  46. package/dist/admin/components/ui/select/SelectItem.vue +53 -53
  47. package/dist/admin/components/ui/select/SelectItemText.vue +15 -15
  48. package/dist/admin/components/ui/select/SelectLabel.vue +17 -17
  49. package/dist/admin/components/ui/select/SelectScrollDownButton.vue +26 -26
  50. package/dist/admin/components/ui/select/SelectScrollUpButton.vue +26 -26
  51. package/dist/admin/components/ui/select/SelectSeparator.vue +19 -19
  52. package/dist/admin/components/ui/select/SelectTrigger.vue +44 -44
  53. package/dist/admin/components/ui/select/SelectValue.vue +15 -15
  54. package/dist/admin/components/ui/select/index.js +1 -1
  55. package/dist/admin/components/ui/separator/Separator.vue +29 -29
  56. package/dist/admin/components/ui/separator/index.js +1 -1
  57. package/dist/admin/components/ui/sheet/Sheet.vue +19 -19
  58. package/dist/admin/components/ui/sheet/SheetClose.vue +15 -15
  59. package/dist/admin/components/ui/sheet/SheetContent.vue +63 -63
  60. package/dist/admin/components/ui/sheet/SheetDescription.vue +21 -21
  61. package/dist/admin/components/ui/sheet/SheetFooter.vue +16 -16
  62. package/dist/admin/components/ui/sheet/SheetHeader.vue +15 -15
  63. package/dist/admin/components/ui/sheet/SheetOverlay.vue +21 -21
  64. package/dist/admin/components/ui/sheet/SheetTitle.vue +21 -21
  65. package/dist/admin/components/ui/sheet/SheetTrigger.vue +15 -15
  66. package/dist/admin/components/ui/sheet/index.js +1 -1
  67. package/dist/admin/components/ui/sonner/Toaster.vue +117 -117
  68. package/dist/admin/components/ui/sonner/index.js +1 -1
  69. package/dist/admin/components/ui/table/Table.vue +16 -16
  70. package/dist/admin/components/ui/table/TableBody.vue +17 -17
  71. package/dist/admin/components/ui/table/TableCaption.vue +17 -17
  72. package/dist/admin/components/ui/table/TableCell.vue +22 -22
  73. package/dist/admin/components/ui/table/TableEmpty.vue +34 -34
  74. package/dist/admin/components/ui/table/TableFooter.vue +17 -17
  75. package/dist/admin/components/ui/table/TableHead.vue +17 -17
  76. package/dist/admin/components/ui/table/TableHeader.vue +17 -17
  77. package/dist/admin/components/ui/table/TableRow.vue +17 -17
  78. package/dist/admin/components/ui/table/index.js +1 -1
  79. package/dist/admin/components/ui/tabs/Tabs.vue +24 -24
  80. package/dist/admin/components/ui/tabs/TabsContent.vue +21 -21
  81. package/dist/admin/components/ui/tabs/TabsList.vue +24 -24
  82. package/dist/admin/components/ui/tabs/TabsTrigger.vue +26 -26
  83. package/dist/admin/components/ui/tabs/index.js +1 -1
  84. package/dist/admin/components/ui/tooltip/Tooltip.vue +19 -19
  85. package/dist/admin/components/ui/tooltip/TooltipContent.vue +34 -34
  86. package/dist/admin/components/ui/tooltip/TooltipProvider.vue +14 -14
  87. package/dist/admin/components/ui/tooltip/TooltipTrigger.vue +15 -15
  88. package/dist/admin/components/ui/tooltip/index.js +1 -1
  89. package/dist/admin/components/vm-action-btn.vue +136 -136
  90. package/dist/admin/components/vm-aside-list-item.vue +136 -136
  91. package/dist/admin/components/vm-aside.vue +229 -229
  92. package/dist/admin/components/vm-auto-perm-dialog.vue +371 -371
  93. package/dist/admin/components/vm-card-grid.vue +37 -37
  94. package/dist/admin/components/vm-card-head.vue +45 -45
  95. package/dist/admin/components/vm-card-meta.vue +22 -22
  96. package/dist/admin/components/vm-card-title.vue +56 -56
  97. package/dist/admin/components/vm-card.vue +176 -176
  98. package/dist/admin/components/vm-check-tree.vue +340 -340
  99. package/dist/admin/components/vm-clamp-text.vue +39 -39
  100. package/dist/admin/components/vm-dept-tree.vue +497 -497
  101. package/dist/admin/components/vm-empty.vue +68 -68
  102. package/dist/admin/components/vm-eps-perm-picker.vue +123 -123
  103. package/dist/admin/components/vm-form-dialog.vue +632 -632
  104. package/dist/admin/components/vm-group-cascader.vue +560 -560
  105. package/dist/admin/components/vm-icon-btn.vue +88 -88
  106. package/dist/admin/components/vm-icon-picker.vue +647 -647
  107. package/dist/admin/components/vm-inline-search.vue +61 -61
  108. package/dist/admin/components/vm-loading.vue +114 -114
  109. package/dist/admin/components/vm-markdown.vue +405 -405
  110. package/dist/admin/components/vm-marquee.vue +265 -265
  111. package/dist/admin/components/vm-page-tabs.vue +94 -94
  112. package/dist/admin/components/vm-perm-section.vue +57 -57
  113. package/dist/admin/components/vm-ri-icon.vue +23 -23
  114. package/dist/admin/components/vm-role-picker.vue +314 -314
  115. package/dist/admin/components/vm-split-layout.vue +106 -106
  116. package/dist/admin/components/vm-thumb.vue +49 -49
  117. package/dist/admin/components/vm-view-path-picker.vue +59 -59
  118. package/dist/admin/crud/auto-fields.js +1 -1
  119. package/dist/admin/crud/components/vm-avatar.vue +104 -104
  120. package/dist/admin/crud/components/vm-color-picker.vue +1626 -1626
  121. package/dist/admin/crud/components/vm-column-custom.vue +193 -193
  122. package/dist/admin/crud/components/vm-context-menu.vue +105 -105
  123. package/dist/admin/crud/components/vm-copy-btn.vue +102 -102
  124. package/dist/admin/crud/components/vm-date-calendar-panel.vue +537 -537
  125. package/dist/admin/crud/components/vm-date-picker.vue +75 -75
  126. package/dist/admin/crud/components/vm-date-range.vue +88 -88
  127. package/dist/admin/crud/components/vm-date-text.vue +42 -42
  128. package/dist/admin/crud/components/vm-datetime-field.vue +215 -215
  129. package/dist/admin/crud/components/vm-datetime-picker.vue +9 -9
  130. package/dist/admin/crud/components/vm-dict-tag.vue +112 -112
  131. package/dist/admin/crud/components/vm-ellipsis-text.vue +34 -34
  132. package/dist/admin/crud/components/vm-form-hint.vue +81 -81
  133. package/dist/admin/crud/components/vm-json-code.vue +41 -41
  134. package/dist/admin/crud/components/vm-json-editor.vue +161 -161
  135. package/dist/admin/crud/components/vm-json-kv-editor.vue +318 -318
  136. package/dist/admin/crud/components/vm-kv-row.vue +51 -51
  137. package/dist/admin/crud/components/vm-multi-select.vue +279 -279
  138. package/dist/admin/crud/components/vm-number-range.vue +204 -204
  139. package/dist/admin/crud/components/vm-preview-viewer.vue +894 -894
  140. package/dist/admin/crud/components/vm-radio.vue +87 -87
  141. package/dist/admin/crud/components/vm-richtext.vue +313 -313
  142. package/dist/admin/crud/components/vm-select.vue +570 -570
  143. package/dist/admin/crud/components/vm-status-tag.vue +158 -158
  144. package/dist/admin/crud/components/vm-switch.vue +158 -158
  145. package/dist/admin/crud/components/vm-tag-list.vue +108 -108
  146. package/dist/admin/crud/components/vm-text-link.vue +52 -52
  147. package/dist/admin/crud/components/vm-toggle-switch.vue +84 -84
  148. package/dist/admin/crud/components/vm-tree-select.vue +450 -450
  149. package/dist/admin/crud/components/vm-upload-item.vue +298 -298
  150. package/dist/admin/crud/components/vm-upload.vue +436 -436
  151. package/dist/admin/crud/config.js +1 -1
  152. package/dist/admin/crud/confirm.js +1 -1
  153. package/dist/admin/crud/crud-page-request.js +1 -1
  154. package/dist/admin/crud/data-i18n.js +1 -1
  155. package/dist/admin/crud/dict.js +1 -1
  156. package/dist/admin/crud/export-fetch.js +1 -1
  157. package/dist/admin/crud/index.js +1 -0
  158. package/dist/admin/crud/key.js +1 -1
  159. package/dist/admin/crud/plugins.js +1 -1
  160. package/dist/admin/crud/span.js +1 -1
  161. package/dist/admin/crud/style.js +1 -1
  162. package/dist/admin/crud/useCrud.js +3 -0
  163. package/dist/admin/crud/validate.js +1 -1
  164. package/dist/admin/crud/vm-add-btn.vue +70 -70
  165. package/dist/admin/crud/vm-adv-search.vue +83 -83
  166. package/dist/admin/crud/vm-crud.vue +711 -711
  167. package/dist/admin/crud/vm-data-i18n-btn.vue +456 -456
  168. package/dist/admin/crud/vm-data-i18n-lang.vue +183 -183
  169. package/dist/admin/crud/vm-export-btn.vue +183 -183
  170. package/dist/admin/crud/vm-flex.vue +14 -14
  171. package/dist/admin/crud/vm-form.vue +33 -15
  172. package/dist/admin/crud/vm-import-btn.vue +747 -747
  173. package/dist/admin/crud/vm-multi-delete-btn.vue +49 -49
  174. package/dist/admin/crud/vm-pagination.vue +212 -212
  175. package/dist/admin/crud/vm-refresh-btn.vue +35 -35
  176. package/dist/admin/crud/vm-row.vue +55 -55
  177. package/dist/admin/crud/vm-search-key.vue +78 -78
  178. package/dist/admin/crud/vm-search.vue +466 -466
  179. package/dist/admin/crud/vm-table.vue +1390 -1390
  180. package/dist/admin/crud/vm-tabs.vue +94 -94
  181. package/dist/admin/crud/vm-toolbar.vue +357 -357
  182. package/dist/admin/crud/vm-upsert.vue +765 -765
  183. package/dist/admin/directives/perm.js +1 -1
  184. package/dist/admin/env.d.ts +1 -1
  185. package/dist/admin/hooks/useUpload.js +1 -1
  186. package/dist/admin/index.d.ts +1 -31
  187. package/dist/admin/lib/browser.js +1 -1
  188. package/dist/admin/lib/cn.js +1 -1
  189. package/dist/admin/lib/dialog-float.js +1 -1
  190. package/dist/admin/lib/export-excel.js +1 -1
  191. package/dist/admin/lib/import-excel.js +1 -1
  192. package/dist/admin/lib/json.js +1 -1
  193. package/dist/admin/lib/menu.js +1 -1
  194. package/dist/admin/lib/micro-locale.js +1 -1
  195. package/dist/admin/lib/micro-theme.js +1 -1
  196. package/dist/admin/lib/shell.js +1 -1
  197. package/dist/admin/lib/theme-color-options.js +1 -1
  198. package/dist/admin/lib/tree.js +1 -1
  199. package/dist/admin/lib/upload.js +1 -1
  200. package/dist/admin/lib/video-frame.js +1 -1
  201. package/dist/admin/router/create.js +1 -1
  202. package/dist/admin/router/guards.js +1 -1
  203. package/dist/admin/router/index.d.ts +1 -5
  204. package/dist/admin/service/index.d.ts +1 -11
  205. package/dist/admin/styles/base.css +121 -121
  206. package/dist/admin/vendor/browser-image-compression.js +1144 -1144
  207. package/dist/admin/vendor/decimal-js.js +12 -12
  208. package/dist/admin/vendor/xlsx.js +263 -263
  209. package/dist/ai/index.js +1 -1
  210. package/dist/client/admin-vendor.d.ts +1 -13
  211. package/dist/client/ai-model-registry.js +1 -1
  212. package/dist/client/base.js +1 -1
  213. package/dist/client/configure.js +1 -1
  214. package/dist/client/dict.js +1 -1
  215. package/dist/client/eps-payload.js +1 -1
  216. package/dist/client/eps.js +1 -1
  217. package/dist/client/index.d.ts +1 -7
  218. package/dist/client/index.js +1 -1
  219. package/dist/client/locale-change.js +1 -1
  220. package/dist/client/locale-store.js +1 -1
  221. package/dist/client/service.js +1 -1
  222. package/dist/client/vite-admin.d.ts +1 -40
  223. package/dist/client/vite-admin.js +1 -1
  224. package/dist/client/vite-auto-import.d.ts +1 -2
  225. package/dist/client/vite-auto-import.js +1 -1
  226. package/dist/client/vite-micro-proxy.d.ts +1 -20
  227. package/dist/client/vite-micro-proxy.js +1 -1
  228. package/dist/client/vite-plugin-eps.d.ts +1 -20
  229. package/dist/client/vite-plugin-eps.js +1 -1
  230. package/dist/client/vite-resolve.d.ts +1 -16
  231. package/dist/client/vite-resolve.js +1 -1
  232. package/dist/config/index.js +1 -1
  233. package/dist/index.js +1 -1
  234. package/dist/server/index.js +1 -1
  235. package/dist/shared/datetime-picker.js +1 -1
  236. package/dist/shared/decimal.js +1 -1
  237. package/dist/shared/excel.js +1 -1
  238. package/dist/shared/index.js +1 -1
  239. package/dist/shared/locale-json.js +1 -1
  240. package/dist/shared/tree.js +1 -1
  241. package/package.json +1 -1
  242. package/typings/admin/base/auth.ts +24 -24
  243. package/typings/admin/base/permission.ts +19 -19
  244. package/typings/admin/comm/cascader.ts +7 -7
  245. package/typings/admin/comm/check-tree.ts +7 -7
  246. package/typings/admin/comm/crud.ts +427 -427
  247. package/typings/admin/comm/eps.ts +86 -86
  248. package/typings/admin/comm/request.ts +16 -16
  249. package/typings/admin/comm/service.ts +43 -43
  250. package/typings/admin/comm/upload.ts +39 -39
  251. package/typings/admin/theme.ts +10 -10
  252. package/typings/admin/vome/browser.ts +11 -11
  253. package/typings/admin/vome/module.ts +13 -13
  254. package/typings/admin/vome/tags.ts +9 -9
  255. package/typings/comm/crud.ts +83 -83
  256. package/typings/comm/page.ts +18 -18
  257. package/typings/comm/scan.ts +6 -6
  258. package/typings/common.ts +4 -4
  259. package/typings/context/data.ts +10 -10
  260. package/typings/ext/market-seat.ts +42 -42
  261. package/typings/ext/market.ts +55 -55
  262. package/typings/ext/module.ts +64 -64
  263. package/typings/ext/plugin.ts +38 -38
  264. package/typings/ioc/metadata.ts +34 -34
  265. package/typings/logger/config.ts +6 -6
  266. package/typings/routing/crud.ts +199 -199
  267. package/typings/routing/eps.ts +88 -88
  268. package/typings/routing/meta.ts +63 -63
  269. package/typings/routing/register.ts +21 -21
  270. package/typings/routing/scan-context.ts +7 -7
  271. package/dist/admin/api/client.d.ts +0 -50
  272. package/dist/admin/components/ui/avatar/index.d.ts +0 -3
  273. package/dist/admin/components/ui/badge/index.d.ts +0 -6
  274. package/dist/admin/components/ui/button/index.d.ts +0 -7
  275. package/dist/admin/components/ui/checkbox/index.d.ts +0 -1
  276. package/dist/admin/components/ui/dialog/index.d.ts +0 -10
  277. package/dist/admin/components/ui/dropdown-menu/index.d.ts +0 -15
  278. package/dist/admin/components/ui/input/index.d.ts +0 -1
  279. package/dist/admin/components/ui/label/index.d.ts +0 -1
  280. package/dist/admin/components/ui/scroll-area/index.d.ts +0 -2
  281. package/dist/admin/components/ui/select/index.d.ts +0 -11
  282. package/dist/admin/components/ui/separator/index.d.ts +0 -1
  283. package/dist/admin/components/ui/sheet/index.d.ts +0 -8
  284. package/dist/admin/components/ui/sonner/index.d.ts +0 -1
  285. package/dist/admin/components/ui/table/index.d.ts +0 -9
  286. package/dist/admin/components/ui/tabs/index.d.ts +0 -4
  287. package/dist/admin/components/ui/tooltip/index.d.ts +0 -4
  288. package/dist/admin/crud/auto-fields.d.ts +0 -68
  289. package/dist/admin/crud/config.d.ts +0 -5
  290. package/dist/admin/crud/confirm.d.ts +0 -19
  291. package/dist/admin/crud/crud-page-request.d.ts +0 -21
  292. package/dist/admin/crud/data-i18n.d.ts +0 -62
  293. package/dist/admin/crud/dict.d.ts +0 -4
  294. package/dist/admin/crud/export-fetch.d.ts +0 -22
  295. package/dist/admin/crud/index.d.ts +0 -60
  296. package/dist/admin/crud/json-kv.d.ts +0 -9
  297. package/dist/admin/crud/key.d.ts +0 -22
  298. package/dist/admin/crud/paging.d.ts +0 -2
  299. package/dist/admin/crud/plugins.d.ts +0 -63
  300. package/dist/admin/crud/registry.d.ts +0 -3
  301. package/dist/admin/crud/span.d.ts +0 -17
  302. package/dist/admin/crud/style.d.ts +0 -82
  303. package/dist/admin/crud/useCrud.d.ts +0 -21
  304. package/dist/admin/crud/validate.d.ts +0 -7
  305. package/dist/admin/directives/perm.d.ts +0 -3
  306. package/dist/admin/hooks/useBrowser.d.ts +0 -6
  307. package/dist/admin/hooks/useUpload.d.ts +0 -8
  308. package/dist/admin/hooks/useVome.d.ts +0 -10
  309. package/dist/admin/lib/browser.d.ts +0 -3
  310. package/dist/admin/lib/cn.d.ts +0 -2
  311. package/dist/admin/lib/compress-image.d.ts +0 -12
  312. package/dist/admin/lib/dialog-float.d.ts +0 -8
  313. package/dist/admin/lib/eps.d.ts +0 -2
  314. package/dist/admin/lib/export-excel.d.ts +0 -9
  315. package/dist/admin/lib/file-preview.d.ts +0 -9
  316. package/dist/admin/lib/import-excel.d.ts +0 -21
  317. package/dist/admin/lib/json.d.ts +0 -11
  318. package/dist/admin/lib/menu.d.ts +0 -3
  319. package/dist/admin/lib/micro-locale.d.ts +0 -5
  320. package/dist/admin/lib/micro-theme.d.ts +0 -14
  321. package/dist/admin/lib/module.d.ts +0 -21
  322. package/dist/admin/lib/scarce-assets.d.ts +0 -5
  323. package/dist/admin/lib/shell.d.ts +0 -8
  324. package/dist/admin/lib/theme-color-options.d.ts +0 -15
  325. package/dist/admin/lib/tree.d.ts +0 -2
  326. package/dist/admin/lib/upload.d.ts +0 -17
  327. package/dist/admin/lib/video-frame.d.ts +0 -6
  328. package/dist/admin/router/create.d.ts +0 -16
  329. package/dist/admin/router/guards.d.ts +0 -6
  330. package/dist/admin/router/menu-routes.d.ts +0 -13
  331. package/dist/admin/stores/app.d.ts +0 -13
  332. package/dist/admin/stores/tags.d.ts +0 -14
  333. package/dist/admin/stores/user.d.ts +0 -48
  334. package/dist/client/ai-model-registry.d.ts +0 -16
  335. package/dist/client/base.d.ts +0 -28
  336. package/dist/client/configure.d.ts +0 -33
  337. package/dist/client/dict.d.ts +0 -31
  338. package/dist/client/eps-payload.d.ts +0 -3
  339. package/dist/client/eps.d.ts +0 -17
  340. package/dist/client/locale-change.d.ts +0 -16
  341. package/dist/client/locale-store.d.ts +0 -105
  342. package/dist/client/service.d.ts +0 -29
@@ -1,711 +1,711 @@
1
- <template>
2
- <div class="vm-crud">
3
- <slot />
4
- </div>
5
- </template>
6
-
7
- <script setup lang="ts">
8
- import { ref, computed, watch, shallowRef, provide, onMounted, onUnmounted } from 'vue'
9
- import { toast } from 'vue-sonner'
10
- import { deepTree } from '../lib/tree'
11
- import { mergeDict, mergePermission } from './config'
12
- import { invokeCrudPageRequest } from './crud-page-request'
13
- import { vmConfirm } from './confirm'
14
- import { CRUD_KEY, injectCrudOptions, injectTableOptions, injectUpsertOptions } from './key'
15
- import { crudLabel } from './style'
16
- import {
17
- canUseDataI18n,
18
- dataPackFromService,
19
- type DataI18nFieldMeta,
20
- } from './data-i18n'
21
- import { useVome } from '../hooks/useVome'
22
- import { subscribeLocaleChange } from 'vome-core/client'
23
- import type {
24
- CrudContext,
25
- CrudPermission,
26
- CrudService,
27
- CrudTrashMode,
28
- } from '../../../typings/admin/comm/crud'
29
- defineOptions({ name: 'vm-crud' })
30
-
31
- const props = withDefaults(
32
- defineProps<{
33
- /** EPS service 对象,如 useCrud({ service: service.base.user }) */
34
- service?: CrudService
35
- params?: Record<string, unknown>
36
- immediate?: boolean
37
- }>(),
38
- {
39
- params: () => ({}),
40
- immediate: false,
41
- },
42
- )
43
-
44
- const hookOptions = injectCrudOptions()
45
- const tableOpts = injectTableOptions()
46
- const upsertOpts = injectUpsertOptions()
47
- const searchActions = shallowRef<{ search: () => void; reset: () => void } | null>(null)
48
- const hasSearchFilters = ref(false)
49
-
50
- function pickService(): CrudService {
51
- const s = props.service ?? hookOptions?.service
52
- return (s || {}) as CrudService
53
- }
54
-
55
- function permFromService(s: CrudService): Partial<CrudPermission> | undefined {
56
- if (s._permission && typeof s._permission === 'object') {
57
- return s._permission as Partial<CrudPermission>
58
- }
59
- return undefined
60
- }
61
-
62
- const service = computed<CrudService>(() => pickService())
63
-
64
- const dict = mergeDict(hookOptions?.dict)
65
- const permission = ref(
66
- mergePermission(hookOptions?.permission, permFromService(service.value)),
67
- )
68
-
69
- const loading = ref(false)
70
- const list = ref<Record<string, unknown>[]>([])
71
- const selection = ref<Record<string, unknown>[]>([])
72
- const pagination = ref({ page: 1, size: 20, total: 0 })
73
- const params = ref<Record<string, unknown>>({ ...props.params })
74
- const trashMode = ref<CrudTrashMode>('default')
75
- const dataI18nLangCode = ref('en-US')
76
- const dataI18nFields = ref<DataI18nFieldMeta[]>([])
77
-
78
- const { service: hostService } = useVome()
79
-
80
- const upsertVisible = ref(false)
81
- const upsertMode = ref<'add' | 'update' | 'info'>('add')
82
- const upsertForm = ref<Record<string, unknown>>({})
83
-
84
- function getPermission(key?: keyof typeof permission.value) {
85
- if (!key) return { ...permission.value }
86
- return Boolean(permission.value[key])
87
- }
88
-
89
- function getParams() {
90
- return { ...params.value }
91
- }
92
-
93
- function applyListMeta(body: Record<string, unknown>) {
94
- const ds = tableOpts?.defaultSort
95
- if (ds?.prop && body[dict.sort.prop] == null) {
96
- body[dict.sort.prop] = ds.prop
97
- const o = ds.order
98
- body[dict.sort.order] =
99
- o === 'asc' || o === 'ascending' ? 'asc' : 'desc'
100
- }
101
- if (trashMode.value === 'onlyTrashed') body.onlyTrashed = true
102
- if (trashMode.value === 'withTrashed') body.withTrashed = true
103
- return body
104
- }
105
-
106
- /** 与 refresh 同源:筛选 params + 排序 + 列表/回收站视图;extra 可覆盖 page/size */
107
- function buildListQuery(extra: Record<string, unknown> = {}) {
108
- const pageKey = dict.pagination.page
109
- const sizeKey = dict.pagination.size
110
- const body: Record<string, unknown> = {
111
- ...params.value,
112
- ...extra,
113
- }
114
- if (extra[pageKey] == null) body[pageKey] = pagination.value.page
115
- if (extra[sizeKey] == null) body[sizeKey] = pagination.value.size
116
- return applyListMeta(body)
117
- }
118
-
119
- /** 导出专用:筛选 params + 排序 + 列表/回收站视图;不含 page、size */
120
- function buildExportQuery(extra: Record<string, unknown> = {}) {
121
- const pageKey = dict.pagination.page
122
- const sizeKey = dict.pagination.size
123
- const body: Record<string, unknown> = {
124
- ...params.value,
125
- ...extra,
126
- }
127
- delete body[pageKey]
128
- delete body[sizeKey]
129
- return applyListMeta(body)
130
- }
131
-
132
- async function rawFetchPage(
133
- req: Record<string, unknown>,
134
- opts?: { timeoutMs?: number },
135
- ) {
136
- const svc = service.value
137
- if (svc.page) {
138
- return await svc.page(req, opts)
139
- }
140
- const rows = (await svc.list!(req)) || []
141
- const pageKey = dict.pagination.page
142
- const sizeKey = dict.pagination.size
143
- return {
144
- list: rows,
145
- pagination: {
146
- page: Number(req[pageKey] ?? 1),
147
- size: Number(req[sizeKey] ?? (rows.length || 20)),
148
- total: rows.length,
149
- },
150
- }
151
- }
152
-
153
- /** 与 refresh 同源:含 onRefresh 注入的筛选条件 */
154
- async function requestPage(
155
- req: Record<string, unknown>,
156
- opts?: { timeoutMs?: number },
157
- ) {
158
- return invokeCrudPageRequest({
159
- body: req,
160
- fetch: rawFetchPage,
161
- onRefresh: hookOptions?.onRefresh,
162
- requestOpts: opts,
163
- })
164
- }
165
-
166
- async function refresh(extra: Record<string, unknown> = {}) {
167
- if (trashMode.value === 'dataI18n') {
168
- await refreshDataI18nEntries(extra)
169
- return
170
- }
171
-
172
- const svc = service.value
173
- if (!svc.page && !svc.list) {
174
- toast.error(crudLabel('noPageList'))
175
- return
176
- }
177
-
178
- const body = buildListQuery(extra)
179
-
180
- loading.value = true
181
-
182
- const done = () => {
183
- loading.value = false
184
- }
185
-
186
- const render = (
187
- data:
188
- | Record<string, unknown>[]
189
- | { list: Record<string, unknown>[]; pagination?: unknown },
190
- pag?: { page?: number; size?: number; total?: number },
191
- ) => {
192
- let rows: Record<string, unknown>[]
193
- if (Array.isArray(data)) {
194
- rows = data
195
- if (pag) {
196
- pagination.value = {
197
- page: pag.page ?? pagination.value.page,
198
- size: pag.size ?? pagination.value.size,
199
- total: pag.total ?? data.length,
200
- }
201
- } else {
202
- pagination.value = { ...pagination.value, total: data.length }
203
- }
204
- } else {
205
- rows = data.list || []
206
- const p = (pag || data.pagination || {}) as {
207
- page?: number
208
- size?: number
209
- total?: number
210
- }
211
- pagination.value = {
212
- page: p.page ?? pagination.value.page,
213
- size: p.size ?? pagination.value.size,
214
- total: p.total ?? 0,
215
- }
216
- }
217
-
218
- // toTree:扁平 parentId → 树
219
- if (tableOpts?.tree) {
220
- const nested = rows.some((r) => Array.isArray(r.children) && r.children.length)
221
- list.value = nested ? rows : (deepTree(rows as never) as Record<string, unknown>[])
222
- } else {
223
- list.value = rows
224
- }
225
-
226
- selection.value = []
227
- done()
228
- }
229
-
230
- const next = async (req: Record<string, unknown>) => {
231
- try {
232
- const data = await rawFetchPage(req)
233
- render(data)
234
- return data
235
- } catch (e) {
236
- console.error(e)
237
- done()
238
- throw e
239
- }
240
- }
241
-
242
- try {
243
- if (hookOptions?.onRefresh) {
244
- await hookOptions.onRefresh(body, { next, done, render })
245
- } else {
246
- await next(body)
247
- }
248
- } catch {
249
- done()
250
- }
251
- }
252
-
253
- async function refreshDataI18nEntries(extra: Record<string, unknown> = {}) {
254
- const meta = canUseDataI18n(service.value, hostService)
255
- if (!meta.ok) {
256
- toast.error(crudLabel('noDataI18n'))
257
- trashMode.value = 'default'
258
- list.value = []
259
- dataI18nFields.value = []
260
- return
261
- }
262
- const dataPack = dataPackFromService(hostService)
263
- if (!dataPack?.entries) {
264
- toast.error(
265
- crudLabel('noDataPackEntries'),
266
- )
267
- list.value = []
268
- dataI18nFields.value = []
269
- pagination.value = { ...pagination.value, total: 0 }
270
- return
271
- }
272
- const lang = String(dataI18nLangCode.value || '').trim()
273
- if (!lang) {
274
- toast.error(crudLabel('pickLang'))
275
- list.value = []
276
- pagination.value = { ...pagination.value, total: 0 }
277
- return
278
- }
279
-
280
- loading.value = true
281
- try {
282
- const res = (await dataPack.entries({
283
- tableName: meta.tableName,
284
- langCode: lang,
285
- })) as {
286
- fields?: DataI18nFieldMeta[]
287
- list?: Array<{
288
- id: string
289
- values: Record<string, string>
290
- sources: Record<string, string>
291
- }>
292
- }
293
- const fields = Array.isArray(res?.fields) ? res.fields : []
294
- dataI18nFields.value = fields.length
295
- ? fields
296
- : (meta.entity?.dataI18nFields || []).map((name) => ({
297
- name,
298
- label: name,
299
- }))
300
- const all = (res?.list || []).map((r) => {
301
- const sources = r.sources || {}
302
- const srcFlat: Record<string, string> = {}
303
- for (const [k, v] of Object.entries(sources)) {
304
- srcFlat[`__src_${k}`] = String(v ?? '')
305
- }
306
- return {
307
- id: String(r.id),
308
- __rowKey: String(r.id),
309
- __sources: sources,
310
- ...srcFlat,
311
- ...(r.values || {}),
312
- }
313
- })
314
- const pageKey = dict.pagination.page
315
- const sizeKey = dict.pagination.size
316
- const page = Number(extra[pageKey] ?? pagination.value.page) || 1
317
- const size = Number(extra[sizeKey] ?? pagination.value.size) || 20
318
- const start = (page - 1) * size
319
- list.value = all.slice(start, start + size)
320
- pagination.value = {
321
- page,
322
- size,
323
- total: all.length,
324
- }
325
- selection.value = []
326
- } catch (e) {
327
- console.error(e)
328
- toast.error(e instanceof Error ? e.message : crudLabel('loadI18nFail'))
329
- list.value = []
330
- dataI18nFields.value = []
331
- pagination.value = { ...pagination.value, total: 0 }
332
- } finally {
333
- loading.value = false
334
- }
335
- }
336
-
337
- function setParams(p: Record<string, unknown>, doRefresh = true) {
338
- params.value = { ...params.value, ...p }
339
- if (doRefresh) {
340
- pagination.value = { ...pagination.value, page: 1 }
341
- void refresh()
342
- }
343
- }
344
-
345
- function resetParams(doRefresh = true) {
346
- params.value = { ...props.params }
347
- pagination.value = { ...pagination.value, page: 1 }
348
- if (doRefresh) void refresh()
349
- }
350
-
351
- function setSelection(rows: Record<string, unknown>[]) {
352
- selection.value = rows
353
- }
354
-
355
- /** 新增时收集 useUpsert items 的 value 作为底稿 */
356
- function collectUpsertItemDefaults() {
357
- const out: Record<string, unknown> = {}
358
- for (const it of upsertOpts?.items || []) {
359
- if (it.value === undefined) continue
360
- out[it.prop] = Array.isArray(it.value) ? [...it.value] : it.value
361
- }
362
- return out
363
- }
364
-
365
- function rowAdd(defaults: Record<string, unknown> = {}) {
366
- if (!permission.value.add) return
367
- upsertMode.value = 'add'
368
- const merged = { ...collectUpsertItemDefaults(), ...defaults }
369
- upsertForm.value = merged
370
- upsertVisible.value = true
371
- }
372
-
373
- function rowAppend(data: Record<string, unknown> = {}) {
374
- rowAdd(data)
375
- }
376
-
377
- async function loadRowForm(row: Record<string, unknown>, mode: 'update' | 'info') {
378
- upsertMode.value = mode
379
- const svc = service.value
380
-
381
- const next = async (data?: Record<string, unknown>) => {
382
- if (svc.info && row[dict.primaryId] != null) {
383
- return await svc.info({
384
- id: row[dict.primaryId] as string | number,
385
- ...data,
386
- })
387
- }
388
- return { ...row, ...data }
389
- }
390
-
391
- const done = (data: Record<string, unknown>) => {
392
- upsertForm.value = data
393
- upsertVisible.value = true
394
- }
395
-
396
- const close = () => upsertClose()
397
-
398
- // 自定义拉详情
399
- if (upsertOpts?.onInfo) {
400
- await upsertOpts.onInfo({ ...row }, { next, done, close })
401
- return
402
- }
403
-
404
- // 默认用 page 行打开,不打 /info;仅 sync: false 才拉详情
405
- if (upsertOpts?.sync !== false) {
406
- done({ ...row })
407
- return
408
- }
409
-
410
- try {
411
- done(await next())
412
- } catch {
413
- done({ ...row })
414
- }
415
- }
416
-
417
- async function rowEdit(row: Record<string, unknown>) {
418
- if (!permission.value.update) return
419
- await loadRowForm(row, 'update')
420
- }
421
-
422
- async function rowInfo(row: Record<string, unknown>) {
423
- if (!permission.value.info) return
424
- await loadRowForm(row, 'info')
425
- }
426
-
427
- function rowClose() {
428
- upsertClose()
429
- }
430
-
431
- /** 打开 upsert(不校验 add 权限;供转移部门等自定义场景) */
432
- function openUpsert(
433
- form: Record<string, unknown> = {},
434
- mode: 'add' | 'update' | 'info' = 'add',
435
- ) {
436
- upsertMode.value = mode
437
- upsertForm.value =
438
- mode === 'add' ? { ...collectUpsertItemDefaults(), ...form } : { ...form }
439
- upsertVisible.value = true
440
- }
441
-
442
- async function doDelete(targets: Record<string, unknown>[], force = false) {
443
- const svc = service.value
444
- if (!svc.delete) {
445
- toast.error(crudLabel('noDeleteApi'))
446
- return
447
- }
448
- const ids = targets
449
- .map((r) => r[dict.primaryId] as string | number)
450
- .filter((id) => id != null)
451
- await svc.delete({ ids, force })
452
- toast.success(
453
- force ? crudLabel('forceDeleted', dict.label) : crudLabel('deleteSuccess', dict.label),
454
- )
455
- await refresh()
456
- }
457
-
458
- async function rowDelete(rows?: Record<string, unknown>[], force = false) {
459
- // 与后端一致:无 restore → 默认硬删;回收站内 / 显式 force 亦硬删
460
- const hard =
461
- force || trashMode.value === 'onlyTrashed' || !service.value.restore
462
- // 删除一律要 delete 权限(含无 restore 的硬删、回收站彻底删)
463
- if (!permission.value.delete) return
464
- const targets = rows?.length ? rows : selection.value
465
- if (!targets.length) {
466
- toast.warning(crudLabel('pleaseSelectData'))
467
- return
468
- }
469
-
470
- const next = async (payload?: Record<string, unknown>) => {
471
- if (payload?.ids) {
472
- const svc = service.value
473
- if (!svc.delete) throw new Error(crudLabel('noDeleteApi'))
474
- await svc.delete({
475
- ...(payload as { ids: Array<string | number> }),
476
- force: hard,
477
- })
478
- toast.success(
479
- hard ? crudLabel('forceDeleted', dict.label) : crudLabel('deleteSuccess', dict.label),
480
- )
481
- await refresh()
482
- return
483
- }
484
- await doDelete(targets, hard)
485
- }
486
-
487
- if (hookOptions?.onDelete && !hard) {
488
- await hookOptions.onDelete(targets, { next })
489
- return
490
- }
491
-
492
- const tip = hard
493
- ? crudLabel('forceDeleteConfirm', dict.label)
494
- : crudLabel('deleteConfirm', dict.label)
495
- const ok = await vmConfirm({
496
- title: hard ? crudLabel('forceDelete', dict.label) : crudLabel('tips', dict.label),
497
- message: tip,
498
- confirmText: crudLabel('confirm', dict.label),
499
- cancelText: crudLabel('close', dict.label),
500
- })
501
- if (!ok) return
502
- await next()
503
- }
504
-
505
- async function rowRestore(rows?: Record<string, unknown>[]) {
506
- if (!permission.value.restore) return
507
- const targets = rows?.length ? rows : selection.value
508
- if (!targets.length) {
509
- toast.warning(crudLabel('pleaseSelectData'))
510
- return
511
- }
512
- const svc = service.value
513
- if (!svc.restore) {
514
- toast.error(crudLabel('noRestoreApi'))
515
- return
516
- }
517
- if (!(await vmConfirm({ title: crudLabel('restoreConfirmTitle'), message: crudLabel('restoreConfirmMsg'), confirmText: crudLabel('confirm'), cancelText: crudLabel('close') }))) return
518
- const ids = targets
519
- .map((r) => r[dict.primaryId] as string | number)
520
- .filter((id) => id != null)
521
- await svc.restore({ ids })
522
- toast.success(crudLabel('restored'))
523
- await refresh()
524
- }
525
-
526
- async function upsertSubmit() {
527
- if (upsertMode.value === 'info') {
528
- upsertClose()
529
- return
530
- }
531
- // 系统时间字段由 BaseService.request 统一剥离,后端 Repository 再兜底
532
- const data = { ...upsertForm.value }
533
- const svc = service.value
534
- try {
535
- if (upsertMode.value === 'add') {
536
- if (!svc.add) throw new Error(crudLabel('noAddApi'))
537
- await svc.add(data)
538
- toast.success(crudLabel('saveSuccess', dict.label))
539
- } else {
540
- if (!svc.update) throw new Error(crudLabel('noUpdateConfig'))
541
- await svc.update(data)
542
- toast.success(crudLabel('saveSuccess', dict.label))
543
- }
544
- const after = async () => {
545
- upsertVisible.value = false
546
- await refresh()
547
- }
548
- if (upsertOpts?.onClose) {
549
- upsertOpts.onClose('submit', () => {
550
- void after()
551
- })
552
- } else {
553
- await after()
554
- }
555
- } catch (e) {
556
- console.error(e)
557
- const err = e as Error & { toasted?: boolean }
558
- const msg =
559
- e instanceof Error && e.message ? e.message : crudLabel('saveFailed')
560
- // request 已 toast 过则不再重复;本地校验/配置错误仍要提示
561
- if (!err.toasted) toast.error(msg.slice(0, 200))
562
- }
563
- }
564
-
565
- function upsertClose() {
566
- const finish = () => {
567
- upsertVisible.value = false
568
- }
569
- if (upsertOpts?.onClose) {
570
- upsertOpts.onClose('close', finish)
571
- return
572
- }
573
- finish()
574
- }
575
-
576
- /** 供页面可靠改表单(避免 template ref 解包 upsertForm 后误写) */
577
- function patchUpsertForm(patch: Record<string, unknown>) {
578
- upsertForm.value = { ...upsertForm.value, ...patch }
579
- }
580
-
581
- function getUpsertForm() {
582
- return upsertForm.value
583
- }
584
-
585
- const ctx: CrudContext = {
586
- service: service.value,
587
- loading,
588
- list,
589
- selection,
590
- pagination,
591
- params,
592
- trashMode,
593
- dataI18nLangCode,
594
- dataI18nFields,
595
- permission,
596
- dict,
597
- refresh,
598
- setParams,
599
- resetParams,
600
- setSelection,
601
- getPermission: getPermission as CrudContext['getPermission'],
602
- getParams,
603
- buildListQuery,
604
- buildExportQuery,
605
- requestPage,
606
- rowAdd,
607
- rowAppend,
608
- rowEdit,
609
- rowInfo,
610
- rowClose,
611
- openUpsert,
612
- rowDelete,
613
- rowRestore,
614
- upsertVisible,
615
- upsertMode,
616
- upsertForm,
617
- upsertSubmit,
618
- upsertClose,
619
- patchUpsertForm,
620
- getUpsertForm,
621
- searchActions,
622
- hasSearchFilters,
623
- }
624
-
625
- provide(CRUD_KEY, ctx)
626
-
627
- watch(
628
- () => props.params,
629
- (v) => {
630
- params.value = { ...v }
631
- },
632
- )
633
-
634
- watch(
635
- service,
636
- (s) => {
637
- ctx.service = s
638
- permission.value = mergePermission(
639
- hookOptions?.permission,
640
- permFromService(s),
641
- )
642
- },
643
- { immediate: true },
644
- )
645
-
646
- // 等子组件(如 vm-search)setup 写完初始 params 后再拉,避免空参多打一次
647
- // 切语种始终 refresh(带新 X-Lang / dataI18n);不依赖 immediate(多数页首拉走 useCrud 回调)
648
- function onLocaleChange() {
649
- void refresh()
650
- }
651
-
652
- let offLocale: (() => void) | undefined
653
-
654
- onMounted(() => {
655
- if (props.immediate) void refresh()
656
- offLocale = subscribeLocaleChange(() => onLocaleChange())
657
- })
658
-
659
- onUnmounted(() => {
660
- offLocale?.()
661
- offLocale = undefined
662
- })
663
-
664
- defineExpose({
665
- ...ctx,
666
- refresh,
667
- setParams,
668
- resetParams,
669
- rowAdd,
670
- rowAppend,
671
- rowEdit,
672
- rowInfo,
673
- rowClose,
674
- openUpsert,
675
- rowDelete,
676
- rowRestore,
677
- upsertForm,
678
- upsertClose,
679
- patchUpsertForm,
680
- getUpsertForm,
681
- selection,
682
- list,
683
- pagination,
684
- trashMode,
685
- dataI18nLangCode,
686
- dataI18nFields,
687
- permission,
688
- getPermission,
689
- getParams,
690
- buildListQuery,
691
- buildExportQuery,
692
- requestPage,
693
- })
694
- </script>
695
-
696
- <style lang="scss" scoped>
697
- .vm-crud {
698
- display: flex;
699
- width: 100%;
700
- min-width: 0;
701
- min-height: 0;
702
- flex: 1;
703
- flex-direction: column;
704
- gap: 8px;
705
- padding: 12px 16px;
706
- overflow: hidden;
707
- border-radius: 24px;
708
- background: var(--card);
709
- box-shadow: var(--shadow-soft);
710
- }
711
- </style>
1
+ <template>
2
+ <div class="vm-crud">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { ref, computed, watch, shallowRef, provide, onMounted, onUnmounted } from 'vue'
9
+ import { toast } from 'vue-sonner'
10
+ import { deepTree } from '../lib/tree'
11
+ import { mergeDict, mergePermission } from './config'
12
+ import { invokeCrudPageRequest } from './crud-page-request'
13
+ import { vmConfirm } from './confirm'
14
+ import { CRUD_KEY, injectCrudOptions, injectTableOptions, injectUpsertOptions } from './key'
15
+ import { crudLabel } from './style'
16
+ import {
17
+ canUseDataI18n,
18
+ dataPackFromService,
19
+ type DataI18nFieldMeta,
20
+ } from './data-i18n'
21
+ import { useVome } from '../hooks/useVome'
22
+ import { subscribeLocaleChange } from 'vome-core/client'
23
+ import type {
24
+ CrudContext,
25
+ CrudPermission,
26
+ CrudService,
27
+ CrudTrashMode,
28
+ } from '../../../typings/admin/comm/crud'
29
+ defineOptions({ name: 'vm-crud' })
30
+
31
+ const props = withDefaults(
32
+ defineProps<{
33
+ /** EPS service 对象,如 useCrud({ service: service.base.user }) */
34
+ service?: CrudService
35
+ params?: Record<string, unknown>
36
+ immediate?: boolean
37
+ }>(),
38
+ {
39
+ params: () => ({}),
40
+ immediate: false,
41
+ },
42
+ )
43
+
44
+ const hookOptions = injectCrudOptions()
45
+ const tableOpts = injectTableOptions()
46
+ const upsertOpts = injectUpsertOptions()
47
+ const searchActions = shallowRef<{ search: () => void; reset: () => void } | null>(null)
48
+ const hasSearchFilters = ref(false)
49
+
50
+ function pickService(): CrudService {
51
+ const s = props.service ?? hookOptions?.service
52
+ return (s || {}) as CrudService
53
+ }
54
+
55
+ function permFromService(s: CrudService): Partial<CrudPermission> | undefined {
56
+ if (s._permission && typeof s._permission === 'object') {
57
+ return s._permission as Partial<CrudPermission>
58
+ }
59
+ return undefined
60
+ }
61
+
62
+ const service = computed<CrudService>(() => pickService())
63
+
64
+ const dict = mergeDict(hookOptions?.dict)
65
+ const permission = ref(
66
+ mergePermission(hookOptions?.permission, permFromService(service.value)),
67
+ )
68
+
69
+ const loading = ref(false)
70
+ const list = ref<Record<string, unknown>[]>([])
71
+ const selection = ref<Record<string, unknown>[]>([])
72
+ const pagination = ref({ page: 1, size: 20, total: 0 })
73
+ const params = ref<Record<string, unknown>>({ ...props.params })
74
+ const trashMode = ref<CrudTrashMode>('default')
75
+ const dataI18nLangCode = ref('en-US')
76
+ const dataI18nFields = ref<DataI18nFieldMeta[]>([])
77
+
78
+ const { service: hostService } = useVome()
79
+
80
+ const upsertVisible = ref(false)
81
+ const upsertMode = ref<'add' | 'update' | 'info'>('add')
82
+ const upsertForm = ref<Record<string, unknown>>({})
83
+
84
+ function getPermission(key?: keyof typeof permission.value) {
85
+ if (!key) return { ...permission.value }
86
+ return Boolean(permission.value[key])
87
+ }
88
+
89
+ function getParams() {
90
+ return { ...params.value }
91
+ }
92
+
93
+ function applyListMeta(body: Record<string, unknown>) {
94
+ const ds = tableOpts?.defaultSort
95
+ if (ds?.prop && body[dict.sort.prop] == null) {
96
+ body[dict.sort.prop] = ds.prop
97
+ const o = ds.order
98
+ body[dict.sort.order] =
99
+ o === 'asc' || o === 'ascending' ? 'asc' : 'desc'
100
+ }
101
+ if (trashMode.value === 'onlyTrashed') body.onlyTrashed = true
102
+ if (trashMode.value === 'withTrashed') body.withTrashed = true
103
+ return body
104
+ }
105
+
106
+ /** 与 refresh 同源:筛选 params + 排序 + 列表/回收站视图;extra 可覆盖 page/size */
107
+ function buildListQuery(extra: Record<string, unknown> = {}) {
108
+ const pageKey = dict.pagination.page
109
+ const sizeKey = dict.pagination.size
110
+ const body: Record<string, unknown> = {
111
+ ...params.value,
112
+ ...extra,
113
+ }
114
+ if (extra[pageKey] == null) body[pageKey] = pagination.value.page
115
+ if (extra[sizeKey] == null) body[sizeKey] = pagination.value.size
116
+ return applyListMeta(body)
117
+ }
118
+
119
+ /** 导出专用:筛选 params + 排序 + 列表/回收站视图;不含 page、size */
120
+ function buildExportQuery(extra: Record<string, unknown> = {}) {
121
+ const pageKey = dict.pagination.page
122
+ const sizeKey = dict.pagination.size
123
+ const body: Record<string, unknown> = {
124
+ ...params.value,
125
+ ...extra,
126
+ }
127
+ delete body[pageKey]
128
+ delete body[sizeKey]
129
+ return applyListMeta(body)
130
+ }
131
+
132
+ async function rawFetchPage(
133
+ req: Record<string, unknown>,
134
+ opts?: { timeoutMs?: number },
135
+ ) {
136
+ const svc = service.value
137
+ if (svc.page) {
138
+ return await svc.page(req, opts)
139
+ }
140
+ const rows = (await svc.list!(req)) || []
141
+ const pageKey = dict.pagination.page
142
+ const sizeKey = dict.pagination.size
143
+ return {
144
+ list: rows,
145
+ pagination: {
146
+ page: Number(req[pageKey] ?? 1),
147
+ size: Number(req[sizeKey] ?? (rows.length || 20)),
148
+ total: rows.length,
149
+ },
150
+ }
151
+ }
152
+
153
+ /** 与 refresh 同源:含 onRefresh 注入的筛选条件 */
154
+ async function requestPage(
155
+ req: Record<string, unknown>,
156
+ opts?: { timeoutMs?: number },
157
+ ) {
158
+ return invokeCrudPageRequest({
159
+ body: req,
160
+ fetch: rawFetchPage,
161
+ onRefresh: hookOptions?.onRefresh,
162
+ requestOpts: opts,
163
+ })
164
+ }
165
+
166
+ async function refresh(extra: Record<string, unknown> = {}) {
167
+ if (trashMode.value === 'dataI18n') {
168
+ await refreshDataI18nEntries(extra)
169
+ return
170
+ }
171
+
172
+ const svc = service.value
173
+ if (!svc.page && !svc.list) {
174
+ toast.error(crudLabel('noPageList'))
175
+ return
176
+ }
177
+
178
+ const body = buildListQuery(extra)
179
+
180
+ loading.value = true
181
+
182
+ const done = () => {
183
+ loading.value = false
184
+ }
185
+
186
+ const render = (
187
+ data:
188
+ | Record<string, unknown>[]
189
+ | { list: Record<string, unknown>[]; pagination?: unknown },
190
+ pag?: { page?: number; size?: number; total?: number },
191
+ ) => {
192
+ let rows: Record<string, unknown>[]
193
+ if (Array.isArray(data)) {
194
+ rows = data
195
+ if (pag) {
196
+ pagination.value = {
197
+ page: pag.page ?? pagination.value.page,
198
+ size: pag.size ?? pagination.value.size,
199
+ total: pag.total ?? data.length,
200
+ }
201
+ } else {
202
+ pagination.value = { ...pagination.value, total: data.length }
203
+ }
204
+ } else {
205
+ rows = data.list || []
206
+ const p = (pag || data.pagination || {}) as {
207
+ page?: number
208
+ size?: number
209
+ total?: number
210
+ }
211
+ pagination.value = {
212
+ page: p.page ?? pagination.value.page,
213
+ size: p.size ?? pagination.value.size,
214
+ total: p.total ?? 0,
215
+ }
216
+ }
217
+
218
+ // toTree:扁平 parentId → 树
219
+ if (tableOpts?.tree) {
220
+ const nested = rows.some((r) => Array.isArray(r.children) && r.children.length)
221
+ list.value = nested ? rows : (deepTree(rows as never) as Record<string, unknown>[])
222
+ } else {
223
+ list.value = rows
224
+ }
225
+
226
+ selection.value = []
227
+ done()
228
+ }
229
+
230
+ const next = async (req: Record<string, unknown>) => {
231
+ try {
232
+ const data = await rawFetchPage(req)
233
+ render(data)
234
+ return data
235
+ } catch (e) {
236
+ console.error(e)
237
+ done()
238
+ throw e
239
+ }
240
+ }
241
+
242
+ try {
243
+ if (hookOptions?.onRefresh) {
244
+ await hookOptions.onRefresh(body, { next, done, render })
245
+ } else {
246
+ await next(body)
247
+ }
248
+ } catch {
249
+ done()
250
+ }
251
+ }
252
+
253
+ async function refreshDataI18nEntries(extra: Record<string, unknown> = {}) {
254
+ const meta = canUseDataI18n(service.value, hostService)
255
+ if (!meta.ok) {
256
+ toast.error(crudLabel('noDataI18n'))
257
+ trashMode.value = 'default'
258
+ list.value = []
259
+ dataI18nFields.value = []
260
+ return
261
+ }
262
+ const dataPack = dataPackFromService(hostService)
263
+ if (!dataPack?.entries) {
264
+ toast.error(
265
+ crudLabel('noDataPackEntries'),
266
+ )
267
+ list.value = []
268
+ dataI18nFields.value = []
269
+ pagination.value = { ...pagination.value, total: 0 }
270
+ return
271
+ }
272
+ const lang = String(dataI18nLangCode.value || '').trim()
273
+ if (!lang) {
274
+ toast.error(crudLabel('pickLang'))
275
+ list.value = []
276
+ pagination.value = { ...pagination.value, total: 0 }
277
+ return
278
+ }
279
+
280
+ loading.value = true
281
+ try {
282
+ const res = (await dataPack.entries({
283
+ tableName: meta.tableName,
284
+ langCode: lang,
285
+ })) as {
286
+ fields?: DataI18nFieldMeta[]
287
+ list?: Array<{
288
+ id: string
289
+ values: Record<string, string>
290
+ sources: Record<string, string>
291
+ }>
292
+ }
293
+ const fields = Array.isArray(res?.fields) ? res.fields : []
294
+ dataI18nFields.value = fields.length
295
+ ? fields
296
+ : (meta.entity?.dataI18nFields || []).map((name) => ({
297
+ name,
298
+ label: name,
299
+ }))
300
+ const all = (res?.list || []).map((r) => {
301
+ const sources = r.sources || {}
302
+ const srcFlat: Record<string, string> = {}
303
+ for (const [k, v] of Object.entries(sources)) {
304
+ srcFlat[`__src_${k}`] = String(v ?? '')
305
+ }
306
+ return {
307
+ id: String(r.id),
308
+ __rowKey: String(r.id),
309
+ __sources: sources,
310
+ ...srcFlat,
311
+ ...(r.values || {}),
312
+ }
313
+ })
314
+ const pageKey = dict.pagination.page
315
+ const sizeKey = dict.pagination.size
316
+ const page = Number(extra[pageKey] ?? pagination.value.page) || 1
317
+ const size = Number(extra[sizeKey] ?? pagination.value.size) || 20
318
+ const start = (page - 1) * size
319
+ list.value = all.slice(start, start + size)
320
+ pagination.value = {
321
+ page,
322
+ size,
323
+ total: all.length,
324
+ }
325
+ selection.value = []
326
+ } catch (e) {
327
+ console.error(e)
328
+ toast.error(e instanceof Error ? e.message : crudLabel('loadI18nFail'))
329
+ list.value = []
330
+ dataI18nFields.value = []
331
+ pagination.value = { ...pagination.value, total: 0 }
332
+ } finally {
333
+ loading.value = false
334
+ }
335
+ }
336
+
337
+ function setParams(p: Record<string, unknown>, doRefresh = true) {
338
+ params.value = { ...params.value, ...p }
339
+ if (doRefresh) {
340
+ pagination.value = { ...pagination.value, page: 1 }
341
+ void refresh()
342
+ }
343
+ }
344
+
345
+ function resetParams(doRefresh = true) {
346
+ params.value = { ...props.params }
347
+ pagination.value = { ...pagination.value, page: 1 }
348
+ if (doRefresh) void refresh()
349
+ }
350
+
351
+ function setSelection(rows: Record<string, unknown>[]) {
352
+ selection.value = rows
353
+ }
354
+
355
+ /** 新增时收集 useUpsert items 的 value 作为底稿 */
356
+ function collectUpsertItemDefaults() {
357
+ const out: Record<string, unknown> = {}
358
+ for (const it of upsertOpts?.items || []) {
359
+ if (it.value === undefined) continue
360
+ out[it.prop] = Array.isArray(it.value) ? [...it.value] : it.value
361
+ }
362
+ return out
363
+ }
364
+
365
+ function rowAdd(defaults: Record<string, unknown> = {}) {
366
+ if (!permission.value.add) return
367
+ upsertMode.value = 'add'
368
+ const merged = { ...collectUpsertItemDefaults(), ...defaults }
369
+ upsertForm.value = merged
370
+ upsertVisible.value = true
371
+ }
372
+
373
+ function rowAppend(data: Record<string, unknown> = {}) {
374
+ rowAdd(data)
375
+ }
376
+
377
+ async function loadRowForm(row: Record<string, unknown>, mode: 'update' | 'info') {
378
+ upsertMode.value = mode
379
+ const svc = service.value
380
+
381
+ const next = async (data?: Record<string, unknown>) => {
382
+ if (svc.info && row[dict.primaryId] != null) {
383
+ return await svc.info({
384
+ id: row[dict.primaryId] as string | number,
385
+ ...data,
386
+ })
387
+ }
388
+ return { ...row, ...data }
389
+ }
390
+
391
+ const done = (data: Record<string, unknown>) => {
392
+ upsertForm.value = data
393
+ upsertVisible.value = true
394
+ }
395
+
396
+ const close = () => upsertClose()
397
+
398
+ // 自定义拉详情
399
+ if (upsertOpts?.onInfo) {
400
+ await upsertOpts.onInfo({ ...row }, { next, done, close })
401
+ return
402
+ }
403
+
404
+ // 默认用 page 行打开,不打 /info;仅 sync: false 才拉详情
405
+ if (upsertOpts?.sync !== false) {
406
+ done({ ...row })
407
+ return
408
+ }
409
+
410
+ try {
411
+ done(await next())
412
+ } catch {
413
+ done({ ...row })
414
+ }
415
+ }
416
+
417
+ async function rowEdit(row: Record<string, unknown>) {
418
+ if (!permission.value.update) return
419
+ await loadRowForm(row, 'update')
420
+ }
421
+
422
+ async function rowInfo(row: Record<string, unknown>) {
423
+ if (!permission.value.info) return
424
+ await loadRowForm(row, 'info')
425
+ }
426
+
427
+ function rowClose() {
428
+ upsertClose()
429
+ }
430
+
431
+ /** 打开 upsert(不校验 add 权限;供转移部门等自定义场景) */
432
+ function openUpsert(
433
+ form: Record<string, unknown> = {},
434
+ mode: 'add' | 'update' | 'info' = 'add',
435
+ ) {
436
+ upsertMode.value = mode
437
+ upsertForm.value =
438
+ mode === 'add' ? { ...collectUpsertItemDefaults(), ...form } : { ...form }
439
+ upsertVisible.value = true
440
+ }
441
+
442
+ async function doDelete(targets: Record<string, unknown>[], force = false) {
443
+ const svc = service.value
444
+ if (!svc.delete) {
445
+ toast.error(crudLabel('noDeleteApi'))
446
+ return
447
+ }
448
+ const ids = targets
449
+ .map((r) => r[dict.primaryId] as string | number)
450
+ .filter((id) => id != null)
451
+ await svc.delete({ ids, force })
452
+ toast.success(
453
+ force ? crudLabel('forceDeleted', dict.label) : crudLabel('deleteSuccess', dict.label),
454
+ )
455
+ await refresh()
456
+ }
457
+
458
+ async function rowDelete(rows?: Record<string, unknown>[], force = false) {
459
+ // 与后端一致:无 restore → 默认硬删;回收站内 / 显式 force 亦硬删
460
+ const hard =
461
+ force || trashMode.value === 'onlyTrashed' || !service.value.restore
462
+ // 删除一律要 delete 权限(含无 restore 的硬删、回收站彻底删)
463
+ if (!permission.value.delete) return
464
+ const targets = rows?.length ? rows : selection.value
465
+ if (!targets.length) {
466
+ toast.warning(crudLabel('pleaseSelectData'))
467
+ return
468
+ }
469
+
470
+ const next = async (payload?: Record<string, unknown>) => {
471
+ if (payload?.ids) {
472
+ const svc = service.value
473
+ if (!svc.delete) throw new Error(crudLabel('noDeleteApi'))
474
+ await svc.delete({
475
+ ...(payload as { ids: Array<string | number> }),
476
+ force: hard,
477
+ })
478
+ toast.success(
479
+ hard ? crudLabel('forceDeleted', dict.label) : crudLabel('deleteSuccess', dict.label),
480
+ )
481
+ await refresh()
482
+ return
483
+ }
484
+ await doDelete(targets, hard)
485
+ }
486
+
487
+ if (hookOptions?.onDelete && !hard) {
488
+ await hookOptions.onDelete(targets, { next })
489
+ return
490
+ }
491
+
492
+ const tip = hard
493
+ ? crudLabel('forceDeleteConfirm', dict.label)
494
+ : crudLabel('deleteConfirm', dict.label)
495
+ const ok = await vmConfirm({
496
+ title: hard ? crudLabel('forceDelete', dict.label) : crudLabel('tips', dict.label),
497
+ message: tip,
498
+ confirmText: crudLabel('confirm', dict.label),
499
+ cancelText: crudLabel('close', dict.label),
500
+ })
501
+ if (!ok) return
502
+ await next()
503
+ }
504
+
505
+ async function rowRestore(rows?: Record<string, unknown>[]) {
506
+ if (!permission.value.restore) return
507
+ const targets = rows?.length ? rows : selection.value
508
+ if (!targets.length) {
509
+ toast.warning(crudLabel('pleaseSelectData'))
510
+ return
511
+ }
512
+ const svc = service.value
513
+ if (!svc.restore) {
514
+ toast.error(crudLabel('noRestoreApi'))
515
+ return
516
+ }
517
+ if (!(await vmConfirm({ title: crudLabel('restoreConfirmTitle'), message: crudLabel('restoreConfirmMsg'), confirmText: crudLabel('confirm'), cancelText: crudLabel('close') }))) return
518
+ const ids = targets
519
+ .map((r) => r[dict.primaryId] as string | number)
520
+ .filter((id) => id != null)
521
+ await svc.restore({ ids })
522
+ toast.success(crudLabel('restored'))
523
+ await refresh()
524
+ }
525
+
526
+ async function upsertSubmit() {
527
+ if (upsertMode.value === 'info') {
528
+ upsertClose()
529
+ return
530
+ }
531
+ // 系统时间字段由 BaseService.request 统一剥离,后端 Repository 再兜底
532
+ const data = { ...upsertForm.value }
533
+ const svc = service.value
534
+ try {
535
+ if (upsertMode.value === 'add') {
536
+ if (!svc.add) throw new Error(crudLabel('noAddApi'))
537
+ await svc.add(data)
538
+ toast.success(crudLabel('saveSuccess', dict.label))
539
+ } else {
540
+ if (!svc.update) throw new Error(crudLabel('noUpdateConfig'))
541
+ await svc.update(data)
542
+ toast.success(crudLabel('saveSuccess', dict.label))
543
+ }
544
+ const after = async () => {
545
+ upsertVisible.value = false
546
+ await refresh()
547
+ }
548
+ if (upsertOpts?.onClose) {
549
+ upsertOpts.onClose('submit', () => {
550
+ void after()
551
+ })
552
+ } else {
553
+ await after()
554
+ }
555
+ } catch (e) {
556
+ console.error(e)
557
+ const err = e as Error & { toasted?: boolean }
558
+ const msg =
559
+ e instanceof Error && e.message ? e.message : crudLabel('saveFailed')
560
+ // request 已 toast 过则不再重复;本地校验/配置错误仍要提示
561
+ if (!err.toasted) toast.error(msg.slice(0, 200))
562
+ }
563
+ }
564
+
565
+ function upsertClose() {
566
+ const finish = () => {
567
+ upsertVisible.value = false
568
+ }
569
+ if (upsertOpts?.onClose) {
570
+ upsertOpts.onClose('close', finish)
571
+ return
572
+ }
573
+ finish()
574
+ }
575
+
576
+ /** 供页面可靠改表单(避免 template ref 解包 upsertForm 后误写) */
577
+ function patchUpsertForm(patch: Record<string, unknown>) {
578
+ upsertForm.value = { ...upsertForm.value, ...patch }
579
+ }
580
+
581
+ function getUpsertForm() {
582
+ return upsertForm.value
583
+ }
584
+
585
+ const ctx: CrudContext = {
586
+ service: service.value,
587
+ loading,
588
+ list,
589
+ selection,
590
+ pagination,
591
+ params,
592
+ trashMode,
593
+ dataI18nLangCode,
594
+ dataI18nFields,
595
+ permission,
596
+ dict,
597
+ refresh,
598
+ setParams,
599
+ resetParams,
600
+ setSelection,
601
+ getPermission: getPermission as CrudContext['getPermission'],
602
+ getParams,
603
+ buildListQuery,
604
+ buildExportQuery,
605
+ requestPage,
606
+ rowAdd,
607
+ rowAppend,
608
+ rowEdit,
609
+ rowInfo,
610
+ rowClose,
611
+ openUpsert,
612
+ rowDelete,
613
+ rowRestore,
614
+ upsertVisible,
615
+ upsertMode,
616
+ upsertForm,
617
+ upsertSubmit,
618
+ upsertClose,
619
+ patchUpsertForm,
620
+ getUpsertForm,
621
+ searchActions,
622
+ hasSearchFilters,
623
+ }
624
+
625
+ provide(CRUD_KEY, ctx)
626
+
627
+ watch(
628
+ () => props.params,
629
+ (v) => {
630
+ params.value = { ...v }
631
+ },
632
+ )
633
+
634
+ watch(
635
+ service,
636
+ (s) => {
637
+ ctx.service = s
638
+ permission.value = mergePermission(
639
+ hookOptions?.permission,
640
+ permFromService(s),
641
+ )
642
+ },
643
+ { immediate: true },
644
+ )
645
+
646
+ // 等子组件(如 vm-search)setup 写完初始 params 后再拉,避免空参多打一次
647
+ // 切语种始终 refresh(带新 X-Lang / dataI18n);不依赖 immediate(多数页首拉走 useCrud 回调)
648
+ function onLocaleChange() {
649
+ void refresh()
650
+ }
651
+
652
+ let offLocale: (() => void) | undefined
653
+
654
+ onMounted(() => {
655
+ if (props.immediate) void refresh()
656
+ offLocale = subscribeLocaleChange(() => onLocaleChange())
657
+ })
658
+
659
+ onUnmounted(() => {
660
+ offLocale?.()
661
+ offLocale = undefined
662
+ })
663
+
664
+ defineExpose({
665
+ ...ctx,
666
+ refresh,
667
+ setParams,
668
+ resetParams,
669
+ rowAdd,
670
+ rowAppend,
671
+ rowEdit,
672
+ rowInfo,
673
+ rowClose,
674
+ openUpsert,
675
+ rowDelete,
676
+ rowRestore,
677
+ upsertForm,
678
+ upsertClose,
679
+ patchUpsertForm,
680
+ getUpsertForm,
681
+ selection,
682
+ list,
683
+ pagination,
684
+ trashMode,
685
+ dataI18nLangCode,
686
+ dataI18nFields,
687
+ permission,
688
+ getPermission,
689
+ getParams,
690
+ buildListQuery,
691
+ buildExportQuery,
692
+ requestPage,
693
+ })
694
+ </script>
695
+
696
+ <style lang="scss" scoped>
697
+ .vm-crud {
698
+ display: flex;
699
+ width: 100%;
700
+ min-width: 0;
701
+ min-height: 0;
702
+ flex: 1;
703
+ flex-direction: column;
704
+ gap: 8px;
705
+ padding: 12px 16px;
706
+ overflow: hidden;
707
+ border-radius: 24px;
708
+ background: var(--card);
709
+ box-shadow: var(--shadow-soft);
710
+ }
711
+ </style>