@veltra/desktop 1.0.8 → 1.0.10

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 (220) hide show
  1. package/dist/components/action/action-group.vue.d.ts +2 -2
  2. package/dist/components/action/action.vue.d.ts +3 -3
  3. package/dist/components/auto-complete/auto-complete.vue.d.ts +1 -1
  4. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  5. package/dist/components/batch-edit/batch-edit.vue.d.ts +2 -2
  6. package/dist/components/batch-edit/use-edit.js.map +1 -1
  7. package/dist/components/button/button.js.map +1 -1
  8. package/dist/components/button/button.vue.d.ts +1 -1
  9. package/dist/components/cascade/cascade.js +15 -13
  10. package/dist/components/cascade/cascade.js.map +1 -1
  11. package/dist/components/cascade/cascade.vue.d.ts +7 -7
  12. package/dist/components/cascade/node.js.map +1 -1
  13. package/dist/components/cascade/use-check.js +3 -2
  14. package/dist/components/cascade/use-check.js.map +1 -1
  15. package/dist/components/cascade/use-select.js +3 -3
  16. package/dist/components/cascade/use-select.js.map +1 -1
  17. package/dist/components/checkbox-group/checkbox-group.js.map +1 -1
  18. package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
  19. package/dist/components/date-panel/date-panel.js.map +1 -1
  20. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  21. package/dist/components/date-picker/date-picker.js +9 -9
  22. package/dist/components/date-picker/date-picker.js.map +1 -1
  23. package/dist/components/date-picker/date-picker.vue.d.ts +1 -1
  24. package/dist/components/date-range-picker/date-range-picker.js +9 -9
  25. package/dist/components/date-range-picker/date-range-picker.js.map +1 -1
  26. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +1 -1
  27. package/dist/components/drawer/drawer.js.map +1 -1
  28. package/dist/components/empty/empty.vue.d.ts +1 -1
  29. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  30. package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js.map +1 -1
  31. package/dist/components/expression-editor/internal/features/commands/register-command-packs.js.map +1 -1
  32. package/dist/components/expression-editor/internal/features/insertion/insertion-service.js.map +1 -1
  33. package/dist/components/expression-editor/use-decorators.js.map +1 -1
  34. package/dist/components/expression-editor/use-expression-drag-drop.js.map +1 -1
  35. package/dist/components/file-picker/file-picker.js.map +1 -1
  36. package/dist/components/file-picker/file-picker.vue.d.ts +1 -1
  37. package/dist/components/file-picker/helper.js +19 -7
  38. package/dist/components/file-picker/helper.js.map +1 -1
  39. package/dist/components/file-viewer/file-viewer.js +294 -0
  40. package/dist/components/file-viewer/file-viewer.js.map +1 -0
  41. package/dist/components/file-viewer/file-viewer.vue.d.ts +42 -0
  42. package/dist/components/file-viewer/helper.js +185 -0
  43. package/dist/components/file-viewer/helper.js.map +1 -0
  44. package/dist/components/file-viewer/index.d.ts +1 -0
  45. package/dist/components/file-viewer/previewers/docx-previewer.js +76 -0
  46. package/dist/components/file-viewer/previewers/docx-previewer.js.map +1 -0
  47. package/dist/components/file-viewer/previewers/image-previewer.js +64 -0
  48. package/dist/components/file-viewer/previewers/image-previewer.js.map +1 -0
  49. package/dist/components/file-viewer/previewers/pdf-previewer.js +90 -0
  50. package/dist/components/file-viewer/previewers/pdf-previewer.js.map +1 -0
  51. package/dist/components/file-viewer/previewers/sheet-previewer.js +175 -0
  52. package/dist/components/file-viewer/previewers/sheet-previewer.js.map +1 -0
  53. package/dist/components/file-viewer/previewers/text-previewer.js +74 -0
  54. package/dist/components/file-viewer/previewers/text-previewer.js.map +1 -0
  55. package/dist/components/file-viewer/previewers/video-previewer.js +62 -0
  56. package/dist/components/file-viewer/previewers/video-previewer.js.map +1 -0
  57. package/dist/components/file-viewer/style.d.ts +1 -0
  58. package/dist/components/file-viewer/style.js +4 -0
  59. package/dist/components/file-viewer/style2.css +445 -0
  60. package/dist/components/form/form-model.js +2 -1
  61. package/dist/components/form/form-model.js.map +1 -1
  62. package/dist/components/grid/grid.js.map +1 -1
  63. package/dist/components/grid/use-responsive.js.map +1 -1
  64. package/dist/components/grid-input/grid-input.vue.d.ts +1 -1
  65. package/dist/components/index.d.ts +12 -9
  66. package/dist/components/input/input.vue.d.ts +3 -3
  67. package/dist/components/list/list.js.map +1 -1
  68. package/dist/components/loading/directive.js.map +1 -1
  69. package/dist/components/menu/menu-item.js.map +1 -1
  70. package/dist/components/message/message2.js.map +1 -1
  71. package/dist/components/multi-select/multi-select-option.js +14 -10
  72. package/dist/components/multi-select/multi-select-option.js.map +1 -1
  73. package/dist/components/multi-select/multi-select.js +16 -19
  74. package/dist/components/multi-select/multi-select.js.map +1 -1
  75. package/dist/components/multi-select/multi-select.vue.d.ts +4 -4
  76. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +7 -7
  77. package/dist/components/notification/index.js.map +1 -1
  78. package/dist/components/notification/notification.vue.d.ts +1 -1
  79. package/dist/components/number-input/number-input.js.map +1 -1
  80. package/dist/components/number-input/number-input.vue.d.ts +1 -1
  81. package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
  82. package/dist/components/palette/color-transform.js.map +1 -1
  83. package/dist/components/palette/di.js.map +1 -1
  84. package/dist/components/palette/palette-alpha.js +7 -10
  85. package/dist/components/palette/palette-alpha.js.map +1 -1
  86. package/dist/components/palette/palette-hue.js +5 -7
  87. package/dist/components/palette/palette-hue.js.map +1 -1
  88. package/dist/components/palette/palette-sv.js +8 -10
  89. package/dist/components/palette/palette-sv.js.map +1 -1
  90. package/dist/components/palette/palette.js +22 -24
  91. package/dist/components/palette/palette.js.map +1 -1
  92. package/dist/components/password-input/password-input.vue.d.ts +1 -1
  93. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +2 -2
  94. package/dist/components/progress/progress.js +1 -1
  95. package/dist/components/progress/progress.js.map +1 -1
  96. package/dist/components/progress-nodes/progress-nodes.js.map +1 -1
  97. package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
  98. package/dist/components/rich-text-editor/rich-text-editor.js.map +1 -1
  99. package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +1 -1
  100. package/dist/components/rich-text-editor/toolbar.js.map +1 -1
  101. package/dist/components/scroll/scroll.js.map +1 -1
  102. package/dist/components/select/select.js +14 -12
  103. package/dist/components/select/select.js.map +1 -1
  104. package/dist/components/select/select.vue.d.ts +3 -3
  105. package/dist/components/slider/slider-thumb.js +10 -13
  106. package/dist/components/slider/slider-thumb.js.map +1 -1
  107. package/dist/components/slider/slider.js +12 -15
  108. package/dist/components/slider/slider.js.map +1 -1
  109. package/dist/components/table/di.js.map +1 -1
  110. package/dist/components/table/node/row.d.ts +10 -1
  111. package/dist/components/table/node/row.js +9 -0
  112. package/dist/components/table/node/row.js.map +1 -1
  113. package/dist/components/table/table-body.js +25 -26
  114. package/dist/components/table/table-body.js.map +1 -1
  115. package/dist/components/table/table-cell.js +16 -9
  116. package/dist/components/table/table-cell.js.map +1 -1
  117. package/dist/components/table/table-row.js +77 -19
  118. package/dist/components/table/table-row.js.map +1 -1
  119. package/dist/components/table/table.js +75 -34
  120. package/dist/components/table/table.js.map +1 -1
  121. package/dist/components/table/table.vue.d.ts +2 -2
  122. package/dist/components/table/use-columns.js +4 -0
  123. package/dist/components/table/use-columns.js.map +1 -1
  124. package/dist/components/table/use-fixed-columns.js +27 -8
  125. package/dist/components/table/use-fixed-columns.js.map +1 -1
  126. package/dist/components/table/use-rows.js +2 -2
  127. package/dist/components/table/use-rows.js.map +1 -1
  128. package/dist/components/table/use-table.js +50 -3
  129. package/dist/components/table/use-table.js.map +1 -1
  130. package/dist/components/tabs/index.d.ts +3 -1
  131. package/dist/components/tabs/style2.css +170 -107
  132. package/dist/components/tabs/tabs-horizontal.js +133 -0
  133. package/dist/components/tabs/tabs-horizontal.js.map +1 -0
  134. package/dist/components/tabs/tabs-horizontal.vue.d.ts +40 -0
  135. package/dist/components/tabs/tabs-vertical.js +89 -0
  136. package/dist/components/tabs/tabs-vertical.js.map +1 -0
  137. package/dist/components/tabs/tabs-vertical.vue.d.ts +38 -0
  138. package/dist/components/tabs/tabs.js +75 -105
  139. package/dist/components/tabs/tabs.js.map +1 -1
  140. package/dist/components/tabs/tabs.vue.d.ts +9 -23
  141. package/dist/components/tabs/use-tabs-bar.js +105 -0
  142. package/dist/components/tabs/use-tabs-bar.js.map +1 -0
  143. package/dist/components/text/text.js.map +1 -1
  144. package/dist/components/textarea/textarea.js.map +1 -1
  145. package/dist/components/textarea/textarea.vue.d.ts +4 -4
  146. package/dist/components/theme/theme.js.map +1 -1
  147. package/dist/components/tip/tip.vue.d.ts +1 -1
  148. package/dist/components/tree/style2.css +45 -23
  149. package/dist/components/tree/tree-node.js +50 -35
  150. package/dist/components/tree/tree-node.js.map +1 -1
  151. package/dist/components/tree/tree-node2.js.map +1 -1
  152. package/dist/components/tree/tree.js +27 -20
  153. package/dist/components/tree/tree.js.map +1 -1
  154. package/dist/components/tree/tree.vue.d.ts +5 -5
  155. package/dist/components/tree/use-check.js +52 -14
  156. package/dist/components/tree/use-check.js.map +1 -1
  157. package/dist/components/tree/use-tree-nodes.js +51 -24
  158. package/dist/components/tree/use-tree-nodes.js.map +1 -1
  159. package/dist/components/tree-select/tree-select.vue.d.ts +7 -7
  160. package/dist/index.d.ts +57 -53
  161. package/dist/index.js +51 -48
  162. package/dist/types/file-viewer.d.ts +71 -0
  163. package/dist/types/index.d.ts +3 -2
  164. package/dist/types/tabs.d.ts +106 -14
  165. package/dist/types/tree.d.ts +6 -1
  166. package/package.json +32 -31
  167. package/src/components/cascade/cascade.vue +18 -18
  168. package/src/components/cascade/node.ts +4 -1
  169. package/src/components/cascade/use-check.ts +5 -4
  170. package/src/components/cascade/use-select.ts +4 -5
  171. package/src/components/date-picker/date-picker.vue +10 -9
  172. package/src/components/date-range-picker/date-range-picker.vue +12 -12
  173. package/src/components/file-picker/helper.ts +21 -10
  174. package/src/components/file-viewer/file-viewer.vue +311 -0
  175. package/src/components/file-viewer/helper.ts +193 -0
  176. package/src/components/file-viewer/index.ts +1 -0
  177. package/src/components/file-viewer/previewers/docx-previewer.vue +78 -0
  178. package/src/components/file-viewer/previewers/image-previewer.vue +64 -0
  179. package/src/components/file-viewer/previewers/pdf-previewer.vue +90 -0
  180. package/src/components/file-viewer/previewers/sheet-previewer.vue +210 -0
  181. package/src/components/file-viewer/previewers/text-previewer.vue +76 -0
  182. package/src/components/file-viewer/previewers/video-previewer.vue +72 -0
  183. package/src/components/file-viewer/style.scss +504 -0
  184. package/src/components/file-viewer/style.ts +4 -0
  185. package/src/components/index.ts +2 -0
  186. package/src/components/multi-select/multi-select-option.vue +16 -3
  187. package/src/components/multi-select/multi-select.vue +17 -9
  188. package/src/components/palette/di.ts +5 -3
  189. package/src/components/palette/palette-alpha.vue +7 -10
  190. package/src/components/palette/palette-hue.vue +5 -7
  191. package/src/components/palette/palette-sv.vue +12 -14
  192. package/src/components/palette/palette.vue +19 -21
  193. package/src/components/progress/progress.vue +1 -1
  194. package/src/components/select/select.vue +21 -19
  195. package/src/components/slider/slider-thumb.vue +11 -14
  196. package/src/components/slider/slider.vue +15 -15
  197. package/src/components/table/di.ts +58 -43
  198. package/src/components/table/node/row.ts +10 -1
  199. package/src/components/table/table-body.vue +28 -33
  200. package/src/components/table/table-cell.vue +17 -9
  201. package/src/components/table/table-row.tsx +83 -17
  202. package/src/components/table/table.vue +72 -45
  203. package/src/components/table/use-fixed-columns.ts +46 -12
  204. package/src/components/table/use-rows.ts +14 -3
  205. package/src/components/table/use-table.ts +27 -3
  206. package/src/components/tabs/index.ts +2 -0
  207. package/src/components/tabs/style.scss +219 -133
  208. package/src/components/tabs/tabs-horizontal.vue +128 -0
  209. package/src/components/tabs/tabs-vertical.vue +77 -0
  210. package/src/components/tabs/tabs.vue +56 -127
  211. package/src/components/tabs/use-tabs-bar.ts +119 -0
  212. package/src/components/tree/style.scss +74 -29
  213. package/src/components/tree/tree-node.vue +10 -4
  214. package/src/components/tree/tree.vue +39 -12
  215. package/src/components/tree/use-check.ts +79 -23
  216. package/src/components/tree/use-tree-nodes.ts +85 -34
  217. package/src/types/file-viewer.ts +69 -0
  218. package/src/types/index.ts +1 -0
  219. package/src/types/tabs.ts +113 -19
  220. package/src/types/tree.ts +6 -1
