vome-core 0.3.64 → 0.3.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/dist/admin/api/client.js +7 -2
  2. package/dist/admin/components/ui/avatar/Avatar.vue +18 -18
  3. package/dist/admin/components/ui/avatar/AvatarFallback.vue +21 -21
  4. package/dist/admin/components/ui/avatar/AvatarImage.vue +16 -16
  5. package/dist/admin/components/ui/avatar/index.js +1 -1
  6. package/dist/admin/components/ui/badge/Badge.vue +26 -26
  7. package/dist/admin/components/ui/button/Button.vue +31 -31
  8. package/dist/admin/components/ui/checkbox/Checkbox.vue +35 -35
  9. package/dist/admin/components/ui/checkbox/index.js +1 -1
  10. package/dist/admin/components/ui/dialog/Dialog.vue +19 -19
  11. package/dist/admin/components/ui/dialog/DialogClose.vue +15 -15
  12. package/dist/admin/components/ui/dialog/DialogContent.vue +267 -267
  13. package/dist/admin/components/ui/dialog/DialogDescription.vue +23 -23
  14. package/dist/admin/components/ui/dialog/DialogFooter.vue +27 -27
  15. package/dist/admin/components/ui/dialog/DialogHeader.vue +17 -17
  16. package/dist/admin/components/ui/dialog/DialogOverlay.vue +21 -21
  17. package/dist/admin/components/ui/dialog/DialogScrollContent.vue +60 -60
  18. package/dist/admin/components/ui/dialog/DialogTitle.vue +23 -23
  19. package/dist/admin/components/ui/dialog/DialogTrigger.vue +15 -15
  20. package/dist/admin/components/ui/dialog/index.js +1 -1
  21. package/dist/admin/components/ui/dropdown-menu/DropdownMenu.vue +19 -19
  22. package/dist/admin/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -39
  23. package/dist/admin/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -39
  24. package/dist/admin/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -15
  25. package/dist/admin/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -31
  26. package/dist/admin/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -23
  27. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -21
  28. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -40
  29. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -23
  30. package/dist/admin/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -17
  31. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -18
  32. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -27
  33. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -31
  34. package/dist/admin/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -17
  35. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  36. package/dist/admin/components/ui/input/Input.vue +183 -183
  37. package/dist/admin/components/ui/input/index.js +1 -1
  38. package/dist/admin/components/ui/label/Label.vue +26 -26
  39. package/dist/admin/components/ui/label/index.js +1 -1
  40. package/dist/admin/components/ui/scroll-area/ScrollArea.vue +33 -33
  41. package/dist/admin/components/ui/scroll-area/ScrollBar.vue +32 -32
  42. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  43. package/dist/admin/components/ui/select/Select.vue +19 -19
  44. package/dist/admin/components/ui/select/SelectContent.vue +51 -51
  45. package/dist/admin/components/ui/select/SelectGroup.vue +15 -15
  46. package/dist/admin/components/ui/select/SelectItem.vue +53 -53
  47. package/dist/admin/components/ui/select/SelectItemText.vue +15 -15
  48. package/dist/admin/components/ui/select/SelectLabel.vue +17 -17
  49. package/dist/admin/components/ui/select/SelectScrollDownButton.vue +26 -26
  50. package/dist/admin/components/ui/select/SelectScrollUpButton.vue +26 -26
  51. package/dist/admin/components/ui/select/SelectSeparator.vue +19 -19
  52. package/dist/admin/components/ui/select/SelectTrigger.vue +44 -44
  53. package/dist/admin/components/ui/select/SelectValue.vue +15 -15
  54. package/dist/admin/components/ui/select/index.js +1 -1
  55. package/dist/admin/components/ui/separator/Separator.vue +29 -29
  56. package/dist/admin/components/ui/separator/index.js +1 -1
  57. package/dist/admin/components/ui/sheet/Sheet.vue +19 -19
  58. package/dist/admin/components/ui/sheet/SheetClose.vue +15 -15
  59. package/dist/admin/components/ui/sheet/SheetContent.vue +63 -63
  60. package/dist/admin/components/ui/sheet/SheetDescription.vue +21 -21
  61. package/dist/admin/components/ui/sheet/SheetFooter.vue +16 -16
  62. package/dist/admin/components/ui/sheet/SheetHeader.vue +15 -15
  63. package/dist/admin/components/ui/sheet/SheetOverlay.vue +21 -21
  64. package/dist/admin/components/ui/sheet/SheetTitle.vue +21 -21
  65. package/dist/admin/components/ui/sheet/SheetTrigger.vue +15 -15
  66. package/dist/admin/components/ui/sheet/index.js +1 -1
  67. package/dist/admin/components/ui/sonner/Toaster.vue +117 -117
  68. package/dist/admin/components/ui/sonner/index.js +1 -1
  69. package/dist/admin/components/ui/table/Table.vue +16 -16
  70. package/dist/admin/components/ui/table/TableBody.vue +17 -17
  71. package/dist/admin/components/ui/table/TableCaption.vue +17 -17
  72. package/dist/admin/components/ui/table/TableCell.vue +22 -22
  73. package/dist/admin/components/ui/table/TableEmpty.vue +34 -34
  74. package/dist/admin/components/ui/table/TableFooter.vue +17 -17
  75. package/dist/admin/components/ui/table/TableHead.vue +17 -17
  76. package/dist/admin/components/ui/table/TableHeader.vue +17 -17
  77. package/dist/admin/components/ui/table/TableRow.vue +17 -17
  78. package/dist/admin/components/ui/table/index.js +1 -1
  79. package/dist/admin/components/ui/tabs/Tabs.vue +24 -24
  80. package/dist/admin/components/ui/tabs/TabsContent.vue +21 -21
  81. package/dist/admin/components/ui/tabs/TabsList.vue +24 -24
  82. package/dist/admin/components/ui/tabs/TabsTrigger.vue +26 -26
  83. package/dist/admin/components/ui/tabs/index.js +1 -1
  84. package/dist/admin/components/ui/tooltip/Tooltip.vue +19 -19
  85. package/dist/admin/components/ui/tooltip/TooltipContent.vue +34 -34
  86. package/dist/admin/components/ui/tooltip/TooltipProvider.vue +14 -14
  87. package/dist/admin/components/ui/tooltip/TooltipTrigger.vue +15 -15
  88. package/dist/admin/components/ui/tooltip/index.js +1 -1
  89. package/dist/admin/components/vm-action-btn.vue +136 -136
  90. package/dist/admin/components/vm-aside-list-item.vue +136 -136
  91. package/dist/admin/components/vm-aside.vue +229 -229
  92. package/dist/admin/components/vm-auto-perm-dialog.vue +371 -371
  93. package/dist/admin/components/vm-card-grid.vue +37 -37
  94. package/dist/admin/components/vm-card-head.vue +45 -45
  95. package/dist/admin/components/vm-card-meta.vue +22 -22
  96. package/dist/admin/components/vm-card-title.vue +56 -56
  97. package/dist/admin/components/vm-card.vue +176 -176
  98. package/dist/admin/components/vm-check-tree.vue +340 -340
  99. package/dist/admin/components/vm-clamp-text.vue +39 -39
  100. package/dist/admin/components/vm-dept-tree.vue +497 -497
  101. package/dist/admin/components/vm-empty.vue +68 -68
  102. package/dist/admin/components/vm-eps-perm-picker.vue +123 -123
  103. package/dist/admin/components/vm-form-dialog.vue +632 -632
  104. package/dist/admin/components/vm-group-cascader.vue +560 -560
  105. package/dist/admin/components/vm-icon-btn.vue +88 -88
  106. package/dist/admin/components/vm-icon-picker.vue +647 -647
  107. package/dist/admin/components/vm-inline-search.vue +61 -61
  108. package/dist/admin/components/vm-loading.vue +114 -114
  109. package/dist/admin/components/vm-markdown.vue +405 -405
  110. package/dist/admin/components/vm-marquee.vue +265 -265
  111. package/dist/admin/components/vm-page-tabs.vue +94 -94
  112. package/dist/admin/components/vm-perm-section.vue +57 -57
  113. package/dist/admin/components/vm-ri-icon.vue +23 -23
  114. package/dist/admin/components/vm-role-picker.vue +314 -314
  115. package/dist/admin/components/vm-split-layout.vue +106 -106
  116. package/dist/admin/components/vm-thumb.vue +49 -49
  117. package/dist/admin/components/vm-view-path-picker.vue +59 -59
  118. package/dist/admin/crud/auto-fields.js +1 -1
  119. package/dist/admin/crud/components/vm-avatar.vue +104 -104
  120. package/dist/admin/crud/components/vm-color-picker.vue +1626 -1626
  121. package/dist/admin/crud/components/vm-column-custom.vue +193 -193
  122. package/dist/admin/crud/components/vm-context-menu.vue +105 -105
  123. package/dist/admin/crud/components/vm-copy-btn.vue +102 -102
  124. package/dist/admin/crud/components/vm-date-calendar-panel.vue +537 -537
  125. package/dist/admin/crud/components/vm-date-picker.vue +75 -75
  126. package/dist/admin/crud/components/vm-date-range.vue +88 -88
  127. package/dist/admin/crud/components/vm-date-text.vue +42 -42
  128. package/dist/admin/crud/components/vm-datetime-field.vue +215 -215
  129. package/dist/admin/crud/components/vm-datetime-picker.vue +9 -9
  130. package/dist/admin/crud/components/vm-dict-tag.vue +112 -112
  131. package/dist/admin/crud/components/vm-ellipsis-text.vue +34 -34
  132. package/dist/admin/crud/components/vm-form-hint.vue +81 -81
  133. package/dist/admin/crud/components/vm-json-code.vue +41 -41
  134. package/dist/admin/crud/components/vm-json-editor.vue +161 -161
  135. package/dist/admin/crud/components/vm-json-kv-editor.vue +318 -318
  136. package/dist/admin/crud/components/vm-kv-row.vue +51 -51
  137. package/dist/admin/crud/components/vm-multi-select.vue +279 -279
  138. package/dist/admin/crud/components/vm-number-range.vue +204 -204
  139. package/dist/admin/crud/components/vm-preview-viewer.vue +894 -894
  140. package/dist/admin/crud/components/vm-radio.vue +87 -87
  141. package/dist/admin/crud/components/vm-richtext.vue +313 -313
  142. package/dist/admin/crud/components/vm-select.vue +570 -570
  143. package/dist/admin/crud/components/vm-status-tag.vue +158 -158
  144. package/dist/admin/crud/components/vm-switch.vue +158 -158
  145. package/dist/admin/crud/components/vm-tag-list.vue +108 -108
  146. package/dist/admin/crud/components/vm-text-link.vue +52 -52
  147. package/dist/admin/crud/components/vm-toggle-switch.vue +84 -84
  148. package/dist/admin/crud/components/vm-tree-select.vue +450 -450
  149. package/dist/admin/crud/components/vm-upload-item.vue +298 -298
  150. package/dist/admin/crud/components/vm-upload.vue +436 -436
  151. package/dist/admin/crud/config.js +1 -1
  152. package/dist/admin/crud/confirm.js +1 -1
  153. package/dist/admin/crud/crud-page-request.js +1 -1
  154. package/dist/admin/crud/data-i18n.js +1 -1
  155. package/dist/admin/crud/dict.js +1 -1
  156. package/dist/admin/crud/export-fetch.js +1 -1
  157. package/dist/admin/crud/index.js +1 -0
  158. package/dist/admin/crud/key.js +1 -1
  159. package/dist/admin/crud/plugins.js +1 -1
  160. package/dist/admin/crud/span.js +1 -1
  161. package/dist/admin/crud/style.js +1 -1
  162. package/dist/admin/crud/useCrud.js +3 -0
  163. package/dist/admin/crud/validate.js +1 -1
  164. package/dist/admin/crud/vm-add-btn.vue +70 -70
  165. package/dist/admin/crud/vm-adv-search.vue +83 -83
  166. package/dist/admin/crud/vm-crud.vue +711 -711
  167. package/dist/admin/crud/vm-data-i18n-btn.vue +456 -456
  168. package/dist/admin/crud/vm-data-i18n-lang.vue +183 -183
  169. package/dist/admin/crud/vm-export-btn.vue +183 -183
  170. package/dist/admin/crud/vm-flex.vue +14 -14
  171. package/dist/admin/crud/vm-form.vue +33 -15
  172. package/dist/admin/crud/vm-import-btn.vue +747 -747
  173. package/dist/admin/crud/vm-multi-delete-btn.vue +49 -49
  174. package/dist/admin/crud/vm-pagination.vue +212 -212
  175. package/dist/admin/crud/vm-refresh-btn.vue +35 -35
  176. package/dist/admin/crud/vm-row.vue +55 -55
  177. package/dist/admin/crud/vm-search-key.vue +78 -78
  178. package/dist/admin/crud/vm-search.vue +466 -466
  179. package/dist/admin/crud/vm-table.vue +1390 -1390
  180. package/dist/admin/crud/vm-tabs.vue +94 -94
  181. package/dist/admin/crud/vm-toolbar.vue +357 -357
  182. package/dist/admin/crud/vm-upsert.vue +765 -765
  183. package/dist/admin/directives/perm.js +1 -1
  184. package/dist/admin/env.d.ts +1 -1
  185. package/dist/admin/hooks/useUpload.js +1 -1
  186. package/dist/admin/index.d.ts +1 -31
  187. package/dist/admin/lib/browser.js +1 -1
  188. package/dist/admin/lib/cn.js +1 -1
  189. package/dist/admin/lib/dialog-float.js +1 -1
  190. package/dist/admin/lib/export-excel.js +1 -1
  191. package/dist/admin/lib/import-excel.js +1 -1
  192. package/dist/admin/lib/json.js +1 -1
  193. package/dist/admin/lib/menu.js +1 -1
  194. package/dist/admin/lib/micro-locale.js +1 -1
  195. package/dist/admin/lib/micro-theme.js +1 -1
  196. package/dist/admin/lib/shell.js +1 -1
  197. package/dist/admin/lib/theme-color-options.js +1 -1
  198. package/dist/admin/lib/tree.js +1 -1
  199. package/dist/admin/lib/upload.js +1 -1
  200. package/dist/admin/lib/video-frame.js +1 -1
  201. package/dist/admin/router/create.js +1 -1
  202. package/dist/admin/router/guards.js +1 -1
  203. package/dist/admin/router/index.d.ts +1 -5
  204. package/dist/admin/service/index.d.ts +1 -11
  205. package/dist/admin/styles/base.css +121 -121
  206. package/dist/admin/vendor/browser-image-compression.js +1144 -1144
  207. package/dist/admin/vendor/decimal-js.js +12 -12
  208. package/dist/admin/vendor/xlsx.js +263 -263
  209. package/dist/ai/index.js +1 -1
  210. package/dist/client/admin-vendor.d.ts +1 -13
  211. package/dist/client/ai-model-registry.js +1 -1
  212. package/dist/client/base.js +1 -1
  213. package/dist/client/configure.js +1 -1
  214. package/dist/client/dict.js +1 -1
  215. package/dist/client/eps-payload.js +1 -1
  216. package/dist/client/eps.js +1 -1
  217. package/dist/client/index.d.ts +1 -7
  218. package/dist/client/index.js +1 -1
  219. package/dist/client/locale-change.js +1 -1
  220. package/dist/client/locale-store.js +1 -1
  221. package/dist/client/service.js +1 -1
  222. package/dist/client/vite-admin.d.ts +1 -40
  223. package/dist/client/vite-admin.js +1 -1
  224. package/dist/client/vite-auto-import.d.ts +1 -2
  225. package/dist/client/vite-auto-import.js +1 -1
  226. package/dist/client/vite-micro-proxy.d.ts +1 -20
  227. package/dist/client/vite-micro-proxy.js +1 -1
  228. package/dist/client/vite-plugin-eps.d.ts +1 -20
  229. package/dist/client/vite-plugin-eps.js +1 -1
  230. package/dist/client/vite-resolve.d.ts +1 -16
  231. package/dist/client/vite-resolve.js +1 -1
  232. package/dist/config/index.js +1 -1
  233. package/dist/core/index.d.ts +1 -1
  234. package/dist/index.js +1 -1
  235. package/dist/ioc/index.d.ts +1 -1
  236. package/dist/server/index.d.ts +1 -1
  237. package/dist/server/index.js +1 -1
  238. package/dist/shared/datetime-picker.js +1 -1
  239. package/dist/shared/decimal.js +1 -1
  240. package/dist/shared/excel.js +1 -1
  241. package/dist/shared/index.js +1 -1
  242. package/dist/shared/locale-json.js +1 -1
  243. package/dist/shared/tree.js +1 -1
  244. package/package.json +1 -1
  245. package/typings/admin/base/auth.ts +24 -24
  246. package/typings/admin/base/permission.ts +19 -19
  247. package/typings/admin/comm/cascader.ts +7 -7
  248. package/typings/admin/comm/check-tree.ts +7 -7
  249. package/typings/admin/comm/crud.ts +427 -427
  250. package/typings/admin/comm/eps.ts +86 -86
  251. package/typings/admin/comm/request.ts +16 -16
  252. package/typings/admin/comm/service.ts +43 -43
  253. package/typings/admin/comm/upload.ts +39 -39
  254. package/typings/admin/theme.ts +10 -10
  255. package/typings/admin/vome/browser.ts +11 -11
  256. package/typings/admin/vome/module.ts +13 -13
  257. package/typings/admin/vome/tags.ts +9 -9
  258. package/typings/comm/crud.ts +83 -83
  259. package/typings/comm/page.ts +18 -18
  260. package/typings/comm/scan.ts +6 -6
  261. package/typings/common.ts +4 -4
  262. package/typings/context/data.ts +10 -10
  263. package/typings/ext/market-seat.ts +42 -42
  264. package/typings/ext/market.ts +55 -55
  265. package/typings/ext/module.ts +64 -64
  266. package/typings/ext/plugin.ts +38 -38
  267. package/typings/ioc/metadata.ts +34 -34
  268. package/typings/logger/config.ts +6 -6
  269. package/typings/routing/crud.ts +199 -199
  270. package/typings/routing/eps.ts +88 -88
  271. package/typings/routing/meta.ts +63 -63
  272. package/typings/routing/register.ts +21 -21
  273. package/typings/routing/scan-context.ts +7 -7
  274. package/dist/admin/api/client.d.ts +0 -50
  275. package/dist/admin/components/ui/avatar/index.d.ts +0 -3
  276. package/dist/admin/components/ui/badge/index.d.ts +0 -6
  277. package/dist/admin/components/ui/button/index.d.ts +0 -7
  278. package/dist/admin/components/ui/checkbox/index.d.ts +0 -1
  279. package/dist/admin/components/ui/dialog/index.d.ts +0 -10
  280. package/dist/admin/components/ui/dropdown-menu/index.d.ts +0 -15
  281. package/dist/admin/components/ui/input/index.d.ts +0 -1
  282. package/dist/admin/components/ui/label/index.d.ts +0 -1
  283. package/dist/admin/components/ui/scroll-area/index.d.ts +0 -2
  284. package/dist/admin/components/ui/select/index.d.ts +0 -11
  285. package/dist/admin/components/ui/separator/index.d.ts +0 -1
  286. package/dist/admin/components/ui/sheet/index.d.ts +0 -8
  287. package/dist/admin/components/ui/sonner/index.d.ts +0 -1
  288. package/dist/admin/components/ui/table/index.d.ts +0 -9
  289. package/dist/admin/components/ui/tabs/index.d.ts +0 -4
  290. package/dist/admin/components/ui/tooltip/index.d.ts +0 -4
  291. package/dist/admin/crud/auto-fields.d.ts +0 -68
  292. package/dist/admin/crud/config.d.ts +0 -5
  293. package/dist/admin/crud/confirm.d.ts +0 -19
  294. package/dist/admin/crud/crud-page-request.d.ts +0 -21
  295. package/dist/admin/crud/data-i18n.d.ts +0 -62
  296. package/dist/admin/crud/dict.d.ts +0 -4
  297. package/dist/admin/crud/export-fetch.d.ts +0 -22
  298. package/dist/admin/crud/index.d.ts +0 -60
  299. package/dist/admin/crud/json-kv.d.ts +0 -9
  300. package/dist/admin/crud/key.d.ts +0 -22
  301. package/dist/admin/crud/paging.d.ts +0 -2
  302. package/dist/admin/crud/plugins.d.ts +0 -63
  303. package/dist/admin/crud/registry.d.ts +0 -3
  304. package/dist/admin/crud/span.d.ts +0 -17
  305. package/dist/admin/crud/style.d.ts +0 -82
  306. package/dist/admin/crud/useCrud.d.ts +0 -21
  307. package/dist/admin/crud/validate.d.ts +0 -7
  308. package/dist/admin/directives/perm.d.ts +0 -3
  309. package/dist/admin/hooks/useBrowser.d.ts +0 -6
  310. package/dist/admin/hooks/useUpload.d.ts +0 -8
  311. package/dist/admin/hooks/useVome.d.ts +0 -10
  312. package/dist/admin/lib/browser.d.ts +0 -3
  313. package/dist/admin/lib/cn.d.ts +0 -2
  314. package/dist/admin/lib/compress-image.d.ts +0 -12
  315. package/dist/admin/lib/dialog-float.d.ts +0 -8
  316. package/dist/admin/lib/eps.d.ts +0 -2
  317. package/dist/admin/lib/export-excel.d.ts +0 -9
  318. package/dist/admin/lib/file-preview.d.ts +0 -9
  319. package/dist/admin/lib/import-excel.d.ts +0 -21
  320. package/dist/admin/lib/json.d.ts +0 -11
  321. package/dist/admin/lib/menu.d.ts +0 -3
  322. package/dist/admin/lib/micro-locale.d.ts +0 -5
  323. package/dist/admin/lib/micro-theme.d.ts +0 -14
  324. package/dist/admin/lib/module.d.ts +0 -21
  325. package/dist/admin/lib/scarce-assets.d.ts +0 -5
  326. package/dist/admin/lib/shell.d.ts +0 -8
  327. package/dist/admin/lib/theme-color-options.d.ts +0 -15
  328. package/dist/admin/lib/tree.d.ts +0 -2
  329. package/dist/admin/lib/upload.d.ts +0 -17
  330. package/dist/admin/lib/video-frame.d.ts +0 -6
  331. package/dist/admin/router/create.d.ts +0 -16
  332. package/dist/admin/router/guards.d.ts +0 -6
  333. package/dist/admin/router/menu-routes.d.ts +0 -13
  334. package/dist/admin/stores/app.d.ts +0 -13
  335. package/dist/admin/stores/tags.d.ts +0 -14
  336. package/dist/admin/stores/user.d.ts +0 -48
  337. package/dist/client/ai-model-registry.d.ts +0 -16
  338. package/dist/client/base.d.ts +0 -28
  339. package/dist/client/configure.d.ts +0 -33
  340. package/dist/client/dict.d.ts +0 -31
  341. package/dist/client/eps-payload.d.ts +0 -3
  342. package/dist/client/eps.d.ts +0 -17
  343. package/dist/client/locale-change.d.ts +0 -16
  344. package/dist/client/locale-store.d.ts +0 -105
  345. package/dist/client/service.d.ts +0 -29
