@veltra/desktop 1.0.10 → 1.0.11

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 (49) hide show
  1. package/dist/components/action/action-group.vue.d.ts +1 -1
  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.vue.d.ts +2 -2
  5. package/dist/components/button/button.vue.d.ts +1 -1
  6. package/dist/components/cascade/cascade.vue.d.ts +6 -6
  7. package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
  8. package/dist/components/code-editor/code-editor.js +2 -0
  9. package/dist/components/code-editor/code-editor.js.map +1 -1
  10. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  11. package/dist/components/date-picker/date-picker.vue.d.ts +1 -1
  12. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +1 -1
  13. package/dist/components/file-picker/file-picker.vue.d.ts +1 -1
  14. package/dist/components/file-viewer/file-viewer.js +195 -30
  15. package/dist/components/file-viewer/file-viewer.js.map +1 -1
  16. package/dist/components/file-viewer/previewers/docx-previewer.js +7 -5
  17. package/dist/components/file-viewer/previewers/docx-previewer.js.map +1 -1
  18. package/dist/components/file-viewer/previewers/image-previewer.js +6 -6
  19. package/dist/components/file-viewer/previewers/image-previewer.js.map +1 -1
  20. package/dist/components/file-viewer/previewers/pdf-previewer.js +5 -3
  21. package/dist/components/file-viewer/previewers/pdf-previewer.js.map +1 -1
  22. package/dist/components/file-viewer/style2.css +387 -136
  23. package/dist/components/grid-input/grid-input.vue.d.ts +1 -1
  24. package/dist/components/input/input.vue.d.ts +3 -3
  25. package/dist/components/multi-select/multi-select.vue.d.ts +4 -4
  26. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +7 -7
  27. package/dist/components/notification/notification.vue.d.ts +1 -1
  28. package/dist/components/number-input/number-input.vue.d.ts +1 -1
  29. package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
  30. package/dist/components/password-input/password-input.vue.d.ts +1 -1
  31. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +2 -2
  32. package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
  33. package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +1 -1
  34. package/dist/components/select/select.vue.d.ts +3 -3
  35. package/dist/components/table/table.vue.d.ts +2 -2
  36. package/dist/components/tabs/tabs-horizontal.vue.d.ts +2 -2
  37. package/dist/components/tabs/tabs-vertical.vue.d.ts +2 -2
  38. package/dist/components/tabs/tabs.vue.d.ts +2 -2
  39. package/dist/components/textarea/textarea.vue.d.ts +4 -4
  40. package/dist/components/tip/tip.vue.d.ts +2 -2
  41. package/dist/components/tree/tree.vue.d.ts +6 -6
  42. package/dist/components/tree-select/tree-select.vue.d.ts +7 -7
  43. package/package.json +13 -12
  44. package/src/components/code-editor/code-editor.vue +2 -0
  45. package/src/components/file-viewer/file-viewer.vue +250 -24
  46. package/src/components/file-viewer/previewers/docx-previewer.vue +4 -3
  47. package/src/components/file-viewer/previewers/image-previewer.vue +4 -5
  48. package/src/components/file-viewer/previewers/pdf-previewer.vue +4 -3
  49. package/src/components/file-viewer/style.scss +503 -161
@@ -20,15 +20,14 @@
20
20
  v-for="f in normalizedFiles"
21
21
  :key="f.id"
22
22
  :class="[cls.e('item'), bem.is('active', f.id === activeId)]"
23
+ :title="f.name"
23
24
  @click="activate(f.id)"
24
25
  >
25
- <span :class="[cls.e('badge'), cls.em('badge', f.kind)]">
26
- {{ label(f.kind) }}
27
- </span>
28
26
  <span :class="cls.e('item-meta')">
29
27
  <span :class="cls.e('item-name')" :title="f.name">{{ f.name }}</span>
30
28
  <span :class="cls.e('item-size')">{{ formatBytes(f.size) }}</span>
31
29
  </span>
30
+ <span :class="cls.e('item-kind')">{{ label(f.kind) }}</span>
32
31
  </li>
33
32
  <li v-if="!normalizedFiles.length" :class="cls.e('list-empty')">
34
33
  <u-empty text="暂无文件" :size="32" />
@@ -42,27 +41,93 @@
42
41
  <span v-if="activeFile" :class="[cls.e('badge'), cls.em('badge', activeFile.kind)]">
