@veltra/desktop 1.1.12 → 1.1.14

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 (38) hide show
  1. package/dist/components/batch-edit/batch-edit.js +1 -1
  2. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  3. package/dist/components/batch-edit/style2.css +7 -1
  4. package/dist/components/cascade/cascade.vue.d.ts +1 -1
  5. package/dist/components/file-viewer/file-viewer.js +62 -19
  6. package/dist/components/file-viewer/file-viewer.js.map +1 -1
  7. package/dist/components/file-viewer/previewers/image-previewer.js +3 -2
  8. package/dist/components/file-viewer/previewers/image-previewer.js.map +1 -1
  9. package/dist/components/file-viewer/previewers/pdf-previewer.js +60 -17
  10. package/dist/components/file-viewer/previewers/pdf-previewer.js.map +1 -1
  11. package/dist/components/file-viewer/previewers/pdf-viewport-scroll.js +83 -0
  12. package/dist/components/file-viewer/previewers/pdf-viewport-scroll.js.map +1 -0
  13. package/dist/components/file-viewer/previewers/pdf-zoom-bridge.js +32 -0
  14. package/dist/components/file-viewer/previewers/pdf-zoom-bridge.js.map +1 -0
  15. package/dist/components/file-viewer/style2.css +14 -1
  16. package/dist/components/multi-select/multi-select.vue.d.ts +1 -1
  17. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +1 -1
  18. package/dist/components/scroll/scroll.js +94 -30
  19. package/dist/components/scroll/scroll.js.map +1 -1
  20. package/dist/components/scroll/style2.css +22 -0
  21. package/dist/components/tabs/tabs-horizontal.vue.d.ts +2 -2
  22. package/dist/components/tabs/tabs-vertical.vue.d.ts +2 -2
  23. package/dist/components/tabs/tabs.vue.d.ts +2 -2
  24. package/dist/components/tree/tree.vue.d.ts +1 -1
  25. package/dist/components/tree-select/tree-select.vue.d.ts +1 -1
  26. package/dist/types/scroll.d.ts +6 -1
  27. package/package.json +12 -11
  28. package/src/components/batch-edit/batch-edit.vue +1 -1
  29. package/src/components/batch-edit/style.scss +9 -1
  30. package/src/components/file-viewer/file-viewer.vue +73 -16
  31. package/src/components/file-viewer/previewers/image-previewer.vue +1 -0
  32. package/src/components/file-viewer/previewers/pdf-previewer.vue +67 -14
  33. package/src/components/file-viewer/previewers/pdf-viewport-scroll.vue +112 -0
  34. package/src/components/file-viewer/previewers/pdf-zoom-bridge.vue +33 -0
  35. package/src/components/file-viewer/style.scss +16 -1
  36. package/src/components/scroll/scroll.vue +124 -75
  37. package/src/components/scroll/style.scss +22 -0
  38. package/src/types/scroll.ts +7 -1
@@ -78,7 +78,7 @@
78
78
  </u-icon>
79
79
  </button>
80
80
  </span>
81
- <span v-if="isTransformable" :class="cls.e('action-group')">
81
+ <span v-if="isZoomable" :class="cls.e('action-group')">
82
82
  <button
83
83
  :class="[cls.e('action'), cls.em('action', 'icon')]"
84
84
  :disabled="zoomOutDisabled"
@@ -154,19 +154,22 @@
154
154
  bem.is('pannable', canPan),
155
155
  bem.is('dragging', isDragging)
156
156
  ]"
157
- @pointerdown="handleViewportPointerDown"
158
- @pointermove="handleViewportPointerMove"
159
- @pointerup="handleViewportPointerEnd"
160
- @pointercancel="handleViewportPointerEnd"
157
+ @pointerdown.capture="handleViewportPointerDown"
158
+ @pointermove.capture="handleViewportPointerMove"
159
+ @pointerup.capture="handleViewportPointerEnd"
160
+ @pointercancel.capture="handleViewportPointerEnd"
161
+ @wheel="handleViewportWheel"
161
162
  @dblclick="handleViewportDblclick"
