vome-core 0.3.65 → 0.3.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/dist/admin/api/client.js +7 -2
  2. package/dist/admin/components/ui/avatar/Avatar.vue +18 -18
  3. package/dist/admin/components/ui/avatar/AvatarFallback.vue +21 -21
  4. package/dist/admin/components/ui/avatar/AvatarImage.vue +16 -16
  5. package/dist/admin/components/ui/avatar/index.js +1 -1
  6. package/dist/admin/components/ui/badge/Badge.vue +26 -26
  7. package/dist/admin/components/ui/button/Button.vue +31 -31
  8. package/dist/admin/components/ui/checkbox/Checkbox.vue +35 -35
  9. package/dist/admin/components/ui/checkbox/index.js +1 -1
  10. package/dist/admin/components/ui/dialog/Dialog.vue +19 -19
  11. package/dist/admin/components/ui/dialog/DialogClose.vue +15 -15
  12. package/dist/admin/components/ui/dialog/DialogContent.vue +267 -267
  13. package/dist/admin/components/ui/dialog/DialogDescription.vue +23 -23
  14. package/dist/admin/components/ui/dialog/DialogFooter.vue +27 -27
  15. package/dist/admin/components/ui/dialog/DialogHeader.vue +17 -17
  16. package/dist/admin/components/ui/dialog/DialogOverlay.vue +21 -21
  17. package/dist/admin/components/ui/dialog/DialogScrollContent.vue +60 -60
  18. package/dist/admin/components/ui/dialog/DialogTitle.vue +23 -23
  19. package/dist/admin/components/ui/dialog/DialogTrigger.vue +15 -15
  20. package/dist/admin/components/ui/dialog/index.js +1 -1
  21. package/dist/admin/components/ui/dropdown-menu/DropdownMenu.vue +19 -19
  22. package/dist/admin/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +39 -39
  23. package/dist/admin/components/ui/dropdown-menu/DropdownMenuContent.vue +39 -39
  24. package/dist/admin/components/ui/dropdown-menu/DropdownMenuGroup.vue +15 -15
  25. package/dist/admin/components/ui/dropdown-menu/DropdownMenuItem.vue +31 -31
  26. package/dist/admin/components/ui/dropdown-menu/DropdownMenuLabel.vue +23 -23
  27. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +21 -21
  28. package/dist/admin/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -40
  29. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSeparator.vue +23 -23
  30. package/dist/admin/components/ui/dropdown-menu/DropdownMenuShortcut.vue +17 -17
  31. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSub.vue +18 -18
  32. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubContent.vue +27 -27
  33. package/dist/admin/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +31 -31
  34. package/dist/admin/components/ui/dropdown-menu/DropdownMenuTrigger.vue +17 -17
  35. package/dist/admin/components/ui/dropdown-menu/index.js +1 -1
  36. package/dist/admin/components/ui/input/Input.vue +183 -183
  37. package/dist/admin/components/ui/input/index.js +1 -1
  38. package/dist/admin/components/ui/label/Label.vue +26 -26
  39. package/dist/admin/components/ui/label/index.js +1 -1
  40. package/dist/admin/components/ui/scroll-area/ScrollArea.vue +33 -33
  41. package/dist/admin/components/ui/scroll-area/ScrollBar.vue +32 -32
  42. package/dist/admin/components/ui/scroll-area/index.js +1 -1
  43. package/dist/admin/components/ui/select/Select.vue +19 -19
  44. package/dist/admin/components/ui/select/SelectContent.vue +51 -51
  45. package/dist/admin/components/ui/select/SelectGroup.vue +15 -15
  46. package/dist/admin/components/ui/select/SelectItem.vue +53 -53
  47. package/dist/admin/components/ui/select/SelectItemText.vue +15 -15
  48. package/dist/admin/components/ui/select/SelectLabel.vue +17 -17
  49. package/dist/admin/components/ui/select/SelectScrollDownButton.vue +26 -26
  50. package/dist/admin/components/ui/select/SelectScrollUpButton.vue +26 -26
  51. package/dist/admin/components/ui/select/SelectSeparator.vue +19 -19
  52. package/dist/admin/components/ui/select/SelectTrigger.vue +44 -44
  53. package/dist/admin/components/ui/select/SelectValue.vue +15 -15
  54. package/dist/admin/components/ui/select/index.js +1 -1
  55. package/dist/admin/components/ui/separator/Separator.vue +29 -29
  56. package/dist/admin/components/ui/separator/index.js +1 -1
  57. package/dist/admin/components/ui/sheet/Sheet.vue +19 -19
  58. package/dist/admin/components/ui/sheet/SheetClose.vue +15 -15
  59. package/dist/admin/components/ui/sheet/SheetContent.vue +63 -63
  60. package/dist/admin/components/ui/sheet/SheetDescription.vue +21 -21
  61. package/dist/admin/components/ui/sheet/SheetFooter.vue +16 -16
  62. package/dist/admin/components/ui/sheet/SheetHeader.vue +15 -15
  63. package/dist/admin/components/ui/sheet/SheetOverlay.vue +21 -21
  64. package/dist/admin/components/ui/sheet/SheetTitle.vue +21 -21
  65. package/dist/admin/components/ui/sheet/SheetTrigger.vue +15 -15
  66. package/dist/admin/components/ui/sheet/index.js +1 -1
  67. package/dist/admin/components/ui/sonner/Toaster.vue +117 -117
  68. package/dist/admin/components/ui/sonner/index.js +1 -1
  69. package/dist/admin/components/ui/table/Table.vue +16 -16
  70. package/dist/admin/components/ui/table/TableBody.vue +17 -17
  71. package/dist/admin/components/ui/table/TableCaption.vue +17 -17
  72. package/dist/admin/components/ui/table/TableCell.vue +22 -22
  73. package/dist/admin/components/ui/table/TableEmpty.vue +34 -34
  74. package/dist/admin/components/ui/table/TableFooter.vue +17 -17
  75. package/dist/admin/components/ui/table/TableHead.vue +17 -17
  76. package/dist/admin/components/ui/table/TableHeader.vue +17 -17
  77. package/dist/admin/components/ui/table/TableRow.vue +17 -17
  78. package/dist/admin/components/ui/table/index.js +1 -1
  79. package/dist/admin/components/ui/tabs/Tabs.vue +24 -24
  80. package/dist/admin/components/ui/tabs/TabsContent.vue +21 -21
  81. package/dist/admin/components/ui/tabs/TabsList.vue +24 -24
  82. package/dist/admin/components/ui/tabs/TabsTrigger.vue +26 -26
  83. package/dist/admin/components/ui/tabs/index.js +1 -1
  84. package/dist/admin/components/ui/tooltip/Tooltip.vue +19 -19
  85. package/dist/admin/components/ui/tooltip/TooltipContent.vue +34 -34
  86. package/dist/admin/components/ui/tooltip/TooltipProvider.vue +14 -14
  87. package/dist/admin/components/ui/tooltip/TooltipTrigger.vue +15 -15
  88. package/dist/admin/components/ui/tooltip/index.js +1 -1
  89. package/dist/admin/components/vm-action-btn.vue +136 -136
  90. package/dist/admin/components/vm-aside-list-item.vue +136 -136
  91. package/dist/admin/components/vm-aside.vue +229 -229
  92. package/dist/admin/components/vm-auto-perm-dialog.vue +371 -371
  93. package/dist/admin/components/vm-card-grid.vue +37 -37
  94. package/dist/admin/components/vm-card-head.vue +45 -45
  95. package/dist/admin/components/vm-card-meta.vue +22 -22
  96. package/dist/admin/components/vm-card-title.vue +56 -56
  97. package/dist/admin/components/vm-card.vue +176 -176
  98. package/dist/admin/components/vm-check-tree.vue +340 -340
  99. package/dist/admin/components/vm-clamp-text.vue +39 -39
  100. package/dist/admin/components/vm-dept-tree.vue +497 -497
  101. package/dist/admin/components/vm-empty.vue +68 -68
  102. package/dist/admin/components/vm-eps-perm-picker.vue +123 -123
  103. package/dist/admin/components/vm-form-dialog.vue +632 -632
  104. package/dist/admin/components/vm-group-cascader.vue +560 -560
  105. package/dist/admin/components/vm-icon-btn.vue +88 -88
  106. package/dist/admin/components/vm-icon-picker.vue +647 -647
  107. package/dist/admin/components/vm-inline-search.vue +61 -61
  108. package/dist/admin/components/vm-loading.vue +114 -114
  109. package/dist/admin/components/vm-markdown.vue +405 -405
  110. package/dist/admin/components/vm-marquee.vue +265 -265
  111. package/dist/admin/components/vm-page-tabs.vue +94 -94
  112. package/dist/admin/components/vm-perm-section.vue +57 -57
  113. package/dist/admin/components/vm-ri-icon.vue +23 -23
  114. package/dist/admin/components/vm-role-picker.vue +314 -314
  115. package/dist/admin/components/vm-split-layout.vue +106 -106
  116. package/dist/admin/components/vm-thumb.vue +49 -49
  117. package/dist/admin/components/vm-view-path-picker.vue +59 -59
  118. package/dist/admin/crud/auto-fields.js +1 -1
  119. package/dist/admin/crud/components/vm-avatar.vue +104 -104
  120. package/dist/admin/crud/components/vm-color-picker.vue +1626 -1626
  121. package/dist/admin/crud/components/vm-column-custom.vue +193 -193
  122. package/dist/admin/crud/components/vm-context-menu.vue +105 -105
  123. package/dist/admin/crud/components/vm-copy-btn.vue +102 -102
  124. package/dist/admin/crud/components/vm-date-calendar-panel.vue +537 -537
  125. package/dist/admin/crud/components/vm-date-picker.vue +75 -75
  126. package/dist/admin/crud/components/vm-date-range.vue +88 -88
  127. package/dist/admin/crud/components/vm-date-text.vue +42 -42
  128. package/dist/admin/crud/components/vm-datetime-field.vue +215 -215
  129. package/dist/admin/crud/components/vm-datetime-picker.vue +9 -9
  130. package/dist/admin/crud/components/vm-dict-tag.vue +112 -112
  131. package/dist/admin/crud/components/vm-ellipsis-text.vue +34 -34
  132. package/dist/admin/crud/components/vm-form-hint.vue +81 -81
  133. package/dist/admin/crud/components/vm-json-code.vue +41 -41
  134. package/dist/admin/crud/components/vm-json-editor.vue +161 -161
  135. package/dist/admin/crud/components/vm-json-kv-editor.vue +318 -318
  136. package/dist/admin/crud/components/vm-kv-row.vue +51 -51
  137. package/dist/admin/crud/components/vm-multi-select.vue +279 -279
  138. package/dist/admin/crud/components/vm-number-range.vue +204 -204
  139. package/dist/admin/crud/components/vm-preview-viewer.vue +894 -894
  140. package/dist/admin/crud/components/vm-radio.vue +87 -87
  141. package/dist/admin/crud/components/vm-richtext.vue +313 -313
  142. package/dist/admin/crud/components/vm-select.vue +570 -570
  143. package/dist/admin/crud/components/vm-status-tag.vue +158 -158
  144. package/dist/admin/crud/components/vm-switch.vue +158 -158
  145. package/dist/admin/crud/components/vm-tag-list.vue +108 -108
  146. package/dist/admin/crud/components/vm-text-link.vue +52 -52
  147. package/dist/admin/crud/components/vm-toggle-switch.vue +84 -84
  148. package/dist/admin/crud/components/vm-tree-select.vue +450 -450
  149. package/dist/admin/crud/components/vm-upload-item.vue +298 -298
  150. package/dist/admin/crud/components/vm-upload.vue +436 -436
  151. package/dist/admin/crud/config.js +1 -1
  152. package/dist/admin/crud/confirm.js +1 -1
  153. package/dist/admin/crud/crud-page-request.js +1 -1
  154. package/dist/admin/crud/data-i18n.js +1 -1
  155. package/dist/admin/crud/dict.js +1 -1
  156. package/dist/admin/crud/export-fetch.js +1 -1
  157. package/dist/admin/crud/index.js +1 -0
  158. package/dist/admin/crud/key.js +1 -1
  159. package/dist/admin/crud/plugins.js +1 -1
  160. package/dist/admin/crud/span.js +1 -1
  161. package/dist/admin/crud/style.js +1 -1
  162. package/dist/admin/crud/useCrud.js +3 -0
  163. package/dist/admin/crud/validate.js +1 -1
  164. package/dist/admin/crud/vm-add-btn.vue +70 -70
  165. package/dist/admin/crud/vm-adv-search.vue +83 -83
  166. package/dist/admin/crud/vm-crud.vue +711 -711
  167. package/dist/admin/crud/vm-data-i18n-btn.vue +456 -456
  168. package/dist/admin/crud/vm-data-i18n-lang.vue +183 -183
  169. package/dist/admin/crud/vm-export-btn.vue +183 -183
  170. package/dist/admin/crud/vm-flex.vue +14 -14
  171. package/dist/admin/crud/vm-form.vue +33 -15
  172. package/dist/admin/crud/vm-import-btn.vue +747 -747
  173. package/dist/admin/crud/vm-multi-delete-btn.vue +49 -49
  174. package/dist/admin/crud/vm-pagination.vue +212 -212
  175. package/dist/admin/crud/vm-refresh-btn.vue +35 -35
  176. package/dist/admin/crud/vm-row.vue +55 -55
  177. package/dist/admin/crud/vm-search-key.vue +78 -78
  178. package/dist/admin/crud/vm-search.vue +466 -466
  179. package/dist/admin/crud/vm-table.vue +1390 -1390
  180. package/dist/admin/crud/vm-tabs.vue +94 -94
  181. package/dist/admin/crud/vm-toolbar.vue +357 -357
  182. package/dist/admin/crud/vm-upsert.vue +765 -765
  183. package/dist/admin/directives/perm.js +1 -1
  184. package/dist/admin/env.d.ts +1 -1
  185. package/dist/admin/hooks/useUpload.js +1 -1
  186. package/dist/admin/index.d.ts +1 -31
  187. package/dist/admin/lib/browser.js +1 -1
  188. package/dist/admin/lib/cn.js +1 -1
  189. package/dist/admin/lib/dialog-float.js +1 -1
  190. package/dist/admin/lib/export-excel.js +1 -1
  191. package/dist/admin/lib/import-excel.js +1 -1
  192. package/dist/admin/lib/json.js +1 -1
  193. package/dist/admin/lib/menu.js +1 -1
  194. package/dist/admin/lib/micro-locale.js +1 -1
  195. package/dist/admin/lib/micro-theme.js +1 -1
  196. package/dist/admin/lib/shell.js +1 -1
  197. package/dist/admin/lib/theme-color-options.js +1 -1
  198. package/dist/admin/lib/tree.js +1 -1
  199. package/dist/admin/lib/upload.js +1 -1
  200. package/dist/admin/lib/video-frame.js +1 -1
  201. package/dist/admin/router/create.js +1 -1
  202. package/dist/admin/router/guards.js +1 -1
  203. package/dist/admin/router/index.d.ts +1 -5
  204. package/dist/admin/service/index.d.ts +1 -11
  205. package/dist/admin/styles/base.css +121 -121
  206. package/dist/admin/vendor/browser-image-compression.js +1144 -1144
  207. package/dist/admin/vendor/decimal-js.js +12 -12
  208. package/dist/admin/vendor/xlsx.js +263 -263
  209. package/dist/ai/index.js +1 -1
  210. package/dist/client/admin-vendor.d.ts +1 -13
  211. package/dist/client/ai-model-registry.js +1 -1
  212. package/dist/client/base.js +1 -1
  213. package/dist/client/configure.js +1 -1
  214. package/dist/client/dict.js +1 -1
  215. package/dist/client/eps-payload.js +1 -1
  216. package/dist/client/eps.js +1 -1
  217. package/dist/client/index.d.ts +1 -7
  218. package/dist/client/index.js +1 -1
  219. package/dist/client/locale-change.js +1 -1
  220. package/dist/client/locale-store.js +1 -1
  221. package/dist/client/service.js +1 -1
  222. package/dist/client/vite-admin.d.ts +1 -40
  223. package/dist/client/vite-admin.js +1 -1
  224. package/dist/client/vite-auto-import.d.ts +1 -2
  225. package/dist/client/vite-auto-import.js +1 -1
  226. package/dist/client/vite-micro-proxy.d.ts +1 -20
  227. package/dist/client/vite-micro-proxy.js +1 -1
  228. package/dist/client/vite-plugin-eps.d.ts +1 -20
  229. package/dist/client/vite-plugin-eps.js +1 -1
  230. package/dist/client/vite-resolve.d.ts +1 -16
  231. package/dist/client/vite-resolve.js +1 -1
  232. package/dist/config/index.js +1 -1
  233. package/dist/index.js +1 -1
  234. package/dist/server/index.js +1 -1
  235. package/dist/shared/datetime-picker.js +1 -1
  236. package/dist/shared/decimal.js +1 -1
  237. package/dist/shared/excel.js +1 -1
  238. package/dist/shared/index.js +1 -1
  239. package/dist/shared/locale-json.js +1 -1
  240. package/dist/shared/tree.js +1 -1
  241. package/package.json +1 -1
  242. package/typings/admin/base/auth.ts +24 -24
  243. package/typings/admin/base/permission.ts +19 -19
  244. package/typings/admin/comm/cascader.ts +7 -7
  245. package/typings/admin/comm/check-tree.ts +7 -7
  246. package/typings/admin/comm/crud.ts +427 -427
  247. package/typings/admin/comm/eps.ts +86 -86
  248. package/typings/admin/comm/request.ts +16 -16
  249. package/typings/admin/comm/service.ts +43 -43
  250. package/typings/admin/comm/upload.ts +39 -39
  251. package/typings/admin/theme.ts +10 -10
  252. package/typings/admin/vome/browser.ts +11 -11
  253. package/typings/admin/vome/module.ts +13 -13
  254. package/typings/admin/vome/tags.ts +9 -9
  255. package/typings/comm/crud.ts +83 -83
  256. package/typings/comm/page.ts +18 -18
  257. package/typings/comm/scan.ts +6 -6
  258. package/typings/common.ts +4 -4
  259. package/typings/context/data.ts +10 -10
  260. package/typings/ext/market-seat.ts +42 -42
  261. package/typings/ext/market.ts +55 -55
  262. package/typings/ext/module.ts +64 -64
  263. package/typings/ext/plugin.ts +38 -38
  264. package/typings/ioc/metadata.ts +34 -34
  265. package/typings/logger/config.ts +6 -6
  266. package/typings/routing/crud.ts +199 -199
  267. package/typings/routing/eps.ts +88 -88
  268. package/typings/routing/meta.ts +63 -63
  269. package/typings/routing/register.ts +21 -21
  270. package/typings/routing/scan-context.ts +7 -7
  271. package/dist/admin/api/client.d.ts +0 -50
  272. package/dist/admin/components/ui/avatar/index.d.ts +0 -3
  273. package/dist/admin/components/ui/badge/index.d.ts +0 -6
  274. package/dist/admin/components/ui/button/index.d.ts +0 -7
  275. package/dist/admin/components/ui/checkbox/index.d.ts +0 -1
  276. package/dist/admin/components/ui/dialog/index.d.ts +0 -10
  277. package/dist/admin/components/ui/dropdown-menu/index.d.ts +0 -15
  278. package/dist/admin/components/ui/input/index.d.ts +0 -1
  279. package/dist/admin/components/ui/label/index.d.ts +0 -1
  280. package/dist/admin/components/ui/scroll-area/index.d.ts +0 -2
  281. package/dist/admin/components/ui/select/index.d.ts +0 -11
  282. package/dist/admin/components/ui/separator/index.d.ts +0 -1
  283. package/dist/admin/components/ui/sheet/index.d.ts +0 -8
  284. package/dist/admin/components/ui/sonner/index.d.ts +0 -1
  285. package/dist/admin/components/ui/table/index.d.ts +0 -9
  286. package/dist/admin/components/ui/tabs/index.d.ts +0 -4
  287. package/dist/admin/components/ui/tooltip/index.d.ts +0 -4
  288. package/dist/admin/crud/auto-fields.d.ts +0 -68
  289. package/dist/admin/crud/config.d.ts +0 -5
  290. package/dist/admin/crud/confirm.d.ts +0 -19
  291. package/dist/admin/crud/crud-page-request.d.ts +0 -21
  292. package/dist/admin/crud/data-i18n.d.ts +0 -62
  293. package/dist/admin/crud/dict.d.ts +0 -4
  294. package/dist/admin/crud/export-fetch.d.ts +0 -22
  295. package/dist/admin/crud/index.d.ts +0 -60
  296. package/dist/admin/crud/json-kv.d.ts +0 -9
  297. package/dist/admin/crud/key.d.ts +0 -22
  298. package/dist/admin/crud/paging.d.ts +0 -2
  299. package/dist/admin/crud/plugins.d.ts +0 -63
  300. package/dist/admin/crud/registry.d.ts +0 -3
  301. package/dist/admin/crud/span.d.ts +0 -17
  302. package/dist/admin/crud/style.d.ts +0 -82
  303. package/dist/admin/crud/useCrud.d.ts +0 -21
  304. package/dist/admin/crud/validate.d.ts +0 -7
  305. package/dist/admin/directives/perm.d.ts +0 -3
  306. package/dist/admin/hooks/useBrowser.d.ts +0 -6
  307. package/dist/admin/hooks/useUpload.d.ts +0 -8
  308. package/dist/admin/hooks/useVome.d.ts +0 -10
  309. package/dist/admin/lib/browser.d.ts +0 -3
  310. package/dist/admin/lib/cn.d.ts +0 -2
  311. package/dist/admin/lib/compress-image.d.ts +0 -12
  312. package/dist/admin/lib/dialog-float.d.ts +0 -8
  313. package/dist/admin/lib/eps.d.ts +0 -2
  314. package/dist/admin/lib/export-excel.d.ts +0 -9
  315. package/dist/admin/lib/file-preview.d.ts +0 -9
  316. package/dist/admin/lib/import-excel.d.ts +0 -21
  317. package/dist/admin/lib/json.d.ts +0 -11
  318. package/dist/admin/lib/menu.d.ts +0 -3
  319. package/dist/admin/lib/micro-locale.d.ts +0 -5
  320. package/dist/admin/lib/micro-theme.d.ts +0 -14
  321. package/dist/admin/lib/module.d.ts +0 -21
  322. package/dist/admin/lib/scarce-assets.d.ts +0 -5
  323. package/dist/admin/lib/shell.d.ts +0 -8
  324. package/dist/admin/lib/theme-color-options.d.ts +0 -15
  325. package/dist/admin/lib/tree.d.ts +0 -2
  326. package/dist/admin/lib/upload.d.ts +0 -17
  327. package/dist/admin/lib/video-frame.d.ts +0 -6
  328. package/dist/admin/router/create.d.ts +0 -16
  329. package/dist/admin/router/guards.d.ts +0 -6
  330. package/dist/admin/router/menu-routes.d.ts +0 -13
  331. package/dist/admin/stores/app.d.ts +0 -13
  332. package/dist/admin/stores/tags.d.ts +0 -14
  333. package/dist/admin/stores/user.d.ts +0 -48
  334. package/dist/client/ai-model-registry.d.ts +0 -16
  335. package/dist/client/base.d.ts +0 -28
  336. package/dist/client/configure.d.ts +0 -33
  337. package/dist/client/dict.d.ts +0 -31
  338. package/dist/client/eps-payload.d.ts +0 -3
  339. package/dist/client/eps.d.ts +0 -17
  340. package/dist/client/locale-change.d.ts +0 -16
  341. package/dist/client/locale-store.d.ts +0 -105
  342. package/dist/client/service.d.ts +0 -29