43
42
  {{ label(activeFile.kind) }}
44
43
  </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) }}
44
+ <span :class="cls.e('stage-copy')">
45
+ <span :class="cls.e('stage-name')" :title="activeFile?.name">
46
+ {{ activeFile?.name ?? '—' }}
47
+ </span>
48
+ <span v-if="activeFile" :class="cls.e('stage-sub')">
49
+ <span>{{ activeIndexLabel }}</span>
50
+ <span v-if="activeFile.size">{{ formatBytes(activeFile.size) }}</span>
51
+ </span>
50
52
  </span>
51
53
  </div>
52
54
  <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>
55
+ <span :class="cls.e('action-group')">
56
+ <button
57
+ :class="[cls.e('action'), cls.em('action', 'icon')]"
58
+ :disabled="!hasPrev"
59
+ type="button"
60
+ aria-label="上一个"
61
+ title="上一个"
62
+ @click="prev"
63
+ >
64
+ <u-icon :size="15">
65
+ <ArrowLeft />
66
+ </u-icon>
67
+ </button>
68
+ <button
69
+ :class="[cls.e('action'), cls.em('action', 'icon')]"
70
+ :disabled="!hasNext"
71
+ type="button"
72
+ aria-label="下一个"
73
+ title="下一个"
74
+ @click="next"
75
+ >
76
+ <u-icon :size="15">
77
+ <ArrowRight />
78
+ </u-icon>
79
+ </button>
80
+ </span>
81
+ <span v-if="isTransformable" :class="cls.e('action-group')">
82
+ <button
83
+ :class="[cls.e('action'), cls.em('action', 'icon')]"
84
+ :disabled="zoomOutDisabled"
85
+ type="button"
86
+ aria-label="缩小"
87
+ title="缩小"
88
+ @click="zoomOut"
89
+ >
90
+ <u-icon :size="15">
91
+ <ZoomOut />
92
+ </u-icon>
93
+ </button>
94
+ <span :class="cls.e('zoom-value')">{{ zoomPercent }}</span>
95
+ <button
96
+ :class="[cls.e('action'), cls.em('action', 'icon')]"
97
+ :disabled="zoomInDisabled"
98
+ type="button"
99
+ aria-label="放大"
100
+ title="放大"
101
+ @click="zoomIn"
102
+ >
103
+ <u-icon :size="15">
104
+ <ZoomIn />
105
+ </u-icon>
106
+ </button>
107
+ <button
108
+ :class="[cls.e('action'), cls.em('action', 'icon')]"
109
+ :disabled="isTransformReset"
110
+ type="button"
111
+ aria-label="重置视图"
112
+ title="重置视图"
113
+ @click="resetTransform"
114
+ >
115
+ <u-icon :size="15">
116
+ <Refresh />
117
+ </u-icon>
118
+ </button>
119
+ </span>
59
120
  <button
60
121
  v-if="downloadable && activeFile"
61
- :class="[cls.e('action'), cls.em('action', 'primary')]"
122
+ :class="[cls.e('action'), cls.em('action', 'primary'), cls.em('action', 'icon')]"
62
123
  type="button"
124
+ aria-label="下载"
125
+ title="下载"
63
126
  @click="download"
64
127
  >
65
- 下载
128
+ <u-icon :size="15">
129
+ <Download />
130
+ </u-icon>
66
131
  </button>
67
132
  <button
68
133
  v-if="isModal"
@@ -80,14 +145,30 @@
80
145
  </header>
81
146
  <div :class="cls.e('body')">
82
147
  <transition name="u-file-viewer-fade" mode="out-in">
83
- <component
84
- :is="PreviewerMap[activeFile.kind]"
148
+ <div
85
149
  v-if="activeFile"
86
150
  :key="activeFile.id"