@@ -0,0 +1,311 @@
1
+ <template>
2
+ <Teleport to="body" :disabled="!isModal">
3
+ <transition :name="isModal ? 'u-file-viewer-modal' : ''">
4
+ <div
5
+ v-if="!isModal || openModel"
6
+ :class="[cls.b, bem.is('modal', isModal)]"
7
+ :tabindex="isModal ? -1 : undefined"
8
+ ref="rootRef"
9
+ @mousedown.self="handleBackdropMousedown"
10
+ >
11
+ <div v-if="isModal" :class="cls.e('backdrop')" @mousedown.self="handleBackdropMousedown" />
12
+ <div :class="cls.e('inner')">
13
+ <aside v-if="showSidebar" :class="cls.e('sidebar')" :style="{ width: sidebarWidthCss }">
14
+ <header :class="cls.e('sidebar-head')">
15
+ <span :class="cls.e('sidebar-title')">文件</span>
16
+ <span :class="cls.e('sidebar-count')">{{ normalizedFiles.length }}</span>
17
+ </header>
18
+ <u-scroll tag="ul" :class="cls.e('list')">
19
+ <li
20
+ v-for="f in normalizedFiles"
21
+ :key="f.id"
22
+ :class="[cls.e('item'), bem.is('active', f.id === activeId)]"
23
+ @click="activate(f.id)"
24
+ >
25
+ <span :class="[cls.e('badge'), cls.em('badge', f.kind)]">
26
+ {{ label(f.kind) }}
27
+ </span>
28
+ <span :class="cls.e('item-meta')">
29
+ <span :class="cls.e('item-name')" :title="f.name">{{ f.name }}</span>
30
+ <span :class="cls.e('item-size')">{{ formatBytes(f.size) }}</span>
31
+ </span>
32
+ </li>
33
+ <li v-if="!normalizedFiles.length" :class="cls.e('list-empty')">
34
+ <u-empty text="暂无文件" :size="32" />
35
+ </li>
36
+ </u-scroll>
37
+ </aside>
38
+
39
+ <section :class="cls.e('stage')">
40
+ <header :class="cls.e('stage-head')">
41
+ <div :class="cls.e('stage-title')">
42
+ <span v-if="activeFile" :class="[cls.e('badge'), cls.em('badge', activeFile.kind)]">
43
+ {{ label(activeFile.kind) }}
44
+ </span>
45
+ <span :class="cls.e('stage-name')">
46
+ {{ activeFile?.name ?? '—' }}
47
+ </span>
48
+ <span v-if="activeFile?.size" :class="cls.e('stage-sub')">
49
+ {{ formatBytes(activeFile.size) }}
50
+ </span>
51
+ </div>
52
+ <div :class="cls.e('stage-actions')">
53
+ <button :class="cls.e('action')" :disabled="!hasPrev" type="button" @click="prev">
54
+ 上一个
55
+ </button>
56
+ <button :class="cls.e('action')" :disabled="!hasNext" type="button" @click="next">
57
+ 下一个
58
+ </button>
59
+ <button
60
+ v-if="downloadable && activeFile"
61
+ :class="[cls.e('action'), cls.em('action', 'primary')]"
62
+ type="button"
63
+ @click="download"
64
+ >
65
+ 下载
66
+ </button>
67
+ <button
68
+ v-if="isModal"
69
+ :class="[cls.e('action'), cls.em('action', 'icon')]"
70
+ type="button"
71
+ aria-label="关闭预览"
72
+ title="关闭"
73
+ @click="handleClose"
74
+ >
75
+ <u-icon :size="16">
76
+ <Close />
77
+ </u-icon>
78
+ </button>
79
+ </div>
80
+ </header>
81
+ <div :class="cls.e('body')">
82
+ <transition name="u-file-viewer-fade" mode="out-in">
83
+ <component
84
+ :is="PreviewerMap[activeFile.kind]"
85
+ v-if="activeFile"
86
+ :key="activeFile.id"
87
+ :file="activeFile"
88
+ :max-rows="sheetMaxRows"
89
+ @error="handleChildError"
90
+ />
91
+ <div v-else :class="cls.e('empty')">
92
+ <u-empty text="暂无可预览文件" />
93
+ </div>
94
+ </transition>
95
+ </div>
96
+ </section>
97
+ </div>
98
+ </div>
99
+ </transition>
100
+ </Teleport>
101
+ </template>
102
+
103
+ <script lang="ts" setup>
104
+ import { Close } from '@veltra/icons/normal'
105
+ import { bem, withUnit } from '@veltra/utils'
106
+ import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, shallowRef, watch } from 'vue'
107
+
108
+ import type {
109
+ _FileViewerExposed,
110
+ FileViewerEmits,
111
+ FileViewerItem,
112
+ FileViewerKind,
113
+ FileViewerProps
114
+ } from '../../types/file-viewer'
115
+ import { UEmpty } from '../empty'
116
+ import { UIcon } from '../icon'
117
+ import { UScroll } from '../scroll'
118
+ import { FILE_VIEWER_KIND_LABEL, downloadFile, formatBytes, inferKind } from './helper'
119
+
120
+ defineOptions({
121
+ name: 'FileViewer',
122
+ inheritAttrs: false
123
+ })
124
+
125
+ const props = withDefaults(defineProps<FileViewerProps>(), {
126
+ modelValue: undefined,
127
+ sidebarWidth: '280px',
128
+ sheetMaxRows: 50_000,
129
+ downloadable: true,
130
+ open: undefined,
131
+ closeOnClickBackdrop: true,
132
+ closeOnEsc: true
133
+ })
134
+
135
+ const emit = defineEmits<FileViewerEmits>()
136
+
137
+ const cls = bem('file-viewer')
138
+
139
+ const activeId = defineModel<string | undefined>('modelValue', { default: undefined })
140
+ const openModel = defineModel<boolean | undefined>('open', { default: undefined })
141
+
142
+ const rootRef = shallowRef<HTMLDivElement>()
143
+
144
+ const PreviewerMap: Record<FileViewerKind, ReturnType<typeof defineAsyncComponent>> = {
145
+ image: defineAsyncComponent(() => import('./previewers/image-previewer.vue')),
146
+ video: defineAsyncComponent(() => import('./previewers/video-previewer.vue')),
147
+ pdf: defineAsyncComponent(() => import('./previewers/pdf-previewer.vue')),
148
+ sheet: defineAsyncComponent(() => import('./previewers/sheet-previewer.vue')),
149
+ docx: defineAsyncComponent(() => import('./previewers/docx-previewer.vue')),
150
+ text: defineAsyncComponent(() => import('./previewers/text-previewer.vue'))
151
+ }
152
+
153
+ interface NormalizedFile extends FileViewerItem {
154
+ id: string
155
+ kind: FileViewerKind
156
+ }
157
+
158
+ const normalizedFiles = computed<NormalizedFile[]>(() =>
159
+ props.files.map((f, i) => ({
160
+ ...f,
161
+ id: f.id ?? `file-${i}`,
162
+ kind: inferKind(f.name, f.kind)
163
+ }))
164
+ )
165
+
166
+ const activeFile = computed<NormalizedFile | undefined>(() =>
167
+ normalizedFiles.value.find((f) => f.id === activeId.value)
168
+ )
169
+
170
+ const activeIndex = computed(() => normalizedFiles.value.findIndex((f) => f.id === activeId.value))
171
+
172
+ const hasPrev = computed(() => activeIndex.value > 0)
173
+ const hasNext = computed(
174
+ () => activeIndex.value >= 0 && activeIndex.value < normalizedFiles.value.length - 1
175
+ )
176
+
177
+ const showSidebar = computed(() => props.sidebarWidth !== false && props.sidebarWidth !== 0)
178
+
179
+ const sidebarWidthCss = computed(() => {
180
+ const w = props.sidebarWidth
181
+ if (w === false || w === 0) return undefined
182
+ return withUnit(w ?? '280px', 'px')
183
+ })
184
+
185
+ /** 是否启用模态模式:只要父组件显式传入 open(含 v-model:open),即进入模态 */
186
+ const isModal = computed(() => openModel.value !== undefined)
187
+
188
+ function label(kind: FileViewerKind): string {
189
+ return FILE_VIEWER_KIND_LABEL[kind]
190
+ }
191
+
192
+ function activate(id: string) {
193
+ if (activeId.value === id) return
194
+ activeId.value = id
195
+ const f = normalizedFiles.value.find((x) => x.id === id)
196
+ if (f) emit('change', f)
197
+ }
198
+
199
+ function prev() {
200
+ const i = activeIndex.value
201
+ if (i <= 0) return
202
+ const target = normalizedFiles.value[i - 1]
203
+ if (target) activate(target.id)
204
+ }
205
+
206
+ function next() {
207
+ const i = activeIndex.value
208
+ if (i < 0 || i >= normalizedFiles.value.length - 1) return
209
+ const target = normalizedFiles.value[i + 1]
210
+ if (target) activate(target.id)
211
+ }
212
+
213
+ async function download() {
214
+ if (!activeFile.value) return
215
+ try {
216
+ await downloadFile(activeFile.value)
217
+ } catch (err) {
218
+ emit('error', { file: activeFile.value, error: err })
219
+ }
220
+ }
221
+
222
+ function handleChildError(err: unknown) {
223
+ if (!activeFile.value) return
224
+ emit('error', { file: activeFile.value, error: err })
225
+ }
226
+
227
+ function handleClose() {
228
+ if (!isModal.value) return
229
+ openModel.value = false
230
+ }
231
+
232
+ function handleWindowKeydown(e: KeyboardEvent) {
233
+ if (e.key !== 'Escape') return
234
+ if (!isModal.value || !openModel.value) return
235
+ if (props.closeOnEsc === false) return
236
+ e.stopPropagation()
237
+ handleClose()
238
+ }
239
+
240
+ function handleBackdropMousedown() {
241
+ if (!isModal.value) return
242
+ if (props.closeOnClickBackdrop === false) return
243
+ handleClose()
244
+ }
245
+
246
+ watch(
247
+ normalizedFiles,
248
+ (files) => {
249
+ if (!files.length) {
250
+ activeId.value = undefined
251
+ return
252
+ }
253
+ if (!activeId.value || !files.some((f) => f.id === activeId.value)) {
254
+ activeId.value = files[0]!.id
255
+ }
256
+ },
257
+ { immediate: true }
258
+ )
259
+
260
+ // 模态打开时:锁定 body 滚动 + 让容器获得焦点以便接收 ESC
261
+ let previousBodyOverflow = ''
262
+ let isBodyLocked = false
263
+
264
+ function lockBody() {
265
+ if (isBodyLocked || typeof document === 'undefined') return
266
+ previousBodyOverflow = document.body.style.overflow
267
+ document.body.style.overflow = 'hidden'
268
+ isBodyLocked = true
269
+ }
270
+
271
+ function unlockBody() {
272
+ if (!isBodyLocked || typeof document === 'undefined') return
273
+ document.body.style.overflow = previousBodyOverflow
274
+ isBodyLocked = false
275
+ }
276
+
277
+ watch(
278
+ [isModal, openModel],
279
+ ([modal, open]) => {
280
+ if (modal && open) {
281
+ lockBody()
282
+ nextTick(() => {
283
+ rootRef.value?.focus()
284
+ })
285
+ if (typeof window !== 'undefined') {
286
+ window.addEventListener('keydown', handleWindowKeydown, true)
287
+ }
288
+ } else {
289
+ unlockBody()
290
+ if (typeof window !== 'undefined') {
291
+ window.removeEventListener('keydown', handleWindowKeydown, true)
292
+ }
293
+ }
294
+ },
295
+ { immediate: true }
296
+ )
297
+
298
+ onBeforeUnmount(() => {
299
+ unlockBody()
300
+ if (typeof window !== 'undefined') {
301
+ window.removeEventListener('keydown', handleWindowKeydown, true)
302
+ }
303
+ })
304
+
305
+ defineExpose<_FileViewerExposed>({
306
+ activeId,
307
+ activate,
308
+ next,
309
+ prev
310
+ })
311
+ </script>
@@ -0,0 +1,193 @@
1
+ import type { FileViewerItem, FileViewerKind } from '../../types/file-viewer'
2
+
3
+ const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg', 'avif', 'ico'])
4
+
5
+ const VIDEO_EXTS = new Set(['mp4', 'webm', 'mov', 'm4v', 'ogv', 'mkv'])
6
+
7
+ const SHEET_EXTS = new Set(['xlsx', 'xlsm', 'xlsb', 'csv'])
8
+
9
+ const TEXT_EXTS = new Set([
10
+ 'txt',
11
+ 'log',
12
+ 'md',
13
+ 'markdown',
14
+ 'json',
15
+ 'yml',
16
+ 'yaml',
17
+ 'xml',
18
+ 'js',
19
+ 'ts',
20
+ 'tsx',
21
+ 'jsx',
22
+ 'css',
23
+ 'scss',
24
+ 'sass',
25
+ 'less',
26
+ 'html',
27
+ 'htm',
28
+ 'ini',
29
+ 'toml',
30
+ 'sh',
31
+ 'bash',
32
+ 'zsh',
33
+ 'env',
34
+ 'sql'
35
+ ])
36
+
37
+ export function getExtension(name: string): string {
38
+ const idx = name.lastIndexOf('.')
39
+ return idx === -1 ? '' : name.slice(idx + 1).toLowerCase()
40
+ }
41
+
42
+ export function inferKind(name: string, explicit?: FileViewerKind): FileViewerKind {
43
+ if (explicit) return explicit
44
+ const ext = getExtension(name)
45
+ if (IMAGE_EXTS.has(ext)) return 'image'
46
+ if (VIDEO_EXTS.has(ext)) return 'video'
47
+ if (ext === 'pdf') return 'pdf'
48
+ if (SHEET_EXTS.has(ext)) return 'sheet'
49
+ if (ext === 'docx') return 'docx'
50
+ if (TEXT_EXTS.has(ext)) return 'text'
51
+ return 'text'
52
+ }
53
+
54
+ export const FILE_VIEWER_KIND_LABEL: Record<FileViewerKind, string> = {
55
+ image: 'IMG',
56
+ video: 'MP4',
57
+ pdf: 'PDF',
58
+ sheet: 'XLS',
59
+ docx: 'DOC',
60
+ text: 'TXT'
61
+ }
62
+
63
+ /** 规范化数据源为 ArrayBuffer,可被 AbortController 中断(仅 URL fetch 场景生效) */
64
+ export async function toArrayBuffer(
65
+ src: FileViewerItem['src'],
66
+ signal?: AbortSignal
67
+ ): Promise<ArrayBuffer> {
68
+ if (typeof src === 'string') {
69
+ const res = await fetch(src, { signal })
70
+ if (!res.ok) {
71
+ throw new Error(`Fetch failed: ${res.status} ${res.statusText}`)
72
+ }
73
+ return await res.arrayBuffer()
74
+ }
75
+ if (src instanceof ArrayBuffer) return src
76
+ if (src instanceof Uint8Array) {
77
+ // 复制底层以切断共享引用,防止外部修改影响预览
78
+ return src.slice().buffer as ArrayBuffer
79
+ }
80
+ // File/Blob
81
+ return await src.arrayBuffer()
82
+ }
83
+
84
+ /** 返回用于 <img> / <video> 等标签 src 的 URL;二进制会创建 ObjectURL 并返回 revoke 钩子 */
85
+ export function toBlobUrl(
86
+ src: FileViewerItem['src'],
87
+ mime?: string
88
+ ): { url: string; revoke: () => void } {
89
+ if (typeof src === 'string') {
90
+ return { url: src, revoke: () => {} }
91
+ }
92
+ let blob: Blob
93
+ if (src instanceof Blob) {
94
+ blob = src
95
+ } else if (src instanceof ArrayBuffer) {
96
+ blob = new Blob([src], { type: mime ?? 'application/octet-stream' })
97
+ } else {
98
+ // Uint8Array: 复制到新的 ArrayBuffer,避免与外部共享底层
99
+ const copy = new Uint8Array(src.byteLength)
100
+ copy.set(src)
101
+ blob = new Blob([copy.buffer], { type: mime ?? 'application/octet-stream' })
102
+ }
103
+ const url = URL.createObjectURL(blob)
104
+ return { url, revoke: () => URL.revokeObjectURL(url) }
105
+ }
106
+
107
+ /**
108
+ * RFC-4180 子集 CSV 解析:支持 "" 转义、\r\n 与 \n 换行、, 分隔。
109
+ * 为性能起见采用状态机而非 split。
110
+ */
111
+ export function parseCsv(text: string): string[][] {
112
+ const rows: string[][] = []
113
+ let row: string[] = []
114
+ let field = ''
115
+ let inQuotes = false
116
+ const len = text.length
117
+
118
+ for (let i = 0; i < len; i++) {
119
+ const ch = text[i]
120
+
121
+ if (inQuotes) {
122
+ if (ch === '"') {
123
+ if (text[i + 1] === '"') {
124
+ field += '"'
125
+ i++
126
+ } else {
127
+ inQuotes = false
128
+ }
129
+ } else {
130
+ field += ch
131
+ }
132
+ continue
133
+ }
134
+
135
+ if (ch === '"') {
136
+ inQuotes = true
137
+ } else if (ch === ',') {
138
+ row.push(field)
139
+ field = ''
140
+ } else if (ch === '\n') {
141
+ row.push(field)
142
+ rows.push(row)
143
+ row = []
144
+ field = ''
145
+ } else if (ch === '\r') {
146
+ // swallow CR; LF will commit the row
147
+ } else {
148
+ field += ch
149
+ }
150
+ }
151
+
152
+ // 最后一行
153
+ if (field.length > 0 || row.length > 0) {
154
+ row.push(field)
155
+ rows.push(row)
156
+ }
157
+ return rows
158
+ }
159
+
160
+ export function formatBytes(bytes?: number): string {
161
+ if (bytes === undefined || bytes === null || Number.isNaN(bytes)) return '-'
162
+ if (bytes < 1024) return `${bytes} B`
163
+ const units = ['KB', 'MB', 'GB', 'TB']
164
+ let v = bytes / 1024
165
+ let i = 0
166
+ while (v >= 1024 && i < units.length - 1) {
167
+ v /= 1024
168
+ i++
169
+ }
170
+ return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`
171
+ }
172
+
173
+ /** 触发浏览器下载 */
174
+ export async function downloadFile(file: FileViewerItem): Promise<void> {
175
+ let href: string
176
+ let revoke: (() => void) | undefined
177
+ if (typeof file.src === 'string') {
178
+ href = file.src
179
+ } else {
180
+ const url = toBlobUrl(file.src, file.mime)
181
+ href = url.url
182
+ revoke = url.revoke
183
+ }
184
+ const a = document.createElement('a')
185
+ a.href = href
186
+ a.download = file.name
187
+ a.rel = 'noopener'
188
+ a.style.display = 'none'
189
+ document.body.appendChild(a)
190
+ a.click()
191
+ document.body.removeChild(a)
192
+ if (revoke) setTimeout(revoke, 4000)
193
+ }
@@ -0,0 +1 @@
1
+ export { default as UFileViewer } from './file-viewer.vue'
@@ -0,0 +1,78 @@
1
+ <template>
2
+ <u-scroll tag="div" :class="cls.e('docx')">
3
+ <div ref="container" :class="cls.e('docx-body')" />
4
+ <div v-if="loading" :class="cls.e('loading')">加载中…</div>
5
+ </u-scroll>
6
+ </template>
7
+
8
+ <script lang="ts" setup>
9
+ import { bem } from '@veltra/utils'
10
+ import { onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'
11
+
12
+ import type { FileViewerItem } from '../../../types/file-viewer'
13
+ import { UScroll } from '../../scroll'
14
+
15
+ defineOptions({ name: 'FileViewerDocxPreviewer' })
16
+
17
+ const props = defineProps<{ file: FileViewerItem }>()
18
+
19
+ const emit = defineEmits<{ (e: 'error', err: unknown): void }>()
20
+
21
+ const cls = bem('file-viewer')
22
+
23
+ const container = useTemplateRef<HTMLDivElement>('container')
24
+ const loading = ref(true)
25
+
26
+ let controller: AbortController | undefined
27
+
28
+ async function load() {
29
+ controller?.abort()
30
+ controller = new AbortController()
31
+ const signal = controller.signal
32
+ loading.value = true
33
+
34
+ try {
35
+ const [{ toArrayBuffer }, docx] = await Promise.all([
36
+ import('../helper'),
37
+ import('docx-preview')
38
+ ])
39
+
40
+ const buf = await toArrayBuffer(props.file.src, signal)
41
+ if (signal.aborted) return
42
+ const el = container.value
43
+ if (!el) return
44
+ el.innerHTML = ''
45
+
46
+ await docx.renderAsync(buf, el, undefined, {
47
+ className: 'u-file-viewer__docx-doc',
48
+ inWrapper: true,
49
+ ignoreWidth: false,
50
+ ignoreHeight: false,
51
+ ignoreFonts: false,
52
+ breakPages: true,
53
+ experimental: false,
54
+ useBase64URL: false,
55
+ renderChanges: false,
56
+ renderHeaders: true,
57
+ renderFooters: true,
58
+ renderFootnotes: true
59
+ })
60
+ } catch (err) {
61
+ if ((err as { name?: string })?.name === 'AbortError') return
62
+ emit('error', err)
63
+ } finally {
64
+ loading.value = false
65
+ }
66
+ }
67
+
68
+ watch(
69
+ () => props.file,
70
+ () => load(),
71
+ { immediate: true }
72
+ )
73
+
74
+ onBeforeUnmount(() => {
75
+ controller?.abort()
76
+ if (container.value) container.value.innerHTML = ''
77
+ })
78
+ </script>
@@ -0,0 +1,64 @@
1
+ <template>
2
+ <div :class="[cls.e('image'), bem.is('actual', actual)]">
3
+ <img
4
+ v-if="url"
5
+ :src="url"
6
+ :alt="file.name"
7
+ draggable="false"
8
+ @load="loading = false"
9
+ @error="onImgError"
10
+ @dblclick="actual = !actual"
11
+ />
12
+ <div v-if="loading" :class="cls.e('loading')">加载中…</div>
13
+ <div v-if="failed" :class="cls.e('loading')">图片加载失败</div>
14
+ </div>
15
+ </template>
16
+
17
+ <script lang="ts" setup>
18
+ import { bem } from '@veltra/utils'
19
+ import { onBeforeUnmount, ref, shallowRef, watch } from 'vue'
20
+
21
+ import type { FileViewerItem } from '../../../types/file-viewer'
22
+
23
+ defineOptions({ name: 'FileViewerImagePreviewer' })
24
+
25
+ const props = defineProps<{ file: FileViewerItem }>()
26
+
27
+ const emit = defineEmits<{ (e: 'error', err: unknown): void }>()
28
+
29
+ const cls = bem('file-viewer')
30
+
31
+ const url = shallowRef<string>('')
32
+ const loading = ref(true)
33
+ const failed = ref(false)
34
+ const actual = ref(false)
35
+
36
+ let revoke: (() => void) | undefined
37
+
38
+ async function load() {
39
+ loading.value = true
40
+ failed.value = false
41
+ revoke?.()
42
+ const { toBlobUrl } = await import('../helper')
43
+ const r = toBlobUrl(props.file.src, props.file.mime)
44
+ url.value = r.url
45
+ revoke = r.revoke
46
+ }
47
+
48
+ function onImgError(e: Event) {
49
+ loading.value = false
50
+ failed.value = true
51
+ emit('error', e)
52
+ }
53
+
54
+ watch(
55
+ () => props.file,
56
+ () => load(),
57
+ { immediate: true }
58
+ )
59
+
60
+ onBeforeUnmount(() => {
61
+ revoke?.()
62
+ url.value = ''
63
+ })
64
+ </script>