162
163
  >
163
164
  <component
164
165
  :is="PreviewerMap[activeFile.kind]"
166
+ ref="previewerRef"
165
167
  :file="activeFile"
166
168
  :max-rows="sheetMaxRows"
167
169
  :class="isTransformable ? cls.e('previewer') : undefined"
168
170
  :style="previewerStyle"
169
171
  @error="handleChildError"
172
+ @zoom-change="handlePdfZoomChange"
170
173
  />
171
174
  </div>
172
175
  <div v-else :class="cls.e('empty')">
@@ -236,14 +239,24 @@ const cls = bem('file-viewer')
236
239
 
237
240
  const MIN_SCALE = 0.5
238
241
  const MAX_SCALE = 3
239
- const SCALE_STEP = 0.25
240
- const TRANSFORMABLE_KINDS = new Set<FileViewerKind>(['image', 'pdf', 'docx'])
242
+ /** 每次缩放固定增减 10% */
243
+ const SCALE_STEP = 0.1
244
+ /** 使用 CSS transform 缩放的类型(PDF 由 EmbedPDF zoom 插件处理) */
245
+ const TRANSFORMABLE_KINDS = new Set<FileViewerKind>(['image'])
246
+ /** 工具栏显示缩放控件的类型 */
247
+ const ZOOMABLE_KINDS = new Set<FileViewerKind>(['image', 'pdf'])
241
248
 
242
249
  const activeId = defineModel<string | undefined>('modelValue', { default: undefined })
243
250
  const openModel = defineModel<boolean | undefined>('open', { default: undefined })
244
251
 
245
252
  const rootRef = shallowRef<HTMLDivElement>()
253
+ const previewerRef = shallowRef<{
254
+ zoomIn?: () => void
255
+ zoomOut?: () => void
256
+ resetZoom?: () => void
257
+ }>()
246
258
  const scale = ref(1)
259
+ const pdfZoomLevel = ref(1)
247
260
  const offsetX = ref(0)
248
261
  const offsetY = ref(0)
249
262
  const isDragging = ref(false)
@@ -310,17 +323,26 @@ const isTransformable = computed(
310
323
  () => !!activeFile.value && TRANSFORMABLE_KINDS.has(activeFile.value.kind)
311
324
  )
312
325
 
326
+ const isZoomable = computed(() => !!activeFile.value && ZOOMABLE_KINDS.has(activeFile.value.kind))
327
+
328
+ const isPdfActive = computed(() => activeFile.value?.kind === 'pdf')
329
+
313
330
  const canPan = computed(() => isTransformable.value && scale.value > 1)
314
331
 
315
- const zoomPercent = computed(() => `${Math.round(scale.value * 100)}%`)
332
+ const displayZoomLevel = computed(() => (isPdfActive.value ? pdfZoomLevel.value : scale.value))
316
333
 
317
- const zoomInDisabled = computed(() => !isTransformable.value || scale.value >= MAX_SCALE)
334
+ const zoomPercent = computed(() => `${Math.round(displayZoomLevel.value * 100)}%`)
318
335
 
319
- const zoomOutDisabled = computed(() => !isTransformable.value || scale.value <= MIN_SCALE)
336
+ const zoomInDisabled = computed(() => !isZoomable.value || displayZoomLevel.value >= MAX_SCALE)
320
337
 
321
- const isTransformReset = computed(
322
- () => scale.value === 1 && offsetX.value === 0 && offsetY.value === 0
323
- )
338
+ const zoomOutDisabled = computed(() => !isZoomable.value || displayZoomLevel.value <= MIN_SCALE)
339
+
340
+ const isTransformReset = computed(() => {
341
+ if (isPdfActive.value) {
342
+ return Math.abs(pdfZoomLevel.value - 1) < 0.02
343
+ }
344
+ return scale.value === 1 && offsetX.value === 0 && offsetY.value === 0
345
+ })
324
346
 