87
- :file="activeFile"
88
- :max-rows="sheetMaxRows"
89
- @error="handleChildError"
90
- />
151
+ :class="[
152
+ cls.e('viewport'),
153
+ bem.is('transformable', isTransformable),
154
+ bem.is('pannable', canPan),
155
+ bem.is('dragging', isDragging)
156
+ ]"
157
+ @pointerdown="handleViewportPointerDown"
158
+ @pointermove="handleViewportPointerMove"
159
+ @pointerup="handleViewportPointerEnd"
160
+ @pointercancel="handleViewportPointerEnd"
161
+ @dblclick="handleViewportDblclick"
162
+ >
163
+ <component
164
+ :is="PreviewerMap[activeFile.kind]"
165
+ :file="activeFile"
166
+ :max-rows="sheetMaxRows"
167
+ :class="isTransformable ? cls.e('previewer') : undefined"
168
+ :style="previewerStyle"
169
+ @error="handleChildError"
170
+ />
171
+ </div>
91
172
  <div v-else :class="cls.e('empty')">
92
173
  <u-empty text="暂无可预览文件" />
93
174
  </div>
@@ -101,9 +182,26 @@
101
182
  </template>
102
183
 
103
184
  <script lang="ts" setup>
104
- import { Close } from '@veltra/icons/normal'
185
+ import {
186
+ ArrowLeft,
187
+ ArrowRight,
188
+ Close,
189
+ Download,
190
+ Refresh,
191
+ ZoomIn,
192
+ ZoomOut
193
+ } from '@veltra/icons/normal'
105
194
  import { bem, withUnit } from '@veltra/utils'
106
- import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, shallowRef, watch } from 'vue'
195
+ import {
196
+ computed,
197
+ defineAsyncComponent,
198
+ nextTick,
199
+ onBeforeUnmount,
200
+ ref,
201
+ shallowRef,
202
+ watch
203
+ } from 'vue'
204
+ import type { CSSProperties } from 'vue'
107
205
 