@@ -1,894 +1,894 @@
1
- <template>
2
- <template v-if="!embedded">
3
- <div v-if="src && showThumb" class="vm-preview-viewer" :class="{ 'is-video': isVideoKind }">
4
- <button
5
- type="button"
6
- class="vm-preview-viewer__trigger"
7
- :style="thumbStyle"
8
- :disabled="!preview"
9
- :title="preview ? (isVideoKind ? crudLabel('playVideo') : crudLabel('viewImage')) : undefined"
10
- @click="openViewer"
11
- >
12
- <img
13
- v-if="!isVideoKind"
14
- class="vm-preview-viewer__media"
15
- :src="src"
16
- alt=""
17
- :style="mediaStyle"
18
- />
19
- <template v-else>
20
- <img
21
- v-if="videoFrameSrc"
22
- class="vm-preview-viewer__media"
23
- :src="videoFrameSrc"
24
- alt=""
25
- :style="mediaStyle"
26
- />
27
- <video
28
- v-else-if="frameFailed"
29
- class="vm-preview-viewer__media"
30
- :src="src"
31
- muted
32
- playsinline
33
- preload="metadata"
34
- :style="mediaStyle"
35
- />
36
- <span v-else class="vm-preview-viewer__loading" aria-hidden="true" />
37
- <span class="vm-preview-viewer__play" aria-hidden="true">
38
- <i class="ri-play-fill" />
39
- </span>
40
- </template>
41
- </button>
42
- </div>
43
- <div v-else-if="src && showButton" class="vm-preview-viewer">
44
- <button
45
- type="button"
46
- class="vm-preview-viewer__btn"
47
- :disabled="!preview"
48
- :title="preview ? triggerTitle : undefined"
49
- @click="openViewer"
50
- >
51
- <i :class="triggerIcon" aria-hidden="true" />
52
- <span>{{ displayLabel }}</span>
53
- </button>
54
- </div>
55
- <span v-else-if="!src" class="vm-preview-viewer is-empty">—</span>
56
- </template>
57
-
58
- <Teleport to="body">
59
- <div
60
- v-if="viewerOpen"
61
- class="vm-file-preview-viewer vm-dialog-float"
62
- role="dialog"
63
- aria-modal="true"
64
- @click.self="closeViewer"
65
- >
66
- <header class="vm-file-preview-viewer__head">
67
- <div class="vm-file-preview-viewer__meta">
68
- <span class="vm-file-preview-viewer__name">{{ displayName }}</span>
69
- <span class="vm-file-preview-viewer__tag">{{ kindLabel }}</span>
70
- </div>
71
- <div class="vm-file-preview-viewer__actions">
72
- <button
73
- type="button"
74
- class="vm-file-preview-viewer__link"
75
- :disabled="opening"
76
- @click="openInNewWindow"
77
- >
78
- {{ opening ? crudLabel('opening') : crudLabel('openNewWindow') }}
79
- </button>
80
- <button type="button" class="vm-file-preview-viewer__close" :title="crudLabel('dismiss')" @click="closeViewer">
81
- <i class="ri-close-line" />
82
- </button>
83
- </div>
84
- </header>
85
-
86
- <div
87
- class="vm-file-preview-viewer__body vm-scroll"
88
- :class="{ 'is-media': kind === 'image' || kind === 'video' }"
89
- >
90
- <p v-if="loading" class="vm-file-preview-viewer__hint">{{ crudLabel('loading') }}</p>
91
- <p v-else-if="error" class="vm-file-preview-viewer__hint is-error">{{ error }}</p>
92
-
93
- <template v-else-if="kind === 'image'">
94
- <div class="vm-file-preview-viewer__image-stage" @wheel.prevent="onWheel">
95
- <img
96
- class="vm-file-preview-viewer__image"
97
- :src="src"
98
- :alt="displayName"
99
- :style="viewerImgStyle"
100
- draggable="false"
101
- @load="resetView"
102
- />
103
- </div>
104
- <div class="vm-file-preview-viewer__toolbar" @click.stop>
105
- <button type="button" :title="crudLabel('zoomOut')" @click="zoomOut">
106
- <i class="ri-subtract-line" />
107
- </button>
108
- <button type="button" :title="crudLabel('zoomIn')" @click="zoomIn">
109
- <i class="ri-add-line" />
110
- </button>
111
- <button type="button" :title="crudLabel('actualSize')" @click="resetView">
112
- <i class="ri-aspect-ratio-line" />
113
- </button>
114
- <button type="button" :title="crudLabel('rotateCCW')" @click="rotateLeft">
115
- <i class="ri-anticlockwise-line" />
116
- </button>
117
- <button type="button" :title="crudLabel('rotateCW')" @click="rotateRight">
118
- <i class="ri-clockwise-line" />
119
- </button>
120
- </div>
121
- </template>
122
-
123
- <div v-else-if="kind === 'video'" class="vm-file-preview-viewer__video-wrap">
124
- <video
125
- ref="videoRef"
126
- class="vm-file-preview-viewer__video"
127
- :src="src"
128
- controls
129
- autoplay
130
- playsinline
131
- />
132
- </div>
133
-
134
- <audio
135
- v-else-if="kind === 'audio'"
136
- ref="audioRef"
137
- class="vm-file-preview-viewer__audio"
138
- :src="src"
139
- controls
140
- autoplay
141
- playsinline
142
- />
143
-
144
- <iframe
145
- v-else-if="kind === 'pdf'"
146
- class="vm-file-preview-viewer__iframe"
147
- :src="src"
148
- :title="crudLabel('pdfPreview')"
149
- />
150
-
151
- <div
152
- v-else-if="kind === 'excel' && excelHtml"
153
- class="vm-file-preview-viewer__excel"
154
- v-html="excelHtml"
155
- />
156
-
157
- <pre v-else-if="kind === 'text' && textContent" class="vm-file-preview-viewer__text">{{
158
- textContent
159
- }}</pre>
160
-
161
- <div v-else-if="kind === 'office'" class="vm-file-preview-viewer__fallback">
162
- <i class="ri-file-3-line" aria-hidden="true" />
163
- <p>{{ crudLabel('officeNoPreview') }}</p>
164
- <p>{{ crudLabel('officeDownloadHint') }}</p>
165
- <button
166
- type="button"
167
- class="vm-file-preview-viewer__dl-btn"
168
- :disabled="downloading"
169
- @click="downloadFile"
170
- >
171
- {{ downloading ? crudLabel('downloadingFile') : crudLabel('downloadFile') }}
172
- </button>
173
- </div>
174
-
175
- <div v-else class="vm-file-preview-viewer__fallback">
176
- <i class="ri-file-3-line" aria-hidden="true" />
177
- <p>{{ crudLabel('typeNoPreview') }}</p>
178
- <p>{{ crudLabel('downloadHint') }}</p>
179
- <button
180
- type="button"
181
- class="vm-file-preview-viewer__dl-btn"
182
- :disabled="downloading"
183
- @click="downloadFile"
184
- >
185
- {{ downloading ? crudLabel('downloadingFile') : crudLabel('downloadFile') }}
186
- </button>
187
- </div>
188
- </div>
189
- </div>
190
- </Teleport>
191
- </template>
192
-
193
- <script setup lang="ts">
194
- import { crudLabel } from '../style'
195
- import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
196
- import DOMPurify from 'dompurify'
197
- import { useVideoFrame } from '../../lib/video-frame'
198
- import {
199
- fileDisplayName,
200
- getMediaPreviewKind,
201
- loadExcelPreviewHtml,
202
- fetchFileText,
203
- type MediaPreviewKind,
204
- } from '../../lib/file-preview'
205
-
206
- /** 多根(缩略图 + Teleport):禁止 fallthrough,避免表格传入的 scope/column/options 告警 */
207
- defineOptions({ name: 'vm-preview-viewer', inheritAttrs: false })
208
-
209
- const VIDEO_EXT = /\.(mp4|webm|mov|m4v|ogv|ogg)(\?|#|$)/i
210
-
211
- const KIND_LABEL: Record<MediaPreviewKind, string> = {
212
- image: 'typeImage',
213
- video: 'typeVideo',
214
- audio: 'typeAudio',
215
- pdf: 'PDF',
216
- excel: 'Excel',
217
- text: 'typeText',
218
- office: 'Office',
219
- unsupported: 'typeFile',
220
- }
221
-
222
- const props = withDefaults(
223
- defineProps<{
224
- /** CRUD 列绑定的 URL */
225
- modelValue?: string | null
226
- /** 表格列透传(vm-table),本组件不使用 */
227
- scope?: Record<string, unknown>
228
- column?: { property?: string; prop?: string }
229
- options?: unknown
230
- /** 仅渲染浮层(vm-upload-item 内部) */
231
- embedded?: boolean
232
- /** embedded 时外部控制开关 */
233
- open?: boolean
234
- /** embedded 时可显式传 src */
235
- src?: string
236
- fileName?: string
237
- /** 按钮触发文案(PDF / Excel 等非缩略图类型) */
238
- label?: string
239
- /** 覆盖 URL 推断的媒体类型 */
240
- mediaKind?: MediaPreviewKind
241
- /** thumb | button | auto:触发方式 */
242
- trigger?: 'thumb' | 'button' | 'auto'
243
- /** image | video | audio | auto(缩略图列或类型覆盖) */
244
- type?: 'image' | 'video' | 'audio' | 'auto'
245
- size?: number | string
246
- radius?: number | string
247
- preview?: boolean
248
- fit?: 'cover' | 'contain'
249
- /** 私有文件:点「下载」时先解析可访问短链 */
250
- resolveDownloadUrl?: (url: string) => Promise<string | undefined>
251
- }>(),
252
- {
253
- embedded: false,
254
- label: undefined,
255
- trigger: 'auto',
256
- type: 'auto',
257
- size: 40,
258
- radius: 8,
259
- preview: true,
260
- fit: 'cover',
261
- },
262
- )
263
-
264
- const emit = defineEmits<{ 'update:open': [v: boolean] }>()
265
-
266
- const internalOpen = ref(false)
267
- const loading = ref(false)
268
- const downloading = ref(false)
269
- const opening = ref(false)
270
- const error = ref('')
271
- const textContent = ref('')
272
- const excelHtml = ref('')
273
- const scale = ref(1)
274
- const rotate = ref(0)
275
- const videoRef = ref<HTMLVideoElement | null>(null)
276
- const audioRef = ref<HTMLAudioElement | null>(null)
277
-
278
- const src = computed(() => {
279
- const raw = props.embedded ? props.src : props.modelValue
280
- if (raw == null) return ''
281
- return String(raw).trim()
282
- })
283
-
284
- const isVideoKind = computed(() => {
285
- if (props.mediaKind === 'video') return true
286
- if (props.mediaKind === 'image' || props.mediaKind === 'audio') return false
287
- if (props.type === 'video') return true
288
- if (props.type === 'image' || props.type === 'audio') return false
289
- return VIDEO_EXT.test(src.value)
290
- })
291
-
292
- const isAudioKind = computed(() => {
293
- if (props.mediaKind === 'audio') return true
294
- if (props.type === 'audio') return true
295
- if (props.mediaKind || props.type === 'image' || props.type === 'video') return false
296
- return getMediaPreviewKind(props.fileName || src.value) === 'audio'
297
- })
298
-
299
- const showThumb = computed(() => {
300
- if (props.embedded) return false
301
- if (props.trigger === 'button') return false
302
- if (props.trigger === 'thumb') return !!src.value
303
- if (props.mediaKind === 'image' || props.mediaKind === 'video') return true
304
- if (props.mediaKind) return false
305
- if (props.type === 'image' || props.type === 'video') return true
306
- if (isVideoKind.value) return true
307
- const k = getMediaPreviewKind(props.fileName || src.value)
308
- return k === 'image' || k === 'video'
309
- })
310
-
311
- const showButton = computed(() => {
312
- if (props.embedded || !src.value) return false
313
- if (props.trigger === 'button') return true
314
- if (props.trigger === 'thumb') return false
315
- return !showThumb.value
316
- })
317
-
318
- const kind = computed((): MediaPreviewKind => {
319
- if (props.mediaKind) return props.mediaKind
320
- if (props.type === 'audio' || isAudioKind.value) return 'audio'
321
- if (props.type === 'video' || isVideoKind.value) return 'video'
322
- if (props.type === 'image') return 'image'
323
- return getMediaPreviewKind(props.fileName || src.value)
324
- })
325
-
326
- const displayLabel = computed(() => {
327
- if (props.label != null && props.label !== '') return props.label
328
- if (kind.value === 'audio') return crudLabel('play')
329
- return crudLabel('view')
330
- })
331
-
332
- const triggerIcon = computed(() =>
333
- kind.value === 'audio' ? 'ri-play-circle-line' : 'ri-file-search-line',
334
- )
335
-
336
- const triggerTitle = computed(() =>
337
- kind.value === 'audio' ? crudLabel('playAudio') : crudLabel('viewFile'),
338
- )
339
-
340
- const kindLabel = computed(() => {
341
- const key = KIND_LABEL[kind.value]
342
- return key.startsWith('type') ? crudLabel(key) : key
343
- })
344
-
345
- const displayName = computed(() =>
346
- fileDisplayName(props.fileName || src.value, crudLabel('unnamedFile')),
347
- )
348
-
349
- /** 无法内嵌预览的类型在正文区提供「下载文件」;顶栏统一「新窗口打开」 */
350
-
351
- async function downloadFile() {
352
- if (downloading.value) return
353
- const raw = src.value
354
- if (!raw) return
355
- downloading.value = true
356
- try {
357
- let url = raw
358
- if (props.resolveDownloadUrl) {
359
- const signed = await props.resolveDownloadUrl(raw)
360
- if (!signed) return
361
- url = signed
362
- }
363
- const a = document.createElement('a')
364
- a.href = url
365
- a.download = displayName.value || 'download'
366
- a.rel = 'noopener'
367
- document.body.appendChild(a)
368
- a.click()
369
- a.remove()
370
- } finally {
371
- downloading.value = false
372
- }
373
- }
374
-
375
- async function openInNewWindow() {
376
- if (opening.value) return
377
- const raw = src.value
378
- if (!raw) return
379
- opening.value = true
380
- try {
381
- let url = raw
382
- if (props.resolveDownloadUrl) {
383
- const signed = await props.resolveDownloadUrl(raw)
384
- if (!signed) return
385
- url = signed
386
- }
387
- window.open(url, '_blank', 'noopener,noreferrer')
388
- } finally {
389
- opening.value = false
390
- }
391
- }
392
-
393
- const viewerOpen = computed({
394
- get: () => (props.embedded ? !!props.open : internalOpen.value),
395
- set: (v: boolean) => {
396
- if (props.embedded) emit('update:open', v)
397
- else internalOpen.value = v
398
- },
399
- })
400
-
401
- const videoThumbSource = computed(() => (isVideoKind.value ? src.value : ''))
402
- const { videoFrameSrc, frameFailed } = useVideoFrame(videoThumbSource)
403
-
404
- const viewerImgStyle = computed(() => ({
405
- transform: `scale(${scale.value}) rotate(${rotate.value}deg)`,
406
- }))
407
-
408
- function toCssUnit(v: number | string) {
409
- return typeof v === 'number' ? `${v}px` : v
410
- }
411
-
412
- const thumbStyle = computed(() => ({
413
- width: toCssUnit(props.size),
414
- height: toCssUnit(props.size),
415
- borderRadius: toCssUnit(props.radius),
416
- }))
417
-
418
- const mediaStyle = computed(() => ({
419
- objectFit: props.fit,
420
- }))
421
-
422
- function resetView() {
423
- scale.value = 1
424
- rotate.value = 0
425
- }
426
-
427
- function zoomIn() {
428
- scale.value = Math.min(5, Math.round((scale.value + 0.2) * 100) / 100)
429
- }
430
-
431
- function zoomOut() {
432
- scale.value = Math.max(0.2, Math.round((scale.value - 0.2) * 100) / 100)
433
- }
434
-
435
- function rotateLeft() {
436
- rotate.value -= 90
437
- }
438
-
439
- function rotateRight() {
440
- rotate.value += 90
441
- }
442
-
443
- function onWheel(e: WheelEvent) {
444
- if (kind.value !== 'image') return
445
- if (e.deltaY < 0) zoomIn()
446
- else zoomOut()
447
- }
448
-
449
- function lockBody(lock: boolean) {
450
- if (typeof document === 'undefined') return
451
- document.body.style.overflow = lock ? 'hidden' : ''
452
- }
453
-
454
- async function loadPreview() {
455
- if (!src.value) return
456
- loading.value = true
457
- error.value = ''
458
- textContent.value = ''
459
- excelHtml.value = ''
460
-
461
- try {
462
- if (kind.value === 'text') {
463
- textContent.value = await fetchFileText(src.value)
464
- } else if (kind.value === 'excel') {
465
- excelHtml.value = DOMPurify.sanitize(await loadExcelPreviewHtml(src.value), {
466
- USE_PROFILES: { html: true },
467
- })
468
- }
469
- } catch (e) {
470
- error.value = e instanceof Error ? e.message : crudLabel('previewFail')
471
- } finally {
472
- loading.value = false
473
- }
474
- }
475
-
476
- function openViewer() {
477
- if (!props.preview || !src.value) return
478
- viewerOpen.value = true
479
- }
480
-
481
- function closeViewer() {
482
- viewerOpen.value = false
483
- }
484
-
485
- function onKeydown(e: KeyboardEvent) {
486
- if (!viewerOpen.value) return
487
- if (e.key !== 'Escape') return
488
- e.preventDefault()
489
- e.stopPropagation()
490
- closeViewer()
491
- }
492
-
493
- watch(viewerOpen, (open) => {
494
- lockBody(open)
495
- if (typeof window === 'undefined') return
496
- if (open) {
497
- resetView()
498
- // 捕获阶段拦截 Escape,避免同时关掉宿主 Dialog
499
- window.addEventListener('keydown', onKeydown, true)
500
- if (kind.value === 'text' || kind.value === 'excel') void loadPreview()
501
- if (kind.value === 'video') {
502
- void nextTick(() => {
503
- videoRef.value?.play?.().catch(() => undefined)
504
- })
505
- }
506
- if (kind.value === 'audio') {
507
- void nextTick(() => {
508
- audioRef.value?.play?.().catch(() => undefined)
509
- })
510
- }
511
- } else {
512
- window.removeEventListener('keydown', onKeydown, true)
513
- loading.value = false
514
- error.value = ''
515
- textContent.value = ''
516
- excelHtml.value = ''
517
- videoRef.value?.pause?.()
518
- audioRef.value?.pause?.()
519
- }
520
- })
521
-
522
- onUnmounted(() => {
523
- lockBody(false)
524
- if (typeof window !== 'undefined') {
525
- window.removeEventListener('keydown', onKeydown, true)
526
- }
527
- })
528
- </script>
529
-
530
- <style lang="scss" scoped>
531
- .vm-preview-viewer {
532
- display: inline-flex;
533
- vertical-align: middle;
534
- }
535
-
536
- .vm-preview-viewer.is-empty {
537
- color: var(--muted-foreground);
538
- font-size: 12px;
539
- }
540
-
541
- .vm-preview-viewer__trigger {
542
- position: relative;
543
- display: inline-flex;
544
- flex-shrink: 0;
545
- align-items: center;
546
- justify-content: center;
547
- padding: 0;
548
- border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
549
- background: var(--muted);
550
- cursor: pointer;
551
- overflow: hidden;
552
- transition:
553
- border-color 0.15s ease,
554
- box-shadow 0.15s ease;
555
-
556
- &:hover:not(:disabled) {
557
- border-color: var(--focus-border);
558
- box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand) 12%, transparent);
559
- }
560
-
561
- &:disabled {
562
- cursor: default;
563
- }
564
- }
565
-
566
- .vm-preview-viewer__media {
567
- display: block;
568
- width: 100%;
569
- height: 100%;
570
- }
571
-
572
- .vm-preview-viewer__loading {
573
- display: block;
574
- width: 100%;
575
- height: 100%;
576
- background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
577
- }
578
-
579
- .vm-preview-viewer__play {
580
- position: absolute;
581
- inset: 0;
582
- display: grid;
583
- place-items: center;
584
- background: rgb(20 24 40 / 28%);
585
- color: #fff;
586
- pointer-events: none;
587
-
588
- i {
589
- font-size: 18px;
590
- line-height: 1;
591
- }
592
- }
593
-
594
- .vm-preview-viewer__btn {
595
- display: inline-flex;
596
- align-items: center;
597
- gap: 4px;
598
- padding: 0;
599
- border: none;
600
- background: transparent;
601
- color: var(--brand);
602
- font-size: 13px;
603
- font-weight: 600;
604
- cursor: pointer;
605
-
606
- i {
607
- font-size: 16px;
608
- line-height: 1;
609
- }
610
-
611
- &:hover:not(:disabled) {
612
- color: var(--brand-deep);
613
- }
614
-
615
- &:disabled {
616
- opacity: 0.55;
617
- cursor: default;
618
- }
619
- }
620
- </style>
621
-
622
- <style lang="scss">
623
- .vm-file-preview-viewer {
624
- position: fixed;
625
- inset: 0;
626
- z-index: 2200;
627
- display: flex;
628
- flex-direction: column;
629
- background: rgb(10 12 20 / 88%);
630
- }
631
-
632
- .vm-file-preview-viewer__head {
633
- display: flex;
634
- flex-shrink: 0;
635
- align-items: center;
636
- justify-content: space-between;
637
- gap: 12px;
638
- padding: 12px 18px;
639
- border-bottom: 1px solid rgb(255 255 255 / 10%);
640
- background: rgb(24 28 40 / 92%);
641
- }
642
-
643
- .vm-file-preview-viewer__meta {
644
- display: flex;
645
- min-width: 0;
646
- align-items: center;
647
- gap: 10px;
648
- }
649
-
650
- .vm-file-preview-viewer__name {
651
- overflow: hidden;
652
- text-overflow: ellipsis;
653
- white-space: nowrap;
654
- font-size: 14px;
655
- font-weight: 650;
656
- color: #fff;
657
- }
658
-
659
- .vm-file-preview-viewer__tag {
660
- flex-shrink: 0;
661
- padding: 2px 8px;
662
- border-radius: 999px;
663
- background: rgb(255 255 255 / 12%);
664
- color: rgb(255 255 255 / 88%);
665
- font-size: 11px;
666
- font-weight: 600;
667
- text-transform: uppercase;
668
- }
669
-
670
- .vm-file-preview-viewer__actions {
671
- display: flex;
672
- flex-shrink: 0;
673
- align-items: center;
674
- gap: 10px;
675
- }
676
-
677
- .vm-file-preview-viewer__link {
678
- appearance: none;
679
- border: 0;
680
- padding: 0;
681
- background: transparent;
682
- color: rgb(147 197 253);
683
- font: inherit;
684
- font-size: 13px;
685
- font-weight: 600;
686
- text-decoration: none;
687
- cursor: pointer;
688
-
689
- &:hover {
690
- text-decoration: underline;
691
- }
692
- }
693
-
694
- .vm-file-preview-viewer__dl-btn {
695
- margin-top: 12px;
696
- padding: 8px 16px;
697
- border: 0;
698
- border-radius: 8px;
699
- background: var(--brand, #4e5dff);
700
- color: #fff;
701
- font-size: 14px;
702
- font-weight: 600;
703
- cursor: pointer;
704
- }
705
-
706
- .vm-file-preview-viewer__close {
707
- display: inline-flex;
708
- width: 34px;
709
- height: 34px;
710
- align-items: center;
711
- justify-content: center;
712
- border: none;
713
- border-radius: 10px;
714
- background: rgb(255 255 255 / 12%);
715
- color: #fff;
716
- font-size: 18px;
717
- cursor: pointer;
718
-
719
- &:hover {
720
- background: rgb(255 255 255 / 22%);
721
- }
722
- }
723
-
724
- .vm-file-preview-viewer__body {
725
- flex: 1 1 auto;
726
- min-height: 0;
727
- padding: 16px;
728
- overflow: auto;
729
-
730
- &.is-media {
731
- display: flex;
732
- flex-direction: column;
733
- padding: 0;
734
- overflow: hidden;
735
- }
736
- }
737
-
738
- .vm-file-preview-viewer__hint {
739
- margin: 24px auto;
740
- color: rgb(255 255 255 / 80%);
741
- text-align: center;
742
-
743
- &.is-error {
744
- color: #fca5a5;
745
- }
746
- }
747
-
748
- .vm-file-preview-viewer__image-stage {
749
- display: flex;
750
- flex: 1 1 auto;
751
- min-height: 0;
752
- width: 100%;
753
- align-items: center;
754
- justify-content: center;
755
- overflow: hidden;
756
- }
757
-
758
- .vm-file-preview-viewer__image {
759
- display: block;
760
- width: auto;
761
- height: auto;
762
- max-width: min(1200px, calc(100vw - 48px));
763
- max-height: calc(100vh - 140px);
764
- object-fit: contain;
765
- transition: transform 0.15s ease;
766
- user-select: none;
767
- }
768
-
769
- .vm-file-preview-viewer__toolbar {
770
- position: relative;
771
- z-index: 2;
772
- display: inline-flex;
773
- flex-shrink: 0;
774
- align-items: center;
775
- gap: 4px;
776
- margin: 0 auto 20px;
777
- padding: 6px 8px;
778
- border-radius: 12px;
779
- background: rgb(24 28 40 / 92%);
780
-
781
- button {
782
- display: inline-flex;
783
- width: 36px;
784
- height: 36px;
785
- align-items: center;
786
- justify-content: center;
787
- border: none;
788
- border-radius: 8px;
789
- background: transparent;
790
- color: rgb(255 255 255 / 88%);
791
- font-size: 18px;
792
- cursor: pointer;
793
-
794
- &:hover {
795
- background: rgb(255 255 255 / 12%);
796
- color: #fff;
797
- }
798
- }
799
- }
800
-
801
- .vm-file-preview-viewer__video-wrap {
802
- display: flex;
803
- flex: 1 1 auto;
804
- min-height: 0;
805
- width: 100%;
806
- align-items: center;
807
- justify-content: center;
808
- padding: 16px;
809
- }
810
-
811
- .vm-file-preview-viewer__video {
812
- display: block;
813
- width: min(960px, calc(100vw - 48px));
814
- max-height: calc(100vh - 120px);
815
- border-radius: 12px;
816
- background: #000;
817
- outline: none;
818
- }
819
-
820
- .vm-file-preview-viewer__audio {
821
- display: block;
822
- width: min(480px, 100%);
823
- margin: 48px auto 0;
824
- }
825
-
826
- .vm-file-preview-viewer__iframe {
827
- display: block;
828
- width: min(1100px, 100%);
829
- height: calc(100vh - 88px);
830
- margin: 0 auto;
831
- border: none;
832
- border-radius: 12px;
833
- background: #fff;
834
- }
835
-
836
- .vm-file-preview-viewer__excel {
837
- max-width: min(1100px, 100%);
838
- margin: 0 auto;
839
- padding: 12px;
840
- border-radius: 12px;
841
- background: #fff;
842
- overflow: auto;
843
-
844
- table {
845
- width: 100%;
846
- border-collapse: collapse;
847
- font-size: 13px;
848
- }
849
-
850
- td,
851
- th {
852
- padding: 6px 8px;
853
- border: 1px solid #e5e7eb;
854
- white-space: nowrap;
855
- }
856
- }
857
-
858
- .vm-file-preview-viewer__text {
859
- max-width: min(960px, 100%);
860
- margin: 0 auto;
861
- padding: 16px 18px;
862
- border-radius: 12px;
863
- background: rgb(15 23 42 / 92%);
864
- color: #e2e8f0;
865
- font-size: 13px;
866
- line-height: 1.6;
867
- white-space: pre-wrap;
868
- word-break: break-word;
869
- }
870
-
871
- .vm-file-preview-viewer__fallback {
872
- display: grid;
873
- gap: 8px;
874
- max-width: 420px;
875
- margin: 48px auto;
876
- padding: 28px 24px;
877
- border-radius: 16px;
878
- background: rgb(255 255 255 / 8%);
879
- color: rgb(255 255 255 / 88%);
880
- text-align: center;
881
-
882
- i {
883
- font-size: 42px;
884
- line-height: 1;
885
- color: rgb(255 255 255 / 72%);
886
- }
887
-
888
- p {
889
- margin: 0;
890
- font-size: 14px;
891
- line-height: 1.6;
892
- }
893
- }
894
- </style>
1
+ <template>
2
+ <template v-if="!embedded">
3
+ <div v-if="src && showThumb" class="vm-preview-viewer" :class="{ 'is-video': isVideoKind }">
4
+ <button
5
+ type="button"
6
+ class="vm-preview-viewer__trigger"
7
+ :style="thumbStyle"
8
+ :disabled="!preview"
9
+ :title="preview ? (isVideoKind ? crudLabel('playVideo') : crudLabel('viewImage')) : undefined"
10
+ @click="openViewer"
11
+ >
12
+ <img
13
+ v-if="!isVideoKind"
14
+ class="vm-preview-viewer__media"
15
+ :src="src"
16
+ alt=""
17
+ :style="mediaStyle"
18
+ />
19
+ <template v-else>
20
+ <img
21
+ v-if="videoFrameSrc"
22
+ class="vm-preview-viewer__media"
23
+ :src="videoFrameSrc"
24
+ alt=""
25
+ :style="mediaStyle"
26
+ />
27
+ <video
28
+ v-else-if="frameFailed"
29
+ class="vm-preview-viewer__media"
30
+ :src="src"
31
+ muted
32
+ playsinline
33
+ preload="metadata"
34
+ :style="mediaStyle"
35
+ />
36
+ <span v-else class="vm-preview-viewer__loading" aria-hidden="true" />
37
+ <span class="vm-preview-viewer__play" aria-hidden="true">
38
+ <i class="ri-play-fill" />
39
+ </span>
40
+ </template>
41
+ </button>
42
+ </div>
43
+ <div v-else-if="src && showButton" class="vm-preview-viewer">
44
+ <button
45
+ type="button"
46
+ class="vm-preview-viewer__btn"
47
+ :disabled="!preview"
48
+ :title="preview ? triggerTitle : undefined"
49
+ @click="openViewer"
50
+ >
51
+ <i :class="triggerIcon" aria-hidden="true" />
52
+ <span>{{ displayLabel }}</span>
53
+ </button>
54
+ </div>
55
+ <span v-else-if="!src" class="vm-preview-viewer is-empty">—</span>
56
+ </template>
57
+
58
+ <Teleport to="body">
59
+ <div
60
+ v-if="viewerOpen"
61
+ class="vm-file-preview-viewer vm-dialog-float"
62
+ role="dialog"
63
+ aria-modal="true"
64
+ @click.self="closeViewer"
65
+ >
66
+ <header class="vm-file-preview-viewer__head">
67
+ <div class="vm-file-preview-viewer__meta">
68
+ <span class="vm-file-preview-viewer__name">{{ displayName }}</span>
69
+ <span class="vm-file-preview-viewer__tag">{{ kindLabel }}</span>
70
+ </div>
71
+ <div class="vm-file-preview-viewer__actions">
72
+ <button
73
+ type="button"
74
+ class="vm-file-preview-viewer__link"
75
+ :disabled="opening"
76
+ @click="openInNewWindow"
77
+ >
78
+ {{ opening ? crudLabel('opening') : crudLabel('openNewWindow') }}
79
+ </button>
80
+ <button type="button" class="vm-file-preview-viewer__close" :title="crudLabel('dismiss')" @click="closeViewer">
81
+ <i class="ri-close-line" />
82
+ </button>
83
+ </div>
84
+ </header>
85
+
86
+ <div
87
+ class="vm-file-preview-viewer__body vm-scroll"
88
+ :class="{ 'is-media': kind === 'image' || kind === 'video' }"
89
+ >
90
+ <p v-if="loading" class="vm-file-preview-viewer__hint">{{ crudLabel('loading') }}</p>
91
+ <p v-else-if="error" class="vm-file-preview-viewer__hint is-error">{{ error }}</p>
92
+
93
+ <template v-else-if="kind === 'image'">
94
+ <div class="vm-file-preview-viewer__image-stage" @wheel.prevent="onWheel">
95
+ <img
96
+ class="vm-file-preview-viewer__image"
97
+ :src="src"
98
+ :alt="displayName"
99
+ :style="viewerImgStyle"
100
+ draggable="false"
101
+ @load="resetView"
102
+ />
103
+ </div>
104
+ <div class="vm-file-preview-viewer__toolbar" @click.stop>
105
+ <button type="button" :title="crudLabel('zoomOut')" @click="zoomOut">
106
+ <i class="ri-subtract-line" />
107
+ </button>
108
+ <button type="button" :title="crudLabel('zoomIn')" @click="zoomIn">
109
+ <i class="ri-add-line" />
110
+ </button>
111
+ <button type="button" :title="crudLabel('actualSize')" @click="resetView">
112
+ <i class="ri-aspect-ratio-line" />
113
+ </button>
114
+ <button type="button" :title="crudLabel('rotateCCW')" @click="rotateLeft">
115
+ <i class="ri-anticlockwise-line" />
116
+ </button>
117
+ <button type="button" :title="crudLabel('rotateCW')" @click="rotateRight">
118
+ <i class="ri-clockwise-line" />
119
+ </button>
120
+ </div>
121
+ </template>
122
+
123
+ <div v-else-if="kind === 'video'" class="vm-file-preview-viewer__video-wrap">
124
+ <video
125
+ ref="videoRef"
126
+ class="vm-file-preview-viewer__video"
127
+ :src="src"
128
+ controls
129
+ autoplay
130
+ playsinline
131
+ />
132
+ </div>
133
+
134
+ <audio
135
+ v-else-if="kind === 'audio'"
136
+ ref="audioRef"
137
+ class="vm-file-preview-viewer__audio"
138
+ :src="src"
139
+ controls
140
+ autoplay
141
+ playsinline
142
+ />
143
+
144
+ <iframe
145
+ v-else-if="kind === 'pdf'"
146
+ class="vm-file-preview-viewer__iframe"
147
+ :src="src"
148
+ :title="crudLabel('pdfPreview')"
149
+ />
150
+
151
+ <div
152
+ v-else-if="kind === 'excel' && excelHtml"
153
+ class="vm-file-preview-viewer__excel"
154
+ v-html="excelHtml"
155
+ />
156
+
157
+ <pre v-else-if="kind === 'text' && textContent" class="vm-file-preview-viewer__text">{{
158
+ textContent
159
+ }}</pre>
160
+
161
+ <div v-else-if="kind === 'office'" class="vm-file-preview-viewer__fallback">
162
+ <i class="ri-file-3-line" aria-hidden="true" />
163
+ <p>{{ crudLabel('officeNoPreview') }}</p>
164
+ <p>{{ crudLabel('officeDownloadHint') }}</p>
165
+ <button
166
+ type="button"
167
+ class="vm-file-preview-viewer__dl-btn"
168
+ :disabled="downloading"
169
+ @click="downloadFile"
170
+ >
171
+ {{ downloading ? crudLabel('downloadingFile') : crudLabel('downloadFile') }}
172
+ </button>
173
+ </div>
174
+
175
+ <div v-else class="vm-file-preview-viewer__fallback">
176
+ <i class="ri-file-3-line" aria-hidden="true" />
177
+ <p>{{ crudLabel('typeNoPreview') }}</p>
178
+ <p>{{ crudLabel('downloadHint') }}</p>
179
+ <button
180
+ type="button"
181
+ class="vm-file-preview-viewer__dl-btn"
182
+ :disabled="downloading"
183
+ @click="downloadFile"
184
+ >
185
+ {{ downloading ? crudLabel('downloadingFile') : crudLabel('downloadFile') }}
186
+ </button>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </Teleport>
191
+ </template>
192
+
193
+ <script setup lang="ts">
194
+ import { crudLabel } from '../style'
195
+ import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
196
+ import DOMPurify from 'dompurify'
197
+ import { useVideoFrame } from '../../lib/video-frame'
198
+ import {
199
+ fileDisplayName,
200
+ getMediaPreviewKind,
201
+ loadExcelPreviewHtml,
202
+ fetchFileText,
203
+ type MediaPreviewKind,
204
+ } from '../../lib/file-preview'
205
+
206
+ /** 多根(缩略图 + Teleport):禁止 fallthrough,避免表格传入的 scope/column/options 告警 */
207
+ defineOptions({ name: 'vm-preview-viewer', inheritAttrs: false })
208
+
209
+ const VIDEO_EXT = /\.(mp4|webm|mov|m4v|ogv|ogg)(\?|#|$)/i
210
+
211
+ const KIND_LABEL: Record<MediaPreviewKind, string> = {
212
+ image: 'typeImage',
213
+ video: 'typeVideo',
214
+ audio: 'typeAudio',
215
+ pdf: 'PDF',
216
+ excel: 'Excel',
217
+ text: 'typeText',
218
+ office: 'Office',
219
+ unsupported: 'typeFile',
220
+ }
221
+
222
+ const props = withDefaults(
223
+ defineProps<{
224
+ /** CRUD 列绑定的 URL */
225
+ modelValue?: string | null
226
+ /** 表格列透传(vm-table),本组件不使用 */
227
+ scope?: Record<string, unknown>
228
+ column?: { property?: string; prop?: string }
229
+ options?: unknown
230
+ /** 仅渲染浮层(vm-upload-item 内部) */
231
+ embedded?: boolean
232
+ /** embedded 时外部控制开关 */
233
+ open?: boolean
234
+ /** embedded 时可显式传 src */
235
+ src?: string
236
+ fileName?: string
237
+ /** 按钮触发文案(PDF / Excel 等非缩略图类型) */
238
+ label?: string
239
+ /** 覆盖 URL 推断的媒体类型 */
240
+ mediaKind?: MediaPreviewKind
241
+ /** thumb | button | auto:触发方式 */
242
+ trigger?: 'thumb' | 'button' | 'auto'
243
+ /** image | video | audio | auto(缩略图列或类型覆盖) */
244
+ type?: 'image' | 'video' | 'audio' | 'auto'
245
+ size?: number | string
246
+ radius?: number | string
247
+ preview?: boolean
248
+ fit?: 'cover' | 'contain'
249
+ /** 私有文件:点「下载」时先解析可访问短链 */
250
+ resolveDownloadUrl?: (url: string) => Promise<string | undefined>
251
+ }>(),
252
+ {
253
+ embedded: false,
254
+ label: undefined,
255
+ trigger: 'auto',
256
+ type: 'auto',
257
+ size: 40,
258
+ radius: 8,
259
+ preview: true,
260
+ fit: 'cover',
261
+ },
262
+ )
263
+
264
+ const emit = defineEmits<{ 'update:open': [v: boolean] }>()
265
+
266
+ const internalOpen = ref(false)
267
+ const loading = ref(false)
268
+ const downloading = ref(false)
269
+ const opening = ref(false)
270
+ const error = ref('')
271
+ const textContent = ref('')
272
+ const excelHtml = ref('')
273
+ const scale = ref(1)
274
+ const rotate = ref(0)
275
+ const videoRef = ref<HTMLVideoElement | null>(null)
276
+ const audioRef = ref<HTMLAudioElement | null>(null)
277
+
278
+ const src = computed(() => {
279
+ const raw = props.embedded ? props.src : props.modelValue
280
+ if (raw == null) return ''
281
+ return String(raw).trim()
282
+ })
283
+
284
+ const isVideoKind = computed(() => {
285
+ if (props.mediaKind === 'video') return true
286
+ if (props.mediaKind === 'image' || props.mediaKind === 'audio') return false
287
+ if (props.type === 'video') return true
288
+ if (props.type === 'image' || props.type === 'audio') return false
289
+ return VIDEO_EXT.test(src.value)
290
+ })
291
+
292
+ const isAudioKind = computed(() => {
293
+ if (props.mediaKind === 'audio') return true
294
+ if (props.type === 'audio') return true
295
+ if (props.mediaKind || props.type === 'image' || props.type === 'video') return false
296
+ return getMediaPreviewKind(props.fileName || src.value) === 'audio'
297
+ })
298
+
299
+ const showThumb = computed(() => {
300
+ if (props.embedded) return false
301
+ if (props.trigger === 'button') return false
302
+ if (props.trigger === 'thumb') return !!src.value
303
+ if (props.mediaKind === 'image' || props.mediaKind === 'video') return true
304
+ if (props.mediaKind) return false
305
+ if (props.type === 'image' || props.type === 'video') return true
306
+ if (isVideoKind.value) return true
307
+ const k = getMediaPreviewKind(props.fileName || src.value)
308
+ return k === 'image' || k === 'video'
309
+ })
310
+
311
+ const showButton = computed(() => {
312
+ if (props.embedded || !src.value) return false
313
+ if (props.trigger === 'button') return true
314
+ if (props.trigger === 'thumb') return false
315
+ return !showThumb.value
316
+ })
317
+
318
+ const kind = computed((): MediaPreviewKind => {
319
+ if (props.mediaKind) return props.mediaKind
320
+ if (props.type === 'audio' || isAudioKind.value) return 'audio'
321
+ if (props.type === 'video' || isVideoKind.value) return 'video'
322
+ if (props.type === 'image') return 'image'
323
+ return getMediaPreviewKind(props.fileName || src.value)
324
+ })
325
+
326
+ const displayLabel = computed(() => {
327
+ if (props.label != null && props.label !== '') return props.label
328
+ if (kind.value === 'audio') return crudLabel('play')
329
+ return crudLabel('view')
330
+ })
331
+
332
+ const triggerIcon = computed(() =>
333
+ kind.value === 'audio' ? 'ri-play-circle-line' : 'ri-file-search-line',
334
+ )
335
+
336
+ const triggerTitle = computed(() =>
337
+ kind.value === 'audio' ? crudLabel('playAudio') : crudLabel('viewFile'),
338
+ )
339
+
340
+ const kindLabel = computed(() => {
341
+ const key = KIND_LABEL[kind.value]
342
+ return key.startsWith('type') ? crudLabel(key) : key
343
+ })
344
+
345
+ const displayName = computed(() =>
346
+ fileDisplayName(props.fileName || src.value, crudLabel('unnamedFile')),
347
+ )
348
+
349
+ /** 无法内嵌预览的类型在正文区提供「下载文件」;顶栏统一「新窗口打开」 */
350
+
351
+ async function downloadFile() {
352
+ if (downloading.value) return
353
+ const raw = src.value
354
+ if (!raw) return
355
+ downloading.value = true
356
+ try {
357
+ let url = raw
358
+ if (props.resolveDownloadUrl) {
359
+ const signed = await props.resolveDownloadUrl(raw)
360
+ if (!signed) return
361
+ url = signed
362
+ }
363
+ const a = document.createElement('a')
364
+ a.href = url
365
+ a.download = displayName.value || 'download'
366
+ a.rel = 'noopener'
367
+ document.body.appendChild(a)
368
+ a.click()
369
+ a.remove()
370
+ } finally {
371
+ downloading.value = false
372
+ }
373
+ }
374
+
375
+ async function openInNewWindow() {
376
+ if (opening.value) return
377
+ const raw = src.value
378
+ if (!raw) return
379
+ opening.value = true
380
+ try {
381
+ let url = raw
382
+ if (props.resolveDownloadUrl) {
383
+ const signed = await props.resolveDownloadUrl(raw)
384
+ if (!signed) return
385
+ url = signed
386
+ }
387
+ window.open(url, '_blank', 'noopener,noreferrer')
388
+ } finally {
389
+ opening.value = false
390
+ }
391
+ }
392
+
393
+ const viewerOpen = computed({
394
+ get: () => (props.embedded ? !!props.open : internalOpen.value),
395
+ set: (v: boolean) => {
396
+ if (props.embedded) emit('update:open', v)
397
+ else internalOpen.value = v
398
+ },
399
+ })
400
+
401
+ const videoThumbSource = computed(() => (isVideoKind.value ? src.value : ''))
402
+ const { videoFrameSrc, frameFailed } = useVideoFrame(videoThumbSource)
403
+
404
+ const viewerImgStyle = computed(() => ({
405
+ transform: `scale(${scale.value}) rotate(${rotate.value}deg)`,
406
+ }))
407
+
408
+ function toCssUnit(v: number | string) {
409
+ return typeof v === 'number' ? `${v}px` : v
410
+ }
411
+
412
+ const thumbStyle = computed(() => ({
413
+ width: toCssUnit(props.size),
414
+ height: toCssUnit(props.size),
415
+ borderRadius: toCssUnit(props.radius),
416
+ }))
417
+
418
+ const mediaStyle = computed(() => ({
419
+ objectFit: props.fit,
420
+ }))
421
+
422
+ function resetView() {
423
+ scale.value = 1
424
+ rotate.value = 0
425
+ }
426
+
427
+ function zoomIn() {
428
+ scale.value = Math.min(5, Math.round((scale.value + 0.2) * 100) / 100)
429
+ }
430
+
431
+ function zoomOut() {
432
+ scale.value = Math.max(0.2, Math.round((scale.value - 0.2) * 100) / 100)
433
+ }
434
+
435
+ function rotateLeft() {
436
+ rotate.value -= 90
437
+ }
438
+
439
+ function rotateRight() {
440
+ rotate.value += 90
441
+ }
442
+
443
+ function onWheel(e: WheelEvent) {
444
+ if (kind.value !== 'image') return
445
+ if (e.deltaY < 0) zoomIn()
446
+ else zoomOut()
447
+ }
448
+
449
+ function lockBody(lock: boolean) {
450
+ if (typeof document === 'undefined') return
451
+ document.body.style.overflow = lock ? 'hidden' : ''
452
+ }
453
+
454
+ async function loadPreview() {
455
+ if (!src.value) return
456
+ loading.value = true
457
+ error.value = ''
458
+ textContent.value = ''
459
+ excelHtml.value = ''
460
+
461
+ try {
462
+ if (kind.value === 'text') {
463
+ textContent.value = await fetchFileText(src.value)
464
+ } else if (kind.value === 'excel') {
465
+ excelHtml.value = DOMPurify.sanitize(await loadExcelPreviewHtml(src.value), {
466
+ USE_PROFILES: { html: true },
467
+ })
468
+ }
469
+ } catch (e) {
470
+ error.value = e instanceof Error ? e.message : crudLabel('previewFail')
471
+ } finally {
472
+ loading.value = false
473
+ }
474
+ }
475
+
476
+ function openViewer() {
477
+ if (!props.preview || !src.value) return
478
+ viewerOpen.value = true
479
+ }
480
+
481
+ function closeViewer() {
482
+ viewerOpen.value = false
483
+ }
484
+
485
+ function onKeydown(e: KeyboardEvent) {
486
+ if (!viewerOpen.value) return
487
+ if (e.key !== 'Escape') return
488
+ e.preventDefault()
489
+ e.stopPropagation()
490
+ closeViewer()
491
+ }
492
+
493
+ watch(viewerOpen, (open) => {
494
+ lockBody(open)
495
+ if (typeof window === 'undefined') return
496
+ if (open) {
497
+ resetView()
498
+ // 捕获阶段拦截 Escape,避免同时关掉宿主 Dialog
499
+ window.addEventListener('keydown', onKeydown, true)
500
+ if (kind.value === 'text' || kind.value === 'excel') void loadPreview()
501
+ if (kind.value === 'video') {
502
+ void nextTick(() => {
503
+ videoRef.value?.play?.().catch(() => undefined)
504
+ })
505
+ }
506
+ if (kind.value === 'audio') {
507
+ void nextTick(() => {
508
+ audioRef.value?.play?.().catch(() => undefined)
509
+ })
510
+ }
511
+ } else {
512
+ window.removeEventListener('keydown', onKeydown, true)
513
+ loading.value = false
514
+ error.value = ''
515
+ textContent.value = ''
516
+ excelHtml.value = ''
517
+ videoRef.value?.pause?.()
518
+ audioRef.value?.pause?.()
519
+ }
520
+ })
521
+
522
+ onUnmounted(() => {
523
+ lockBody(false)
524
+ if (typeof window !== 'undefined') {
525
+ window.removeEventListener('keydown', onKeydown, true)
526
+ }
527
+ })
528
+ </script>
529
+
530
+ <style lang="scss" scoped>
531
+ .vm-preview-viewer {
532
+ display: inline-flex;
533
+ vertical-align: middle;
534
+ }
535
+
536
+ .vm-preview-viewer.is-empty {
537
+ color: var(--muted-foreground);
538
+ font-size: 12px;
539
+ }
540
+
541
+ .vm-preview-viewer__trigger {
542
+ position: relative;
543
+ display: inline-flex;
544
+ flex-shrink: 0;
545
+ align-items: center;
546
+ justify-content: center;
547
+ padding: 0;
548
+ border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
549
+ background: var(--muted);
550
+ cursor: pointer;
551
+ overflow: hidden;
552
+ transition:
553
+ border-color 0.15s ease,
554
+ box-shadow 0.15s ease;
555
+
556
+ &:hover:not(:disabled) {
557
+ border-color: var(--focus-border);
558
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand) 12%, transparent);
559
+ }
560
+
561
+ &:disabled {
562
+ cursor: default;
563
+ }
564
+ }
565
+
566
+ .vm-preview-viewer__media {
567
+ display: block;
568
+ width: 100%;
569
+ height: 100%;
570
+ }
571
+
572
+ .vm-preview-viewer__loading {
573
+ display: block;
574
+ width: 100%;
575
+ height: 100%;
576
+ background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
577
+ }
578
+
579
+ .vm-preview-viewer__play {
580
+ position: absolute;
581
+ inset: 0;
582
+ display: grid;
583
+ place-items: center;
584
+ background: rgb(20 24 40 / 28%);
585
+ color: #fff;
586
+ pointer-events: none;
587
+
588
+ i {
589
+ font-size: 18px;
590
+ line-height: 1;
591
+ }
592
+ }
593
+
594
+ .vm-preview-viewer__btn {
595
+ display: inline-flex;
596
+ align-items: center;
597
+ gap: 4px;
598
+ padding: 0;
599
+ border: none;
600
+ background: transparent;
601
+ color: var(--brand);
602
+ font-size: 13px;
603
+ font-weight: 600;
604
+ cursor: pointer;
605
+
606
+ i {
607
+ font-size: 16px;
608
+ line-height: 1;
609
+ }
610
+
611
+ &:hover:not(:disabled) {
612
+ color: var(--brand-deep);
613
+ }
614
+
615
+ &:disabled {
616
+ opacity: 0.55;
617
+ cursor: default;
618
+ }
619
+ }
620
+ </style>
621
+
622
+ <style lang="scss">
623
+ .vm-file-preview-viewer {
624
+ position: fixed;
625
+ inset: 0;
626
+ z-index: 2200;
627
+ display: flex;
628
+ flex-direction: column;
629
+ background: rgb(10 12 20 / 88%);
630
+ }
631
+
632
+ .vm-file-preview-viewer__head {
633
+ display: flex;
634
+ flex-shrink: 0;
635
+ align-items: center;
636
+ justify-content: space-between;
637
+ gap: 12px;
638
+ padding: 12px 18px;
639
+ border-bottom: 1px solid rgb(255 255 255 / 10%);
640
+ background: rgb(24 28 40 / 92%);
641
+ }
642
+
643
+ .vm-file-preview-viewer__meta {
644
+ display: flex;
645
+ min-width: 0;
646
+ align-items: center;
647
+ gap: 10px;
648
+ }
649
+
650
+ .vm-file-preview-viewer__name {
651
+ overflow: hidden;
652
+ text-overflow: ellipsis;
653
+ white-space: nowrap;
654
+ font-size: 14px;
655
+ font-weight: 650;
656
+ color: #fff;
657
+ }
658
+
659
+ .vm-file-preview-viewer__tag {
660
+ flex-shrink: 0;
661
+ padding: 2px 8px;
662
+ border-radius: 999px;
663
+ background: rgb(255 255 255 / 12%);
664
+ color: rgb(255 255 255 / 88%);
665
+ font-size: 11px;
666
+ font-weight: 600;
667
+ text-transform: uppercase;
668
+ }
669
+
670
+ .vm-file-preview-viewer__actions {
671
+ display: flex;
672
+ flex-shrink: 0;
673
+ align-items: center;
674
+ gap: 10px;
675
+ }
676
+
677
+ .vm-file-preview-viewer__link {
678
+ appearance: none;
679
+ border: 0;
680
+ padding: 0;
681
+ background: transparent;
682
+ color: rgb(147 197 253);
683
+ font: inherit;
684
+ font-size: 13px;
685
+ font-weight: 600;
686
+ text-decoration: none;
687
+ cursor: pointer;
688
+
689
+ &:hover {
690
+ text-decoration: underline;
691
+ }
692
+ }
693
+
694
+ .vm-file-preview-viewer__dl-btn {
695
+ margin-top: 12px;
696
+ padding: 8px 16px;
697
+ border: 0;
698
+ border-radius: 8px;
699
+ background: var(--brand, #4e5dff);
700
+ color: #fff;
701
+ font-size: 14px;
702
+ font-weight: 600;
703
+ cursor: pointer;
704
+ }
705
+
706
+ .vm-file-preview-viewer__close {
707
+ display: inline-flex;
708
+ width: 34px;
709
+ height: 34px;
710
+ align-items: center;
711
+ justify-content: center;
712
+ border: none;
713
+ border-radius: 10px;
714
+ background: rgb(255 255 255 / 12%);
715
+ color: #fff;
716
+ font-size: 18px;
717
+ cursor: pointer;
718
+
719
+ &:hover {
720
+ background: rgb(255 255 255 / 22%);
721
+ }
722
+ }
723
+
724
+ .vm-file-preview-viewer__body {
725
+ flex: 1 1 auto;
726
+ min-height: 0;
727
+ padding: 16px;
728
+ overflow: auto;
729
+
730
+ &.is-media {
731
+ display: flex;
732
+ flex-direction: column;
733
+ padding: 0;
734
+ overflow: hidden;
735
+ }
736
+ }
737
+
738
+ .vm-file-preview-viewer__hint {
739
+ margin: 24px auto;
740
+ color: rgb(255 255 255 / 80%);
741
+ text-align: center;
742
+
743
+ &.is-error {
744
+ color: #fca5a5;
745
+ }
746
+ }
747
+
748
+ .vm-file-preview-viewer__image-stage {
749
+ display: flex;
750
+ flex: 1 1 auto;
751
+ min-height: 0;
752
+ width: 100%;
753
+ align-items: center;
754
+ justify-content: center;
755
+ overflow: hidden;
756
+ }
757
+
758
+ .vm-file-preview-viewer__image {
759
+ display: block;
760
+ width: auto;
761
+ height: auto;
762
+ max-width: min(1200px, calc(100vw - 48px));
763
+ max-height: calc(100vh - 140px);
764
+ object-fit: contain;
765
+ transition: transform 0.15s ease;
766
+ user-select: none;
767
+ }
768
+
769
+ .vm-file-preview-viewer__toolbar {
770
+ position: relative;
771
+ z-index: 2;
772
+ display: inline-flex;
773
+ flex-shrink: 0;
774
+ align-items: center;
775
+ gap: 4px;
776
+ margin: 0 auto 20px;
777
+ padding: 6px 8px;
778
+ border-radius: 12px;
779
+ background: rgb(24 28 40 / 92%);
780
+
781
+ button {
782
+ display: inline-flex;
783
+ width: 36px;
784
+ height: 36px;
785
+ align-items: center;
786
+ justify-content: center;
787
+ border: none;
788
+ border-radius: 8px;
789
+ background: transparent;
790
+ color: rgb(255 255 255 / 88%);
791
+ font-size: 18px;
792
+ cursor: pointer;
793
+
794
+ &:hover {
795
+ background: rgb(255 255 255 / 12%);
796
+ color: #fff;
797
+ }
798
+ }
799
+ }
800
+
801
+ .vm-file-preview-viewer__video-wrap {
802
+ display: flex;
803
+ flex: 1 1 auto;
804
+ min-height: 0;
805
+ width: 100%;
806
+ align-items: center;
807
+ justify-content: center;
808
+ padding: 16px;
809
+ }
810
+
811
+ .vm-file-preview-viewer__video {
812
+ display: block;
813
+ width: min(960px, calc(100vw - 48px));
814
+ max-height: calc(100vh - 120px);
815
+ border-radius: 12px;
816
+ background: #000;
817
+ outline: none;
818
+ }
819
+
820
+ .vm-file-preview-viewer__audio {
821
+ display: block;
822
+ width: min(480px, 100%);
823
+ margin: 48px auto 0;
824
+ }
825
+
826
+ .vm-file-preview-viewer__iframe {
827
+ display: block;
828
+ width: min(1100px, 100%);
829
+ height: calc(100vh - 88px);
830
+ margin: 0 auto;
831
+ border: none;
832
+ border-radius: 12px;
833
+ background: #fff;
834
+ }
835
+
836
+ .vm-file-preview-viewer__excel {
837
+ max-width: min(1100px, 100%);
838
+ margin: 0 auto;
839
+ padding: 12px;
840
+ border-radius: 12px;
841
+ background: #fff;
842
+ overflow: auto;
843
+
844
+ table {
845
+ width: 100%;
846
+ border-collapse: collapse;
847
+ font-size: 13px;
848
+ }
849
+
850
+ td,
851
+ th {
852
+ padding: 6px 8px;
853
+ border: 1px solid #e5e7eb;
854
+ white-space: nowrap;
855
+ }
856
+ }
857
+
858
+ .vm-file-preview-viewer__text {
859
+ max-width: min(960px, 100%);
860
+ margin: 0 auto;
861
+ padding: 16px 18px;
862
+ border-radius: 12px;
863
+ background: rgb(15 23 42 / 92%);
864
+ color: #e2e8f0;
865
+ font-size: 13px;
866
+ line-height: 1.6;
867
+ white-space: pre-wrap;
868
+ word-break: break-word;
869
+ }
870
+
871
+ .vm-file-preview-viewer__fallback {
872
+ display: grid;
873
+ gap: 8px;
874
+ max-width: 420px;
875
+ margin: 48px auto;
876
+ padding: 28px 24px;
877
+ border-radius: 16px;
878
+ background: rgb(255 255 255 / 8%);
879
+ color: rgb(255 255 255 / 88%);
880
+ text-align: center;
881
+
882
+ i {
883
+ font-size: 42px;
884
+ line-height: 1;
885
+ color: rgb(255 255 255 / 72%);
886
+ }
887
+
888
+ p {
889
+ margin: 0;
890
+ font-size: 14px;
891
+ line-height: 1.6;
892
+ }
893
+ }
894
+ </style>