@veltra/desktop 1.7.12 → 1.7.15
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.
- package/dist/components/batch-edit/batch-edit-form.js +9 -3
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit-list.js +2 -1
- package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js +57 -30
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/batch-edit/helper.d.ts +1 -2
- package/dist/components/batch-edit/style.d.ts +1 -0
- package/dist/components/batch-edit/style.js +1 -0
- package/dist/components/batch-edit/style2.css +14 -0
- package/dist/components/batch-edit/use-dialog-effect.js +20 -0
- package/dist/components/batch-edit/use-dialog-effect.js.map +1 -0
- package/dist/components/batch-edit/use-edit-state.js +22 -3
- package/dist/components/batch-edit/use-edit-state.js.map +1 -1
- package/dist/components/batch-edit/use-handlers.js +5 -0
- package/dist/components/batch-edit/use-handlers.js.map +1 -1
- package/dist/components/batch-edit/use-shortcut-key.js +5 -2
- package/dist/components/batch-edit/use-shortcut-key.js.map +1 -1
- package/dist/components/condition-editor/core/evaluator.d.ts +4 -5
- package/dist/components/contextmenu/api.d.ts +1 -2
- package/dist/components/contextmenu/di.d.ts +2 -3
- package/dist/components/file-viewer/file-viewer.js +21 -14
- package/dist/components/file-viewer/file-viewer.js.map +1 -1
- package/dist/components/file-viewer/helper.js +2 -0
- package/dist/components/file-viewer/helper.js.map +1 -1
- package/dist/components/file-viewer/previewers/ofd-previewer.js +196 -0
- package/dist/components/file-viewer/previewers/ofd-previewer.js.map +1 -0
- package/dist/components/file-viewer/style2.css +27 -0
- package/dist/components/loading/directive.d.ts +1 -2
- package/dist/components/message/message.d.ts +1 -2
- package/dist/components/message-confirm/message-confirm.d.ts +1 -2
- package/dist/components/notification/notification.d.ts +1 -2
- package/dist/components/table/node/col.js +19 -0
- package/dist/components/table/node/col.js.map +1 -1
- package/dist/components/table/table-cell.js +2 -0
- package/dist/components/table/table-cell.js.map +1 -1
- package/dist/components/table/table-foot.js +2 -0
- package/dist/components/table/table-foot.js.map +1 -1
- package/dist/components/table/table-head.js +1 -0
- package/dist/components/table/table-head.js.map +1 -1
- package/dist/components/table/table-row.js +2 -0
- package/dist/components/table/table-row.js.map +1 -1
- package/dist/components/table/use-columns.d.ts +1 -2
- package/dist/components/table-editor/style2.css +23 -3
- package/dist/components/table-editor/table-editor.js +213 -63
- package/dist/components/table-editor/table-editor.js.map +1 -1
- package/dist/components/tip/style2.css +63 -0
- package/dist/components/tip/tip.js +6 -1
- package/dist/components/tip/tip.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/install.d.ts +3 -3
- package/dist/packages/ofd-core/dist/color.js +69 -0
- package/dist/packages/ofd-core/dist/color.js.map +1 -0
- package/dist/packages/ofd-core/dist/ctm.js +55 -0
- package/dist/packages/ofd-core/dist/ctm.js.map +1 -0
- package/dist/packages/ofd-core/dist/error.js +14 -0
- package/dist/packages/ofd-core/dist/error.js.map +1 -0
- package/dist/packages/ofd-core/dist/font.js +384 -0
- package/dist/packages/ofd-core/dist/font.js.map +1 -0
- package/dist/packages/ofd-core/dist/index.js +6 -0
- package/dist/packages/ofd-core/dist/number.js +13 -0
- package/dist/packages/ofd-core/dist/number.js.map +1 -0
- package/dist/packages/ofd-core/dist/page-objects.js +181 -0
- package/dist/packages/ofd-core/dist/page-objects.js.map +1 -0
- package/dist/packages/ofd-core/dist/parse.js +65 -0
- package/dist/packages/ofd-core/dist/parse.js.map +1 -0
- package/dist/packages/ofd-core/dist/render.js +361 -0
- package/dist/packages/ofd-core/dist/render.js.map +1 -0
- package/dist/packages/ofd-core/dist/xml.js +199 -0
- package/dist/packages/ofd-core/dist/xml.js.map +1 -0
- package/dist/packages/ofd-core/dist/zip.js +93 -0
- package/dist/packages/ofd-core/dist/zip.js.map +1 -0
- package/dist/style.d.ts +1 -1
- package/dist/style.js +1 -1
- package/dist/types/action.d.ts +7 -8
- package/dist/types/animation.d.ts +4 -5
- package/dist/types/auto-complete.d.ts +4 -5
- package/dist/types/badge.d.ts +4 -5
- package/dist/types/batch-edit.d.ts +15 -10
- package/dist/types/breadcrumb.d.ts +6 -7
- package/dist/types/button.d.ts +5 -6
- package/dist/types/calendar.d.ts +7 -8
- package/dist/types/card.d.ts +7 -8
- package/dist/types/cascade.d.ts +6 -7
- package/dist/types/check-tag.d.ts +4 -5
- package/dist/types/checkbox-group.d.ts +3 -4
- package/dist/types/checkbox.d.ts +5 -6
- package/dist/types/code-editor.d.ts +5 -6
- package/dist/types/collapse.d.ts +6 -7
- package/dist/types/condition-editor.d.ts +11 -12
- package/dist/types/contextmenu.d.ts +5 -6
- package/dist/types/css-transition.d.ts +4 -5
- package/dist/types/date-panel.d.ts +3 -4
- package/dist/types/date-picker.d.ts +5 -6
- package/dist/types/date-range-picker.d.ts +6 -7
- package/dist/types/dialog.d.ts +5 -6
- package/dist/types/drawer.d.ts +6 -7
- package/dist/types/dropdown.d.ts +4 -5
- package/dist/types/dual-nav.d.ts +6 -7
- package/dist/types/empty.d.ts +4 -5
- package/dist/types/expression-editor.d.ts +6 -7
- package/dist/types/file-picker.d.ts +4 -5
- package/dist/types/file-viewer.d.ts +6 -7
- package/dist/types/float-button.d.ts +5 -6
- package/dist/types/form-item.d.ts +3 -4
- package/dist/types/form.d.ts +4 -5
- package/dist/types/gantt-chart.d.ts +4 -5
- package/dist/types/grid-input.d.ts +4 -5
- package/dist/types/grid.d.ts +7 -8
- package/dist/types/group-input.d.ts +4 -5
- package/dist/types/group-nav.d.ts +3 -4
- package/dist/types/icon.d.ts +3 -4
- package/dist/types/image-cropper.d.ts +9 -10
- package/dist/types/index.d.ts +4 -4
- package/dist/types/input.d.ts +4 -5
- package/dist/types/layout.d.ts +4 -5
- package/dist/types/list.d.ts +4 -5
- package/dist/types/loading.d.ts +5 -6
- package/dist/types/message-confirm.d.ts +8 -9
- package/dist/types/message.d.ts +8 -9
- package/dist/types/multi-auto-complete.d.ts +4 -5
- package/dist/types/multi-select.d.ts +4 -5
- package/dist/types/multi-tree-select.d.ts +4 -5
- package/dist/types/nav.d.ts +5 -6
- package/dist/types/node-render.d.ts +2 -3
- package/dist/types/notification.d.ts +8 -9
- package/dist/types/number-input.d.ts +3 -4
- package/dist/types/number-range-input.d.ts +4 -5
- package/dist/types/number.d.ts +3 -4
- package/dist/types/paginator.d.ts +4 -5
- package/dist/types/palette.d.ts +7 -8
- package/dist/types/password-input.d.ts +4 -5
- package/dist/types/pop-confirm.d.ts +4 -5
- package/dist/types/pop.d.ts +4 -5
- package/dist/types/progress-nodes.d.ts +4 -5
- package/dist/types/progress.d.ts +4 -5
- package/dist/types/quick-batch-edit.d.ts +4 -5
- package/dist/types/radio-group.d.ts +4 -5
- package/dist/types/radio.d.ts +4 -5
- package/dist/types/rich-text-editor.d.ts +6 -7
- package/dist/types/scroll.d.ts +5 -6
- package/dist/types/segment.d.ts +5 -6
- package/dist/types/select.d.ts +4 -5
- package/dist/types/slider.d.ts +4 -5
- package/dist/types/steps.d.ts +5 -6
- package/dist/types/switch.d.ts +4 -5
- package/dist/types/table-editor.d.ts +30 -10
- package/dist/types/table.d.ts +23 -13
- package/dist/types/tabs.d.ts +13 -14
- package/dist/types/tag.d.ts +3 -4
- package/dist/types/text-editor.d.ts +5 -6
- package/dist/types/text.d.ts +4 -5
- package/dist/types/textarea.d.ts +4 -5
- package/dist/types/theme.d.ts +3 -4
- package/dist/types/tip.d.ts +11 -8
- package/dist/types/tree-select.d.ts +4 -5
- package/dist/types/tree.d.ts +6 -6
- package/dist/types/watermark.d.ts +4 -5
- package/package.json +8 -7
- package/src/components/batch-edit/__test__/batch-edit.test.ts +191 -4
- package/src/components/batch-edit/batch-edit-form.vue +17 -3
- package/src/components/batch-edit/batch-edit-list.vue +2 -1
- package/src/components/batch-edit/batch-edit.vue +22 -4
- package/src/components/batch-edit/style.scss +20 -0
- package/src/components/batch-edit/style.ts +1 -0
- package/src/components/batch-edit/use-dialog-effect.ts +9 -3
- package/src/components/batch-edit/use-edit-state.ts +32 -4
- package/src/components/batch-edit/use-handlers.ts +10 -0
- package/src/components/batch-edit/use-shortcut-key.ts +6 -2
- package/src/components/file-viewer/__test__/helper.test.ts +13 -0
- package/src/components/file-viewer/file-viewer.vue +23 -16
- package/src/components/file-viewer/helper.ts +2 -0
- package/src/components/file-viewer/previewers/ofd-previewer.vue +216 -0
- package/src/components/file-viewer/style.scss +33 -0
- package/src/components/table/__test__/column-style.test.ts +67 -0
- package/src/components/table/node/col.ts +30 -1
- package/src/components/table/table-cell.vue +7 -2
- package/src/components/table/table-foot.vue +2 -0
- package/src/components/table/table-head.tsx +1 -0
- package/src/components/table/table-row.tsx +2 -0
- package/src/components/table-editor/__test__/table-editor.test.ts +471 -0
- package/src/components/table-editor/style.scss +42 -4
- package/src/components/table-editor/table-editor.vue +265 -38
- package/src/components/tip/style.scss +15 -0
- package/src/components/tip/tip.vue +1 -1
- package/src/types/batch-edit.ts +6 -0
- package/src/types/file-viewer.ts +1 -1
- package/src/types/table-editor.ts +29 -7
- package/src/types/table.ts +12 -0
- package/src/types/tip.ts +6 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { FILE_VIEWER_KIND_LABEL, inferKind } from '../helper'
|
|
4
|
+
|
|
5
|
+
describe('file-viewer helper', () => {
|
|
6
|
+
it('.ofd 按 OFD 类别识别,不落入 text 兜底', () => {
|
|
7
|
+
expect(inferKind('invoice.ofd')).toBe('ofd')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('OFD 类别的展示标签为 OFD', () => {
|
|
11
|
+
expect(FILE_VIEWER_KIND_LABEL.ofd).toBe('OFD')
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
:class="isTransformable ? cls.e('previewer') : undefined"
|
|
170
170
|
:style="previewerStyle"
|
|
171
171
|
@error="handleChildError"
|
|
172
|
-
@zoom-change="
|
|
172
|
+
@zoom-change="handlePreviewerZoomChange"
|
|
173
173
|
/>
|
|
174
174
|
</div>
|
|
175
175
|
<div v-else :class="cls.e('empty')">
|
|
@@ -238,10 +238,12 @@ const MIN_SCALE = 0.5
|
|
|
238
238
|
const MAX_SCALE = 3
|
|
239
239
|
/** 每次缩放固定增减 10% */
|
|
240
240
|
const SCALE_STEP = 0.1
|
|
241
|
-
/** 使用 CSS transform 缩放的类型(PDF
|
|
241
|
+
/** 使用 CSS transform 缩放的类型(PDF / OFD 由各自 previewer 内部缩放) */
|
|
242
242
|
const TRANSFORMABLE_KINDS = new Set<FileViewerKind>(['image'])
|
|
243
243
|
/** 工具栏显示缩放控件的类型 */
|
|
244
|
-
const ZOOMABLE_KINDS = new Set<FileViewerKind>(['image', 'pdf'])
|
|
244
|
+
const ZOOMABLE_KINDS = new Set<FileViewerKind>(['image', 'pdf', 'ofd'])
|
|
245
|
+
/** 缩放由 previewer 内部实现、组件只透传命令并回显级别的类型 */
|
|
246
|
+
const DELEGATED_ZOOM_KINDS = new Set<FileViewerKind>(['pdf', 'ofd'])
|
|
245
247
|
|
|
246
248
|
const activeId = defineModel<string | undefined>('modelValue', { default: undefined })
|
|
247
249
|
const openModel = defineModel<boolean | undefined>('open', { default: undefined })
|
|
@@ -253,7 +255,7 @@ const previewerRef = shallowRef<{
|
|
|
253
255
|
resetZoom?: () => void
|
|
254
256
|
}>()
|
|
255
257
|
const scale = ref(1)
|
|
256
|
-
const
|
|
258
|
+
const previewerZoomLevel = ref(1)
|
|
257
259
|
const offsetX = ref(0)
|
|
258
260
|
const offsetY = ref(0)
|
|
259
261
|
const isDragging = ref(false)
|
|
@@ -268,6 +270,7 @@ const PreviewerMap: Record<FileViewerKind, ReturnType<typeof defineAsyncComponen
|
|
|
268
270
|
pdf: defineAsyncComponent(() => import('./previewers/pdf-previewer.vue')),
|
|
269
271
|
sheet: defineAsyncComponent(() => import('./previewers/sheet-previewer.vue')),
|
|
270
272
|
docx: defineAsyncComponent(() => import('./previewers/docx-previewer.vue')),
|
|
273
|
+
ofd: defineAsyncComponent(() => import('./previewers/ofd-previewer.vue')),
|
|
271
274
|
text: defineAsyncComponent(() => import('./previewers/text-previewer.vue'))
|
|
272
275
|
}
|
|
273
276
|
|
|
@@ -312,11 +315,15 @@ const isTransformable = computed(
|
|
|
312
315
|
|
|
313
316
|
const isZoomable = computed(() => !!activeFile.value && ZOOMABLE_KINDS.has(activeFile.value.kind))
|
|
314
317
|
|
|
315
|
-
const
|
|
318
|
+
const isDelegatedZoom = computed(
|
|
319
|
+
() => !!activeFile.value && DELEGATED_ZOOM_KINDS.has(activeFile.value.kind)
|
|
320
|
+
)
|
|
316
321
|
|
|
317
322
|
const canPan = computed(() => isTransformable.value && scale.value > 1)
|
|
318
323
|
|
|
319
|
-
const displayZoomLevel = computed(() =>
|
|
324
|
+
const displayZoomLevel = computed(() =>
|
|
325
|
+
isDelegatedZoom.value ? previewerZoomLevel.value : scale.value
|
|
326
|
+
)
|
|
320
327
|
|
|
321
328
|
const zoomPercent = computed(() => `${Math.round(displayZoomLevel.value * 100)}%`)
|
|
322
329
|
|
|
@@ -325,8 +332,8 @@ const zoomInDisabled = computed(() => !isZoomable.value || displayZoomLevel.valu
|
|
|
325
332
|
const zoomOutDisabled = computed(() => !isZoomable.value || displayZoomLevel.value <= MIN_SCALE)
|
|
326
333
|
|
|
327
334
|
const isTransformReset = computed(() => {
|
|
328
|
-
if (
|
|
329
|
-
return Math.abs(
|
|
335
|
+
if (isDelegatedZoom.value) {
|
|
336
|
+
return Math.abs(previewerZoomLevel.value - 1) < 0.02
|
|
330
337
|
}
|
|
331
338
|
return scale.value === 1 && offsetX.value === 0 && offsetY.value === 0
|
|
332
339
|
})
|
|
@@ -378,7 +385,7 @@ function setScale(value: number) {
|
|
|
378
385
|
|
|
379
386
|
function zoomIn() {
|
|
380
387
|
if (!isZoomable.value) return
|
|
381
|
-
if (
|
|
388
|
+
if (isDelegatedZoom.value) {
|
|
382
389
|
previewerRef.value?.zoomIn?.()
|
|
383
390
|
return
|
|
384
391
|
}
|
|
@@ -387,7 +394,7 @@ function zoomIn() {
|
|
|
387
394
|
|
|
388
395
|
function zoomOut() {
|
|
389
396
|
if (!isZoomable.value) return
|
|
390
|
-
if (
|
|
397
|
+
if (isDelegatedZoom.value) {
|
|
391
398
|
previewerRef.value?.zoomOut?.()
|
|
392
399
|
return
|
|
393
400
|
}
|
|
@@ -395,9 +402,9 @@ function zoomOut() {
|
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
function resetTransform() {
|
|
398
|
-
if (
|
|
405
|
+
if (isDelegatedZoom.value) {
|
|
399
406
|
previewerRef.value?.resetZoom?.()
|
|
400
|
-
|
|
407
|
+
previewerZoomLevel.value = 1
|
|
401
408
|
return
|
|
402
409
|
}
|
|
403
410
|
scale.value = 1
|
|
@@ -407,8 +414,8 @@ function resetTransform() {
|
|
|
407
414
|
isDragging.value = false
|
|
408
415
|
}
|
|
409
416
|
|
|
410
|
-
function
|
|
411
|
-
|
|
417
|
+
function handlePreviewerZoomChange(level: number) {
|
|
418
|
+
previewerZoomLevel.value = level
|
|
412
419
|
}
|
|
413
420
|
|
|
414
421
|
function isZoomWheel(e: WheelEvent): boolean {
|
|
@@ -462,7 +469,7 @@ function handleViewportPointerEnd(e: PointerEvent) {
|
|
|
462
469
|
}
|
|
463
470
|
|
|
464
471
|
function handleViewportDblclick() {
|
|
465
|
-
if (
|
|
472
|
+
if (isDelegatedZoom.value) return
|
|
466
473
|
if (!isTransformable.value) return
|
|
467
474
|
if (scale.value === 1) {
|
|
468
475
|
setScale(2)
|
|
@@ -521,7 +528,7 @@ watch(
|
|
|
521
528
|
watch(
|
|
522
529
|
() => activeFile.value?.id,
|
|
523
530
|
async () => {
|
|
524
|
-
|
|
531
|
+
previewerZoomLevel.value = 1
|
|
525
532
|
await nextTick()
|
|
526
533
|
resetTransform()
|
|
527
534
|
}
|
|
@@ -47,6 +47,7 @@ export function inferKind(name: string, explicit?: FileViewerKind): FileViewerKi
|
|
|
47
47
|
if (ext === 'pdf') return 'pdf'
|
|
48
48
|
if (SHEET_EXTS.has(ext)) return 'sheet'
|
|
49
49
|
if (ext === 'docx') return 'docx'
|
|
50
|
+
if (ext === 'ofd') return 'ofd'
|
|
50
51
|
if (TEXT_EXTS.has(ext)) return 'text'
|
|
51
52
|
return 'text'
|
|
52
53
|
}
|
|
@@ -57,6 +58,7 @@ export const FILE_VIEWER_KIND_LABEL: Record<FileViewerKind, string> = {
|
|
|
57
58
|
pdf: 'PDF',
|
|
58
59
|
sheet: 'XLS',
|
|
59
60
|
docx: 'DOC',
|
|
61
|
+
ofd: 'OFD',
|
|
60
62
|
text: 'TXT'
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="cls.e('ofd')">
|
|
3
|
+
<div ref="scroll" :class="cls.e('ofd-scroll')">
|
|
4
|
+
<div
|
|
5
|
+
v-for="(page, i) in pages"
|
|
6
|
+
:key="i"
|
|
7
|
+
:data-index="i"
|
|
8
|
+
:class="cls.e('ofd-page')"
|
|
9
|
+
:style="{
|
|
10
|
+
width: page.widthPx * zoomLevel + 'px',
|
|
11
|
+
height: page.heightPx * zoomLevel + 'px'
|
|
12
|
+
}"
|
|
13
|
+
>
|
|
14
|
+
<div v-if="page.svg" :class="cls.e('ofd-page-body')" v-html="page.svg" />
|
|
15
|
+
</div>
|
|
16
|
+
<div v-if="!pages.length && !loading" :class="cls.e('empty')">
|
|
17
|
+
<u-empty text="该文件没有可预览的页面" :size="32" />
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div v-if="loading" :class="cls.e('loading')">正在解析 OFD…</div>
|
|
21
|
+
</div>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script lang="ts" setup>
|
|
25
|
+
import { bem } from '@veltra/utils'
|
|
26
|
+
import { nextTick, onBeforeUnmount, ref, useTemplateRef, watch } from 'vue'
|
|
27
|
+
|
|
28
|
+
import type { FileViewerItem } from '../../../types/file-viewer'
|
|
29
|
+
import { UEmpty } from '../../empty'
|
|
30
|
+
import { toArrayBuffer } from '../helper'
|
|
31
|
+
|
|
32
|
+
defineOptions({ name: 'UFileViewerOfdPreviewer' })
|
|
33
|
+
|
|
34
|
+
/** 仅类型查询;运行时动态 import 按需加载,构建时随 alwaysBundle 内联进 dist */
|
|
35
|
+
type OfdCoreModule = typeof import('@veltra/ofd-core')
|
|
36
|
+
type OfdContainer = import('@veltra/ofd-core').OfdContainer
|
|
37
|
+
type OfdZip = import('@veltra/ofd-core').OfdZip
|
|
38
|
+
|
|
39
|
+
/** OFD 毫米 → CSS 像素(96dpi),与内核 SVG 输出的换算一致 */
|
|
40
|
+
const PX_PER_MM = 96 / 25.4
|
|
41
|
+
|
|
42
|
+
const MIN_ZOOM = 0.5
|
|
43
|
+
const MAX_ZOOM = 3
|
|
44
|
+
/** 每次缩放固定增减 10%,对齐 pdf 缩放配置 */
|
|
45
|
+
const ZOOM_STEP = 0.1
|
|
46
|
+
/** 可视区上下各预渲染一个视口高度的页 */
|
|
47
|
+
const RENDER_MARGIN = '100% 0px'
|
|
48
|
+
|
|
49
|
+
interface OfdPageView {
|
|
50
|
+
docIndex: number
|
|
51
|
+
pageIndex: number
|
|
52
|
+
/** 页面基准尺寸(缩放 1 时),毫米按 96dpi 换算为像素 */
|
|
53
|
+
widthPx: number
|
|
54
|
+
heightPx: number
|
|
55
|
+
/** null = 未渲染;'' = 已尝试但渲染失败,不再重试 */
|
|
56
|
+
svg: string | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const props = defineProps<{ file: FileViewerItem }>()
|
|
60
|
+
|
|
61
|
+
const emit = defineEmits<{
|
|
62
|
+
(e: 'error', err: unknown): void
|
|
63
|
+
(e: 'zoom-change', level: number): void
|
|
64
|
+
}>()
|
|
65
|
+
|
|
66
|
+
const cls = bem('file-viewer')
|
|
67
|
+
const scrollEl = useTemplateRef<HTMLDivElement>('scroll')
|
|
68
|
+
const loading = ref(true)
|
|
69
|
+
const pages = ref<OfdPageView[]>([])
|
|
70
|
+
const zoomLevel = ref(1)
|
|
71
|
+
|
|
72
|
+
let controller: AbortController | undefined
|
|
73
|
+
/** 递增 token:防止切文件后旧解析/渲染结果落地 */
|
|
74
|
+
let loadToken = 0
|
|
75
|
+
let ofdCore: OfdCoreModule | undefined
|
|
76
|
+
let observer: IntersectionObserver | undefined
|
|
77
|
+
let activeZip: OfdZip | undefined
|
|
78
|
+
let activeContainer: OfdContainer | undefined
|
|
79
|
+
/** 已进入渲染流程(进行中或已完成)的页,防止滚动重复触发 */
|
|
80
|
+
const scheduledPages = new Set<number>()
|
|
81
|
+
|
|
82
|
+
// ---- 缩放:作用于页面容器尺寸,滚动高度随之自适应 ----
|
|
83
|
+
|
|
84
|
+
function clampZoom(value: number): number {
|
|
85
|
+
return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.round(value * 100) / 100))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function setZoom(value: number) {
|
|
89
|
+
const next = clampZoom(value)
|
|
90
|
+
if (zoomLevel.value === next) return
|
|
91
|
+
zoomLevel.value = next
|
|
92
|
+
emit('zoom-change', next)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function zoomIn() {
|
|
96
|
+
setZoom(zoomLevel.value + ZOOM_STEP)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function zoomOut() {
|
|
100
|
+
setZoom(zoomLevel.value - ZOOM_STEP)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resetZoom() {
|
|
104
|
+
setZoom(1)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
defineExpose({ zoomIn, zoomOut, resetZoom })
|
|
108
|
+
|
|
109
|
+
// ---- 解析与懒渲染 ----
|
|
110
|
+
|
|
111
|
+
/** 动态加载 ofd-core chunk;首次加载后缓存模块复用 */
|
|
112
|
+
async function resolveOfdCore(): Promise<OfdCoreModule> {
|
|
113
|
+
const mod = ofdCore ?? (await import('@veltra/ofd-core'))
|
|
114
|
+
ofdCore = mod
|
|
115
|
+
return mod
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function teardownRender() {
|
|
119
|
+
observer?.disconnect()
|
|
120
|
+
observer = undefined
|
|
121
|
+
activeZip = undefined
|
|
122
|
+
activeContainer = undefined
|
|
123
|
+
scheduledPages.clear()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function observePages() {
|
|
127
|
+
const root = scrollEl.value
|
|
128
|
+
if (!root || typeof IntersectionObserver === 'undefined') return
|
|
129
|
+
observer = new IntersectionObserver(
|
|
130
|
+
(entries) => {
|
|
131
|
+
const token = loadToken
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
if (!entry.isIntersecting) continue
|
|
134
|
+
observer?.unobserve(entry.target)
|
|
135
|
+
const index = Number((entry.target as HTMLElement).dataset.index)
|
|
136
|
+
if (Number.isNaN(index) || scheduledPages.has(index)) continue
|
|
137
|
+
scheduledPages.add(index)
|
|
138
|
+
void renderPage(index, token)
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{ root, rootMargin: RENDER_MARGIN }
|
|
142
|
+
)
|
|
143
|
+
for (const el of root.querySelectorAll<HTMLElement>('[data-index]')) {
|
|
144
|
+
observer.observe(el)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 渲染单页 SVG;失败置空串避免滚动时反复重试同一页 */
|
|
149
|
+
async function renderPage(index: number, token: number) {
|
|
150
|
+
const page = pages.value[index]
|
|
151
|
+
const core = ofdCore
|
|
152
|
+
if (!page || page.svg !== null || !core || !activeZip || !activeContainer) return
|
|
153
|
+
try {
|
|
154
|
+
const svg = await core.pageToSvg(activeZip, activeContainer, page.docIndex, page.pageIndex)
|
|
155
|
+
if (token !== loadToken) return
|
|
156
|
+
pages.value[index] = { ...page, svg }
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (token !== loadToken) return
|
|
159
|
+
pages.value[index] = { ...page, svg: '' }
|
|
160
|
+
emit('error', err)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function load() {
|
|
165
|
+
const token = ++loadToken
|
|
166
|
+
controller?.abort()
|
|
167
|
+
controller = new AbortController()
|
|
168
|
+
teardownRender()
|
|
169
|
+
loading.value = true
|
|
170
|
+
pages.value = []
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const core = await resolveOfdCore()
|
|
174
|
+
if (token !== loadToken) return
|
|
175
|
+
|
|
176
|
+
const buf = await toArrayBuffer(props.file.src, controller.signal)
|
|
177
|
+
if (token !== loadToken) return
|
|
178
|
+
const zip = core.openOfdZip(new Uint8Array(buf))
|
|
179
|
+
const container = await core.parseOfdContainer(zip)
|
|
180
|
+
if (token !== loadToken) return
|
|
181
|
+
|
|
182
|
+
activeZip = zip
|
|
183
|
+
activeContainer = container
|
|
184
|
+
// 未声明尺寸时按 A4 兜底,与内核 pageToSvg 的兜底一致
|
|
185
|
+
pages.value = container.docs.flatMap((doc, docIndex) =>
|
|
186
|
+
doc.pages.map((page) => {
|
|
187
|
+
const size = page.size ?? doc.pageSize ?? { width: 210, height: 297 }
|
|
188
|
+
return {
|
|
189
|
+
docIndex,
|
|
190
|
+
pageIndex: page.index,
|
|
191
|
+
widthPx: Math.round(size.width * PX_PER_MM),
|
|
192
|
+
heightPx: Math.round(size.height * PX_PER_MM),
|
|
193
|
+
svg: null
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
)
|
|
197
|
+
await nextTick()
|
|
198
|
+
if (token !== loadToken) return
|
|
199
|
+
observePages()
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (token !== loadToken) return
|
|
202
|
+
if ((err as { name?: string })?.name === 'AbortError') return
|
|
203
|
+
emit('error', err)
|
|
204
|
+
} finally {
|
|
205
|
+
if (token === loadToken) loading.value = false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
watch(() => props.file, load, { immediate: true })
|
|
210
|
+
|
|
211
|
+
onBeforeUnmount(() => {
|
|
212
|
+
controller?.abort()
|
|
213
|
+
teardownRender()
|
|
214
|
+
pages.value = []
|
|
215
|
+
})
|
|
216
|
+
</script>
|
|
@@ -302,6 +302,12 @@
|
|
|
302
302
|
--u-file-viewer-kind-bg: rgba(100, 116, 139, 0.1);
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
@include m.em(badge, ofd) {
|
|
306
|
+
--u-file-viewer-kind: #7c3aed;
|
|
307
|
+
--u-file-viewer-kind-border: rgba(124, 58, 237, 0.28);
|
|
308
|
+
--u-file-viewer-kind-bg: rgba(124, 58, 237, 0.1);
|
|
309
|
+
}
|
|
310
|
+
|
|
305
311
|
@include m.e(stage) {
|
|
306
312
|
display: flex;
|
|
307
313
|
flex: 1;
|
|
@@ -686,6 +692,33 @@
|
|
|
686
692
|
0 0 0 1px rgba(255, 255, 255, 0.04);
|
|
687
693
|
}
|
|
688
694
|
|
|
695
|
+
@include m.e(ofd) {
|
|
696
|
+
position: absolute;
|
|
697
|
+
inset: 0;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
@include m.e(ofd-scroll) {
|
|
701
|
+
width: 100%;
|
|
702
|
+
height: 100%;
|
|
703
|
+
overflow: auto;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
@include m.e(ofd-page) {
|
|
707
|
+
margin: 24px auto;
|
|
708
|
+
background: #fff;
|
|
709
|
+
box-shadow: var(--u-file-viewer-page-shadow);
|
|
710
|
+
|
|
711
|
+
svg {
|
|
712
|
+
display: block;
|
|
713
|
+
width: 100%;
|
|
714
|
+
height: 100%;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
&.is-modal &__ofd-page {
|
|
719
|
+
margin: clamp(24px, 3vw, 44px) auto;
|
|
720
|
+
}
|
|
721
|
+
|
|
689
722
|
@include m.e(docx) {
|
|
690
723
|
position: absolute;
|
|
691
724
|
inset: 0;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { ColumnNode } from '../node/col'
|
|
4
|
+
|
|
5
|
+
describe('ColumnNode column styles', () => {
|
|
6
|
+
it('headerAlign 未指定时回退到 align', () => {
|
|
7
|
+
const column = new ColumnNode({ key: 'a', name: 'A', align: 'center' }, 0, 0)
|
|
8
|
+
|
|
9
|
+
expect(column.headerAlign).toBe('center')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('headerAlign 显式指定时优先于 align', () => {
|
|
13
|
+
const column = new ColumnNode(
|
|
14
|
+
{ key: 'a', name: 'A', align: 'center', headerAlign: 'right' },
|
|
15
|
+
0,
|
|
16
|
+
0
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
expect(column.headerAlign).toBe('right')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('headerCellStyle 未指定 headerStyle 时回退到 style', () => {
|
|
23
|
+
const column = new ColumnNode(
|
|
24
|
+
{ key: 'a', name: 'A', style: { color: 'red', fontSize: 14 } },
|
|
25
|
+
0,
|
|
26
|
+
0
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
expect(column.cellStyle).toEqual({ color: 'red', fontSize: '14px' })
|
|
30
|
+
expect(column.headerCellStyle).toEqual({ color: 'red', fontSize: '14px' })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('headerCellStyle 显式指定 headerStyle 时优先于 style', () => {
|
|
34
|
+
const column = new ColumnNode(
|
|
35
|
+
{
|
|
36
|
+
key: 'a',
|
|
37
|
+
name: 'A',
|
|
38
|
+
style: { color: 'red', fontSize: 12 },
|
|
39
|
+
headerStyle: { color: 'blue', fontSize: 16 }
|
|
40
|
+
},
|
|
41
|
+
0,
|
|
42
|
+
0
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
expect(column.cellStyle).toEqual({ color: 'red', fontSize: '12px' })
|
|
46
|
+
expect(column.headerCellStyle).toEqual({ color: 'blue', fontSize: '16px' })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('fontSize 数字转为 px,字符串原样保留', () => {
|
|
50
|
+
const column = new ColumnNode({ key: 'a', name: 'A', style: { fontSize: 18 } }, 0, 0)
|
|
51
|
+
const columnWithUnit = new ColumnNode(
|
|
52
|
+
{ key: 'b', name: 'B', style: { fontSize: '1.2rem' } },
|
|
53
|
+
0,
|
|
54
|
+
0
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
expect(column.cellStyle).toEqual({ fontSize: '18px' })
|
|
58
|
+
expect(columnWithUnit.cellStyle).toEqual({ fontSize: '1.2rem' })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('未配置样式时 getter 返回 undefined', () => {
|
|
62
|
+
const column = new ColumnNode({ key: 'a', name: 'A' }, 0, 0)
|
|
63
|
+
|
|
64
|
+
expect(column.cellStyle).toBeUndefined()
|
|
65
|
+
expect(column.headerCellStyle).toBeUndefined()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import { TreeNode } from '@cat-kit/core'
|
|
2
|
+
import { withUnit } from '@veltra/utils'
|
|
2
3
|
import { isReactive, reactive, shallowReactive } from 'vue'
|
|
3
4
|
|
|
4
|
-
import type { TableColumn, TableColumnAlign } from '../../../types'
|
|
5
|
+
import type { TableColumn, TableColumnAlign, TableColumnStyle } from '../../../types'
|
|
6
|
+
|
|
7
|
+
function normalizeColumnStyle(style?: TableColumnStyle): Record<string, string> | undefined {
|
|
8
|
+
if (!style) return undefined
|
|
9
|
+
|
|
10
|
+
const result: Record<string, string> = {}
|
|
11
|
+
|
|
12
|
+
if (style.color !== undefined) {
|
|
13
|
+
result.color = style.color
|
|
14
|
+
}
|
|
15
|
+
if (style.fontSize !== undefined) {
|
|
16
|
+
const fontSize = withUnit(style.fontSize, 'px')
|
|
17
|
+
if (fontSize !== undefined) {
|
|
18
|
+
result.fontSize = fontSize
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return Object.keys(result).length ? result : undefined
|
|
23
|
+
}
|
|
5
24
|
|
|
6
25
|
export class ColumnNode extends TreeNode<TableColumn, ColumnNode> {
|
|
7
26
|
declare children?: ColumnNode[] | undefined
|
|
@@ -44,6 +63,16 @@ export class ColumnNode extends TreeNode<TableColumn, ColumnNode> {
|
|
|
44
63
|
this.data.headerAlign = val
|
|
45
64
|
}
|
|
46
65
|
|
|
66
|
+
/** 表体/表尾单元格样式 */
|
|
67
|
+
get cellStyle(): Record<string, string> | undefined {
|
|
68
|
+
return normalizeColumnStyle(this.data.style)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 表头单元格样式 */
|
|
72
|
+
get headerCellStyle(): Record<string, string> | undefined {
|
|
73
|
+
return normalizeColumnStyle(this.data.headerStyle ?? this.data.style)
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
/** 宽度 */
|
|
48
77
|
get width(): number | undefined {
|
|
49
78
|
const width = this.data.width ?? this.derived.allocatedWidth
|
|
@@ -24,13 +24,18 @@ defineOptions({ name: 'UTableCell' })
|
|
|
24
24
|
* 父组件 `TableRow` 已经持有注入上下文,直接把 `cellClass` 传下来即可;
|
|
25
25
|
* 这里变成「完全受控」的纯渲染单元。
|
|
26
26
|
*/
|
|
27
|
-
const { left, right } = defineProps<{
|
|
27
|
+
const { left, right, cellStyle } = defineProps<{
|
|
28
28
|
cellClass: string
|
|
29
29
|
left?: number
|
|
30
30
|
right?: number
|
|
31
|
+
cellStyle?: Record<string, string>
|
|
31
32
|
rowspan?: number
|
|
32
33
|
colspan?: number
|
|
33
34
|
}>()
|
|
34
35
|
|
|
35
|
-
const style = computed(() => ({
|
|
36
|
+
const style = computed(() => ({
|
|
37
|
+
...cellStyle,
|
|
38
|
+
left: withUnit(left, 'px'),
|
|
39
|
+
right: withUnit(right, 'px')
|
|
40
|
+
}))
|
|
36
41
|
</script>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
v-if="leafColumns[0]"
|
|
6
6
|
:class="getCellClass(leafColumns[0])"
|
|
7
7
|
:style="{
|
|
8
|
+
...leafColumns[0].cellStyle,
|
|
8
9
|
right: withUnit(leafColumns[0].style.right, 'px'),
|
|
9
10
|
left: withUnit(leafColumns[0].style.left, 'px')
|
|
10
11
|
}"
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
:class="getCellClass(column)"
|
|
17
18
|
:key="column.key"
|
|
18
19
|
:style="{
|
|
20
|
+
...column.cellStyle,
|
|
19
21
|
right: withUnit(column.style.right, 'px'),
|
|
20
22
|
left: withUnit(column.style.left, 'px')
|
|
21
23
|
}"
|
|
@@ -26,6 +26,7 @@ const TableHead: DefineComponent = defineComponent({
|
|
|
26
26
|
colspan={column.leafs}
|
|
27
27
|
rowspan={column.children?.length ? undefined : headers.value.length - headerIndex}
|
|
28
28
|
style={{
|
|
29
|
+
...column.headerCellStyle,
|
|
29
30
|
left: withUnit(column.style.left, 'px'),
|
|
30
31
|
right: withUnit(column.style.right, 'px')
|
|
31
32
|
}}
|
|
@@ -99,6 +99,7 @@ export const UTableRow: DefineComponent<{ row: ITableRow; index: number }> = def
|
|
|
99
99
|
cellClass={getCellClass(_expandColumn)}
|
|
100
100
|
left={_expandColumn.style.left}
|
|
101
101
|
right={_expandColumn.style.right}
|
|
102
|
+
cellStyle={_expandColumn.cellStyle}
|
|
102
103
|
key={row.uid + _expandColumn.key}
|
|
103
104
|
{...tableProps.mergeCell?.(expandCtx)}
|
|
104
105
|
>
|
|
@@ -156,6 +157,7 @@ export const UTableRow: DefineComponent<{ row: ITableRow; index: number }> = def
|
|
|
156
157
|
cellClass={getCellClass(column)}
|
|
157
158
|
left={column.style.left}
|
|
158
159
|
right={column.style.right}
|
|
160
|
+
cellStyle={column.cellStyle}
|
|
159
161
|
key={row.uid + column.key}
|
|
160
162
|
// @ts-ignore
|
|
161
163
|
onClick={(e) => handleCellClick(row, column, e)}
|