108
206
  import type {
109
207
  _FileViewerExposed,
@@ -136,10 +234,29 @@ const emit = defineEmits<FileViewerEmits>()
136
234
 
137
235
  const cls = bem('file-viewer')
138
236
 
237
+ const MIN_SCALE = 0.5
238
+ const MAX_SCALE = 3
239
+ const SCALE_STEP = 0.25
240
+ const TRANSFORMABLE_KINDS = new Set<FileViewerKind>(['image', 'pdf', 'docx'])
241
+
139
242
  const activeId = defineModel<string | undefined>('modelValue', { default: undefined })
140
243
  const openModel = defineModel<boolean | undefined>('open', { default: undefined })
141
244
 
142
245
  const rootRef = shallowRef<HTMLDivElement>()
246
+ const scale = ref(1)
247
+ const offsetX = ref(0)
248
+ const offsetY = ref(0)
249
+ const isDragging = ref(false)
250
+
251
+ let dragState:
252
+ | {
253
+ pointerId: number
254
+ startX: number
255
+ startY: number
256
+ originX: number
257
+ originY: number
258
+ }
259
+ | undefined
143
260
 
144
261
  const PreviewerMap: Record<FileViewerKind, ReturnType<typeof defineAsyncComponent>> = {
145
262
  image: defineAsyncComponent(() => import('./previewers/image-previewer.vue')),
@@ -185,6 +302,33 @@ const sidebarWidthCss = computed(() => {
185
302
  /** 是否启用模态模式:只要父组件显式传入 open(含 v-model:open),即进入模态 */
186
303
  const isModal = computed(() => openModel.value !== undefined)
187
304
 
305
+ const activeIndexLabel = computed(() =>
306
+ activeIndex.value >= 0 ? `${activeIndex.value + 1} / ${normalizedFiles.value.length}` : ''
307
+ )
308
+
309
+ const isTransformable = computed(
310
+ () => !!activeFile.value && TRANSFORMABLE_KINDS.has(activeFile.value.kind)
311
+ )
312
+
313
+ const canPan = computed(() => isTransformable.value && scale.value > 1)
314
+
315
+ const zoomPercent = computed(() => `${Math.round(scale.value * 100)}%`)
316
+
317
+ const zoomInDisabled = computed(() => !isTransformable.value || scale.value >= MAX_SCALE)
318
+
319
+ const zoomOutDisabled = computed(() => !isTransformable.value || scale.value <= MIN_SCALE)
320
+
321
+ const isTransformReset = computed(
322
+ () => scale.value === 1 && offsetX.value === 0 && offsetY.value === 0
323
+ )
324
+
325
+ const previewerStyle = computed<CSSProperties | undefined>(() => {
326
+ if (!isTransformable.value) return undefined
327
+ return {
328
+ transform: `translate3d(${offsetX.value}px, ${offsetY.value}px, 0) scale(${scale.value})`
329
+ }
330
+ })
331
+
188
332
  function label(kind: FileViewerKind): string {
189
333
  return FILE_VIEWER_KIND_LABEL[kind]
190
334
  }
@@ -210,6 +354,83 @@ function next() {
210
354
  if (target) activate(target.id)
211
355
  }
212
356
 
357
+ function normalizeScale(value: number): number {
358
+ const clamped = Math.min(MAX_SCALE, Math.max(MIN_SCALE, value))
359
+ return Math.round(clamped * 100) / 100
360
+ }
361
+
362
+ function setScale(value: number) {
363
+ scale.value = normalizeScale(value)
364
+ if (scale.value <= 1) {
365
+ offsetX.value = 0
366
+ offsetY.value = 0
367
+ }
368
+ }
369
+
370
+ function zoomIn() {
371
+ if (!isTransformable.value) return
372
+ setScale(scale.value + SCALE_STEP)
373
+ }
374
+
375
+ function zoomOut() {
376
+ if (!isTransformable.value) return
377
+ setScale(scale.value - SCALE_STEP)
378
+ }
379
+
380
+ function resetTransform() {
381
+ scale.value = 1
382
+ offsetX.value = 0
383
+ offsetY.value = 0
384
+ dragState = undefined
385
+ isDragging.value = false
386
+ }
387
+
388
+ function isInteractiveTarget(target: EventTarget | null): boolean {
389
+ if (!(target instanceof HTMLElement)) return false
390
+ return !!target.closest('button, a, input, textarea, select, video, [contenteditable="true"]')
391
+ }
392
+
393
+ function handleViewportPointerDown(e: PointerEvent) {
394
+ if (!canPan.value || e.button !== 0 || isInteractiveTarget(e.target)) return
395
+
396
+ dragState = {
397
+ pointerId: e.pointerId,
398
+ startX: e.clientX,
399
+ startY: e.clientY,
400
+ originX: offsetX.value,
401
+ originY: offsetY.value
402
+ }
403
+ isDragging.value = true
404
+ const el = e.currentTarget as HTMLElement
405
+ el.setPointerCapture(e.pointerId)
406
+ e.preventDefault()
407
+ }
408
+
409
+ function handleViewportPointerMove(e: PointerEvent) {
410
+ if (!dragState || dragState.pointerId !== e.pointerId) return
411
+ offsetX.value = dragState.originX + e.clientX - dragState.startX
412
+ offsetY.value = dragState.originY + e.clientY - dragState.startY
413
+ }
414
+
415
+ function handleViewportPointerEnd(e: PointerEvent) {
416
+ if (!dragState || dragState.pointerId !== e.pointerId) return
417
+ const el = e.currentTarget as HTMLElement
418
+ if (el.hasPointerCapture(e.pointerId)) {
419
+ el.releasePointerCapture(e.pointerId)
420
+ }
421
+ dragState = undefined
422
+ isDragging.value = false
423
+ }
424
+
425
+ function handleViewportDblclick() {
426
+ if (!isTransformable.value) return
427
+ if (scale.value === 1) {
428
+ setScale(2)
429
+ } else {
430
+ resetTransform()
431
+ }
432
+ }
433
+
213
434
  async function download() {
214
435
  if (!activeFile.value) return
215
436
  try {
@@ -257,6 +478,11 @@ watch(
257
478
  { immediate: true }
258
479
  )
259
480
 
481
+ watch(
482
+ () => activeFile.value?.id,
483
+ () => resetTransform()
484
+ )
485
+
260
486
  // 模态打开时:锁定 body 滚动 + 让容器获得焦点以便接收 ESC
261
487
  let previousBodyOverflow = ''
262
488
  let isBodyLocked = false
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <u-scroll tag="div" :class="cls.e('docx')">
2
+ <u-scroll tag="div" :class="cls.e('docx')" v-bind="attrs">
3
3
  <div ref="container" :class="cls.e('docx-body')" />
4
4
  <div v-if="loading" :class="cls.e('loading')">加载中…</div>
5
5
  </u-scroll>
@@ -7,18 +7,19 @@
7
7
 
8
8
  <script lang="ts" setup>
9
9
  import { bem } from '@veltra/utils'
10
- import { onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'
10
+ import { onBeforeUnmount, ref, useAttrs, useTemplateRef, watch } from 'vue'
11
11
 
12
12
  import type { FileViewerItem } from '../../../types/file-viewer'
13
13
  import { UScroll } from '../../scroll'
14
14
 
15
- defineOptions({ name: 'FileViewerDocxPreviewer' })
15
+ defineOptions({ name: 'FileViewerDocxPreviewer', inheritAttrs: false })
16
16
 
17
17
  const props = defineProps<{ file: FileViewerItem }>()
18
18
 
19
19
  const emit = defineEmits<{ (e: 'error', err: unknown): void }>()
20
20
 
21
21
  const cls = bem('file-viewer')
22
+ const attrs = useAttrs()
22
23
 
23
24
  const container = useTemplateRef<HTMLDivElement>('container')
24
25
  const loading = ref(true)
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="[cls.e('image'), bem.is('actual', actual)]">
2
+ <div :class="cls.e('image')" v-bind="attrs">
3
3
  <img
4
4
  v-if="url"
5
5
  :src="url"
@@ -7,7 +7,6 @@
7
7
  draggable="false"
8
8
  @load="loading = false"
9
9
  @error="onImgError"
10
- @dblclick="actual = !actual"
11
10
  />
12
11
  <div v-if="loading" :class="cls.e('loading')">加载中…</div>
13
12
  <div v-if="failed" :class="cls.e('loading')">图片加载失败</div>
@@ -16,22 +15,22 @@
16
15
 
17
16
  <script lang="ts" setup>
18
17
  import { bem } from '@veltra/utils'
19
- import { onBeforeUnmount, ref, shallowRef, watch } from 'vue'
18
+ import { onBeforeUnmount, ref, shallowRef, useAttrs, watch } from 'vue'
20
19
 
21
20
  import type { FileViewerItem } from '../../../types/file-viewer'
22
21
 
23
- defineOptions({ name: 'FileViewerImagePreviewer' })
22
+ defineOptions({ name: 'FileViewerImagePreviewer', inheritAttrs: false })
24
23
 
25
24
  const props = defineProps<{ file: FileViewerItem }>()
26
25
 
27
26
  const emit = defineEmits<{ (e: 'error', err: unknown): void }>()
28
27
 
29
28
  const cls = bem('file-viewer')
29
+ const attrs = useAttrs()
30
30
 
31
31
  const url = shallowRef<string>('')
32
32
  const loading = ref(true)
33
33
  const failed = ref(false)
34
- const actual = ref(false)
35
34
 
36
35
  let revoke: (() => void) | undefined
37
36
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :class="cls.e('pdf')">
2
+ <div :class="cls.e('pdf')" v-bind="attrs">
3
3
  <div v-if="!engine || isLoading" :class="cls.e('loading')">正在加载 PDF 引擎…</div>
4
4
  <EmbedPDF v-else-if="pdfUrl" :engine="engine" :plugins="plugins" v-slot="{ activeDocumentId }">
5
5
  <DocumentContent
@@ -36,17 +36,18 @@ import { RenderLayer, RenderPluginPackage } from '@embedpdf/plugin-render/vue'
36
36
  import { ScrollPluginPackage, Scroller } from '@embedpdf/plugin-scroll/vue'
37
37
  import { ViewportPluginPackage, Viewport } from '@embedpdf/plugin-viewport/vue'
38
38
  import { bem } from '@veltra/utils'
39
- import { computed, onBeforeUnmount, shallowRef, watch } from 'vue'
39
+ import { computed, onBeforeUnmount, shallowRef, useAttrs, watch } from 'vue'
40
40
 
41
41
  import type { FileViewerItem } from '../../../types/file-viewer'
42
42
 
43
- defineOptions({ name: 'FileViewerPdfPreviewer' })
43
+ defineOptions({ name: 'FileViewerPdfPreviewer', inheritAttrs: false })
44
44
 
45
45
  const props = defineProps<{ file: FileViewerItem }>()
46
46
 
47
47
  const emit = defineEmits<{ (e: 'error', err: unknown): void }>()
48
48
 
49
49
  const cls = bem('file-viewer')
50
+ const attrs = useAttrs()
50
51
 
51
52
  const { engine, isLoading, error } = usePdfiumEngine()
52
53