@@ -1,1390 +1,1390 @@
1
- <template>
2
- <div
3
- class="vm-crud-table"
4
- :class="{
5
- 'is-border': border,
6
- 'is-auto-height': autoHeight,
7
- }"
8
- >
9
- <div v-if="crud.loading.value" class="vm-crud-table__loading">
10
- {{ crudLabel('loading', crud.dict.label) }}
11
- </div>
12
- <div ref="scrollRef" class="vm-crud-table__scroll vm-scroll">
13
- <Table>
14
- <TableHeader>
15
- <TableRow class="vm-crud-table__head-row">
16
- <TableHead v-if="showSelection" class="vm-crud-table__th vm-crud-table__check">
17
- <Checkbox :model-value="allChecked" @update:model-value="toggleAll" />
18
- </TableHead>
19
- <TableHead
20
- v-for="(col, i) in dataColumns"
21
- :key="col.prop || col.type || i"
22
- class="vm-crud-table__th"
23
- :class="{
24
- 'vm-crud-table__op': col.type === 'op',
25
- 'is-sortable': isSortable(col),
26
- 'is-fixed-right': colFixed(col) === 'right',
27
- 'is-fixed-left': colFixed(col) === 'left',
28
- }"
29
- :style="colStyle(col, i)"
30
- @click="onSortClick(col)"
31
- >
32
- <span class="vm-crud-table__th-inner">
33
- {{ columnLabel(col) }}
34
- <i
35
- v-if="isSortable(col) && col.prop"
36
- class="vm-crud-table__sort"
37
- :class="sortIconClass(col.prop)"
38
- />
39
- </span>
40
- </TableHead>
41
- <TableHead
42
- v-if="showSlotOp"
43
- class="vm-crud-table__th vm-crud-table__op"
44
- :style="{ width: `${opWidth}px`, minWidth: `${opWidth}px` }"
45
- >
46
- {{ crudLabel('op', crud.dict.label) }}
47
- </TableHead>
48
- </TableRow>
49
- </TableHeader>
50
- <TableBody>
51
- <!-- empty-block:表体内 sticky,宽度=可视区,不盖表头、不跟总表宽跑 -->
52
- <TableRow
53
- v-if="!displayRows.length && !crud.loading.value"
54
- class="vm-crud-table__empty-row"
55
- >
56
- <TableCell :colspan="colSpan" class="vm-crud-table__empty-cell">
57
- <div class="vm-crud-table__empty-block" :style="emptyBlockStyle">
58
- <vm-empty kind="暂无数据" />
59
- </div>
60
- </TableCell>
61
- </TableRow>
62
- <TableRow
63
- v-for="(row, rowIndex) in displayRows"
64
- :key="vueRowKey(row, rowIndex)"
65
- class="vm-crud-table__row"
66
- :class="{ 'is-current': highlight && String(row[rowKey]) === currentId }"
67
- @click="onRowClick(row)"
68
- @contextmenu="(e: MouseEvent) => onContextMenu(e, row)"
69
- >
70
- <TableCell v-if="showSelection" class="vm-crud-table__check">
71
- <Checkbox
72
- :model-value="selectedIds.has(String(row[rowKey]))"
73
- @update:model-value="(v) => toggleRow(row, v)"
74
- />
75
- </TableCell>
76
- <TableCell
77
- v-for="(col, i) in dataColumns"
78
- :key="col.prop || col.type || i"
79
- class="vm-crud-table__td"
80
- :class="{
81
- 'vm-crud-table__op': col.type === 'op',
82
- 'is-fixed-right': colFixed(col) === 'right',
83
- 'is-fixed-left': colFixed(col) === 'left',
84
- }"
85
- :style="colStyle(col, i)"
86
- >
87
- <template v-if="col.type === 'index'">
88
- {{ displayIndex(rowIndex) }}
89
- </template>
90
- <template v-else-if="col.type === 'op'">
91
- <div
92
- v-if="crud.trashMode.value === 'onlyTrashed'"
93
- class="vm-crud-table__ops"
94
- >
95
- <Button
96
- v-if="crud.getPermission('restore')"
97
- size="xs"
98
- variant="softPrimary"
99
- @click="crud.rowRestore([row])"
100
- >
101
- {{ crudLabel('restore', crud.dict.label) }}
102
- </Button>
103
- <Button
104
- v-if="crud.getPermission('delete')"
105
- size="xs"
106
- variant="softDestructive"
107
- @click="crud.rowDelete(deleteRowTargets(row), true)"
108
- >
109
- {{ crudLabel('forceDelete', crud.dict.label) }}
110
- </Button>
111
- </div>
112
- <div
113
- v-else-if="crud.trashMode.value === 'dataI18n'"
114
- class="vm-crud-table__ops"
115
- >
116
- <Button
117
- type="button"
118
- size="xs"
119
- variant="softPrimary"
120
- @click.stop="openDataI18nEdit(row)"
121
- >
122
- {{ crudLabel('update', crud.dict.label) }}
123
- </Button>
124
- <Button
125
- type="button"
126
- size="xs"
127
- variant="softDestructive"
128
- @click.stop="deleteDataI18nEntry(row)"
129
- >
130
- {{ crudLabel('delete', crud.dict.label) }}
131
- </Button>
132
- </div>
133
- <div v-else class="vm-crud-table__ops">
134
- <template v-for="(btn, bi) in resolveButtons(col, row)" :key="bi">
135
- <Button
136
- v-if="btn === 'edit' && crud.getPermission('update')"
137
- type="button"
138
- size="xs"
139
- variant="softPrimary"
140
- @click.stop="crud.rowEdit(row)"
141
- >
142
- {{ crudLabel('update', crud.dict.label) }}
143
- </Button>
144
- <Button
145
- v-else-if="btn === 'delete' && crud.getPermission('delete')"
146
- type="button"
147
- size="xs"
148
- variant="softDestructive"
149
- @click.stop="crud.rowDelete(deleteRowTargets(row))"
150
- >
151
- {{ crudLabel('delete', crud.dict.label) }}
152
- </Button>
153
- <Button
154
- v-else-if="btn === 'info' && crud.getPermission('info')"
155
- type="button"
156
- size="xs"
157
- variant="ghost"
158
- @click.stop="crud.rowInfo(row)"
159
- >
160
- {{ crudLabel('info', crud.dict.label) }}
161
- </Button>
162
- <Button
163
- v-else-if="typeof btn === 'object' && !btn.hidden"
164
- type="button"
165
- size="xs"
166
- :disabled="btn.disabled"
167
- :variant="btn.variant || 'softSuccess'"
168
- @click.stop="btn.onClick(row)"
169
- >
170
- {{ btn.label }}
171
- </Button>
172
- </template>
173
- </div>
174
- </template>
175
- <div
176
- v-else-if="isTree && i === 0"
177
- class="vm-crud-table__tree-cell"
178
- :style="{ paddingLeft: `${(row.depth || 0) * 18}px` }"
179
- >
180
- <button
181
- v-if="row.hasChildren"
182
- type="button"
183
- class="vm-crud-table__fold"
184
- @click="toggleExpand(row[rowKey] as string | number)"
185
- >
186
- <i
187
- :class="
188
- expanded.has(row[rowKey] as string | number)
189
- ? 'ri-arrow-down-s-line'
190
- : 'ri-arrow-right-s-line'
191
- "
192
- />
193
- </button>
194
- <span v-else class="vm-crud-table__fold is-spacer" />
195
- <slot
196
- :name="col.slot || `cell-${col.prop}`"
197
- :row="row"
198
- :value="col.prop ? row[col.prop] : undefined"
199
- >
200
- <component
201
- :is="cellComp(col)"
202
- v-if="cellComp(col)"
203
- :model-value="col.prop ? row[col.prop] : undefined"
204
- :scope="row"
205
- :column="{ property: col.prop, prop: col.prop }"
206
- :options="resolveCellOptions(col)"
207
- v-bind="resolveCellProps(col)"
208
- />
209
- <vm-dict-tag
210
- v-else-if="resolveDict(col).length"
211
- :model-value="col.prop ? (row[col.prop] as string | number) : null"
212
- :options="resolveDict(col)"
213
- />
214
- <template v-else>{{ cellText(col, row) }}</template>
215
- </slot>
216
- </div>
217
- <slot
218
- v-else
219
- :name="col.slot || `cell-${col.prop}`"
220
- :row="row"
221
- :value="col.prop ? row[col.prop] : undefined"
222
- >
223
- <component
224
- :is="cellComp(col)"
225
- v-if="cellComp(col)"
226
- :model-value="col.prop ? row[col.prop] : undefined"
227
- :scope="row"
228
- :column="{ property: col.prop, prop: col.prop }"
229
- :options="resolveCellOptions(col)"
230
- v-bind="resolveCellProps(col)"
231
- />
232
- <vm-dict-tag
233
- v-else-if="resolveDict(col).length"
234
- :model-value="col.prop ? (row[col.prop] as string | number) : null"
235
- :options="resolveDict(col)"
236
- />
237
- <template v-else>{{ cellText(col, row) }}</template>
238
- </slot>
239
- </TableCell>
240
- <TableCell v-if="showSlotOp" class="vm-crud-table__td">
241
- <div
242
- v-if="crud.trashMode.value === 'onlyTrashed'"
243
- class="vm-crud-table__ops"
244
- >
245
- <Button
246
- v-if="crud.getPermission('restore')"
247
- size="xs"
248
- variant="softPrimary"
249
- @click="crud.rowRestore([row])"
250
- >
251
- {{ crudLabel('restore', crud.dict.label) }}
252
- </Button>
253
- <Button
254
- v-if="crud.getPermission('delete')"
255
- size="xs"
256
- variant="softDestructive"
257
- @click="crud.rowDelete(deleteRowTargets(row), true)"
258
- >
259
- {{ crudLabel('forceDelete', crud.dict.label) }}
260
- </Button>
261
- </div>
262
- <slot v-else name="op" :row="row" />
263
- </TableCell>
264
- </TableRow>
265
- </TableBody>
266
- </Table>
267
- </div>
268
- <vm-context-menu ref="ctxRef" :items="ctxItems" @select="onCtxSelect" />
269
-
270
- <Dialog :open="dataI18nEditOpen" @update:open="onDataI18nEditOpenChange">
271
- <DialogContent
272
- shell="crud"
273
- :show-close-button="false"
274
- class="vm-data-i18n-edit__dialog"
275
- @pointer-down-outside="preventDialogOutsideClose"
276
- @focus-outside="preventDialogOutsideClose"
277
- @interact-outside="preventDialogOutsideClose"
278
- >
279
- <DialogHeader class="vm-crud-shell__head">
280
- <DialogTitle class="vm-crud-shell__title">
281
- {{ crudLabel('dataI18nEdit', crud.dict.label) }}
282
- </DialogTitle>
283
- <DialogClose class="vm-crud-shell__close">
284
- <i class="ri-close-line" />
285
- <span class="sr-only">Close</span>
286
- </DialogClose>
287
- </DialogHeader>
288
- <div class="vm-crud-shell__body vm-data-i18n-edit__body">
289
- <div
290
- class="vm-data-i18n-edit__field"
291
- :style="{ gridColumn: `span ${dataI18nEditSpan}` }"
292
- >
293
- <Label class="vm-data-i18n-edit__label">ID</Label>
294
- <Input
295
- class="vm-data-i18n-edit__control is-readonly"
296
- :model-value="String(dataI18nEditForm.id ?? '')"
297
- disabled
298
- />
299
- </div>
300
- <div
301
- v-for="f in dataI18nEditFields"
302
- :key="f.name"
303
- class="vm-data-i18n-edit__field"
304
- :style="{ gridColumn: `span ${dataI18nEditSpan}` }"
305
- >
306
- <Label class="vm-data-i18n-edit__label">{{ f.name }}</Label>
307
- <Input
308
- class="vm-data-i18n-edit__control"
309
- v-model="dataI18nEditForm.values[f.name]"
310
- />
311
- </div>
312
- </div>
313
- <DialogFooter class="vm-crud-shell__foot">
314
- <button
315
- type="button"
316
- class="vm-crud-shell__btn is-ghost"
317
- :disabled="dataI18nEditSubmitting"
318
- @click="dataI18nEditOpen = false"
319
- >
320
- {{ crudLabel('close', crud.dict.label) }}
321
- </button>
322
- <button
323
- type="button"
324
- class="vm-crud-shell__btn is-primary"
325
- :disabled="dataI18nEditSubmitting"
326
- @click="saveDataI18nEdit"
327
- >
328
- {{
329
- dataI18nEditSubmitting
330
- ? crudLabel('saving', crud.dict.label)
331
- : crudLabel('save', crud.dict.label)
332
- }}
333
- </button>
334
- </DialogFooter>
335
- </DialogContent>
336
- </Dialog>
337
- </div>
338
- </template>
339
-
340
- <script setup lang="ts">
341
- import { ref, computed, watch, onMounted, onBeforeUnmount, provide, useSlots, toValue } from 'vue'
342
- import { toast } from 'vue-sonner'
343
- import { useCrud } from './useCrud'
344
- import { injectTableOptions, TABLE_API_KEY } from './key'
345
- import { flattenTree } from '../lib/tree'
346
- import { resolveCrudComponent } from './registry'
347
- import { getCrudStyle, crudLabel, crudT, crudFormat, crudLocaleRev } from './style'
348
- import { findEpsEntity } from '../lib/eps'
349
- import { useVome } from '../hooks/useVome'
350
- import { vmConfirm } from './confirm'
351
- import { preventDialogOutsideClose } from '../lib/dialog-float'
352
- import { resolveFormSpan, useFormCols } from './span'
353
- import {
354
- canUseDataI18n,
355
- dataPackFromService,
356
- } from './data-i18n'
357
- import { pinOpColumnsLast, resolveMergedTableColumns } from './auto-fields'
358
- import type {
359
- CrudColumn,
360
- CrudOpButton,
361
- CtxMenuItem,
362
- } from '../../../typings/admin/comm/crud'
363
- defineOptions({ name: 'vm-table' })
364
-
365
- const props = withDefaults(
366
- defineProps<{
367
- columns?: CrudColumn[]
368
- selection?: boolean
369
- rowKey?: string
370
- }>(),
371
- {
372
- selection: undefined,
373
- rowKey: 'id',
374
- },
375
- )
376
-
377
- const slots = useSlots()
378
- const crud = useCrud()
379
- const tableOpts = injectTableOptions()
380
- const style = getCrudStyle().table
381
-
382
- const scrollRef = ref<HTMLElement | null>(null)
383
- /** empty-block:宽度取滚动容器可视宽,不跟表内容总宽 */
384
- const emptyWidth = ref(0)
385
-
386
- function syncEmptyWidth() {
387
- const el = scrollRef.value
388
- if (!el) return
389
- emptyWidth.value = el.clientWidth
390
- }
391
-
392
- const emptyBlockStyle = computed(() => ({
393
- width: emptyWidth.value ? `${emptyWidth.value}px` : '100%',
394
- }))
395
-
396
- let emptyRo: ResizeObserver | null = null
397
-
398
- const overrideColumns = ref<CrudColumn[] | null>(null)
399
-
400
- const DATA_I18N_ID_COL: CrudColumn = {
401
- prop: 'id',
402
- label: 'ID',
403
- minWidth: 100,
404
- }
405
-
406
- const DATA_I18N_OP_COL: CrudColumn = {
407
- type: 'op',
408
- buttons: ['edit', 'delete'],
409
- width: 140,
410
- fixed: 'right',
411
- }
412
-
413
- const isDataI18nView = computed(() => crud.trashMode.value === 'dataI18n')
414
-
415
- /** 语言 Tab:按表头/当前页内容估 minWidth(限幅) */
416
- function dataI18nColMinWidth(header: string, samples: string[]) {
417
- let maxChars = [...header].length
418
- for (const s of samples) {
419
- const n = [...String(s || '')].length
420
- if (n > maxChars) maxChars = n
421
- }
422
- return Math.min(360, Math.max(108, maxChars * 14 + 28))
423
- }
424
-
425
- function resolveManualColumns(): CrudColumn[] {
426
- return props.columns?.length ? props.columns : tableOpts?.columns || []
427
- }
428
-
429
- function resolveMergedColumns(): CrudColumn[] {
430
- // 依赖 permission,登录/换权后按 service._permission 重建 op
431
- void crud.permission.value
432
- return resolveMergedTableColumns({
433
- service: crud.service,
434
- tableOpts,
435
- tablePlugins: (getCrudStyle().table.plugins || []) as Array<{
436
- __plugin?: string
437
- ignoreFields?: string[]
438
- }>,
439
- manual: resolveManualColumns(),
440
- })
441
- }
442
-
443
- const columns = computed((): CrudColumn[] => {
444
- if (isDataI18nView.value) {
445
- void crudLocaleRev.value
446
- const fields = crud.dataI18nFields?.value || []
447
- const rows = crud.list.value || []
448
- const origSuffix = crudLabel('dataI18nOrigSuffix', crud.dict.label)
449
- const fieldCols: CrudColumn[] = []
450
- for (const f of fields) {
451
- const fieldName = f.name
452
- const srcProp = `__src_${fieldName}`
453
- const srcHeader = `${fieldName}${origSuffix}`
454
- const srcSamples = rows.map((r) => String(r[srcProp] ?? ''))
455
- const valSamples = rows.map((r) => String(r[fieldName] ?? ''))
456
- fieldCols.push({
457
- prop: srcProp,
458
- label: srcHeader,
459
- minWidth: dataI18nColMinWidth(srcHeader, srcSamples),
460
- })
461
- fieldCols.push({
462
- prop: fieldName,
463
- label: fieldName,
464
- minWidth: dataI18nColMinWidth(fieldName, valSamples),
465
- })
466
- }
467
- return pinOpColumnsLast([
468
- DATA_I18N_ID_COL,
469
- ...fieldCols,
470
- DATA_I18N_OP_COL,
471
- ])
472
- }
473
- const raw = overrideColumns.value || resolveMergedColumns()
474
- return pinOpColumnsLast(raw.filter((c) => !c.hidden))
475
- })
476
-
477
- const hasSelectionCol = computed(() => columns.value.some((c) => c.type === 'selection'))
478
- const hasOpCol = computed(() => columns.value.some((c) => c.type === 'op'))
479
- const showSelection = computed(() => {
480
- // 语言 Tab:始终多选(批量删译文)
481
- if (isDataI18nView.value) return true
482
- return props.selection !== undefined ? props.selection : hasSelectionCol.value
483
- })
484
- const dataColumns = computed(() =>
485
- columns.value.filter((c) => c.type !== 'selection'),
486
- )
487
- const showSlotOp = computed(
488
- () => Boolean(slots.op) && !hasOpCol.value && !isDataI18nView.value,
489
- )
490
-
491
- /** EPS 列 → field.{table}.{prop} */
492
- function epsFieldKey(prop: string): string | undefined {
493
- const ns = (crud.service as { namespace?: string } | undefined)?.namespace
494
- if (!ns || !prop) return undefined
495
- const entity = findEpsEntity(`/${ns}`)
496
- if (!entity) return undefined
497
- const cols = [
498
- ...((entity as { columns?: Array<{ propertyName?: string; table?: string; source?: string }> }).columns || []),
499
- ...((entity as { pageColumns?: Array<{ propertyName?: string; table?: string; source?: string }> }).pageColumns || []),
500
- ]
501
- const meta =
502
- cols.find((c) => c.propertyName === prop) ||
503
- cols.find((c) => c.source === `a.${prop}`) ||
504
- cols.find((c) => c.source?.endsWith(`.${prop}`))
505
- const table = meta?.table?.trim()
506
- const name = (meta?.propertyName || prop).trim()
507
- if (!table || !name) return undefined
508
- return `${table}.${name}`
509
- }
510
-
511
- /** 表头文案:labelKey → field.* → label / op;语言 Tab 用实体字段名,不走 field.* */
512
- function columnLabel(col: CrudColumn): string {
513
- void crudLocaleRev.value
514
- if (col.type === 'op') {
515
- return col.labelKey
516
- ? crudT(col.labelKey, col.label || crudLabel('op', crud.dict.label))
517
- : col.label || crudLabel('op', crud.dict.label)
518
- }
519
- const fb = col.label || col.prop || ''
520
- if (isDataI18nView.value && col.prop && col.prop !== 'id') {
521
- return fb
522
- }
523
- if (col.labelKey) return crudT(col.labelKey, fb)
524
- const fk = col.fieldKey || (col.prop ? epsFieldKey(col.prop) : undefined)
525
- if (fk) return crudT(`field.${fk}`, fb)
526
- return fb
527
- }
528
-
529
- const colSpan = computed(
530
- () =>
531
- dataColumns.value.length +
532
- (showSelection.value ? 1 : 0) +
533
- (showSlotOp.value ? 1 : 0),
534
- )
535
-
536
- const rowKey = computed(() =>
537
- isDataI18nView.value ? '__rowKey' : tableOpts?.rowKey || props.rowKey,
538
- )
539
-
540
- /** 行 Vue key:缺 rowKey 时用下标兜底,避免 Duplicate keys "undefined" */
541
- function vueRowKey(row: Record<string, unknown>, index: number) {
542
- const k = row[rowKey.value]
543
- if (k != null && String(k) !== '') return String(k)
544
- return `idx-${index}`
545
- }
546
-
547
- const isTree = computed(() => Boolean(tableOpts?.tree))
548
- const border = computed(() => tableOpts?.border ?? style.border)
549
- const autoHeight = computed(() => tableOpts?.autoHeight ?? style.autoHeight)
550
- const highlight = computed(() => style.highlightCurrentRow)
551
- const opWidth = computed(() => style.column.opWidth)
552
- const defaultAlign = computed(() => style.column.align)
553
- const sortRefresh = computed(() => tableOpts?.sortRefresh !== false)
554
-
555
- function normalizeSortDir(
556
- order?: string,
557
- ): 'asc' | 'desc' | undefined {
558
- if (order === 'asc' || order === 'ascending') return 'asc'
559
- if (order === 'desc' || order === 'descending') return 'desc'
560
- return undefined
561
- }
562
-
563
- function initSortState() {
564
- const ds = tableOpts?.defaultSort
565
- if (!ds?.prop) return null
566
- return {
567
- prop: ds.prop,
568
- order: normalizeSortDir(ds.order) || ('asc' as const),
569
- }
570
- }
571
-
572
- const sortState = ref<{ prop: string; order: 'asc' | 'desc' } | null>(
573
- initSortState(),
574
- )
575
-
576
- /** dict.sort → 本项目为 { order: 字段, sort: 方向 } */
577
- function sortPayload(prop: string, order: 'asc' | 'desc') {
578
- return {
579
- [crud.dict.sort.prop]: prop,
580
- [crud.dict.sort.order]: order,
581
- }
582
- }
583
-
584
- function isSortable(col: CrudColumn) {
585
- if (!col.prop || col.type === 'op' || col.type === 'selection' || col.type === 'index') {
586
- return false
587
- }
588
- return Boolean(col.sortable)
589
- }
590
-
591
- function sortIconClass(prop: string) {
592
- if (sortState.value?.prop !== prop) return 'ri-arrow-up-down-line is-muted'
593
- return sortState.value.order === 'asc'
594
- ? 'ri-arrow-up-s-fill is-active'
595
- : 'ri-arrow-down-s-fill is-active'
596
- }
597
-
598
- function displayIndex(idx: string | number) {
599
- return Number(idx) + 1
600
- }
601
-
602
- function onSortClick(col: CrudColumn) {
603
- if (!isSortable(col) || !col.prop) return
604
- const prop = col.prop
605
- let next: 'asc' | 'desc' = 'asc'
606
- const current = sortState.value
607
- if (current && current.prop === prop) {
608
- next = current.order === 'asc' ? 'desc' : 'asc'
609
- } else {
610
- const hint = normalizeSortDir(
611
- typeof col.sortable === 'string' ? col.sortable : undefined,
612
- )
613
- next = hint || 'asc'
614
- }
615
- sortState.value = { prop, order: next }
616
- if (sortRefresh.value) void crud.refresh(sortPayload(prop, next))
617
- }
618
-
619
- /** col.width / col.minWidth 优先;op 列未设 width 时回退全局 opWidth */
620
- function toPx(v: string | number) {
621
- return typeof v === 'number' ? `${v}px` : v
622
- }
623
-
624
- function colFixed(col: CrudColumn): 'left' | 'right' | undefined {
625
- if (col.fixed === 'left' || col.fixed === 'right') return col.fixed
626
- if (col.type === 'op') return 'right'
627
- return undefined
628
- }
629
-
630
- function colWidthPx(col: CrudColumn): number {
631
- if (col.type === 'op') {
632
- const w = col.width ?? opWidth.value
633
- return typeof w === 'number' ? w : Number.parseInt(String(w), 10) || 140
634
- }
635
- if (col.width != null) {
636
- return typeof col.width === 'number'
637
- ? col.width
638
- : Number.parseInt(String(col.width), 10) || 100
639
- }
640
- if (col.minWidth != null) {
641
- return typeof col.minWidth === 'number'
642
- ? col.minWidth
643
- : Number.parseInt(String(col.minWidth), 10) || 100
644
- }
645
- return 100
646
- }
647
-
648
- /** 自右向左累加 sticky right 偏移(多列 fixed:right 堆叠) */
649
- const fixedRightOffset = computed(() => {
650
- const map = new Map<number, number>()
651
- let acc = 0
652
- const cols = dataColumns.value
653
- for (let i = cols.length - 1; i >= 0; i--) {
654
- if (colFixed(cols[i]!) !== 'right') continue
655
- map.set(i, acc)
656
- acc += colWidthPx(cols[i]!)
657
- }
658
- return map
659
- })
660
-
661
- function colStyle(col: CrudColumn, index?: number) {
662
- const base: Record<string, string | undefined> = {
663
- textAlign: (col.align || defaultAlign.value) as string | undefined,
664
- }
665
- if (col.type === 'op') {
666
- const w = col.width ?? opWidth.value
667
- base.width = toPx(w)
668
- base.minWidth = toPx(w)
669
- base.whiteSpace = 'nowrap'
670
- } else {
671
- if (col.width != null) base.width = toPx(col.width)
672
- if (col.minWidth != null) base.minWidth = toPx(col.minWidth)
673
- }
674
- const fixed = colFixed(col)
675
- if (fixed === 'right' && index != null) {
676
- base.position = 'sticky'
677
- base.right = `${fixedRightOffset.value.get(index) ?? 0}px`
678
- // z-index 只走 CSS:表头高于正文,禁止内联 3/4 压过 th
679
- } else if (fixed === 'left') {
680
- base.position = 'sticky'
681
- base.left = '0'
682
- }
683
- return base
684
- }
685
- const ctxEnabled = computed(() => {
686
- if (isDataI18nView.value) return false
687
- const c = tableOpts?.contextMenu ?? style.contextMenu
688
- return c !== false && !(Array.isArray(c) && c.length === 0)
689
- })
690
-
691
- const expanded = ref(new Set<string | number>())
692
- const currentId = ref('')
693
- const ctxRef = ref<{ open: (e: MouseEvent) => void } | null>(null)
694
- const ctxRow = ref<Record<string, unknown> | null>(null)
695
-
696
- const ctxItems = computed<CtxMenuItem[]>(() => {
697
- const conf = tableOpts?.contextMenu ?? style.contextMenu
698
- const keys = Array.isArray(conf)
699
- ? conf
700
- : ['refresh', 'check', 'edit', 'delete', 'order-asc', 'order-desc']
701
- const map: Record<string, string> = {
702
- refresh: crudLabel('refresh', crud.dict.label),
703
- check: crudLabel('select', crud.dict.label),
704
- edit: crudLabel('update', crud.dict.label),
705
- delete: crudLabel('delete', crud.dict.label),
706
- 'order-asc': crudLabel('asc', crud.dict.label),
707
- 'order-desc': crudLabel('desc', crud.dict.label),
708
- }
709
- return keys.map((k) => ({
710
- key: k,
711
- label: map[k] || k,
712
- disabled:
713
- (k === 'edit' && !crud.getPermission('update')) ||
714
- (k === 'delete' && !crud.getPermission('delete')),
715
- }))
716
- })
717
-
718
- /** 树表默认仅展示一级(不展开);defaultExpandAll / defaultExpandLevel 可覆盖 */
719
- watch(
720
- () => crud.list.value,
721
- (rows) => {
722
- if (!isTree.value || expanded.value.size) return
723
- const next = new Set<string | number>()
724
- if (tableOpts?.defaultExpandAll) {
725
- for (const r of rows) {
726
- if (Array.isArray(r.children) && (r.children as unknown[]).length) {
727
- next.add(r[rowKey.value] as string | number)
728
- const stack = [...(r.children as Record<string, unknown>[])]
729
- while (stack.length) {
730
- const n = stack.pop()!
731
- if (Array.isArray(n.children) && (n.children as unknown[]).length) {
732
- next.add(n[rowKey.value] as string | number)
733
- stack.push(...(n.children as Record<string, unknown>[]))
734
- }
735
- }
736
- }
737
- }
738
- } else {
739
- const level = Math.max(1, tableOpts?.defaultExpandLevel ?? 1)
740
- const walk = (nodes: Record<string, unknown>[], depth: number) => {
741
- if (depth >= level - 1) return
742
- for (const n of nodes) {
743
- const kids = n.children as Record<string, unknown>[] | undefined
744
- if (!kids?.length) continue
745
- next.add(n[rowKey.value] as string | number)
746
- walk(kids, depth + 1)
747
- }
748
- }
749
- walk(rows, 0)
750
- }
751
- if (next.size) expanded.value = next
752
- },
753
- { immediate: true },
754
- )
755
-
756
- type FlatRow = Record<string, unknown> & {
757
- depth?: number
758
- hasChildren?: boolean
759
- }
760
-
761
- const displayRows = computed<FlatRow[]>(() => {
762
- const rows = crud.list.value as FlatRow[]
763
- if (!isTree.value) return rows
764
- return flattenTree(rows as never, expanded.value) as FlatRow[]
765
- })
766
-
767
- const selectedIds = computed(
768
- () => new Set(crud.selection.value.map((r) => String(r[rowKey.value]))),
769
- )
770
-
771
- const allChecked = computed(() => {
772
- const rows = displayRows.value
773
- return (
774
- rows.length > 0 &&
775
- rows.every((r) => selectedIds.value.has(String(r[rowKey.value])))
776
- )
777
- })
778
-
779
- /** 自身 + 全部子孙(树勾选 / 行删级联) */
780
- function collectSubtree(row: Record<string, unknown>): Record<string, unknown>[] {
781
- const out: Record<string, unknown>[] = [row]
782
- const kids = row.children as Record<string, unknown>[] | undefined
783
- if (!kids?.length) return out
784
- for (const c of kids) out.push(...collectSubtree(c))
785
- return out
786
- }
787
-
788
- /** 树表行删带上子集;普通表仍只删当前行 */
789
- function deleteRowTargets(row: Record<string, unknown>) {
790
- return isTree.value ? collectSubtree(row) : [row]
791
- }
792
-
793
- const { service: hostService } = useVome()
794
- const dataI18nEditOpen = ref(false)
795
- const dataI18nEditSubmitting = ref(false)
796
- const dataI18nEditForm = ref<{
797
- id: string
798
- values: Record<string, string>
799
- sources: Record<string, string>
800
- }>({ id: '', values: {}, sources: {} })
801
- const dataI18nEditFormCols = useFormCols()
802
- const dataI18nEditSpan = computed(() =>
803
- resolveFormSpan(12, dataI18nEditFormCols.value),
804
- )
805
- const dataI18nEditFields = computed(
806
- () => crud.dataI18nFields?.value || [],
807
- )
808
-
809
- function onDataI18nEditOpenChange(v: boolean) {
810
- dataI18nEditOpen.value = v
811
- }
812
-
813
- function openDataI18nEdit(row: Record<string, unknown>) {
814
- const fields = crud.dataI18nFields?.value || []
815
- const sources = (row.__sources as Record<string, string>) || {}
816
- const values: Record<string, string> = {}
817
- const src: Record<string, string> = {}
818
- for (const f of fields) {
819
- values[f.name] = String(row[f.name] ?? '')
820
- src[f.name] = String(sources[f.name] ?? '')
821
- }
822
- dataI18nEditForm.value = {
823
- id: String(row.id ?? ''),
824
- values,
825
- sources: src,
826
- }
827
- dataI18nEditOpen.value = true
828
- }
829
-
830
- async function saveDataI18nEdit() {
831
- if (dataI18nEditSubmitting.value) return
832
- const meta = canUseDataI18n(crud.service, hostService)
833
- const dataPack = dataPackFromService(hostService)
834
- const lang = crud.dataI18nLangCode?.value
835
- if (!meta.ok || !dataPack?.updateEntry || !lang) {
836
- toast.error(crudLabel('dataI18nNoService', crud.dict.label))
837
- return
838
- }
839
- dataI18nEditSubmitting.value = true
840
- try {
841
- await dataPack.updateEntry({
842
- tableName: meta.tableName,
843
- langCode: lang,
844
- id: dataI18nEditForm.value.id,
845
- values: { ...dataI18nEditForm.value.values },
846
- })
847
- toast.success(crudLabel('saveSuccess', crud.dict.label))
848
- dataI18nEditOpen.value = false
849
- await crud.refresh()
850
- } catch (e) {
851
- toast.error(e instanceof Error ? e.message : crudLabel('saveFailed'))
852
- } finally {
853
- dataI18nEditSubmitting.value = false
854
- }
855
- }
856
-
857
- async function deleteDataI18nEntry(row: Record<string, unknown>) {
858
- const meta = canUseDataI18n(crud.service, hostService)
859
- const dataPack = dataPackFromService(hostService)
860
- const lang = crud.dataI18nLangCode?.value
861
- if (!meta.ok || !dataPack?.deleteEntry || !lang) {
862
- toast.error(crudLabel('dataI18nNoService', crud.dict.label))
863
- return
864
- }
865
- const id = String(row.id ?? '')
866
- if (!id) return
867
- const ok = await vmConfirm({
868
- title: crudLabel('tips', crud.dict.label),
869
- message: crudFormat(
870
- crudLabel('dataI18nDeleteConfirm', crud.dict.label),
871
- id,
872
- ),
873
- confirmText: crudLabel('confirm', crud.dict.label),
874
- cancelText: crudLabel('close', crud.dict.label),
875
- })
876
- if (!ok) return
877
- try {
878
- await dataPack.deleteEntry({
879
- tableName: meta.tableName,
880
- langCode: lang,
881
- id,
882
- })
883
- toast.success(crudLabel('deleteSuccess', crud.dict.label))
884
- await crud.refresh()
885
- } catch (e) {
886
- toast.error(e instanceof Error ? e.message : crudLabel('deleteFailed'))
887
- }
888
- }
889
-
890
- function toggleAll(checked: boolean | 'indeterminate') {
891
- if (checked === true) {
892
- // 树表:含折叠未展示的子孙
893
- if (isTree.value) {
894
- const all: Record<string, unknown>[] = []
895
- for (const r of crud.list.value) all.push(...collectSubtree(r))
896
- crud.setSelection(all)
897
- return
898
- }
899
- crud.setSelection([...displayRows.value])
900
- } else {
901
- crud.setSelection([])
902
- }
903
- }
904
-
905
- function toggleRow(row: Record<string, unknown>, checked: boolean | 'indeterminate') {
906
- const key = rowKey.value
907
- if (isTree.value) {
908
- const subtree = collectSubtree(row)
909
- const ids = new Set(subtree.map((r) => String(r[key])))
910
- if (checked === true) {
911
- const map = new Map(
912
- crud.selection.value.map((r) => [String(r[key]), r] as const),
913
- )
914
- for (const r of subtree) map.set(String(r[key]), r)
915
- crud.setSelection([...map.values()])
916
- } else {
917
- crud.setSelection(
918
- crud.selection.value.filter((r) => !ids.has(String(r[key]))),
919
- )
920
- }
921
- return
922
- }
923
- const id = String(row[key])
924
- if (checked === true) {
925
- if (!selectedIds.value.has(id)) crud.setSelection([...crud.selection.value, row])
926
- } else {
927
- crud.setSelection(
928
- crud.selection.value.filter((r) => String(r[key]) !== id),
929
- )
930
- }
931
- }
932
-
933
- function toggleExpand(id: string | number) {
934
- const next = new Set(expanded.value)
935
- if (next.has(id)) next.delete(id)
936
- else next.add(id)
937
- expanded.value = next
938
- }
939
-
940
- function resolveButtons(col: CrudColumn, row: Record<string, unknown>): CrudOpButton[] {
941
- const raw = col.buttons
942
- if (typeof raw === 'function') return raw(row)
943
- return raw?.length ? raw : (['edit', 'delete'] as CrudOpButton[])
944
- }
945
-
946
- function cellComp(col: CrudColumn) {
947
- return resolveCrudComponent(col.component?.name)
948
- }
949
-
950
- /** dict: dict.options('x') → 彩色标签;props 可为 dict.get(...) → switch 等 */
951
- function resolveDict(col: CrudColumn) {
952
- const list = col.dict == null ? [] : toValue(col.dict)
953
- return Array.isArray(list) ? list : []
954
- }
955
-
956
- /** props 可为 dict.get(...)(ComputedRef / 数组)→ 作 options */
957
- function resolveCellOptions(col: CrudColumn) {
958
- const fromDict = resolveDict(col)
959
- if (fromDict.length) return fromDict
960
- const raw = toValue(col.component?.props)
961
- return Array.isArray(raw) ? raw : []
962
- }
963
-
964
- function resolveCellProps(col: CrudColumn) {
965
- const raw = col.component?.props
966
- const val = toValue(raw)
967
- if (val == null || Array.isArray(val)) return {}
968
- if (typeof val === 'object') return val as Record<string, unknown>
969
- return {}
970
- }
971
-
972
- function cellText(col: CrudColumn, row: Record<string, unknown>) {
973
- if (!col.prop) return ''
974
- const val = row[col.prop]
975
- if (col.formatter) return col.formatter(row, val)
976
- if (val == null) return ''
977
- return String(val)
978
- }
979
-
980
- function onRowClick(row: Record<string, unknown>) {
981
- if (highlight.value) currentId.value = String(row[rowKey.value])
982
- }
983
-
984
- function onContextMenu(e: MouseEvent, row: Record<string, unknown>) {
985
- if (!ctxEnabled.value) return
986
- ctxRow.value = row
987
- ctxRef.value?.open(e)
988
- }
989
-
990
- function onCtxSelect(key: string) {
991
- const row = ctxRow.value
992
- if (!row) return
993
- if (key === 'refresh') void crud.refresh()
994
- if (key === 'check') toggleRow(row, true)
995
- if (key === 'edit') void crud.rowEdit(row)
996
- if (key === 'delete') void crud.rowDelete(deleteRowTargets(row))
997
- if (key === 'order-asc') {
998
- sortState.value = { prop: 'orderNum', order: 'asc' }
999
- void crud.refresh(sortPayload('orderNum', 'asc'))
1000
- }
1001
- if (key === 'order-desc') {
1002
- sortState.value = { prop: 'orderNum', order: 'desc' }
1003
- void crud.refresh(sortPayload('orderNum', 'desc'))
1004
- }
1005
- }
1006
-
1007
- function baseColumns(): CrudColumn[] {
1008
- return overrideColumns.value || resolveMergedColumns()
1009
- }
1010
-
1011
- function setColumns(cols: CrudColumn[]) {
1012
- overrideColumns.value = pinOpColumnsLast(cols)
1013
- }
1014
-
1015
- function showColumn(prop: string) {
1016
- setColumns(
1017
- baseColumns().map((c) =>
1018
- c.prop === prop ? { ...c, hidden: false } : c,
1019
- ),
1020
- )
1021
- }
1022
-
1023
- function hideColumn(prop: string) {
1024
- setColumns(
1025
- baseColumns().map((c) =>
1026
- c.prop === prop ? { ...c, hidden: true } : c,
1027
- ),
1028
- )
1029
- }
1030
-
1031
- function getColumns() {
1032
- return baseColumns()
1033
- }
1034
-
1035
- function columnCustomKey() {
1036
- return `column-custom__${typeof location !== 'undefined' ? location.pathname : 'default'}`
1037
- }
1038
-
1039
- function applyColumnCustomMap(
1040
- cols: CrudColumn[],
1041
- map: Record<string, { checked: boolean; order?: number }>,
1042
- ) {
1043
- const ordered = [...cols].sort((a, b) => {
1044
- const ao = a.prop != null ? map[a.prop]?.order ?? 999 : 999
1045
- const bo = b.prop != null ? map[b.prop]?.order ?? 999 : 999
1046
- return ao - bo
1047
- })
1048
- return ordered.map((c) => {
1049
- if (!c.prop || c.type === 'selection' || c.type === 'op') return c
1050
- return { ...c, hidden: !(map[c.prop]?.checked ?? true) }
1051
- })
1052
- }
1053
-
1054
- onMounted(() => {
1055
- syncEmptyWidth()
1056
- if (typeof ResizeObserver !== 'undefined' && scrollRef.value) {
1057
- emptyRo = new ResizeObserver(() => syncEmptyWidth())
1058
- emptyRo.observe(scrollRef.value)
1059
- }
1060
-
1061
- try {
1062
- const raw = localStorage.getItem(columnCustomKey())
1063
- if (!raw) return
1064
- const map = JSON.parse(raw) as Record<
1065
- string,
1066
- { checked: boolean; order?: number }
1067
- >
1068
- const base = resolveMergedColumns()
1069
- if (!base.length) return
1070
- overrideColumns.value = applyColumnCustomMap(base, map)
1071
- } catch {
1072
- /* ignore */
1073
- }
1074
- })
1075
-
1076
- onBeforeUnmount(() => {
1077
- emptyRo?.disconnect()
1078
- emptyRo = null
1079
- })
1080
-
1081
- watch(
1082
- () => displayRows.value.length,
1083
- () => {
1084
- requestAnimationFrame(syncEmptyWidth)
1085
- },
1086
- )
1087
-
1088
- provide(TABLE_API_KEY, {
1089
- setColumns,
1090
- showColumn,
1091
- hideColumn,
1092
- getColumns,
1093
- })
1094
-
1095
- defineExpose({
1096
- showColumn,
1097
- hideColumn,
1098
- setColumns,
1099
- data: displayRows,
1100
- })
1101
- </script>
1102
-
1103
- <style lang="scss" scoped>
1104
- .vm-crud-table {
1105
- position: relative;
1106
- display: flex;
1107
- width: 100%;
1108
- flex: 1 1 auto;
1109
- flex-direction: column;
1110
- overflow: hidden;
1111
-
1112
- &.is-border {
1113
- border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
1114
- }
1115
-
1116
- &.is-auto-height {
1117
- min-height: 0;
1118
- }
1119
- }
1120
-
1121
- .vm-crud-table__scroll {
1122
- width: 100%;
1123
- min-width: 0;
1124
- min-height: 0;
1125
- flex: 1 1 auto;
1126
- overflow: auto;
1127
- overscroll-behavior: contain;
1128
- }
1129
-
1130
- /* 表铺满容器;列 minWidth 过大时由 __scroll 横向滚动 */
1131
- :deep([data-slot='table-container']) {
1132
- width: 100%;
1133
- min-width: 100%;
1134
- overflow: visible !important;
1135
- }
1136
-
1137
- :deep([data-slot='table']) {
1138
- width: 100%;
1139
- }
1140
-
1141
- .vm-crud-table__loading {
1142
- position: absolute;
1143
- inset: 0;
1144
- z-index: 10;
1145
- display: flex;
1146
- align-items: center;
1147
- justify-content: center;
1148
- background: color-mix(in srgb, var(--card) 70%, transparent);
1149
- color: var(--muted-foreground);
1150
- font-size: 12px;
1151
- backdrop-filter: blur(1px);
1152
- }
1153
-
1154
- /* empty-block:在表体内,sticky 贴可视左缘,不盖表头 */
1155
- :deep(.vm-crud-table__empty-row) {
1156
- &:hover {
1157
- background: transparent !important;
1158
- }
1159
- }
1160
-
1161
- :deep(.vm-crud-table__empty-cell) {
1162
- height: 240px;
1163
- padding: 0 !important;
1164
- border-bottom: none !important;
1165
- }
1166
-
1167
- .vm-crud-table__empty-block {
1168
- position: sticky;
1169
- left: 0;
1170
- display: flex;
1171
- height: 240px;
1172
- align-items: center;
1173
- justify-content: center;
1174
- overflow: hidden;
1175
- }
1176
-
1177
- :deep(.vm-crud-table__head-row) {
1178
- border-color: color-mix(in srgb, var(--border) 70%, transparent);
1179
- background: var(--card);
1180
-
1181
- &:hover {
1182
- background: var(--card) !important;
1183
- }
1184
- }
1185
-
1186
- /* 纵向滚动时表头固定;横向 fixed 列靠 CSS z-index,表头必须高于正文 sticky td */
1187
- :deep(.vm-crud-table__th),
1188
- :deep(.vm-crud-table__head-row .vm-crud-table__check) {
1189
- position: sticky;
1190
- top: 0;
1191
- z-index: 4;
1192
- background: var(--card);
1193
- }
1194
-
1195
- :deep(.vm-crud-table__td.is-fixed-left),
1196
- :deep(.vm-crud-table__td.is-fixed-right) {
1197
- z-index: 1;
1198
- }
1199
-
1200
- :deep(.vm-crud-table__th.is-fixed-left) {
1201
- z-index: 5;
1202
- }
1203
-
1204
- :deep(.vm-crud-table__th.is-fixed-right) {
1205
- z-index: 6;
1206
- }
1207
-
1208
- :deep(.vm-crud-table__th),
1209
- :deep(.vm-crud-table__td) {
1210
- &.is-fixed-right {
1211
- box-shadow: -4px 0 8px color-mix(in srgb, var(--foreground) 6%, transparent);
1212
- }
1213
-
1214
- &.is-fixed-left {
1215
- box-shadow: 4px 0 8px color-mix(in srgb, var(--foreground) 6%, transparent);
1216
- }
1217
- }
1218
-
1219
- /* 表头 sticky 自带实底;正文 fixed 继承行底,禁止写死 --card 再靠 hover 改色盖住 */
1220
- :deep(.vm-crud-table__th.is-fixed-right),
1221
- :deep(.vm-crud-table__th.is-fixed-left) {
1222
- background: var(--card);
1223
- }
1224
-
1225
- :deep(.vm-crud-table__td.is-fixed-right),
1226
- :deep(.vm-crud-table__td.is-fixed-left) {
1227
- background: inherit;
1228
- }
1229
-
1230
- :deep(.vm-crud-table__th) {
1231
- height: 42px;
1232
- max-height: 42px;
1233
- box-sizing: border-box;
1234
- overflow: hidden;
1235
- color: var(--muted-foreground);
1236
- font-size: 12px;
1237
- font-weight: 600;
1238
- white-space: nowrap;
1239
- vertical-align: middle;
1240
-
1241
- &.is-sortable {
1242
- cursor: pointer;
1243
- user-select: none;
1244
-
1245
- &:hover {
1246
- color: var(--foreground);
1247
- }
1248
- }
1249
- }
1250
-
1251
- .vm-crud-table__th-inner {
1252
- display: inline-flex;
1253
- align-items: center;
1254
- gap: 4px;
1255
- }
1256
-
1257
- .vm-crud-table__sort {
1258
- font-size: 14px;
1259
- line-height: 1;
1260
- opacity: 0.45;
1261
-
1262
- &.is-active {
1263
- color: var(--brand);
1264
- opacity: 1;
1265
- }
1266
-
1267
- &.is-muted {
1268
- opacity: 0.35;
1269
- }
1270
- }
1271
-
1272
- .vm-crud-table__ops {
1273
- display: inline-flex;
1274
- flex-wrap: nowrap;
1275
- gap: 8px;
1276
- white-space: nowrap;
1277
- }
1278
-
1279
- :deep(.vm-crud-table__op) {
1280
- white-space: nowrap;
1281
- }
1282
-
1283
- :deep(.vm-crud-table__check) {
1284
- width: 40px;
1285
- padding-left: 4px;
1286
- }
1287
-
1288
- :deep(.vm-crud-table__row) {
1289
- height: 48px;
1290
- border-color: color-mix(in srgb, var(--border) 55%, transparent);
1291
- /* 行自带实底,fixed td inherit 才不透横滚;高亮只改 tr,不改子节点色 */
1292
- background: var(--card);
1293
- transition: background 0.15s ease;
1294
-
1295
- &:hover,
1296
- &.is-current {
1297
- background: var(--brand-soft);
1298
- }
1299
- }
1300
-
1301
- :deep(.vm-crud-table__td) {
1302
- padding-top: 10px;
1303
- padding-bottom: 10px;
1304
- font-size: 13px;
1305
- }
1306
-
1307
- .vm-crud-table__tree-cell {
1308
- display: flex;
1309
- align-items: center;
1310
- gap: 4px;
1311
- }
1312
-
1313
- .vm-crud-table__fold {
1314
- display: inline-flex;
1315
- width: 22px;
1316
- height: 22px;
1317
- flex-shrink: 0;
1318
- align-items: center;
1319
- justify-content: center;
1320
- border: none;
1321
- border-radius: 8px;
1322
- background: transparent;
1323
- color: var(--muted-foreground);
1324
- cursor: pointer;
1325
-
1326
- &.is-spacer {
1327
- cursor: default;
1328
- }
1329
-
1330
- &:not(.is-spacer):hover {
1331
- background: var(--muted);
1332
- color: var(--foreground);
1333
- }
1334
- }
1335
-
1336
- .vm-data-i18n-edit__body {
1337
- /* 与业务翻译弹窗一致:24 栅格 + span12;继承 shell body 的 flex:1 */
1338
- display: grid;
1339
- grid-template-columns: repeat(24, minmax(0, 1fr));
1340
- gap: 12px 16px;
1341
- align-content: start;
1342
- padding: 18px 22px 16px;
1343
-
1344
- @media (max-width: 768px) {
1345
- grid-template-columns: repeat(12, minmax(0, 1fr));
1346
- gap: 10px;
1347
- padding: 12px;
1348
- }
1349
- }
1350
-
1351
- .vm-data-i18n-edit__field {
1352
- min-width: 0;
1353
- }
1354
-
1355
- .vm-data-i18n-edit__label {
1356
- display: inline-flex;
1357
- align-items: center;
1358
- gap: 2px;
1359
- margin-bottom: 6px;
1360
- font-size: 12px;
1361
- color: var(--muted-foreground);
1362
- }
1363
-
1364
- .vm-data-i18n-edit__req {
1365
- color: var(--destructive);
1366
- }
1367
-
1368
- .vm-data-i18n-edit__control {
1369
- width: 100%;
1370
- height: var(--vm-control-height) !important;
1371
- min-height: var(--vm-control-height);
1372
- border-radius: var(--vm-control-radius) !important;
1373
- font-size: var(--vm-control-font-size) !important;
1374
-
1375
- &.is-readonly {
1376
- background: var(--muted) !important;
1377
- color: var(--muted-foreground) !important;
1378
- opacity: 1 !important;
1379
- cursor: default;
1380
- }
1381
- }
1382
-
1383
- .vm-data-i18n-edit__source {
1384
- margin-top: 6px;
1385
- font-size: 12px;
1386
- line-height: 1.4;
1387
- color: var(--muted-foreground);
1388
- word-break: break-word;
1389
- }
1390
- </style>
1
+ <template>
2
+ <div
3
+ class="vm-crud-table"
4
+ :class="{
5
+ 'is-border': border,
6
+ 'is-auto-height': autoHeight,
7
+ }"
8
+ >
9
+ <div v-if="crud.loading.value" class="vm-crud-table__loading">
10
+ {{ crudLabel('loading', crud.dict.label) }}
11
+ </div>
12
+ <div ref="scrollRef" class="vm-crud-table__scroll vm-scroll">
13
+ <Table>
14
+ <TableHeader>
15
+ <TableRow class="vm-crud-table__head-row">
16
+ <TableHead v-if="showSelection" class="vm-crud-table__th vm-crud-table__check">
17
+ <Checkbox :model-value="allChecked" @update:model-value="toggleAll" />
18
+ </TableHead>
19
+ <TableHead
20
+ v-for="(col, i) in dataColumns"
21
+ :key="col.prop || col.type || i"
22
+ class="vm-crud-table__th"
23
+ :class="{
24
+ 'vm-crud-table__op': col.type === 'op',
25
+ 'is-sortable': isSortable(col),
26
+ 'is-fixed-right': colFixed(col) === 'right',
27
+ 'is-fixed-left': colFixed(col) === 'left',
28
+ }"
29
+ :style="colStyle(col, i)"
30
+ @click="onSortClick(col)"
31
+ >
32
+ <span class="vm-crud-table__th-inner">
33
+ {{ columnLabel(col) }}
34
+ <i
35
+ v-if="isSortable(col) && col.prop"
36
+ class="vm-crud-table__sort"
37
+ :class="sortIconClass(col.prop)"
38
+ />
39
+ </span>
40
+ </TableHead>
41
+ <TableHead
42
+ v-if="showSlotOp"
43
+ class="vm-crud-table__th vm-crud-table__op"
44
+ :style="{ width: `${opWidth}px`, minWidth: `${opWidth}px` }"
45
+ >
46
+ {{ crudLabel('op', crud.dict.label) }}
47
+ </TableHead>
48
+ </TableRow>
49
+ </TableHeader>
50
+ <TableBody>
51
+ <!-- empty-block:表体内 sticky,宽度=可视区,不盖表头、不跟总表宽跑 -->
52
+ <TableRow
53
+ v-if="!displayRows.length && !crud.loading.value"
54
+ class="vm-crud-table__empty-row"
55
+ >
56
+ <TableCell :colspan="colSpan" class="vm-crud-table__empty-cell">
57
+ <div class="vm-crud-table__empty-block" :style="emptyBlockStyle">
58
+ <vm-empty kind="暂无数据" />
59
+ </div>
60
+ </TableCell>
61
+ </TableRow>
62
+ <TableRow
63
+ v-for="(row, rowIndex) in displayRows"
64
+ :key="vueRowKey(row, rowIndex)"
65
+ class="vm-crud-table__row"
66
+ :class="{ 'is-current': highlight && String(row[rowKey]) === currentId }"
67
+ @click="onRowClick(row)"
68
+ @contextmenu="(e: MouseEvent) => onContextMenu(e, row)"
69
+ >
70
+ <TableCell v-if="showSelection" class="vm-crud-table__check">
71
+ <Checkbox
72
+ :model-value="selectedIds.has(String(row[rowKey]))"
73
+ @update:model-value="(v) => toggleRow(row, v)"
74
+ />
75
+ </TableCell>
76
+ <TableCell
77
+ v-for="(col, i) in dataColumns"
78
+ :key="col.prop || col.type || i"
79
+ class="vm-crud-table__td"
80
+ :class="{
81
+ 'vm-crud-table__op': col.type === 'op',
82
+ 'is-fixed-right': colFixed(col) === 'right',
83
+ 'is-fixed-left': colFixed(col) === 'left',
84
+ }"
85
+ :style="colStyle(col, i)"
86
+ >
87
+ <template v-if="col.type === 'index'">
88
+ {{ displayIndex(rowIndex) }}
89
+ </template>
90
+ <template v-else-if="col.type === 'op'">
91
+ <div
92
+ v-if="crud.trashMode.value === 'onlyTrashed'"
93
+ class="vm-crud-table__ops"
94
+ >
95
+ <Button
96
+ v-if="crud.getPermission('restore')"
97
+ size="xs"
98
+ variant="softPrimary"
99
+ @click="crud.rowRestore([row])"
100
+ >
101
+ {{ crudLabel('restore', crud.dict.label) }}
102
+ </Button>
103
+ <Button
104
+ v-if="crud.getPermission('delete')"
105
+ size="xs"
106
+ variant="softDestructive"
107
+ @click="crud.rowDelete(deleteRowTargets(row), true)"
108
+ >
109
+ {{ crudLabel('forceDelete', crud.dict.label) }}
110
+ </Button>
111
+ </div>
112
+ <div
113
+ v-else-if="crud.trashMode.value === 'dataI18n'"
114
+ class="vm-crud-table__ops"
115
+ >
116
+ <Button
117
+ type="button"
118
+ size="xs"
119
+ variant="softPrimary"
120
+ @click.stop="openDataI18nEdit(row)"
121
+ >
122
+ {{ crudLabel('update', crud.dict.label) }}
123
+ </Button>
124
+ <Button
125
+ type="button"
126
+ size="xs"
127
+ variant="softDestructive"
128
+ @click.stop="deleteDataI18nEntry(row)"
129
+ >
130
+ {{ crudLabel('delete', crud.dict.label) }}
131
+ </Button>
132
+ </div>
133
+ <div v-else class="vm-crud-table__ops">
134
+ <template v-for="(btn, bi) in resolveButtons(col, row)" :key="bi">
135
+ <Button
136
+ v-if="btn === 'edit' && crud.getPermission('update')"
137
+ type="button"
138
+ size="xs"
139
+ variant="softPrimary"
140
+ @click.stop="crud.rowEdit(row)"
141
+ >
142
+ {{ crudLabel('update', crud.dict.label) }}
143
+ </Button>
144
+ <Button
145
+ v-else-if="btn === 'delete' && crud.getPermission('delete')"
146
+ type="button"
147
+ size="xs"
148
+ variant="softDestructive"
149
+ @click.stop="crud.rowDelete(deleteRowTargets(row))"
150
+ >
151
+ {{ crudLabel('delete', crud.dict.label) }}
152
+ </Button>
153
+ <Button
154
+ v-else-if="btn === 'info' && crud.getPermission('info')"
155
+ type="button"
156
+ size="xs"
157
+ variant="ghost"
158
+ @click.stop="crud.rowInfo(row)"
159
+ >
160
+ {{ crudLabel('info', crud.dict.label) }}
161
+ </Button>
162
+ <Button
163
+ v-else-if="typeof btn === 'object' && !btn.hidden"
164
+ type="button"
165
+ size="xs"
166
+ :disabled="btn.disabled"
167
+ :variant="btn.variant || 'softSuccess'"
168
+ @click.stop="btn.onClick(row)"
169
+ >
170
+ {{ btn.label }}
171
+ </Button>
172
+ </template>
173
+ </div>
174
+ </template>
175
+ <div
176
+ v-else-if="isTree && i === 0"
177
+ class="vm-crud-table__tree-cell"
178
+ :style="{ paddingLeft: `${(row.depth || 0) * 18}px` }"
179
+ >
180
+ <button
181
+ v-if="row.hasChildren"
182
+ type="button"
183
+ class="vm-crud-table__fold"
184
+ @click="toggleExpand(row[rowKey] as string | number)"
185
+ >
186
+ <i
187
+ :class="
188
+ expanded.has(row[rowKey] as string | number)
189
+ ? 'ri-arrow-down-s-line'
190
+ : 'ri-arrow-right-s-line'
191
+ "
192
+ />
193
+ </button>
194
+ <span v-else class="vm-crud-table__fold is-spacer" />
195
+ <slot
196
+ :name="col.slot || `cell-${col.prop}`"
197
+ :row="row"
198
+ :value="col.prop ? row[col.prop] : undefined"
199
+ >
200
+ <component
201
+ :is="cellComp(col)"
202
+ v-if="cellComp(col)"
203
+ :model-value="col.prop ? row[col.prop] : undefined"
204
+ :scope="row"
205
+ :column="{ property: col.prop, prop: col.prop }"
206
+ :options="resolveCellOptions(col)"
207
+ v-bind="resolveCellProps(col)"
208
+ />
209
+ <vm-dict-tag
210
+ v-else-if="resolveDict(col).length"
211
+ :model-value="col.prop ? (row[col.prop] as string | number) : null"
212
+ :options="resolveDict(col)"
213
+ />
214
+ <template v-else>{{ cellText(col, row) }}</template>
215
+ </slot>
216
+ </div>
217
+ <slot
218
+ v-else
219
+ :name="col.slot || `cell-${col.prop}`"
220
+ :row="row"
221
+ :value="col.prop ? row[col.prop] : undefined"
222
+ >
223
+ <component
224
+ :is="cellComp(col)"
225
+ v-if="cellComp(col)"
226
+ :model-value="col.prop ? row[col.prop] : undefined"
227
+ :scope="row"
228
+ :column="{ property: col.prop, prop: col.prop }"
229
+ :options="resolveCellOptions(col)"
230
+ v-bind="resolveCellProps(col)"
231
+ />
232
+ <vm-dict-tag
233
+ v-else-if="resolveDict(col).length"
234
+ :model-value="col.prop ? (row[col.prop] as string | number) : null"
235
+ :options="resolveDict(col)"
236
+ />
237
+ <template v-else>{{ cellText(col, row) }}</template>
238
+ </slot>
239
+ </TableCell>
240
+ <TableCell v-if="showSlotOp" class="vm-crud-table__td">
241
+ <div
242
+ v-if="crud.trashMode.value === 'onlyTrashed'"
243
+ class="vm-crud-table__ops"
244
+ >
245
+ <Button
246
+ v-if="crud.getPermission('restore')"
247
+ size="xs"
248
+ variant="softPrimary"
249
+ @click="crud.rowRestore([row])"
250
+ >
251
+ {{ crudLabel('restore', crud.dict.label) }}
252
+ </Button>
253
+ <Button
254
+ v-if="crud.getPermission('delete')"
255
+ size="xs"
256
+ variant="softDestructive"
257
+ @click="crud.rowDelete(deleteRowTargets(row), true)"
258
+ >
259
+ {{ crudLabel('forceDelete', crud.dict.label) }}
260
+ </Button>
261
+ </div>
262
+ <slot v-else name="op" :row="row" />
263
+ </TableCell>
264
+ </TableRow>
265
+ </TableBody>
266
+ </Table>
267
+ </div>
268
+ <vm-context-menu ref="ctxRef" :items="ctxItems" @select="onCtxSelect" />
269
+
270
+ <Dialog :open="dataI18nEditOpen" @update:open="onDataI18nEditOpenChange">
271
+ <DialogContent
272
+ shell="crud"
273
+ :show-close-button="false"
274
+ class="vm-data-i18n-edit__dialog"
275
+ @pointer-down-outside="preventDialogOutsideClose"
276
+ @focus-outside="preventDialogOutsideClose"
277
+ @interact-outside="preventDialogOutsideClose"
278
+ >
279
+ <DialogHeader class="vm-crud-shell__head">
280
+ <DialogTitle class="vm-crud-shell__title">
281
+ {{ crudLabel('dataI18nEdit', crud.dict.label) }}
282
+ </DialogTitle>
283
+ <DialogClose class="vm-crud-shell__close">
284
+ <i class="ri-close-line" />
285
+ <span class="sr-only">Close</span>
286
+ </DialogClose>
287
+ </DialogHeader>
288
+ <div class="vm-crud-shell__body vm-data-i18n-edit__body">
289
+ <div
290
+ class="vm-data-i18n-edit__field"
291
+ :style="{ gridColumn: `span ${dataI18nEditSpan}` }"
292
+ >
293
+ <Label class="vm-data-i18n-edit__label">ID</Label>
294
+ <Input
295
+ class="vm-data-i18n-edit__control is-readonly"
296
+ :model-value="String(dataI18nEditForm.id ?? '')"
297
+ disabled
298
+ />
299
+ </div>
300
+ <div
301
+ v-for="f in dataI18nEditFields"
302
+ :key="f.name"
303
+ class="vm-data-i18n-edit__field"
304
+ :style="{ gridColumn: `span ${dataI18nEditSpan}` }"
305
+ >
306
+ <Label class="vm-data-i18n-edit__label">{{ f.name }}</Label>
307
+ <Input
308
+ class="vm-data-i18n-edit__control"
309
+ v-model="dataI18nEditForm.values[f.name]"
310
+ />
311
+ </div>
312
+ </div>
313
+ <DialogFooter class="vm-crud-shell__foot">
314
+ <button
315
+ type="button"
316
+ class="vm-crud-shell__btn is-ghost"
317
+ :disabled="dataI18nEditSubmitting"
318
+ @click="dataI18nEditOpen = false"
319
+ >
320
+ {{ crudLabel('close', crud.dict.label) }}
321
+ </button>
322
+ <button
323
+ type="button"
324
+ class="vm-crud-shell__btn is-primary"
325
+ :disabled="dataI18nEditSubmitting"
326
+ @click="saveDataI18nEdit"
327
+ >
328
+ {{
329
+ dataI18nEditSubmitting
330
+ ? crudLabel('saving', crud.dict.label)
331
+ : crudLabel('save', crud.dict.label)
332
+ }}
333
+ </button>
334
+ </DialogFooter>
335
+ </DialogContent>
336
+ </Dialog>
337
+ </div>
338
+ </template>
339
+
340
+ <script setup lang="ts">
341
+ import { ref, computed, watch, onMounted, onBeforeUnmount, provide, useSlots, toValue } from 'vue'
342
+ import { toast } from 'vue-sonner'
343
+ import { useCrud } from './useCrud'
344
+ import { injectTableOptions, TABLE_API_KEY } from './key'
345
+ import { flattenTree } from '../lib/tree'
346
+ import { resolveCrudComponent } from './registry'
347
+ import { getCrudStyle, crudLabel, crudT, crudFormat, crudLocaleRev } from './style'
348
+ import { findEpsEntity } from '../lib/eps'
349
+ import { useVome } from '../hooks/useVome'
350
+ import { vmConfirm } from './confirm'
351
+ import { preventDialogOutsideClose } from '../lib/dialog-float'
352
+ import { resolveFormSpan, useFormCols } from './span'
353
+ import {
354
+ canUseDataI18n,
355
+ dataPackFromService,
356
+ } from './data-i18n'
357
+ import { pinOpColumnsLast, resolveMergedTableColumns } from './auto-fields'
358
+ import type {
359
+ CrudColumn,
360
+ CrudOpButton,
361
+ CtxMenuItem,
362
+ } from '../../../typings/admin/comm/crud'
363
+ defineOptions({ name: 'vm-table' })
364
+
365
+ const props = withDefaults(
366
+ defineProps<{
367
+ columns?: CrudColumn[]
368
+ selection?: boolean
369
+ rowKey?: string
370
+ }>(),
371
+ {
372
+ selection: undefined,
373
+ rowKey: 'id',
374
+ },
375
+ )
376
+
377
+ const slots = useSlots()
378
+ const crud = useCrud()
379
+ const tableOpts = injectTableOptions()
380
+ const style = getCrudStyle().table
381
+
382
+ const scrollRef = ref<HTMLElement | null>(null)
383
+ /** empty-block:宽度取滚动容器可视宽,不跟表内容总宽 */
384
+ const emptyWidth = ref(0)
385
+
386
+ function syncEmptyWidth() {
387
+ const el = scrollRef.value
388
+ if (!el) return
389
+ emptyWidth.value = el.clientWidth
390
+ }
391
+
392
+ const emptyBlockStyle = computed(() => ({
393
+ width: emptyWidth.value ? `${emptyWidth.value}px` : '100%',
394
+ }))
395
+
396
+ let emptyRo: ResizeObserver | null = null
397
+
398
+ const overrideColumns = ref<CrudColumn[] | null>(null)
399
+
400
+ const DATA_I18N_ID_COL: CrudColumn = {
401
+ prop: 'id',
402
+ label: 'ID',
403
+ minWidth: 100,
404
+ }
405
+
406
+ const DATA_I18N_OP_COL: CrudColumn = {
407
+ type: 'op',
408
+ buttons: ['edit', 'delete'],
409
+ width: 140,
410
+ fixed: 'right',
411
+ }
412
+
413
+ const isDataI18nView = computed(() => crud.trashMode.value === 'dataI18n')
414
+
415
+ /** 语言 Tab:按表头/当前页内容估 minWidth(限幅) */
416
+ function dataI18nColMinWidth(header: string, samples: string[]) {
417
+ let maxChars = [...header].length
418
+ for (const s of samples) {
419
+ const n = [...String(s || '')].length
420
+ if (n > maxChars) maxChars = n
421
+ }
422
+ return Math.min(360, Math.max(108, maxChars * 14 + 28))
423
+ }
424
+
425
+ function resolveManualColumns(): CrudColumn[] {
426
+ return props.columns?.length ? props.columns : tableOpts?.columns || []
427
+ }
428
+
429
+ function resolveMergedColumns(): CrudColumn[] {
430
+ // 依赖 permission,登录/换权后按 service._permission 重建 op
431
+ void crud.permission.value
432
+ return resolveMergedTableColumns({
433
+ service: crud.service,
434
+ tableOpts,
435
+ tablePlugins: (getCrudStyle().table.plugins || []) as Array<{
436
+ __plugin?: string
437
+ ignoreFields?: string[]
438
+ }>,
439
+ manual: resolveManualColumns(),
440
+ })
441
+ }
442
+
443
+ const columns = computed((): CrudColumn[] => {
444
+ if (isDataI18nView.value) {
445
+ void crudLocaleRev.value
446
+ const fields = crud.dataI18nFields?.value || []
447
+ const rows = crud.list.value || []
448
+ const origSuffix = crudLabel('dataI18nOrigSuffix', crud.dict.label)
449
+ const fieldCols: CrudColumn[] = []
450
+ for (const f of fields) {
451
+ const fieldName = f.name
452
+ const srcProp = `__src_${fieldName}`
453
+ const srcHeader = `${fieldName}${origSuffix}`
454
+ const srcSamples = rows.map((r) => String(r[srcProp] ?? ''))
455
+ const valSamples = rows.map((r) => String(r[fieldName] ?? ''))
456
+ fieldCols.push({
457
+ prop: srcProp,
458
+ label: srcHeader,
459
+ minWidth: dataI18nColMinWidth(srcHeader, srcSamples),
460
+ })
461
+ fieldCols.push({
462
+ prop: fieldName,
463
+ label: fieldName,
464
+ minWidth: dataI18nColMinWidth(fieldName, valSamples),
465
+ })
466
+ }
467
+ return pinOpColumnsLast([
468
+ DATA_I18N_ID_COL,
469
+ ...fieldCols,
470
+ DATA_I18N_OP_COL,
471
+ ])
472
+ }
473
+ const raw = overrideColumns.value || resolveMergedColumns()
474
+ return pinOpColumnsLast(raw.filter((c) => !c.hidden))
475
+ })
476
+
477
+ const hasSelectionCol = computed(() => columns.value.some((c) => c.type === 'selection'))
478
+ const hasOpCol = computed(() => columns.value.some((c) => c.type === 'op'))
479
+ const showSelection = computed(() => {
480
+ // 语言 Tab:始终多选(批量删译文)
481
+ if (isDataI18nView.value) return true
482
+ return props.selection !== undefined ? props.selection : hasSelectionCol.value
483
+ })
484
+ const dataColumns = computed(() =>
485
+ columns.value.filter((c) => c.type !== 'selection'),
486
+ )
487
+ const showSlotOp = computed(
488
+ () => Boolean(slots.op) && !hasOpCol.value && !isDataI18nView.value,
489
+ )
490
+
491
+ /** EPS 列 → field.{table}.{prop} */
492
+ function epsFieldKey(prop: string): string | undefined {
493
+ const ns = (crud.service as { namespace?: string } | undefined)?.namespace
494
+ if (!ns || !prop) return undefined
495
+ const entity = findEpsEntity(`/${ns}`)
496
+ if (!entity) return undefined
497
+ const cols = [
498
+ ...((entity as { columns?: Array<{ propertyName?: string; table?: string; source?: string }> }).columns || []),
499
+ ...((entity as { pageColumns?: Array<{ propertyName?: string; table?: string; source?: string }> }).pageColumns || []),
500
+ ]
501
+ const meta =
502
+ cols.find((c) => c.propertyName === prop) ||
503
+ cols.find((c) => c.source === `a.${prop}`) ||
504
+ cols.find((c) => c.source?.endsWith(`.${prop}`))
505
+ const table = meta?.table?.trim()
506
+ const name = (meta?.propertyName || prop).trim()
507
+ if (!table || !name) return undefined
508
+ return `${table}.${name}`
509
+ }
510
+
511
+ /** 表头文案:labelKey → field.* → label / op;语言 Tab 用实体字段名,不走 field.* */
512
+ function columnLabel(col: CrudColumn): string {
513
+ void crudLocaleRev.value
514
+ if (col.type === 'op') {
515
+ return col.labelKey
516
+ ? crudT(col.labelKey, col.label || crudLabel('op', crud.dict.label))
517
+ : col.label || crudLabel('op', crud.dict.label)
518
+ }
519
+ const fb = col.label || col.prop || ''
520
+ if (isDataI18nView.value && col.prop && col.prop !== 'id') {
521
+ return fb
522
+ }
523
+ if (col.labelKey) return crudT(col.labelKey, fb)
524
+ const fk = col.fieldKey || (col.prop ? epsFieldKey(col.prop) : undefined)
525
+ if (fk) return crudT(`field.${fk}`, fb)
526
+ return fb
527
+ }
528
+
529
+ const colSpan = computed(
530
+ () =>
531
+ dataColumns.value.length +
532
+ (showSelection.value ? 1 : 0) +
533
+ (showSlotOp.value ? 1 : 0),
534
+ )
535
+
536
+ const rowKey = computed(() =>
537
+ isDataI18nView.value ? '__rowKey' : tableOpts?.rowKey || props.rowKey,
538
+ )
539
+
540
+ /** 行 Vue key:缺 rowKey 时用下标兜底,避免 Duplicate keys "undefined" */
541
+ function vueRowKey(row: Record<string, unknown>, index: number) {
542
+ const k = row[rowKey.value]
543
+ if (k != null && String(k) !== '') return String(k)
544
+ return `idx-${index}`
545
+ }
546
+
547
+ const isTree = computed(() => Boolean(tableOpts?.tree))
548
+ const border = computed(() => tableOpts?.border ?? style.border)
549
+ const autoHeight = computed(() => tableOpts?.autoHeight ?? style.autoHeight)
550
+ const highlight = computed(() => style.highlightCurrentRow)
551
+ const opWidth = computed(() => style.column.opWidth)
552
+ const defaultAlign = computed(() => style.column.align)
553
+ const sortRefresh = computed(() => tableOpts?.sortRefresh !== false)
554
+
555
+ function normalizeSortDir(
556
+ order?: string,
557
+ ): 'asc' | 'desc' | undefined {
558
+ if (order === 'asc' || order === 'ascending') return 'asc'
559
+ if (order === 'desc' || order === 'descending') return 'desc'
560
+ return undefined
561
+ }
562
+
563
+ function initSortState() {
564
+ const ds = tableOpts?.defaultSort
565
+ if (!ds?.prop) return null
566
+ return {
567
+ prop: ds.prop,
568
+ order: normalizeSortDir(ds.order) || ('asc' as const),
569
+ }
570
+ }
571
+
572
+ const sortState = ref<{ prop: string; order: 'asc' | 'desc' } | null>(
573
+ initSortState(),
574
+ )
575
+
576
+ /** dict.sort → 本项目为 { order: 字段, sort: 方向 } */
577
+ function sortPayload(prop: string, order: 'asc' | 'desc') {
578
+ return {
579
+ [crud.dict.sort.prop]: prop,
580
+ [crud.dict.sort.order]: order,
581
+ }
582
+ }
583
+
584
+ function isSortable(col: CrudColumn) {
585
+ if (!col.prop || col.type === 'op' || col.type === 'selection' || col.type === 'index') {
586
+ return false
587
+ }
588
+ return Boolean(col.sortable)
589
+ }
590
+
591
+ function sortIconClass(prop: string) {
592
+ if (sortState.value?.prop !== prop) return 'ri-arrow-up-down-line is-muted'
593
+ return sortState.value.order === 'asc'
594
+ ? 'ri-arrow-up-s-fill is-active'
595
+ : 'ri-arrow-down-s-fill is-active'
596
+ }
597
+
598
+ function displayIndex(idx: string | number) {
599
+ return Number(idx) + 1
600
+ }
601
+
602
+ function onSortClick(col: CrudColumn) {
603
+ if (!isSortable(col) || !col.prop) return
604
+ const prop = col.prop
605
+ let next: 'asc' | 'desc' = 'asc'
606
+ const current = sortState.value
607
+ if (current && current.prop === prop) {
608
+ next = current.order === 'asc' ? 'desc' : 'asc'
609
+ } else {
610
+ const hint = normalizeSortDir(
611
+ typeof col.sortable === 'string' ? col.sortable : undefined,
612
+ )
613
+ next = hint || 'asc'
614
+ }
615
+ sortState.value = { prop, order: next }
616
+ if (sortRefresh.value) void crud.refresh(sortPayload(prop, next))
617
+ }
618
+
619
+ /** col.width / col.minWidth 优先;op 列未设 width 时回退全局 opWidth */
620
+ function toPx(v: string | number) {
621
+ return typeof v === 'number' ? `${v}px` : v
622
+ }
623
+
624
+ function colFixed(col: CrudColumn): 'left' | 'right' | undefined {
625
+ if (col.fixed === 'left' || col.fixed === 'right') return col.fixed
626
+ if (col.type === 'op') return 'right'
627
+ return undefined
628
+ }
629
+
630
+ function colWidthPx(col: CrudColumn): number {
631
+ if (col.type === 'op') {
632
+ const w = col.width ?? opWidth.value
633
+ return typeof w === 'number' ? w : Number.parseInt(String(w), 10) || 140
634
+ }
635
+ if (col.width != null) {
636
+ return typeof col.width === 'number'
637
+ ? col.width
638
+ : Number.parseInt(String(col.width), 10) || 100
639
+ }
640
+ if (col.minWidth != null) {
641
+ return typeof col.minWidth === 'number'
642
+ ? col.minWidth
643
+ : Number.parseInt(String(col.minWidth), 10) || 100
644
+ }
645
+ return 100
646
+ }
647
+
648
+ /** 自右向左累加 sticky right 偏移(多列 fixed:right 堆叠) */
649
+ const fixedRightOffset = computed(() => {
650
+ const map = new Map<number, number>()
651
+ let acc = 0
652
+ const cols = dataColumns.value
653
+ for (let i = cols.length - 1; i >= 0; i--) {
654
+ if (colFixed(cols[i]!) !== 'right') continue
655
+ map.set(i, acc)
656
+ acc += colWidthPx(cols[i]!)
657
+ }
658
+ return map
659
+ })
660
+
661
+ function colStyle(col: CrudColumn, index?: number) {
662
+ const base: Record<string, string | undefined> = {
663
+ textAlign: (col.align || defaultAlign.value) as string | undefined,
664
+ }
665
+ if (col.type === 'op') {
666
+ const w = col.width ?? opWidth.value
667
+ base.width = toPx(w)
668
+ base.minWidth = toPx(w)
669
+ base.whiteSpace = 'nowrap'
670
+ } else {
671
+ if (col.width != null) base.width = toPx(col.width)
672
+ if (col.minWidth != null) base.minWidth = toPx(col.minWidth)
673
+ }
674
+ const fixed = colFixed(col)
675
+ if (fixed === 'right' && index != null) {
676
+ base.position = 'sticky'
677
+ base.right = `${fixedRightOffset.value.get(index) ?? 0}px`
678
+ // z-index 只走 CSS:表头高于正文,禁止内联 3/4 压过 th
679
+ } else if (fixed === 'left') {
680
+ base.position = 'sticky'
681
+ base.left = '0'
682
+ }
683
+ return base
684
+ }
685
+ const ctxEnabled = computed(() => {
686
+ if (isDataI18nView.value) return false
687
+ const c = tableOpts?.contextMenu ?? style.contextMenu
688
+ return c !== false && !(Array.isArray(c) && c.length === 0)
689
+ })
690
+
691
+ const expanded = ref(new Set<string | number>())
692
+ const currentId = ref('')
693
+ const ctxRef = ref<{ open: (e: MouseEvent) => void } | null>(null)
694
+ const ctxRow = ref<Record<string, unknown> | null>(null)
695
+
696
+ const ctxItems = computed<CtxMenuItem[]>(() => {
697
+ const conf = tableOpts?.contextMenu ?? style.contextMenu
698
+ const keys = Array.isArray(conf)
699
+ ? conf
700
+ : ['refresh', 'check', 'edit', 'delete', 'order-asc', 'order-desc']
701
+ const map: Record<string, string> = {
702
+ refresh: crudLabel('refresh', crud.dict.label),
703
+ check: crudLabel('select', crud.dict.label),
704
+ edit: crudLabel('update', crud.dict.label),
705
+ delete: crudLabel('delete', crud.dict.label),
706
+ 'order-asc': crudLabel('asc', crud.dict.label),
707
+ 'order-desc': crudLabel('desc', crud.dict.label),
708
+ }
709
+ return keys.map((k) => ({
710
+ key: k,
711
+ label: map[k] || k,
712
+ disabled:
713
+ (k === 'edit' && !crud.getPermission('update')) ||
714
+ (k === 'delete' && !crud.getPermission('delete')),
715
+ }))
716
+ })
717
+
718
+ /** 树表默认仅展示一级(不展开);defaultExpandAll / defaultExpandLevel 可覆盖 */
719
+ watch(
720
+ () => crud.list.value,
721
+ (rows) => {
722
+ if (!isTree.value || expanded.value.size) return
723
+ const next = new Set<string | number>()
724
+ if (tableOpts?.defaultExpandAll) {
725
+ for (const r of rows) {
726
+ if (Array.isArray(r.children) && (r.children as unknown[]).length) {
727
+ next.add(r[rowKey.value] as string | number)
728
+ const stack = [...(r.children as Record<string, unknown>[])]
729
+ while (stack.length) {
730
+ const n = stack.pop()!
731
+ if (Array.isArray(n.children) && (n.children as unknown[]).length) {
732
+ next.add(n[rowKey.value] as string | number)
733
+ stack.push(...(n.children as Record<string, unknown>[]))
734
+ }
735
+ }
736
+ }
737
+ }
738
+ } else {
739
+ const level = Math.max(1, tableOpts?.defaultExpandLevel ?? 1)
740
+ const walk = (nodes: Record<string, unknown>[], depth: number) => {
741
+ if (depth >= level - 1) return
742
+ for (const n of nodes) {
743
+ const kids = n.children as Record<string, unknown>[] | undefined
744
+ if (!kids?.length) continue
745
+ next.add(n[rowKey.value] as string | number)
746
+ walk(kids, depth + 1)
747
+ }
748
+ }
749
+ walk(rows, 0)
750
+ }
751
+ if (next.size) expanded.value = next
752
+ },
753
+ { immediate: true },
754
+ )
755
+
756
+ type FlatRow = Record<string, unknown> & {
757
+ depth?: number
758
+ hasChildren?: boolean
759
+ }
760
+
761
+ const displayRows = computed<FlatRow[]>(() => {
762
+ const rows = crud.list.value as FlatRow[]
763
+ if (!isTree.value) return rows
764
+ return flattenTree(rows as never, expanded.value) as FlatRow[]
765
+ })
766
+
767
+ const selectedIds = computed(
768
+ () => new Set(crud.selection.value.map((r) => String(r[rowKey.value]))),
769
+ )
770
+
771
+ const allChecked = computed(() => {
772
+ const rows = displayRows.value
773
+ return (
774
+ rows.length > 0 &&
775
+ rows.every((r) => selectedIds.value.has(String(r[rowKey.value])))
776
+ )
777
+ })
778
+
779
+ /** 自身 + 全部子孙(树勾选 / 行删级联) */
780
+ function collectSubtree(row: Record<string, unknown>): Record<string, unknown>[] {
781
+ const out: Record<string, unknown>[] = [row]
782
+ const kids = row.children as Record<string, unknown>[] | undefined
783
+ if (!kids?.length) return out
784
+ for (const c of kids) out.push(...collectSubtree(c))
785
+ return out
786
+ }
787
+
788
+ /** 树表行删带上子集;普通表仍只删当前行 */
789
+ function deleteRowTargets(row: Record<string, unknown>) {
790
+ return isTree.value ? collectSubtree(row) : [row]
791
+ }
792
+
793
+ const { service: hostService } = useVome()
794
+ const dataI18nEditOpen = ref(false)
795
+ const dataI18nEditSubmitting = ref(false)
796
+ const dataI18nEditForm = ref<{
797
+ id: string
798
+ values: Record<string, string>
799
+ sources: Record<string, string>
800
+ }>({ id: '', values: {}, sources: {} })
801
+ const dataI18nEditFormCols = useFormCols()
802
+ const dataI18nEditSpan = computed(() =>
803
+ resolveFormSpan(12, dataI18nEditFormCols.value),
804
+ )
805
+ const dataI18nEditFields = computed(
806
+ () => crud.dataI18nFields?.value || [],
807
+ )
808
+
809
+ function onDataI18nEditOpenChange(v: boolean) {
810
+ dataI18nEditOpen.value = v
811
+ }
812
+
813
+ function openDataI18nEdit(row: Record<string, unknown>) {
814
+ const fields = crud.dataI18nFields?.value || []
815
+ const sources = (row.__sources as Record<string, string>) || {}
816
+ const values: Record<string, string> = {}
817
+ const src: Record<string, string> = {}
818
+ for (const f of fields) {
819
+ values[f.name] = String(row[f.name] ?? '')
820
+ src[f.name] = String(sources[f.name] ?? '')
821
+ }
822
+ dataI18nEditForm.value = {
823
+ id: String(row.id ?? ''),
824
+ values,
825
+ sources: src,
826
+ }
827
+ dataI18nEditOpen.value = true
828
+ }
829
+
830
+ async function saveDataI18nEdit() {
831
+ if (dataI18nEditSubmitting.value) return
832
+ const meta = canUseDataI18n(crud.service, hostService)
833
+ const dataPack = dataPackFromService(hostService)
834
+ const lang = crud.dataI18nLangCode?.value
835
+ if (!meta.ok || !dataPack?.updateEntry || !lang) {
836
+ toast.error(crudLabel('dataI18nNoService', crud.dict.label))
837
+ return
838
+ }
839
+ dataI18nEditSubmitting.value = true
840
+ try {
841
+ await dataPack.updateEntry({
842
+ tableName: meta.tableName,
843
+ langCode: lang,
844
+ id: dataI18nEditForm.value.id,
845
+ values: { ...dataI18nEditForm.value.values },
846
+ })
847
+ toast.success(crudLabel('saveSuccess', crud.dict.label))
848
+ dataI18nEditOpen.value = false
849
+ await crud.refresh()
850
+ } catch (e) {
851
+ toast.error(e instanceof Error ? e.message : crudLabel('saveFailed'))
852
+ } finally {
853
+ dataI18nEditSubmitting.value = false
854
+ }
855
+ }
856
+
857
+ async function deleteDataI18nEntry(row: Record<string, unknown>) {
858
+ const meta = canUseDataI18n(crud.service, hostService)
859
+ const dataPack = dataPackFromService(hostService)
860
+ const lang = crud.dataI18nLangCode?.value
861
+ if (!meta.ok || !dataPack?.deleteEntry || !lang) {
862
+ toast.error(crudLabel('dataI18nNoService', crud.dict.label))
863
+ return
864
+ }
865
+ const id = String(row.id ?? '')
866
+ if (!id) return
867
+ const ok = await vmConfirm({
868
+ title: crudLabel('tips', crud.dict.label),
869
+ message: crudFormat(
870
+ crudLabel('dataI18nDeleteConfirm', crud.dict.label),
871
+ id,
872
+ ),
873
+ confirmText: crudLabel('confirm', crud.dict.label),
874
+ cancelText: crudLabel('close', crud.dict.label),
875
+ })
876
+ if (!ok) return
877
+ try {
878
+ await dataPack.deleteEntry({
879
+ tableName: meta.tableName,
880
+ langCode: lang,
881
+ id,
882
+ })
883
+ toast.success(crudLabel('deleteSuccess', crud.dict.label))
884
+ await crud.refresh()
885
+ } catch (e) {
886
+ toast.error(e instanceof Error ? e.message : crudLabel('deleteFailed'))
887
+ }
888
+ }
889
+
890
+ function toggleAll(checked: boolean | 'indeterminate') {
891
+ if (checked === true) {
892
+ // 树表:含折叠未展示的子孙
893
+ if (isTree.value) {
894
+ const all: Record<string, unknown>[] = []
895
+ for (const r of crud.list.value) all.push(...collectSubtree(r))
896
+ crud.setSelection(all)
897
+ return
898
+ }
899
+ crud.setSelection([...displayRows.value])
900
+ } else {
901
+ crud.setSelection([])
902
+ }
903
+ }
904
+
905
+ function toggleRow(row: Record<string, unknown>, checked: boolean | 'indeterminate') {
906
+ const key = rowKey.value
907
+ if (isTree.value) {
908
+ const subtree = collectSubtree(row)
909
+ const ids = new Set(subtree.map((r) => String(r[key])))
910
+ if (checked === true) {
911
+ const map = new Map(
912
+ crud.selection.value.map((r) => [String(r[key]), r] as const),
913
+ )
914
+ for (const r of subtree) map.set(String(r[key]), r)
915
+ crud.setSelection([...map.values()])
916
+ } else {
917
+ crud.setSelection(
918
+ crud.selection.value.filter((r) => !ids.has(String(r[key]))),
919
+ )
920
+ }
921
+ return
922
+ }
923
+ const id = String(row[key])
924
+ if (checked === true) {
925
+ if (!selectedIds.value.has(id)) crud.setSelection([...crud.selection.value, row])
926
+ } else {
927
+ crud.setSelection(
928
+ crud.selection.value.filter((r) => String(r[key]) !== id),
929
+ )
930
+ }
931
+ }
932
+
933
+ function toggleExpand(id: string | number) {
934
+ const next = new Set(expanded.value)
935
+ if (next.has(id)) next.delete(id)
936
+ else next.add(id)
937
+ expanded.value = next
938
+ }
939
+
940
+ function resolveButtons(col: CrudColumn, row: Record<string, unknown>): CrudOpButton[] {
941
+ const raw = col.buttons
942
+ if (typeof raw === 'function') return raw(row)
943
+ return raw?.length ? raw : (['edit', 'delete'] as CrudOpButton[])
944
+ }
945
+
946
+ function cellComp(col: CrudColumn) {
947
+ return resolveCrudComponent(col.component?.name)
948
+ }
949
+
950
+ /** dict: dict.options('x') → 彩色标签;props 可为 dict.get(...) → switch 等 */
951
+ function resolveDict(col: CrudColumn) {
952
+ const list = col.dict == null ? [] : toValue(col.dict)
953
+ return Array.isArray(list) ? list : []
954
+ }
955
+
956
+ /** props 可为 dict.get(...)(ComputedRef / 数组)→ 作 options */
957
+ function resolveCellOptions(col: CrudColumn) {
958
+ const fromDict = resolveDict(col)
959
+ if (fromDict.length) return fromDict
960
+ const raw = toValue(col.component?.props)
961
+ return Array.isArray(raw) ? raw : []
962
+ }
963
+
964
+ function resolveCellProps(col: CrudColumn) {
965
+ const raw = col.component?.props
966
+ const val = toValue(raw)
967
+ if (val == null || Array.isArray(val)) return {}
968
+ if (typeof val === 'object') return val as Record<string, unknown>
969
+ return {}
970
+ }
971
+
972
+ function cellText(col: CrudColumn, row: Record<string, unknown>) {
973
+ if (!col.prop) return ''
974
+ const val = row[col.prop]
975
+ if (col.formatter) return col.formatter(row, val)
976
+ if (val == null) return ''
977
+ return String(val)
978
+ }
979
+
980
+ function onRowClick(row: Record<string, unknown>) {
981
+ if (highlight.value) currentId.value = String(row[rowKey.value])
982
+ }
983
+
984
+ function onContextMenu(e: MouseEvent, row: Record<string, unknown>) {
985
+ if (!ctxEnabled.value) return
986
+ ctxRow.value = row
987
+ ctxRef.value?.open(e)
988
+ }
989
+
990
+ function onCtxSelect(key: string) {
991
+ const row = ctxRow.value
992
+ if (!row) return
993
+ if (key === 'refresh') void crud.refresh()
994
+ if (key === 'check') toggleRow(row, true)
995
+ if (key === 'edit') void crud.rowEdit(row)
996
+ if (key === 'delete') void crud.rowDelete(deleteRowTargets(row))
997
+ if (key === 'order-asc') {
998
+ sortState.value = { prop: 'orderNum', order: 'asc' }
999
+ void crud.refresh(sortPayload('orderNum', 'asc'))
1000
+ }
1001
+ if (key === 'order-desc') {
1002
+ sortState.value = { prop: 'orderNum', order: 'desc' }
1003
+ void crud.refresh(sortPayload('orderNum', 'desc'))
1004
+ }
1005
+ }
1006
+
1007
+ function baseColumns(): CrudColumn[] {
1008
+ return overrideColumns.value || resolveMergedColumns()
1009
+ }
1010
+
1011
+ function setColumns(cols: CrudColumn[]) {
1012
+ overrideColumns.value = pinOpColumnsLast(cols)
1013
+ }
1014
+
1015
+ function showColumn(prop: string) {
1016
+ setColumns(
1017
+ baseColumns().map((c) =>
1018
+ c.prop === prop ? { ...c, hidden: false } : c,
1019
+ ),
1020
+ )
1021
+ }
1022
+
1023
+ function hideColumn(prop: string) {
1024
+ setColumns(
1025
+ baseColumns().map((c) =>
1026
+ c.prop === prop ? { ...c, hidden: true } : c,
1027
+ ),
1028
+ )
1029
+ }
1030
+
1031
+ function getColumns() {
1032
+ return baseColumns()
1033
+ }
1034
+
1035
+ function columnCustomKey() {
1036
+ return `column-custom__${typeof location !== 'undefined' ? location.pathname : 'default'}`
1037
+ }
1038
+
1039
+ function applyColumnCustomMap(
1040
+ cols: CrudColumn[],
1041
+ map: Record<string, { checked: boolean; order?: number }>,
1042
+ ) {
1043
+ const ordered = [...cols].sort((a, b) => {
1044
+ const ao = a.prop != null ? map[a.prop]?.order ?? 999 : 999
1045
+ const bo = b.prop != null ? map[b.prop]?.order ?? 999 : 999
1046
+ return ao - bo
1047
+ })
1048
+ return ordered.map((c) => {
1049
+ if (!c.prop || c.type === 'selection' || c.type === 'op') return c
1050
+ return { ...c, hidden: !(map[c.prop]?.checked ?? true) }
1051
+ })
1052
+ }
1053
+
1054
+ onMounted(() => {
1055
+ syncEmptyWidth()
1056
+ if (typeof ResizeObserver !== 'undefined' && scrollRef.value) {
1057
+ emptyRo = new ResizeObserver(() => syncEmptyWidth())
1058
+ emptyRo.observe(scrollRef.value)
1059
+ }
1060
+
1061
+ try {
1062
+ const raw = localStorage.getItem(columnCustomKey())
1063
+ if (!raw) return
1064
+ const map = JSON.parse(raw) as Record<
1065
+ string,
1066
+ { checked: boolean; order?: number }
1067
+ >
1068
+ const base = resolveMergedColumns()
1069
+ if (!base.length) return
1070
+ overrideColumns.value = applyColumnCustomMap(base, map)
1071
+ } catch {
1072
+ /* ignore */
1073
+ }
1074
+ })
1075
+
1076
+ onBeforeUnmount(() => {
1077
+ emptyRo?.disconnect()
1078
+ emptyRo = null
1079
+ })
1080
+
1081
+ watch(
1082
+ () => displayRows.value.length,
1083
+ () => {
1084
+ requestAnimationFrame(syncEmptyWidth)
1085
+ },
1086
+ )
1087
+
1088
+ provide(TABLE_API_KEY, {
1089
+ setColumns,
1090
+ showColumn,
1091
+ hideColumn,
1092
+ getColumns,
1093
+ })
1094
+
1095
+ defineExpose({
1096
+ showColumn,
1097
+ hideColumn,
1098
+ setColumns,
1099
+ data: displayRows,
1100
+ })
1101
+ </script>
1102
+
1103
+ <style lang="scss" scoped>
1104
+ .vm-crud-table {
1105
+ position: relative;
1106
+ display: flex;
1107
+ width: 100%;
1108
+ flex: 1 1 auto;
1109
+ flex-direction: column;
1110
+ overflow: hidden;
1111
+
1112
+ &.is-border {
1113
+ border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
1114
+ }
1115
+
1116
+ &.is-auto-height {
1117
+ min-height: 0;
1118
+ }
1119
+ }
1120
+
1121
+ .vm-crud-table__scroll {
1122
+ width: 100%;
1123
+ min-width: 0;
1124
+ min-height: 0;
1125
+ flex: 1 1 auto;
1126
+ overflow: auto;
1127
+ overscroll-behavior: contain;
1128
+ }
1129
+
1130
+ /* 表铺满容器;列 minWidth 过大时由 __scroll 横向滚动 */
1131
+ :deep([data-slot='table-container']) {
1132
+ width: 100%;
1133
+ min-width: 100%;
1134
+ overflow: visible !important;
1135
+ }
1136
+
1137
+ :deep([data-slot='table']) {
1138
+ width: 100%;
1139
+ }
1140
+
1141
+ .vm-crud-table__loading {
1142
+ position: absolute;
1143
+ inset: 0;
1144
+ z-index: 10;
1145
+ display: flex;
1146
+ align-items: center;
1147
+ justify-content: center;
1148
+ background: color-mix(in srgb, var(--card) 70%, transparent);
1149
+ color: var(--muted-foreground);
1150
+ font-size: 12px;
1151
+ backdrop-filter: blur(1px);
1152
+ }
1153
+
1154
+ /* empty-block:在表体内,sticky 贴可视左缘,不盖表头 */
1155
+ :deep(.vm-crud-table__empty-row) {
1156
+ &:hover {
1157
+ background: transparent !important;
1158
+ }
1159
+ }
1160
+
1161
+ :deep(.vm-crud-table__empty-cell) {
1162
+ height: 240px;
1163
+ padding: 0 !important;
1164
+ border-bottom: none !important;
1165
+ }
1166
+
1167
+ .vm-crud-table__empty-block {
1168
+ position: sticky;
1169
+ left: 0;
1170
+ display: flex;
1171
+ height: 240px;
1172
+ align-items: center;
1173
+ justify-content: center;
1174
+ overflow: hidden;
1175
+ }
1176
+
1177
+ :deep(.vm-crud-table__head-row) {
1178
+ border-color: color-mix(in srgb, var(--border) 70%, transparent);
1179
+ background: var(--card);
1180
+
1181
+ &:hover {
1182
+ background: var(--card) !important;
1183
+ }
1184
+ }
1185
+
1186
+ /* 纵向滚动时表头固定;横向 fixed 列靠 CSS z-index,表头必须高于正文 sticky td */
1187
+ :deep(.vm-crud-table__th),
1188
+ :deep(.vm-crud-table__head-row .vm-crud-table__check) {
1189
+ position: sticky;
1190
+ top: 0;
1191
+ z-index: 4;
1192
+ background: var(--card);
1193
+ }
1194
+
1195
+ :deep(.vm-crud-table__td.is-fixed-left),
1196
+ :deep(.vm-crud-table__td.is-fixed-right) {
1197
+ z-index: 1;
1198
+ }
1199
+
1200
+ :deep(.vm-crud-table__th.is-fixed-left) {
1201
+ z-index: 5;
1202
+ }
1203
+
1204
+ :deep(.vm-crud-table__th.is-fixed-right) {
1205
+ z-index: 6;
1206
+ }
1207
+
1208
+ :deep(.vm-crud-table__th),
1209
+ :deep(.vm-crud-table__td) {
1210
+ &.is-fixed-right {
1211
+ box-shadow: -4px 0 8px color-mix(in srgb, var(--foreground) 6%, transparent);
1212
+ }
1213
+
1214
+ &.is-fixed-left {
1215
+ box-shadow: 4px 0 8px color-mix(in srgb, var(--foreground) 6%, transparent);
1216
+ }
1217
+ }
1218
+
1219
+ /* 表头 sticky 自带实底;正文 fixed 继承行底,禁止写死 --card 再靠 hover 改色盖住 */
1220
+ :deep(.vm-crud-table__th.is-fixed-right),
1221
+ :deep(.vm-crud-table__th.is-fixed-left) {
1222
+ background: var(--card);
1223
+ }
1224
+
1225
+ :deep(.vm-crud-table__td.is-fixed-right),
1226
+ :deep(.vm-crud-table__td.is-fixed-left) {
1227
+ background: inherit;
1228
+ }
1229
+
1230
+ :deep(.vm-crud-table__th) {
1231
+ height: 42px;
1232
+ max-height: 42px;
1233
+ box-sizing: border-box;
1234
+ overflow: hidden;
1235
+ color: var(--muted-foreground);
1236
+ font-size: 12px;
1237
+ font-weight: 600;
1238
+ white-space: nowrap;
1239
+ vertical-align: middle;
1240
+
1241
+ &.is-sortable {
1242
+ cursor: pointer;
1243
+ user-select: none;
1244
+
1245
+ &:hover {
1246
+ color: var(--foreground);
1247
+ }
1248
+ }
1249
+ }
1250
+
1251
+ .vm-crud-table__th-inner {
1252
+ display: inline-flex;
1253
+ align-items: center;
1254
+ gap: 4px;
1255
+ }
1256
+
1257
+ .vm-crud-table__sort {
1258
+ font-size: 14px;
1259
+ line-height: 1;
1260
+ opacity: 0.45;
1261
+
1262
+ &.is-active {
1263
+ color: var(--brand);
1264
+ opacity: 1;
1265
+ }
1266
+
1267
+ &.is-muted {
1268
+ opacity: 0.35;
1269
+ }
1270
+ }
1271
+
1272
+ .vm-crud-table__ops {
1273
+ display: inline-flex;
1274
+ flex-wrap: nowrap;
1275
+ gap: 8px;
1276
+ white-space: nowrap;
1277
+ }
1278
+
1279
+ :deep(.vm-crud-table__op) {
1280
+ white-space: nowrap;
1281
+ }
1282
+
1283
+ :deep(.vm-crud-table__check) {
1284
+ width: 40px;
1285
+ padding-left: 4px;
1286
+ }
1287
+
1288
+ :deep(.vm-crud-table__row) {
1289
+ height: 48px;
1290
+ border-color: color-mix(in srgb, var(--border) 55%, transparent);
1291
+ /* 行自带实底,fixed td inherit 才不透横滚;高亮只改 tr,不改子节点色 */
1292
+ background: var(--card);
1293
+ transition: background 0.15s ease;
1294
+
1295
+ &:hover,
1296
+ &.is-current {
1297
+ background: var(--brand-soft);
1298
+ }
1299
+ }
1300
+
1301
+ :deep(.vm-crud-table__td) {
1302
+ padding-top: 10px;
1303
+ padding-bottom: 10px;
1304
+ font-size: 13px;
1305
+ }
1306
+
1307
+ .vm-crud-table__tree-cell {
1308
+ display: flex;
1309
+ align-items: center;
1310
+ gap: 4px;
1311
+ }
1312
+
1313
+ .vm-crud-table__fold {
1314
+ display: inline-flex;
1315
+ width: 22px;
1316
+ height: 22px;
1317
+ flex-shrink: 0;
1318
+ align-items: center;
1319
+ justify-content: center;
1320
+ border: none;
1321
+ border-radius: 8px;
1322
+ background: transparent;
1323
+ color: var(--muted-foreground);
1324
+ cursor: pointer;
1325
+
1326
+ &.is-spacer {
1327
+ cursor: default;
1328
+ }
1329
+
1330
+ &:not(.is-spacer):hover {
1331
+ background: var(--muted);
1332
+ color: var(--foreground);
1333
+ }
1334
+ }
1335
+
1336
+ .vm-data-i18n-edit__body {
1337
+ /* 与业务翻译弹窗一致:24 栅格 + span12;继承 shell body 的 flex:1 */
1338
+ display: grid;
1339
+ grid-template-columns: repeat(24, minmax(0, 1fr));
1340
+ gap: 12px 16px;
1341
+ align-content: start;
1342
+ padding: 18px 22px 16px;
1343
+
1344
+ @media (max-width: 768px) {
1345
+ grid-template-columns: repeat(12, minmax(0, 1fr));
1346
+ gap: 10px;
1347
+ padding: 12px;
1348
+ }
1349
+ }
1350
+
1351
+ .vm-data-i18n-edit__field {
1352
+ min-width: 0;
1353
+ }
1354
+
1355
+ .vm-data-i18n-edit__label {
1356
+ display: inline-flex;
1357
+ align-items: center;
1358
+ gap: 2px;
1359
+ margin-bottom: 6px;
1360
+ font-size: 12px;
1361
+ color: var(--muted-foreground);
1362
+ }
1363
+
1364
+ .vm-data-i18n-edit__req {
1365
+ color: var(--destructive);
1366
+ }
1367
+
1368
+ .vm-data-i18n-edit__control {
1369
+ width: 100%;
1370
+ height: var(--vm-control-height) !important;
1371
+ min-height: var(--vm-control-height);
1372
+ border-radius: var(--vm-control-radius) !important;
1373
+ font-size: var(--vm-control-font-size) !important;
1374
+
1375
+ &.is-readonly {
1376
+ background: var(--muted) !important;
1377
+ color: var(--muted-foreground) !important;
1378
+ opacity: 1 !important;
1379
+ cursor: default;
1380
+ }
1381
+ }
1382
+
1383
+ .vm-data-i18n-edit__source {
1384
+ margin-top: 6px;
1385
+ font-size: 12px;
1386
+ line-height: 1.4;
1387
+ color: var(--muted-foreground);
1388
+ word-break: break-word;
1389
+ }
1390
+ </style>