325
347
  const previewerStyle = computed<CSSProperties | undefined>(() => {
326
348
  if (!isTransformable.value) return undefined
@@ -368,16 +390,29 @@ function setScale(value: number) {
368
390
  }
369
391
 
370
392
  function zoomIn() {
371
- if (!isTransformable.value) return
393
+ if (!isZoomable.value) return
394
+ if (isPdfActive.value) {
395
+ previewerRef.value?.zoomIn?.()
396
+ return
397
+ }
372
398
  setScale(scale.value + SCALE_STEP)
373
399
  }
374
400
 
375
401
  function zoomOut() {
376
- if (!isTransformable.value) return
402
+ if (!isZoomable.value) return
403
+ if (isPdfActive.value) {
404
+ previewerRef.value?.zoomOut?.()
405
+ return
406
+ }
377
407
  setScale(scale.value - SCALE_STEP)
378
408
  }
379
409
 
380
410
  function resetTransform() {
411
+ if (isPdfActive.value) {
412
+ previewerRef.value?.resetZoom?.()
413
+ pdfZoomLevel.value = 1
414
+ return
415
+ }
381
416
  scale.value = 1
382
417
  offsetX.value = 0
383
418
  offsetY.value = 0
@@ -385,6 +420,23 @@ function resetTransform() {
385
420
  isDragging.value = false
386
421
  }
387
422
 
423
+ function handlePdfZoomChange(level: number) {
424
+ pdfZoomLevel.value = level
425
+ }
426
+
427
+ function isZoomWheel(e: WheelEvent): boolean {
428
+ return e.ctrlKey || e.metaKey
429
+ }
430
+
431
+ function handleViewportWheel(e: WheelEvent) {
432
+ if (!activeFile.value || activeFile.value.kind !== 'image') return
433
+ if (!isZoomWheel(e)) return
434
+ if (isInteractiveTarget(e.target)) return
435
+ e.preventDefault()
436
+ const delta = e.deltaY > 0 ? -SCALE_STEP : SCALE_STEP
437
+ setScale(scale.value + delta)
438
+ }
439
+
388
440
  function isInteractiveTarget(target: EventTarget | null): boolean {
389
441
  if (!(target instanceof HTMLElement)) return false
390
442
  return !!target.closest('button, a, input, textarea, select, video, [contenteditable="true"]')
@@ -423,6 +475,7 @@ function handleViewportPointerEnd(e: PointerEvent) {
423
475
  }
424
476
 
425
477
  function handleViewportDblclick() {
478
+ if (isPdfActive.value) return
426
479
  if (!isTransformable.value) return
427
480
  if (scale.value === 1) {
428
481
  setScale(2)
@@ -480,7 +533,11 @@ watch(
480
533
 
481
534
  watch(
482
535
  () => activeFile.value?.id,
483
- () => resetTransform()
536
+ async () => {
537
+ pdfZoomLevel.value = 1
538
+ await nextTick()
539
+ resetTransform()
540
+ }
484
541
  )
485
542
 
486
543
  // 模态打开时:锁定 body 滚动 + 让容器获得焦点以便接收 ESC
@@ -5,6 +5,7 @@
5
5
  :src="url"
6
6
  :alt="file.name"
7
7
  draggable="false"
8
+ @dragstart.prevent
8
9
  @load="loading = false"
9
10
  @error="onImgError"
10
11
  />
@@ -2,22 +2,31 @@
2
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
+ <PdfZoomBridge
6
+ v-if="activeDocumentId"
7
+ :document-id="activeDocumentId"
8
+ @zoom-change="onZoomChange"
9
+ />
5
10
  <DocumentContent
6
11
  v-if="activeDocumentId"
7
12
  :document-id="activeDocumentId"
8
13
  v-slot="{ isLoaded }"
9
14
  >
10
15
  <Viewport v-if="isLoaded" :document-id="activeDocumentId" :class="cls.e('pdf-viewport')">
11
- <Scroller :document-id="activeDocumentId">
12
- <template #default="{ page }">
13
- <div
14
- :class="cls.e('pdf-page')"
15
- :style="{ width: page.width + 'px', height: page.height + 'px' }"
16
- >
17
- <RenderLayer :document-id="activeDocumentId" :page-index="page.pageIndex" />
18
- </div>
19
- </template>
20
- </Scroller>
16
+ <PdfViewportScroll>
17
+ <ZoomGestureWrapper :document-id="activeDocumentId" :enable-wheel="true">
18
+ <Scroller :document-id="activeDocumentId">
19
+ <template #default="{ page }">
20
+ <div
21
+ :class="cls.e('pdf-page')"
22
+ :style="{ width: page.width + 'px', height: page.height + 'px' }"
23
+ >
24
+ <RenderLayer :document-id="activeDocumentId" :page-index="page.pageIndex" />
25
+ </div>
26
+ </template>
27
+ </Scroller>
28
+ </ZoomGestureWrapper>
29
+ </PdfViewportScroll>
21
30
  </Viewport>
22
31
  </DocumentContent>
23
32
  </EmbedPDF>
@@ -35,17 +44,28 @@ import {
35
44
  import { RenderLayer, RenderPluginPackage } from '@embedpdf/plugin-render/vue'
36
45
  import { ScrollPluginPackage, Scroller } from '@embedpdf/plugin-scroll/vue'
37
46
  import { ViewportPluginPackage, Viewport } from '@embedpdf/plugin-viewport/vue'
47
+ import {
48
+ ZoomGestureWrapper,
49
+ ZoomMode,
50
+ ZoomPluginPackage,
51
+ type ZoomScope
52
+ } from '@embedpdf/plugin-zoom/vue'
38
53
  import { bem } from '@veltra/utils'
39
54
  import { computed, onBeforeUnmount, shallowRef, useAttrs, watch } from 'vue'
40
55
 
41
56
  import type { FileViewerItem } from '../../../types/file-viewer'
42
57
  import { toBlobUrl } from '../helper'
58
+ import PdfViewportScroll from './pdf-viewport-scroll.vue'
59
+ import PdfZoomBridge from './pdf-zoom-bridge.vue'
43
60
 
44
61
  defineOptions({ name: 'FileViewerPdfPreviewer', inheritAttrs: false })
45
62
 
46
63
  const props = defineProps<{ file: FileViewerItem }>()
47
64
 
48
- const emit = defineEmits<{ (e: 'error', err: unknown): void }>()
65
+ const emit = defineEmits<{
66
+ (e: 'error', err: unknown): void
67
+ (e: 'zoom-change', level: number): void
68
+ }>()
49
69
 
50
70
  const cls = bem('file-viewer')
51
71
  const attrs = useAttrs()
@@ -55,6 +75,9 @@ const { engine, isLoading, error } = usePdfiumEngine()
55
75
  const pdfUrl = shallowRef<string>('')
56
76
  let revoke: (() => void) | undefined
57
77
 
78
+ /** 当前文档的 zoom API(由 PdfZoomBridge 注入) */
79
+ let zoomScope: ZoomScope | null = null
80
+
58
81
  function load() {
59
82
  revoke?.()
60
83
  const r = toBlobUrl(props.file.src, props.file.mime ?? 'application/pdf')
@@ -68,12 +91,35 @@ const plugins = computed(() => {
68
91
  createPluginRegistration(DocumentManagerPluginPackage, {
69
92
  initialDocuments: [{ url: pdfUrl.value }]
70
93
  }),
71
- createPluginRegistration(ViewportPluginPackage),
72
- createPluginRegistration(ScrollPluginPackage),
73
- createPluginRegistration(RenderPluginPackage)
94
+ createPluginRegistration(ViewportPluginPackage, { viewportGap: 0 }),
95
+ createPluginRegistration(ScrollPluginPackage, { defaultBufferSize: 2 }),
96
+ createPluginRegistration(RenderPluginPackage),
97
+ createPluginRegistration(ZoomPluginPackage, {
98
+ defaultZoomLevel: ZoomMode.FitPage,
99
+ minZoom: 0.5,
100
+ maxZoom: 3,
101
+ zoomStep: 0.1
102
+ })
74
103
  ]
75
104
  })
76
105
 
106
+ function onZoomChange(payload: { level: number; scope: ZoomScope | null }) {
107
+ zoomScope = payload.scope
108
+ emit('zoom-change', payload.level)
109
+ }
110
+
111
+ function zoomIn() {
112
+ zoomScope?.zoomIn()
113
+ }
114
+
115
+ function zoomOut() {
116
+ zoomScope?.zoomOut()
117
+ }
118
+
119
+ function resetZoom() {
120
+ zoomScope?.requestZoom(ZoomMode.FitPage)
121
+ }
122
+
77
123
  watch(
78
124
  () => props.file,
79
125
  () => load(),
@@ -87,5 +133,12 @@ watch(error, (err) => {
87
133
  onBeforeUnmount(() => {
88
134
  revoke?.()
89
135
  pdfUrl.value = ''
136
+ zoomScope = null
137
+ })
138
+
139
+ defineExpose({
140
+ zoomIn,
141
+ zoomOut,
142
+ resetZoom
90
143
  })
91
144
  </script>
@@ -0,0 +1,112 @@
1
+ <template>
2
+ <!-- EmbedPDF Viewport 自带滚动;u-scroll 仅同步自定义滚动条 -->
3
+ <u-scroll :target="viewportEl">
4
+ <slot />
5
+ </u-scroll>
6
+ </template>
7
+
8
+ <script lang="ts" setup>
9
+ import { computed, inject, onBeforeUnmount, type Ref, watch } from 'vue'
10
+
11
+ import { UScroll } from '../../scroll'
12
+
13
+ defineOptions({ name: 'FileViewerPdfViewportScroll' })
14
+
15
+ /** EmbedPDF Viewport 向子树注入的滚动容器 ref */
16
+ const VIEWPORT_ELEMENT_KEY = 'viewport-element'
17
+
18
+ const viewportRef = inject<Ref<HTMLDivElement | null>>(VIEWPORT_ELEMENT_KEY)
19
+ const viewportEl = computed(() => viewportRef?.value ?? null)
20
+
21
+ let panState:
22
+ | {
23
+ pointerId: number
24
+ startX: number
25
+ startY: number
26
+ scrollLeft: number
27
+ scrollTop: number
28
+ }
29
+ | undefined
30
+
31
+ function isInteractiveTarget(target: EventTarget | null): boolean {
32
+ if (!(target instanceof HTMLElement)) return false
33
+ return !!target.closest(
34
+ 'button, a, input, textarea, select, video, [contenteditable="true"], .u-scroll__bar-x, .u-scroll__bar-y'
35
+ )
36
+ }
37
+
38
+ function handlePointerDown(e: PointerEvent) {
39
+ if (e.button !== 0 || isInteractiveTarget(e.target)) return
40
+
41
+ const el = viewportEl.value
42
+ if (!el) return
43
+
44
+ panState = {
45
+ pointerId: e.pointerId,
46
+ startX: e.clientX,
47
+ startY: e.clientY,
48
+ scrollLeft: el.scrollLeft,
49
+ scrollTop: el.scrollTop
50
+ }
51
+ el.classList.add('is-panning')
52
+ el.setPointerCapture(e.pointerId)
53
+ e.preventDefault()
54
+ }
55
+
56
+ function handlePointerMove(e: PointerEvent) {
57
+ if (!panState || panState.pointerId !== e.pointerId) return
58
+
59
+ const el = viewportEl.value
60
+ if (!el) return
61
+
62
+ el.scrollLeft = panState.scrollLeft - (e.clientX - panState.startX)
63
+ el.scrollTop = panState.scrollTop - (e.clientY - panState.startY)
64
+ }
65
+
66
+ function endPan(e: PointerEvent) {
67
+ if (!panState || panState.pointerId !== e.pointerId) return
68
+
69
+ const el = viewportEl.value
70
+ if (el?.hasPointerCapture(e.pointerId)) {
71
+ el.releasePointerCapture(e.pointerId)
72
+ }
73
+ el?.classList.remove('is-panning')
74
+ panState = undefined
75
+ }
76
+
77
+ let detachPan: (() => void) | undefined
78
+
79
+ watch(
80
+ viewportEl,
81
+ (el, _, onCleanup) => {
82
+ detachPan?.()
83
+ detachPan = undefined
84
+ if (!el) return
85
+
86
+ el.addEventListener('pointerdown', handlePointerDown, { capture: true })
87
+ el.addEventListener('pointermove', handlePointerMove, { capture: true })
88
+ el.addEventListener('pointerup', endPan, { capture: true })
89
+ el.addEventListener('pointercancel', endPan, { capture: true })
90
+
91
+ detachPan = () => {
92
+ el.removeEventListener('pointerdown', handlePointerDown, { capture: true })
93
+ el.removeEventListener('pointermove', handlePointerMove, { capture: true })
94
+ el.removeEventListener('pointerup', endPan, { capture: true })
95
+ el.removeEventListener('pointercancel', endPan, { capture: true })
96
+ el.classList.remove('is-panning')
97
+ panState = undefined
98
+ }
99
+
100
+ onCleanup(() => {
101
+ detachPan?.()
102
+ detachPan = undefined
103
+ })
104
+ },
105
+ { immediate: true }
106
+ )
107
+
108
+ onBeforeUnmount(() => {
109
+ detachPan?.()
110
+ detachPan = undefined
111
+ })
112
+ </script>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <span hidden aria-hidden="true" />
3
+ </template>
4
+
5
+ <script lang="ts" setup>
6
+ import { useZoom, ZoomMode, type ZoomScope } from '@embedpdf/plugin-zoom/vue'
7
+ import { watch } from 'vue'
8
+
9
+ defineOptions({ name: 'FileViewerPdfZoomBridge' })
10
+
11
+ const props = defineProps<{ documentId: string }>()
12
+
13
+ const emit = defineEmits<{
14
+ (e: 'zoom-change', payload: { level: number; scope: ZoomScope | null }): void
15
+ }>()
16
+
17
+ const { provides: zoom, state } = useZoom(() => props.documentId)
18
+
19
+ watch(
20
+ [zoom, () => state.value.currentZoomLevel],
21
+ () => {
22
+ emit('zoom-change', {
23
+ level: state.value.currentZoomLevel,
24
+ scope: zoom.value
25
+ })
26
+ },
27
+ { immediate: true }
28
+ )
29
+
30
+ defineExpose({
31
+ resetZoom: () => zoom.value?.requestZoom(ZoomMode.FitPage)
32
+ })
33
+ </script>
@@ -547,7 +547,6 @@
547
547
 
548
548
  &__viewport.is-dragging &__previewer {
549
549
  transition: none;
550
- pointer-events: none;
551
550
  }
552
551
 
553
552
  @include m.e(empty) {
@@ -638,9 +637,25 @@
638
637
  }
639
638
 
640
639
  @include m.e(pdf-viewport) {
640
+ position: relative;
641
641
  width: 100%;
642
642
  height: 100%;
643
643
  background: transparent;
644
+ cursor: grab;
645
+
646
+ &.is-panning {
647
+ cursor: grabbing;
648
+ user-select: none;
649
+ }
650
+
651
+ // 隐藏原生滚动条,由 PdfViewportScroll 通过 u-scroll target 挂载自定义滚动条
652
+ scrollbar-width: none;
653
+ scrollbar-color: transparent transparent;
654
+
655
+ &::-webkit-scrollbar {
656
+ width: 0;
657
+ height: 0;
658
+ }
644
659
  }
645
660
 
646
661
  @include m.e(pdf-page) {