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,747 +1,747 @@
1
- <template>
2
- <div v-if="visible" class="vm-import-btn">
3
- <button
4
- type="button"
5
- class="vm-crud-toolbar__btn is-ghost"
6
- :disabled="disabled"
7
- @click="openDialog"
8
- >
9
- <i class="ri-upload-2-line" />
10
- {{ buttonText }}
11
- </button>
12
-
13
- <Dialog :open="open" @update:open="onOpenChange">
14
- <DialogContent
15
- shell="crud"
16
- class="vm-import-btn__dialog is-height"
17
- :style="{ '--vm-dialog-height': '600px' }"
18
- @pointer-down-outside="preventDialogOutsideClose"
19
- @focus-outside="preventDialogOutsideClose"
20
- @interact-outside="preventDialogOutsideClose"
21
- >
22
- <DialogHeader class="vm-crud-shell__head">
23
- <DialogTitle class="vm-crud-shell__title">{{ crudLabel('import') }}</DialogTitle>
24
- </DialogHeader>
25
-
26
- <div class="vm-crud-shell__body vm-import-btn__body">
27
- <div class="vm-import-btn__bar">
28
- <p v-if="tip" class="vm-import-btn__tip">{{ tip }}</p>
29
- <span v-else-if="file" class="vm-import-btn__file-name">
30
- {{ file.name }}
31
- </span>
32
- <span v-else />
33
- <div class="vm-import-btn__bar-actions">
34
- <button
35
- v-if="previewRows.length"
36
- type="button"
37
- class="vm-import-btn__repick"
38
- :disabled="submitting || parsing"
39
- @click="onPick"
40
- >
41
- {{ crudLabel('reselect') }}
42
- </button>
43
- <button
44
- type="button"
45
- class="vm-import-btn__tpl"
46
- :disabled="submitting || downloading || parsing"
47
- @click="onTemplate"
48
- >
49
- {{ downloading ? crudLabel('downloading') : crudLabel('downloadTpl') }}
50
- </button>
51
- </div>
52
- </div>
53
-
54
- <div
55
- v-if="!previewRows.length"
56
- class="vm-import-btn__drop"
57
- :class="{
58
- 'is-drag': dragging,
59
- 'is-disabled': submitting || parsing,
60
- }"
61
- @click="onPick"
62
- @dragenter.prevent="onDragEnter"
63
- @dragover.prevent
64
- @dragleave.prevent="onDragLeave"
65
- @drop.prevent="onDrop"
66
- >
67
- <i class="ri-upload-cloud-2-line" />
68
- <span v-if="parsing">{{ crudLabel('parsing') }}</span>
69
- <span v-else>
70
- {{ crudFormat(crudLabel('importDropHint'), maxSizeMb) }}
71
- </span>
72
- <span class="vm-import-btn__ext">{{ crudLabel('importExtHint') }}</span>
73
- </div>
74
-
75
- <div v-if="previewRows.length" class="vm-import-btn__preview">
76
- <div class="vm-import-btn__table-wrap">
77
- <table class="vm-import-btn__table">
78
- <thead>
79
- <tr>
80
- <th
81
- v-for="col in previewHeaders"
82
- :key="col"
83
- class="vm-import-btn__th"
84
- >
85
- {{ col }}
86
- </th>
87
- </tr>
88
- </thead>
89
- <tbody>
90
- <tr v-for="(row, idx) in pageRows" :key="pageStart + idx">
91
- <td
92
- v-for="col in previewHeaders"
93
- :key="col"
94
- class="vm-import-btn__td"
95
- >
96
- {{ formatCell(row[col]) }}
97
- </td>
98
- </tr>
99
- </tbody>
100
- </table>
101
- </div>
102
- <div class="vm-import-btn__pager">
103
- <span class="vm-import-btn__total">{{ crudFormat(crudLabel('total'), previewRows.length) }}</span>
104
- <div class="vm-import-btn__pager-actions">
105
- <button
106
- type="button"
107
- class="vm-import-btn__page-btn"
108
- :disabled="page <= 1 || submitting"
109
- @click="goPrevPage"
110
- >
111
- {{ crudLabel('prevPage') }}
112
- </button>
113
- <span class="vm-import-btn__page-num">
114
- {{ page }} / {{ totalPages }}
115
- </span>
116
- <button
117
- type="button"
118
- class="vm-import-btn__page-btn"
119
- :disabled="page >= totalPages || submitting"
120
- @click="goNextPage"
121
- >
122
- {{ crudLabel('nextPage') }}
123
- </button>
124
- </div>
125
- </div>
126
- </div>
127
-
128
- <input
129
- ref="inputRef"
130
- type="file"
131
- class="vm-import-btn__input"
132
- accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
133
- @change="onFileInput"
134
- />
135
- </div>
136
-
137
- <DialogFooter class="vm-crud-shell__foot">
138
- <button
139
- type="button"
140
- class="vm-crud-shell__btn is-ghost"
141
- :disabled="submitting"
142
- @click="closeDialog"
143
- >
144
- {{ crudLabel('close') }}
145
- </button>
146
- <button
147
- type="button"
148
- class="vm-crud-shell__btn is-primary"
149
- :disabled="submitting || parsing || !canSubmit"
150
- @click="onSubmit"
151
- >
152
- {{ submitting ? crudLabel('submitting') : crudLabel('submit') }}
153
- </button>
154
- </DialogFooter>
155
- </DialogContent>
156
- </Dialog>
157
- </div>
158
- </template>
159
-
160
- <script setup lang="ts">
161
- import { computed, ref } from 'vue'
162
- import { toast } from 'vue-sonner'
163
- import { preventDialogOutsideClose } from '../lib/dialog-float'
164
- import {
165
- downloadImportTemplate,
166
- parseImportExcelPreview,
167
- uploadImportExcel,
168
- uploadImportRows,
169
- type ImportResult,
170
- } from '../lib/import-excel'
171
- import { useCrud } from './useCrud'
172
- import { injectImportOptions } from './key'
173
- import { crudFormat, crudLabel } from './style'
174
- import type { CrudPermission } from '../../../typings/admin/comm/crud'
175
-
176
- defineOptions({ name: 'vm-import-btn' })
177
-
178
- /** 与服务端 importTemplate(excelFilename → .xlsx)保持一致 */
179
- const ACCEPT_EXT = '.xlsx'
180
- const MAX_SIZE_MB_DEFAULT = 10
181
- const PAGE_SIZE = 20
182
-
183
- const props = withDefaults(
184
- defineProps<{
185
- disabled?: boolean
186
- text?: string
187
- /** 弹窗左上提示文案 */
188
- tip?: string
189
- templateFilename?: string | (() => string)
190
- /** 文件大小上限(MB) */
191
- maxSizeMb?: number
192
- /** 权限键,默认 add */
193
- permission?: keyof CrudPermission
194
- }>(),
195
- {
196
- disabled: false,
197
- tip: '',
198
- maxSizeMb: MAX_SIZE_MB_DEFAULT,
199
- permission: 'add',
200
- },
201
- )
202
-
203
- const emit = defineEmits<{
204
- success: [result: { inserted: number; updated: number; failed: number }]
205
- }>()
206
-
207
- const crud = useCrud()
208
- const importOpts = injectImportOptions()
209
- const open = ref(false)
210
- const submitting = ref(false)
211
- const downloading = ref(false)
212
- const parsing = ref(false)
213
- const dragging = ref(false)
214
- const file = ref<File | null>(null)
215
- const previewHeaders = ref<string[]>([])
216
- const previewRows = ref<Record<string, unknown>[]>([])
217
- const page = ref(1)
218
- const inputRef = ref<HTMLInputElement | null>(null)
219
- const dragDepth = ref(0)
220
-
221
- const visible = computed(() => Boolean(crud.getPermission(props.permission)))
222
-
223
- const buttonText = computed(
224
- () => props.text || crudLabel('import', crud.dict.label),
225
- )
226
-
227
- const totalPages = computed(() =>
228
- Math.max(1, Math.ceil(previewRows.value.length / PAGE_SIZE)),
229
- )
230
-
231
- const pageStart = computed(() => (page.value - 1) * PAGE_SIZE)
232
-
233
- const pageRows = computed(() =>
234
- previewRows.value.slice(pageStart.value, pageStart.value + PAGE_SIZE),
235
- )
236
-
237
- const canSubmit = computed(
238
- () => Boolean(file.value) && previewRows.value.length > 0,
239
- )
240
-
241
- function goPrevPage() {
242
- if (page.value <= 1) return
243
- page.value -= 1
244
- }
245
-
246
- function goNextPage() {
247
- if (page.value >= totalPages.value) return
248
- page.value += 1
249
- }
250
-
251
- function formatCell(v: unknown) {
252
- if (v == null) return ''
253
- return String(v)
254
- }
255
-
256
- function resolveTemplateName() {
257
- const raw = props.templateFilename
258
- const name = typeof raw === 'function' ? raw() : raw
259
- const base = name || crudLabel('importTpl')
260
- return base.endsWith(ACCEPT_EXT) ? base : `${base}${ACCEPT_EXT}`
261
- }
262
-
263
- function resetPreview() {
264
- file.value = null
265
- previewHeaders.value = []
266
- previewRows.value = []
267
- page.value = 1
268
- parsing.value = false
269
- }
270
-
271
- function openDialog() {
272
- if (props.disabled) return
273
- resetPreview()
274
- open.value = true
275
- }
276
-
277
- function closeDialog() {
278
- if (submitting.value) return
279
- open.value = false
280
- resetPreview()
281
- dragging.value = false
282
- dragDepth.value = 0
283
- }
284
-
285
- function onOpenChange(v: boolean) {
286
- if (!v) {
287
- closeDialog()
288
- return
289
- }
290
- open.value = true
291
- }
292
-
293
- function onPick() {
294
- if (submitting.value || parsing.value || props.disabled) return
295
- inputRef.value?.click()
296
- }
297
-
298
- function isXlsx(f: File) {
299
- return f.name.toLowerCase().endsWith(ACCEPT_EXT)
300
- }
301
-
302
- async function acceptFile(f: File | undefined | null) {
303
- if (!f || parsing.value || submitting.value) return
304
- if (!isXlsx(f)) {
305
- toast.error(crudFormat(crudLabel('importExtOnly'), ACCEPT_EXT))
306
- return
307
- }
308
- const limit = props.maxSizeMb * 1024 * 1024
309
- if (f.size > limit) {
310
- toast.error(crudFormat(crudLabel('fileSizeLimit'), props.maxSizeMb))
311
- return
312
- }
313
-
314
- parsing.value = true
315
- try {
316
- const preview = await parseImportExcelPreview(f)
317
- if (!preview.rows.length) {
318
- resetPreview()
319
- toast.error(crudLabel('noImportRows'))
320
- return
321
- }
322
- file.value = f
323
- previewHeaders.value = preview.headers
324
- previewRows.value = preview.rows
325
- page.value = 1
326
- } catch (e) {
327
- resetPreview()
328
- toast.error(e instanceof Error ? e.message : crudLabel('parseExcelFail'))
329
- } finally {
330
- parsing.value = false
331
- }
332
- }
333
-
334
- function onFileInput(ev: Event) {
335
- const input = ev.target as HTMLInputElement
336
- const f = input.files?.[0]
337
- input.value = ''
338
- void acceptFile(f)
339
- }
340
-
341
- function onDragEnter() {
342
- if (submitting.value || parsing.value) return
343
- dragDepth.value += 1
344
- dragging.value = true
345
- }
346
-
347
- function onDragLeave() {
348
- dragDepth.value = Math.max(0, dragDepth.value - 1)
349
- if (dragDepth.value === 0) dragging.value = false
350
- }
351
-
352
- function onDrop(ev: DragEvent) {
353
- dragDepth.value = 0
354
- dragging.value = false
355
- if (submitting.value || parsing.value) return
356
- void acceptFile(ev.dataTransfer?.files?.[0])
357
- }
358
-
359
- async function onTemplate() {
360
- if (downloading.value || submitting.value || props.disabled) return
361
- const ns = crud.service?.namespace
362
- if (!ns) {
363
- toast.error(crudLabel('noServiceTpl'))
364
- return
365
- }
366
- downloading.value = true
367
- try {
368
- await downloadImportTemplate(ns, resolveTemplateName())
369
- toast.success(crudLabel('tplDownloaded'))
370
- } catch (e) {
371
- toast.error(e instanceof Error ? e.message : crudLabel('downloadTplFail'))
372
- } finally {
373
- downloading.value = false
374
- }
375
- }
376
-
377
- async function finishImport(result: ImportResult) {
378
- const failN = result.failed?.length || 0
379
- if (failN) {
380
- const tipText = result.failed
381
- .slice(0, 3)
382
- .map((f) => crudFormat(crudLabel('importRowFail'), f.row, f.message))
383
- .join(';')
384
- toast.warning(
385
- crudFormat(
386
- crudLabel('importDone'),
387
- result.inserted,
388
- result.updated,
389
- failN,
390
- tipText ? `(${tipText})` : '',
391
- ),
392
- )
393
- } else {
394
- toast.success(
395
- crudFormat(crudLabel('importSuccess'), result.inserted, result.updated),
396
- )
397
- }
398
- emit('success', {
399
- inserted: result.inserted,
400
- updated: result.updated,
401
- failed: failN,
402
- })
403
- await crud.refresh()
404
- open.value = false
405
- resetPreview()
406
- }
407
-
408
- async function runDefaultImport() {
409
- const ns = crud.service?.namespace
410
- if (!ns) {
411
- toast.error(crudLabel('noServiceImport'))
412
- return null
413
- }
414
- // 与预览一致:走 JSON `{ rows }`,避免再传文件触发 multipart 二次读 body
415
- const result = await uploadImportRows(ns, previewRows.value)
416
- await finishImport(result)
417
- return result
418
- }
419
-
420
- async function onSubmit() {
421
- if (submitting.value || parsing.value || props.disabled || !canSubmit.value) {
422
- return
423
- }
424
- if (!file.value || !isXlsx(file.value)) {
425
- toast.error(crudFormat(crudLabel('importExtOnly'), ACCEPT_EXT))
426
- return
427
- }
428
- if (!previewRows.value.length) {
429
- toast.error(crudLabel('pickExcelFirst'))
430
- return
431
- }
432
-
433
- const ns = crud.service?.namespace
434
- if (!ns) {
435
- toast.error(crudLabel('noServiceImport'))
436
- return
437
- }
438
-
439
- submitting.value = true
440
- try {
441
- if (importOpts?.onSubmit) {
442
- await importOpts.onSubmit(
443
- {
444
- file: file.value,
445
- rows: previewRows.value,
446
- headers: previewHeaders.value,
447
- },
448
- {
449
- next: async (payload) => {
450
- if (Array.isArray(payload)) {
451
- if (!payload.length) {
452
- throw new Error(crudLabel('noImportRows'))
453
- }
454
- const result = await uploadImportRows(ns, payload)
455
- await finishImport(result)
456
- return result
457
- }
458
- const f = payload ?? file.value
459
- if (!f || !isXlsx(f)) {
460
- throw new Error(crudFormat(crudLabel('importExtOnly'), ACCEPT_EXT))
461
- }
462
- const result = await uploadImportExcel(ns, f)
463
- await finishImport(result)
464
- return result
465
- },
466
- done: () => {},
467
- close: () => {
468
- open.value = false
469
- resetPreview()
470
- },
471
- },
472
- )
473
- return
474
- }
475
- await runDefaultImport()
476
- } catch (e) {
477
- toast.error(e instanceof Error ? e.message : crudLabel('importFail'))
478
- } finally {
479
- submitting.value = false
480
- }
481
- }
482
- </script>
483
-
484
- <style lang="scss" scoped>
485
- .vm-import-btn {
486
- display: inline-flex;
487
- align-items: center;
488
- }
489
-
490
- .vm-import-btn__input {
491
- display: none;
492
- }
493
-
494
- /* 导入弹窗:高度 600,宽度沿用默认 CRUD(50vw) */
495
- :deep(.vm-import-btn__dialog[data-slot='dialog-content']),
496
- :deep(.vm-import-btn__dialog.vm-dialog-shell-crud) {
497
- height: 600px !important;
498
- max-height: 600px !important;
499
- }
500
-
501
- .vm-import-btn__body {
502
- display: flex;
503
- min-height: 0;
504
- flex: 1;
505
- flex-direction: column;
506
- gap: 12px;
507
- overflow: hidden;
508
- }
509
-
510
- .vm-import-btn__bar {
511
- display: flex;
512
- flex-shrink: 0;
513
- align-items: center;
514
- justify-content: space-between;
515
- gap: 12px;
516
- }
517
-
518
- .vm-import-btn__bar-actions {
519
- display: inline-flex;
520
- flex-shrink: 0;
521
- align-items: center;
522
- gap: 8px;
523
- }
524
-
525
- .vm-import-btn__file-name {
526
- overflow: hidden;
527
- color: var(--muted-foreground);
528
- font-size: 13px;
529
- text-overflow: ellipsis;
530
- white-space: nowrap;
531
- }
532
-
533
- .vm-import-btn__tip {
534
- margin: 0;
535
- color: var(--warning);
536
- font-size: 13px;
537
- font-weight: 500;
538
- }
539
-
540
- .vm-import-btn__repick {
541
- flex-shrink: 0;
542
- height: 32px;
543
- padding: 0 12px;
544
- border: none;
545
- border-radius: 10px;
546
- background: var(--muted);
547
- color: var(--foreground);
548
- font-size: 13px;
549
- font-weight: 600;
550
- cursor: pointer;
551
-
552
- &:hover:not(:disabled) {
553
- background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
554
- }
555
-
556
- &:disabled {
557
- cursor: not-allowed;
558
- opacity: 0.45;
559
- }
560
- }
561
-
562
- .vm-import-btn__tpl {
563
- flex-shrink: 0;
564
- height: 32px;
565
- padding: 0 12px;
566
- border: none;
567
- border-radius: 10px;
568
- background: var(--brand-soft);
569
- color: var(--brand);
570
- font-size: 13px;
571
- font-weight: 600;
572
- cursor: pointer;
573
- transition: background 0.15s ease, opacity 0.15s ease;
574
-
575
- &:hover:not(:disabled) {
576
- background: color-mix(in srgb, var(--brand-soft) 70%, var(--brand) 12%);
577
- }
578
-
579
- &:disabled {
580
- cursor: not-allowed;
581
- opacity: 0.45;
582
- }
583
- }
584
-
585
- .vm-import-btn__drop {
586
- display: grid;
587
- flex: 1;
588
- min-height: 0;
589
- place-items: center;
590
- place-content: center;
591
- gap: 8px;
592
- padding: 20px 16px;
593
- border: 1px dashed var(--border);
594
- border-radius: 12px;
595
- background: var(--muted);
596
- color: var(--muted-foreground);
597
- font-size: 13px;
598
- text-align: center;
599
- cursor: pointer;
600
- transition:
601
- border-color 0.15s ease,
602
- color 0.15s ease,
603
- background 0.15s ease;
604
-
605
- i {
606
- font-size: 32px;
607
- line-height: 1;
608
- }
609
-
610
- &:hover:not(.is-disabled),
611
- &.is-drag {
612
- border-color: var(--brand);
613
- background: var(--brand-soft);
614
- color: var(--brand);
615
- }
616
-
617
- &.is-disabled {
618
- cursor: not-allowed;
619
- opacity: 0.55;
620
- }
621
- }
622
-
623
- .vm-import-btn__ext {
624
- font-size: 12px;
625
- opacity: 0.8;
626
- }
627
-
628
- .vm-import-btn__preview {
629
- display: flex;
630
- min-height: 0;
631
- flex: 1;
632
- flex-direction: column;
633
- gap: 10px;
634
- overflow: hidden;
635
- }
636
-
637
- .vm-import-btn__table-wrap {
638
- min-height: 0;
639
- flex: 1;
640
- overflow: auto;
641
- border: 1px solid var(--border);
642
- border-radius: 12px;
643
- background: var(--card);
644
- }
645
-
646
- .vm-import-btn__table {
647
- width: max-content;
648
- min-width: 100%;
649
- border-collapse: collapse;
650
- font-size: 13px;
651
- }
652
-
653
- .vm-import-btn__th,
654
- .vm-import-btn__td {
655
- max-width: 220px;
656
- padding: 10px 12px;
657
- border-bottom: 1px solid var(--border);
658
- overflow: hidden;
659
- text-overflow: ellipsis;
660
- white-space: nowrap;
661
- text-align: left;
662
- }
663
-
664
- .vm-import-btn__th {
665
- position: sticky;
666
- top: 0;
667
- z-index: 1;
668
- background: var(--muted);
669
- color: var(--muted-foreground);
670
- font-weight: 600;
671
- }
672
-
673
- .vm-import-btn__pager {
674
- display: flex;
675
- flex-shrink: 0;
676
- align-items: center;
677
- justify-content: space-between;
678
- gap: 12px;
679
- }
680
-
681
- .vm-import-btn__total {
682
- color: var(--muted-foreground);
683
- font-size: 13px;
684
- }
685
-
686
- .vm-import-btn__pager-actions {
687
- display: inline-flex;
688
- align-items: center;
689
- gap: 8px;
690
- }
691
-
692
- .vm-import-btn__page-num {
693
- min-width: 52px;
694
- color: var(--muted-foreground);
695
- font-size: 12px;
696
- text-align: center;
697
- }
698
-
699
- .vm-import-btn__page-btn {
700
- height: 30px;
701
- padding: 0 12px;
702
- border: none;
703
- border-radius: 10px;
704
- background: var(--muted);
705
- color: var(--foreground);
706
- font-size: 12px;
707
- font-weight: 600;
708
- cursor: pointer;
709
-
710
- &:hover:not(:disabled) {
711
- background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
712
- }
713
-
714
- &:disabled {
715
- cursor: not-allowed;
716
- opacity: 0.45;
717
- }
718
- }
719
-
720
- .vm-crud-toolbar__btn {
721
- display: inline-flex;
722
- height: 36px;
723
- align-items: center;
724
- gap: 4px;
725
- padding: 0 14px;
726
- border: none;
727
- border-radius: 12px;
728
- font-size: 13px;
729
- font-weight: 600;
730
- cursor: pointer;
731
- transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
732
-
733
- &:disabled {
734
- cursor: not-allowed;
735
- opacity: 0.45;
736
- }
737
-
738
- &.is-ghost {
739
- background: var(--muted);
740
- color: var(--foreground);
741
-
742
- &:hover:not(:disabled) {
743
- background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
744
- }
745
- }
746
- }
747
- </style>
1
+ <template>
2
+ <div v-if="visible" class="vm-import-btn">
3
+ <button
4
+ type="button"
5
+ class="vm-crud-toolbar__btn is-ghost"
6
+ :disabled="disabled"
7
+ @click="openDialog"
8
+ >
9
+ <i class="ri-upload-2-line" />
10
+ {{ buttonText }}
11
+ </button>
12
+
13
+ <Dialog :open="open" @update:open="onOpenChange">
14
+ <DialogContent
15
+ shell="crud"
16
+ class="vm-import-btn__dialog is-height"
17
+ :style="{ '--vm-dialog-height': '600px' }"
18
+ @pointer-down-outside="preventDialogOutsideClose"
19
+ @focus-outside="preventDialogOutsideClose"
20
+ @interact-outside="preventDialogOutsideClose"
21
+ >
22
+ <DialogHeader class="vm-crud-shell__head">
23
+ <DialogTitle class="vm-crud-shell__title">{{ crudLabel('import') }}</DialogTitle>
24
+ </DialogHeader>
25
+
26
+ <div class="vm-crud-shell__body vm-import-btn__body">
27
+ <div class="vm-import-btn__bar">
28
+ <p v-if="tip" class="vm-import-btn__tip">{{ tip }}</p>
29
+ <span v-else-if="file" class="vm-import-btn__file-name">
30
+ {{ file.name }}
31
+ </span>
32
+ <span v-else />
33
+ <div class="vm-import-btn__bar-actions">
34
+ <button
35
+ v-if="previewRows.length"
36
+ type="button"
37
+ class="vm-import-btn__repick"
38
+ :disabled="submitting || parsing"
39
+ @click="onPick"
40
+ >
41
+ {{ crudLabel('reselect') }}
42
+ </button>
43
+ <button
44
+ type="button"
45
+ class="vm-import-btn__tpl"
46
+ :disabled="submitting || downloading || parsing"
47
+ @click="onTemplate"
48
+ >
49
+ {{ downloading ? crudLabel('downloading') : crudLabel('downloadTpl') }}
50
+ </button>
51
+ </div>
52
+ </div>
53
+
54
+ <div
55
+ v-if="!previewRows.length"
56
+ class="vm-import-btn__drop"
57
+ :class="{
58
+ 'is-drag': dragging,
59
+ 'is-disabled': submitting || parsing,
60
+ }"
61
+ @click="onPick"
62
+ @dragenter.prevent="onDragEnter"
63
+ @dragover.prevent
64
+ @dragleave.prevent="onDragLeave"
65
+ @drop.prevent="onDrop"
66
+ >
67
+ <i class="ri-upload-cloud-2-line" />
68
+ <span v-if="parsing">{{ crudLabel('parsing') }}</span>
69
+ <span v-else>
70
+ {{ crudFormat(crudLabel('importDropHint'), maxSizeMb) }}
71
+ </span>
72
+ <span class="vm-import-btn__ext">{{ crudLabel('importExtHint') }}</span>
73
+ </div>
74
+
75
+ <div v-if="previewRows.length" class="vm-import-btn__preview">
76
+ <div class="vm-import-btn__table-wrap">
77
+ <table class="vm-import-btn__table">
78
+ <thead>
79
+ <tr>
80
+ <th
81
+ v-for="col in previewHeaders"
82
+ :key="col"
83
+ class="vm-import-btn__th"
84
+ >
85
+ {{ col }}
86
+ </th>
87
+ </tr>
88
+ </thead>
89
+ <tbody>
90
+ <tr v-for="(row, idx) in pageRows" :key="pageStart + idx">
91
+ <td
92
+ v-for="col in previewHeaders"
93
+ :key="col"
94
+ class="vm-import-btn__td"
95
+ >
96
+ {{ formatCell(row[col]) }}
97
+ </td>
98
+ </tr>
99
+ </tbody>
100
+ </table>
101
+ </div>
102
+ <div class="vm-import-btn__pager">
103
+ <span class="vm-import-btn__total">{{ crudFormat(crudLabel('total'), previewRows.length) }}</span>
104
+ <div class="vm-import-btn__pager-actions">
105
+ <button
106
+ type="button"
107
+ class="vm-import-btn__page-btn"
108
+ :disabled="page <= 1 || submitting"
109
+ @click="goPrevPage"
110
+ >
111
+ {{ crudLabel('prevPage') }}
112
+ </button>
113
+ <span class="vm-import-btn__page-num">
114
+ {{ page }} / {{ totalPages }}
115
+ </span>
116
+ <button
117
+ type="button"
118
+ class="vm-import-btn__page-btn"
119
+ :disabled="page >= totalPages || submitting"
120
+ @click="goNextPage"
121
+ >
122
+ {{ crudLabel('nextPage') }}
123
+ </button>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <input
129
+ ref="inputRef"
130
+ type="file"
131
+ class="vm-import-btn__input"
132
+ accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
133
+ @change="onFileInput"
134
+ />
135
+ </div>
136
+
137
+ <DialogFooter class="vm-crud-shell__foot">
138
+ <button
139
+ type="button"
140
+ class="vm-crud-shell__btn is-ghost"
141
+ :disabled="submitting"
142
+ @click="closeDialog"
143
+ >
144
+ {{ crudLabel('close') }}
145
+ </button>
146
+ <button
147
+ type="button"
148
+ class="vm-crud-shell__btn is-primary"
149
+ :disabled="submitting || parsing || !canSubmit"
150
+ @click="onSubmit"
151
+ >
152
+ {{ submitting ? crudLabel('submitting') : crudLabel('submit') }}
153
+ </button>
154
+ </DialogFooter>
155
+ </DialogContent>
156
+ </Dialog>
157
+ </div>
158
+ </template>
159
+
160
+ <script setup lang="ts">
161
+ import { computed, ref } from 'vue'
162
+ import { toast } from 'vue-sonner'
163
+ import { preventDialogOutsideClose } from '../lib/dialog-float'
164
+ import {
165
+ downloadImportTemplate,
166
+ parseImportExcelPreview,
167
+ uploadImportExcel,
168
+ uploadImportRows,
169
+ type ImportResult,
170
+ } from '../lib/import-excel'
171
+ import { useCrud } from './useCrud'
172
+ import { injectImportOptions } from './key'
173
+ import { crudFormat, crudLabel } from './style'
174
+ import type { CrudPermission } from '../../../typings/admin/comm/crud'
175
+
176
+ defineOptions({ name: 'vm-import-btn' })
177
+
178
+ /** 与服务端 importTemplate(excelFilename → .xlsx)保持一致 */
179
+ const ACCEPT_EXT = '.xlsx'
180
+ const MAX_SIZE_MB_DEFAULT = 10
181
+ const PAGE_SIZE = 20
182
+
183
+ const props = withDefaults(
184
+ defineProps<{
185
+ disabled?: boolean
186
+ text?: string
187
+ /** 弹窗左上提示文案 */
188
+ tip?: string
189
+ templateFilename?: string | (() => string)
190
+ /** 文件大小上限(MB) */
191
+ maxSizeMb?: number
192
+ /** 权限键,默认 add */
193
+ permission?: keyof CrudPermission
194
+ }>(),
195
+ {
196
+ disabled: false,
197
+ tip: '',
198
+ maxSizeMb: MAX_SIZE_MB_DEFAULT,
199
+ permission: 'add',
200
+ },
201
+ )
202
+
203
+ const emit = defineEmits<{
204
+ success: [result: { inserted: number; updated: number; failed: number }]
205
+ }>()
206
+
207
+ const crud = useCrud()
208
+ const importOpts = injectImportOptions()
209
+ const open = ref(false)
210
+ const submitting = ref(false)
211
+ const downloading = ref(false)
212
+ const parsing = ref(false)
213
+ const dragging = ref(false)
214
+ const file = ref<File | null>(null)
215
+ const previewHeaders = ref<string[]>([])
216
+ const previewRows = ref<Record<string, unknown>[]>([])
217
+ const page = ref(1)
218
+ const inputRef = ref<HTMLInputElement | null>(null)
219
+ const dragDepth = ref(0)
220
+
221
+ const visible = computed(() => Boolean(crud.getPermission(props.permission)))
222
+
223
+ const buttonText = computed(
224
+ () => props.text || crudLabel('import', crud.dict.label),
225
+ )
226
+
227
+ const totalPages = computed(() =>
228
+ Math.max(1, Math.ceil(previewRows.value.length / PAGE_SIZE)),
229
+ )
230
+
231
+ const pageStart = computed(() => (page.value - 1) * PAGE_SIZE)
232
+
233
+ const pageRows = computed(() =>
234
+ previewRows.value.slice(pageStart.value, pageStart.value + PAGE_SIZE),
235
+ )
236
+
237
+ const canSubmit = computed(
238
+ () => Boolean(file.value) && previewRows.value.length > 0,
239
+ )
240
+
241
+ function goPrevPage() {
242
+ if (page.value <= 1) return
243
+ page.value -= 1
244
+ }
245
+
246
+ function goNextPage() {
247
+ if (page.value >= totalPages.value) return
248
+ page.value += 1
249
+ }
250
+
251
+ function formatCell(v: unknown) {
252
+ if (v == null) return ''
253
+ return String(v)
254
+ }
255
+
256
+ function resolveTemplateName() {
257
+ const raw = props.templateFilename
258
+ const name = typeof raw === 'function' ? raw() : raw
259
+ const base = name || crudLabel('importTpl')
260
+ return base.endsWith(ACCEPT_EXT) ? base : `${base}${ACCEPT_EXT}`
261
+ }
262
+
263
+ function resetPreview() {
264
+ file.value = null
265
+ previewHeaders.value = []
266
+ previewRows.value = []
267
+ page.value = 1
268
+ parsing.value = false
269
+ }
270
+
271
+ function openDialog() {
272
+ if (props.disabled) return
273
+ resetPreview()
274
+ open.value = true
275
+ }
276
+
277
+ function closeDialog() {
278
+ if (submitting.value) return
279
+ open.value = false
280
+ resetPreview()
281
+ dragging.value = false
282
+ dragDepth.value = 0
283
+ }
284
+
285
+ function onOpenChange(v: boolean) {
286
+ if (!v) {
287
+ closeDialog()
288
+ return
289
+ }
290
+ open.value = true
291
+ }
292
+
293
+ function onPick() {
294
+ if (submitting.value || parsing.value || props.disabled) return
295
+ inputRef.value?.click()
296
+ }
297
+
298
+ function isXlsx(f: File) {
299
+ return f.name.toLowerCase().endsWith(ACCEPT_EXT)
300
+ }
301
+
302
+ async function acceptFile(f: File | undefined | null) {
303
+ if (!f || parsing.value || submitting.value) return
304
+ if (!isXlsx(f)) {
305
+ toast.error(crudFormat(crudLabel('importExtOnly'), ACCEPT_EXT))
306
+ return
307
+ }
308
+ const limit = props.maxSizeMb * 1024 * 1024
309
+ if (f.size > limit) {
310
+ toast.error(crudFormat(crudLabel('fileSizeLimit'), props.maxSizeMb))
311
+ return
312
+ }
313
+
314
+ parsing.value = true
315
+ try {
316
+ const preview = await parseImportExcelPreview(f)
317
+ if (!preview.rows.length) {
318
+ resetPreview()
319
+ toast.error(crudLabel('noImportRows'))
320
+ return
321
+ }
322
+ file.value = f
323
+ previewHeaders.value = preview.headers
324
+ previewRows.value = preview.rows
325
+ page.value = 1
326
+ } catch (e) {
327
+ resetPreview()
328
+ toast.error(e instanceof Error ? e.message : crudLabel('parseExcelFail'))
329
+ } finally {
330
+ parsing.value = false
331
+ }
332
+ }
333
+
334
+ function onFileInput(ev: Event) {
335
+ const input = ev.target as HTMLInputElement
336
+ const f = input.files?.[0]
337
+ input.value = ''
338
+ void acceptFile(f)
339
+ }
340
+
341
+ function onDragEnter() {
342
+ if (submitting.value || parsing.value) return
343
+ dragDepth.value += 1
344
+ dragging.value = true
345
+ }
346
+
347
+ function onDragLeave() {
348
+ dragDepth.value = Math.max(0, dragDepth.value - 1)
349
+ if (dragDepth.value === 0) dragging.value = false
350
+ }
351
+
352
+ function onDrop(ev: DragEvent) {
353
+ dragDepth.value = 0
354
+ dragging.value = false
355
+ if (submitting.value || parsing.value) return
356
+ void acceptFile(ev.dataTransfer?.files?.[0])
357
+ }
358
+
359
+ async function onTemplate() {
360
+ if (downloading.value || submitting.value || props.disabled) return
361
+ const ns = crud.service?.namespace
362
+ if (!ns) {
363
+ toast.error(crudLabel('noServiceTpl'))
364
+ return
365
+ }
366
+ downloading.value = true
367
+ try {
368
+ await downloadImportTemplate(ns, resolveTemplateName())
369
+ toast.success(crudLabel('tplDownloaded'))
370
+ } catch (e) {
371
+ toast.error(e instanceof Error ? e.message : crudLabel('downloadTplFail'))
372
+ } finally {
373
+ downloading.value = false
374
+ }
375
+ }
376
+
377
+ async function finishImport(result: ImportResult) {
378
+ const failN = result.failed?.length || 0
379
+ if (failN) {
380
+ const tipText = result.failed
381
+ .slice(0, 3)
382
+ .map((f) => crudFormat(crudLabel('importRowFail'), f.row, f.message))
383
+ .join(';')
384
+ toast.warning(
385
+ crudFormat(
386
+ crudLabel('importDone'),
387
+ result.inserted,
388
+ result.updated,
389
+ failN,
390
+ tipText ? `(${tipText})` : '',
391
+ ),
392
+ )
393
+ } else {
394
+ toast.success(
395
+ crudFormat(crudLabel('importSuccess'), result.inserted, result.updated),
396
+ )
397
+ }
398
+ emit('success', {
399
+ inserted: result.inserted,
400
+ updated: result.updated,
401
+ failed: failN,
402
+ })
403
+ await crud.refresh()
404
+ open.value = false
405
+ resetPreview()
406
+ }
407
+
408
+ async function runDefaultImport() {
409
+ const ns = crud.service?.namespace
410
+ if (!ns) {
411
+ toast.error(crudLabel('noServiceImport'))
412
+ return null
413
+ }
414
+ // 与预览一致:走 JSON `{ rows }`,避免再传文件触发 multipart 二次读 body
415
+ const result = await uploadImportRows(ns, previewRows.value)
416
+ await finishImport(result)
417
+ return result
418
+ }
419
+
420
+ async function onSubmit() {
421
+ if (submitting.value || parsing.value || props.disabled || !canSubmit.value) {
422
+ return
423
+ }
424
+ if (!file.value || !isXlsx(file.value)) {
425
+ toast.error(crudFormat(crudLabel('importExtOnly'), ACCEPT_EXT))
426
+ return
427
+ }
428
+ if (!previewRows.value.length) {
429
+ toast.error(crudLabel('pickExcelFirst'))
430
+ return
431
+ }
432
+
433
+ const ns = crud.service?.namespace
434
+ if (!ns) {
435
+ toast.error(crudLabel('noServiceImport'))
436
+ return
437
+ }
438
+
439
+ submitting.value = true
440
+ try {
441
+ if (importOpts?.onSubmit) {
442
+ await importOpts.onSubmit(
443
+ {
444
+ file: file.value,
445
+ rows: previewRows.value,
446
+ headers: previewHeaders.value,
447
+ },
448
+ {
449
+ next: async (payload) => {
450
+ if (Array.isArray(payload)) {
451
+ if (!payload.length) {
452
+ throw new Error(crudLabel('noImportRows'))
453
+ }
454
+ const result = await uploadImportRows(ns, payload)
455
+ await finishImport(result)
456
+ return result
457
+ }
458
+ const f = payload ?? file.value
459
+ if (!f || !isXlsx(f)) {
460
+ throw new Error(crudFormat(crudLabel('importExtOnly'), ACCEPT_EXT))
461
+ }
462
+ const result = await uploadImportExcel(ns, f)
463
+ await finishImport(result)
464
+ return result
465
+ },
466
+ done: () => {},
467
+ close: () => {
468
+ open.value = false
469
+ resetPreview()
470
+ },
471
+ },
472
+ )
473
+ return
474
+ }
475
+ await runDefaultImport()
476
+ } catch (e) {
477
+ toast.error(e instanceof Error ? e.message : crudLabel('importFail'))
478
+ } finally {
479
+ submitting.value = false
480
+ }
481
+ }
482
+ </script>
483
+
484
+ <style lang="scss" scoped>
485
+ .vm-import-btn {
486
+ display: inline-flex;
487
+ align-items: center;
488
+ }
489
+
490
+ .vm-import-btn__input {
491
+ display: none;
492
+ }
493
+
494
+ /* 导入弹窗:高度 600,宽度沿用默认 CRUD(50vw) */
495
+ :deep(.vm-import-btn__dialog[data-slot='dialog-content']),
496
+ :deep(.vm-import-btn__dialog.vm-dialog-shell-crud) {
497
+ height: 600px !important;
498
+ max-height: 600px !important;
499
+ }
500
+
501
+ .vm-import-btn__body {
502
+ display: flex;
503
+ min-height: 0;
504
+ flex: 1;
505
+ flex-direction: column;
506
+ gap: 12px;
507
+ overflow: hidden;
508
+ }
509
+
510
+ .vm-import-btn__bar {
511
+ display: flex;
512
+ flex-shrink: 0;
513
+ align-items: center;
514
+ justify-content: space-between;
515
+ gap: 12px;
516
+ }
517
+
518
+ .vm-import-btn__bar-actions {
519
+ display: inline-flex;
520
+ flex-shrink: 0;
521
+ align-items: center;
522
+ gap: 8px;
523
+ }
524
+
525
+ .vm-import-btn__file-name {
526
+ overflow: hidden;
527
+ color: var(--muted-foreground);
528
+ font-size: 13px;
529
+ text-overflow: ellipsis;
530
+ white-space: nowrap;
531
+ }
532
+
533
+ .vm-import-btn__tip {
534
+ margin: 0;
535
+ color: var(--warning);
536
+ font-size: 13px;
537
+ font-weight: 500;
538
+ }
539
+
540
+ .vm-import-btn__repick {
541
+ flex-shrink: 0;
542
+ height: 32px;
543
+ padding: 0 12px;
544
+ border: none;
545
+ border-radius: 10px;
546
+ background: var(--muted);
547
+ color: var(--foreground);
548
+ font-size: 13px;
549
+ font-weight: 600;
550
+ cursor: pointer;
551
+
552
+ &:hover:not(:disabled) {
553
+ background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
554
+ }
555
+
556
+ &:disabled {
557
+ cursor: not-allowed;
558
+ opacity: 0.45;
559
+ }
560
+ }
561
+
562
+ .vm-import-btn__tpl {
563
+ flex-shrink: 0;
564
+ height: 32px;
565
+ padding: 0 12px;
566
+ border: none;
567
+ border-radius: 10px;
568
+ background: var(--brand-soft);
569
+ color: var(--brand);
570
+ font-size: 13px;
571
+ font-weight: 600;
572
+ cursor: pointer;
573
+ transition: background 0.15s ease, opacity 0.15s ease;
574
+
575
+ &:hover:not(:disabled) {
576
+ background: color-mix(in srgb, var(--brand-soft) 70%, var(--brand) 12%);
577
+ }
578
+
579
+ &:disabled {
580
+ cursor: not-allowed;
581
+ opacity: 0.45;
582
+ }
583
+ }
584
+
585
+ .vm-import-btn__drop {
586
+ display: grid;
587
+ flex: 1;
588
+ min-height: 0;
589
+ place-items: center;
590
+ place-content: center;
591
+ gap: 8px;
592
+ padding: 20px 16px;
593
+ border: 1px dashed var(--border);
594
+ border-radius: 12px;
595
+ background: var(--muted);
596
+ color: var(--muted-foreground);
597
+ font-size: 13px;
598
+ text-align: center;
599
+ cursor: pointer;
600
+ transition:
601
+ border-color 0.15s ease,
602
+ color 0.15s ease,
603
+ background 0.15s ease;
604
+
605
+ i {
606
+ font-size: 32px;
607
+ line-height: 1;
608
+ }
609
+
610
+ &:hover:not(.is-disabled),
611
+ &.is-drag {
612
+ border-color: var(--brand);
613
+ background: var(--brand-soft);
614
+ color: var(--brand);
615
+ }
616
+
617
+ &.is-disabled {
618
+ cursor: not-allowed;
619
+ opacity: 0.55;
620
+ }
621
+ }
622
+
623
+ .vm-import-btn__ext {
624
+ font-size: 12px;
625
+ opacity: 0.8;
626
+ }
627
+
628
+ .vm-import-btn__preview {
629
+ display: flex;
630
+ min-height: 0;
631
+ flex: 1;
632
+ flex-direction: column;
633
+ gap: 10px;
634
+ overflow: hidden;
635
+ }
636
+
637
+ .vm-import-btn__table-wrap {
638
+ min-height: 0;
639
+ flex: 1;
640
+ overflow: auto;
641
+ border: 1px solid var(--border);
642
+ border-radius: 12px;
643
+ background: var(--card);
644
+ }
645
+
646
+ .vm-import-btn__table {
647
+ width: max-content;
648
+ min-width: 100%;
649
+ border-collapse: collapse;
650
+ font-size: 13px;
651
+ }
652
+
653
+ .vm-import-btn__th,
654
+ .vm-import-btn__td {
655
+ max-width: 220px;
656
+ padding: 10px 12px;
657
+ border-bottom: 1px solid var(--border);
658
+ overflow: hidden;
659
+ text-overflow: ellipsis;
660
+ white-space: nowrap;
661
+ text-align: left;
662
+ }
663
+
664
+ .vm-import-btn__th {
665
+ position: sticky;
666
+ top: 0;
667
+ z-index: 1;
668
+ background: var(--muted);
669
+ color: var(--muted-foreground);
670
+ font-weight: 600;
671
+ }
672
+
673
+ .vm-import-btn__pager {
674
+ display: flex;
675
+ flex-shrink: 0;
676
+ align-items: center;
677
+ justify-content: space-between;
678
+ gap: 12px;
679
+ }
680
+
681
+ .vm-import-btn__total {
682
+ color: var(--muted-foreground);
683
+ font-size: 13px;
684
+ }
685
+
686
+ .vm-import-btn__pager-actions {
687
+ display: inline-flex;
688
+ align-items: center;
689
+ gap: 8px;
690
+ }
691
+
692
+ .vm-import-btn__page-num {
693
+ min-width: 52px;
694
+ color: var(--muted-foreground);
695
+ font-size: 12px;
696
+ text-align: center;
697
+ }
698
+
699
+ .vm-import-btn__page-btn {
700
+ height: 30px;
701
+ padding: 0 12px;
702
+ border: none;
703
+ border-radius: 10px;
704
+ background: var(--muted);
705
+ color: var(--foreground);
706
+ font-size: 12px;
707
+ font-weight: 600;
708
+ cursor: pointer;
709
+
710
+ &:hover:not(:disabled) {
711
+ background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
712
+ }
713
+
714
+ &:disabled {
715
+ cursor: not-allowed;
716
+ opacity: 0.45;
717
+ }
718
+ }
719
+
720
+ .vm-crud-toolbar__btn {
721
+ display: inline-flex;
722
+ height: 36px;
723
+ align-items: center;
724
+ gap: 4px;
725
+ padding: 0 14px;
726
+ border: none;
727
+ border-radius: 12px;
728
+ font-size: 13px;
729
+ font-weight: 600;
730
+ cursor: pointer;
731
+ transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
732
+
733
+ &:disabled {
734
+ cursor: not-allowed;
735
+ opacity: 0.45;
736
+ }
737
+
738
+ &.is-ghost {
739
+ background: var(--muted);
740
+ color: var(--foreground);
741
+
742
+ &:hover:not(:disabled) {
743
+ background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
744
+ }
745
+ }
746
+ }
747
+ </style>