pdfwright-web 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/PKG-INFO +1 -1
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/package.json +1 -1
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/App.vue +39 -6
- pdfwright_web-0.4.0/frontend/src/api/client.spec.ts +88 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/api/client.ts +34 -4
- pdfwright_web-0.4.0/frontend/src/designer/DataPanel.vue +193 -0
- pdfwright_web-0.4.0/frontend/src/designer/JsonPanel.spec.ts +37 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/JsonPanel.vue +7 -0
- pdfwright_web-0.4.0/frontend/src/designer/PagePanel.vue +407 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/PropsPanel.spec.ts +76 -1
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/PropsPanel.vue +63 -950
- pdfwright_web-0.4.0/frontend/src/designer/data-panel.css +82 -0
- pdfwright_web-0.4.0/frontend/src/designer/fontOptions.ts +61 -0
- pdfwright_web-0.4.0/frontend/src/designer/panel-shared.css +107 -0
- pdfwright_web-0.4.0/frontend/src/designer/props-panel.css +219 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/fill/FillView.vue +4 -2
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/expand.ts +0 -4
- pdfwright_web-0.4.0/frontend/src/lib/fontTables.generated.ts +82 -0
- pdfwright_web-0.4.0/frontend/src/lib/fonts.spec.ts +90 -0
- pdfwright_web-0.4.0/frontend/src/lib/fonts.ts +121 -0
- pdfwright_web-0.4.0/frontend/src/lib/markup.spec.ts +116 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/markup.ts +80 -12
- pdfwright_web-0.4.0/frontend/src/lib/table-vectors.spec.ts +54 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/table.spec.ts +79 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/table.ts +57 -19
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/tableResize.ts +1 -9
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/template.ts +0 -12
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/units.ts +0 -5
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/renderer/ElementRenderer.spec.ts +57 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/renderer/ElementRenderer.vue +46 -5
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/renderer/InlineText.vue +7 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/store/designer.spec.ts +175 -12
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/store/designer.ts +185 -394
- pdfwright_web-0.4.0/frontend/src/store/dropRules.spec.ts +170 -0
- pdfwright_web-0.4.0/frontend/src/store/dropRules.ts +126 -0
- pdfwright_web-0.4.0/frontend/src/store/tableGrid.spec.ts +216 -0
- pdfwright_web-0.4.0/frontend/src/store/tableGrid.ts +231 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/types/schema.ts +14 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/pyproject.toml +6 -2
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/__init__.py +1 -1
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/app.py +63 -16
- pdfwright_web-0.4.0/src/pdfwright_web/component_catalog.py +769 -0
- pdfwright_web-0.3.0/src/pdfwright_web/meta.py → pdfwright_web-0.4.0/src/pdfwright_web/component_props.py +95 -745
- pdfwright_web-0.4.0/src/pdfwright_web/font_tables.py +119 -0
- pdfwright_web-0.4.0/src/pdfwright_web/meta.py +97 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/mock.py +124 -11
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/batch.py +21 -14
- pdfwright_web-0.4.0/src/pdfwright_web/routers/codes.py +121 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/meta.py +5 -2
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/mock.py +7 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/render.py +40 -2
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/verify.py +35 -9
- pdfwright_web-0.4.0/src/pdfwright_web/static/assets/index-C7PqDIDc.js +88 -0
- pdfwright_web-0.3.0/src/pdfwright_web/static/assets/index-CrZ4dGgx.css → pdfwright_web-0.4.0/src/pdfwright_web/static/assets/index-ds5v6EJJ.css +1 -1
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/static/index.html +2 -2
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/uploads.py +17 -16
- pdfwright_web-0.4.0/tests/conftest.py +45 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/tests/test_api.py +194 -0
- pdfwright_web-0.4.0/tests/test_font_consistency.py +186 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/tests/test_mock.py +8 -3
- pdfwright_web-0.4.0/tests/test_mock_shapes.py +188 -0
- pdfwright_web-0.4.0/tests/test_service_guards.py +642 -0
- pdfwright_web-0.3.0/frontend/src/api/client.spec.ts +0 -31
- pdfwright_web-0.3.0/frontend/src/lib/fonts.ts +0 -63
- pdfwright_web-0.3.0/frontend/src/lib/markup.spec.ts +0 -58
- pdfwright_web-0.3.0/src/pdfwright_web/routers/codes.py +0 -75
- pdfwright_web-0.3.0/src/pdfwright_web/static/assets/index-K7XXzc42.js +0 -86
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/.gitignore +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/README.md +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/env.d.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/eslint.config.js +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/index.html +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/pnpm-lock.yaml +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/components/BoxInput.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/components/BoxInput.vue +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/components/UnitInput.vue +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/CanvasArea.vue +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/ComponentPalette.vue +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/ElementTree.vue +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/ElementTreeNode.vue +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/drag-affordance.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/dragState.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/table-resize-affordance.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/treeCollapse.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/treeCollapse.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/designer/treeDrag.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/clone.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/colors.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/colors.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/convert.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/convert.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/download.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/drop.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/drop.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/expand.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/format.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/format.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/json-prop.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/json-prop.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/layout.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/layout.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/metadata.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/metadata.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/pages.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/pages.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/required.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/required.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/snippet.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/snippet.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/tableResize.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/tree.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/tree.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/lib/units.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/main.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/renderer/WatermarkLayer.vue +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/store/cell-ops-daily.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/store/cell-ops.spec.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/src/styles/main.css +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/tsconfig.json +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/frontend/vite.config.ts +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/concurrency.py +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/__init__.py +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/convert.py +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/examples.py +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/options.py +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/schemas.py +0 -0
- {pdfwright_web-0.3.0 → pdfwright_web-0.4.0}/src/pdfwright_web/security_defaults.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** 应用外壳:设计(组件树 + 画布 + 属性)/ 填写;预览在画布区就地切换。 */
|
|
3
|
-
import { computed, onMounted, ref } from 'vue'
|
|
3
|
+
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
4
4
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
5
5
|
import CanvasArea from '@/designer/CanvasArea.vue'
|
|
6
6
|
import ComponentPalette from '@/designer/ComponentPalette.vue'
|
|
@@ -10,7 +10,7 @@ import PropsPanel from '@/designer/PropsPanel.vue'
|
|
|
10
10
|
import FillView from '@/fill/FillView.vue'
|
|
11
11
|
import * as api from '@/api/client'
|
|
12
12
|
import type { ExampleItem } from '@/api/client'
|
|
13
|
-
import { downloadPdf, exportSchema, extractLegacyData, importSchema, importSchemaJson, initialize, redo, removeSelected, restoreDraft,
|
|
13
|
+
import { downloadPdf, exportSchema, extractLegacyData, importSchema, importSchemaJson, initialize, redo, removeSelected, restoreDraft, peekDraft, clearDraft, setMode, state, undo } from '@/store/designer'
|
|
14
14
|
import { downloadBlob } from '@/lib/download'
|
|
15
15
|
import type { DocumentSchema } from '@/types/schema'
|
|
16
16
|
|
|
@@ -21,8 +21,12 @@ const xlsxInput = ref<HTMLInputElement | null>(null)
|
|
|
21
21
|
//: 默认 **块级**(各自独立成块,可自由拖拽),需要保留 Word 的同行排版时从下拉菜单选。
|
|
22
22
|
const docxInline = ref(false)
|
|
23
23
|
const jsonOpen = ref(false)
|
|
24
|
+
/** JSON 抽屉里的面板(``@open`` 时要让它重新同步,见 JsonPanel.sync 的注释)。 */
|
|
25
|
+
const jsonPanelRef = ref<InstanceType<typeof import('./designer/JsonPanel.vue')['default']> | null>(null)
|
|
24
26
|
const leftTab = ref('palette')
|
|
25
27
|
const examples = ref<ExampleItem[]>([])
|
|
28
|
+
/** 新构建探测轮询的定时器句柄(`onUnmounted` 里要清)。 */
|
|
29
|
+
let buildTimer: number | undefined
|
|
26
30
|
|
|
27
31
|
const errorCount = computed(() => state.issues.filter((issue) => issue.level === 'error').length)
|
|
28
32
|
const warnCount = computed(() => state.issues.filter((issue) => issue.level === 'warning').length)
|
|
@@ -30,16 +34,25 @@ const warnCount = computed(() => state.issues.filter((issue) => issue.level ===
|
|
|
30
34
|
onMounted(() => {
|
|
31
35
|
void bootstrap()
|
|
32
36
|
void watchBuild()
|
|
33
|
-
setInterval(() => void watchBuild(), 30000)
|
|
37
|
+
buildTimer = window.setInterval(() => void watchBuild(), 30000)
|
|
34
38
|
document.addEventListener('visibilitychange', onVisibility)
|
|
35
39
|
window.addEventListener('keydown', onKeydown)
|
|
36
40
|
void loadExamples()
|
|
37
41
|
})
|
|
38
42
|
|
|
43
|
+
// ⚠️ 轮询与两个全局监听器必须成对注销:组件被卸载后旧实例仍会每 30s 打一次
|
|
44
|
+
// `/api/version`,且监听器会随每次重新挂载(HMR / 测试)不断累积。
|
|
45
|
+
onUnmounted(() => {
|
|
46
|
+
if (buildTimer !== undefined) window.clearInterval(buildTimer)
|
|
47
|
+
buildTimer = undefined
|
|
48
|
+
document.removeEventListener('visibilitychange', onVisibility)
|
|
49
|
+
window.removeEventListener('keydown', onKeydown)
|
|
50
|
+
})
|
|
51
|
+
|
|
39
52
|
/** 启动:先尝试恢复上次的草稿(刷新/误关不丢工作),用户可选择放弃。 */
|
|
40
53
|
async function bootstrap(): Promise<void> {
|
|
41
54
|
await initialize()
|
|
42
|
-
if (!
|
|
55
|
+
if (!peekDraft()) return
|
|
43
56
|
try {
|
|
44
57
|
await ElMessageBox.confirm('检测到上次未导出的草稿,是否恢复?', '恢复草稿', {
|
|
45
58
|
confirmButtonText: '恢复',
|
|
@@ -245,6 +258,7 @@ function pickDocxFile(inline: boolean): void {
|
|
|
245
258
|
<div class="toolbar-spacer" />
|
|
246
259
|
|
|
247
260
|
<span v-if="errorCount" class="issue-badge error">{{ errorCount }} 个错误</span>
|
|
261
|
+
<span v-else-if="state.renderWarnings.length" class="issue-badge warn">{{ state.renderWarnings.length }} 个渲染提示</span>
|
|
248
262
|
<span v-else-if="warnCount" class="issue-badge warn">{{ warnCount }} 个提示</span>
|
|
249
263
|
|
|
250
264
|
<el-button size="small" :disabled="state.historyIndex <= 0" @click="undo">撤销</el-button>
|
|
@@ -292,6 +306,12 @@ function pickDocxFile(inline: boolean): void {
|
|
|
292
306
|
ⓘ {{ issue.path }}:{{ issue.message }}
|
|
293
307
|
</span>
|
|
294
308
|
</div>
|
|
309
|
+
<!-- 渲染期告警:静态校验查不出来的东西(图片没加载出来 / 变量未定义 / 字符被忽略)。
|
|
310
|
+
独立一条是因为它属于「**这份数据渲染时**」,且下载成功≠配置正确。 -->
|
|
311
|
+
<div v-if="state.renderWarnings.length" class="issue-bar render-warn">
|
|
312
|
+
<span v-for="warning in state.renderWarnings.slice(0, 3)" :key="warning">⚠ {{ warning }}</span>
|
|
313
|
+
<el-button size="small" text @click="state.renderWarnings = []">知道了</el-button>
|
|
314
|
+
</div>
|
|
295
315
|
|
|
296
316
|
<main class="main">
|
|
297
317
|
<template v-if="state.mode === 'design'">
|
|
@@ -311,8 +331,14 @@ function pickDocxFile(inline: boolean): void {
|
|
|
311
331
|
<FillView v-else />
|
|
312
332
|
</main>
|
|
313
333
|
|
|
314
|
-
<el-drawer
|
|
315
|
-
|
|
334
|
+
<el-drawer
|
|
335
|
+
v-model="jsonOpen"
|
|
336
|
+
title="页面配置 JSON"
|
|
337
|
+
size="46%"
|
|
338
|
+
:with-header="true"
|
|
339
|
+
@open="jsonPanelRef?.sync()"
|
|
340
|
+
>
|
|
341
|
+
<JsonPanel ref="jsonPanelRef" />
|
|
316
342
|
</el-drawer>
|
|
317
343
|
</div>
|
|
318
344
|
</template>
|
|
@@ -393,6 +419,13 @@ function pickDocxFile(inline: boolean): void {
|
|
|
393
419
|
color: #b45309;
|
|
394
420
|
border-bottom-color: #fde68a;
|
|
395
421
|
}
|
|
422
|
+
/* 渲染告警:用中性橙底与静态校验的黄色条区分(一个来自配置、一个来自这次渲染) */
|
|
423
|
+
.issue-bar.render-warn {
|
|
424
|
+
background: #fff7ed;
|
|
425
|
+
color: #c2410c;
|
|
426
|
+
border-bottom-color: #fed7aa;
|
|
427
|
+
align-items: center;
|
|
428
|
+
}
|
|
396
429
|
.example-item {
|
|
397
430
|
padding: 2px 0;
|
|
398
431
|
max-width: 320px;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** API 客户端:导入 Word 的「同排」开关要走对 URL(默认块级 = 不带参数)。 */
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { convertDocx, renderHtml, renderPdf } from './client'
|
|
6
|
+
|
|
7
|
+
const OK = { document: { page: {}, body: [] }, fields: [], warnings: [] }
|
|
8
|
+
|
|
9
|
+
function stubFetch(): ReturnType<typeof vi.fn> {
|
|
10
|
+
const spy = vi.fn(async () => new Response(JSON.stringify(OK), { status: 200 }))
|
|
11
|
+
vi.stubGlobal('fetch', spy)
|
|
12
|
+
return spy
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('convertDocx', () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.unstubAllGlobals()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('默认(块级)不打 inline_commands', async () => {
|
|
21
|
+
const spy = stubFetch()
|
|
22
|
+
await convertDocx(new File(['x'], 'a.docx'))
|
|
23
|
+
expect(String(spy.mock.calls[0][0])).toBe('/api/convert/docx')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('开启同排 → ?inline_commands=true', async () => {
|
|
27
|
+
const spy = stubFetch()
|
|
28
|
+
await convertDocx(new File(['x'], 'a.docx'), true)
|
|
29
|
+
expect(String(spy.mock.calls[0][0])).toBe('/api/convert/docx?inline_commands=true')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('renderPdf / renderHtml 的告警头', () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
vi.unstubAllGlobals()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/** 服务端把告警序列化成 base64url 的 JSON 数组放进 ``X-PdfWright-Warnings``。 */
|
|
39
|
+
function warningsHeader(items: string[]): Headers {
|
|
40
|
+
const bytes = new TextEncoder().encode(JSON.stringify(items))
|
|
41
|
+
let binary = ''
|
|
42
|
+
bytes.forEach((byte) => (binary += String.fromCharCode(byte)))
|
|
43
|
+
return new Headers({ 'x-pdfwright-warnings': btoa(binary).replace(/\+/g, '-').replace(/\//g, '_') })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it('PDF:告警从响应头解出来(中文不乱码),值仍是 Blob', async () => {
|
|
47
|
+
vi.stubGlobal(
|
|
48
|
+
'fetch',
|
|
49
|
+
vi.fn(
|
|
50
|
+
async () =>
|
|
51
|
+
new Response(new Blob([new Uint8Array([1, 2, 3])]), {
|
|
52
|
+
status: 200,
|
|
53
|
+
headers: warningsHeader(['图片加载失败,已跳过:/etc/hosts']),
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
const result = await renderPdf({ page: {}, body: [] } as never)
|
|
58
|
+
expect(result.warnings).toEqual(['图片加载失败,已跳过:/etc/hosts'])
|
|
59
|
+
expect(result.value).toBeInstanceOf(Blob)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('HTML:告警从响应头解出来', async () => {
|
|
63
|
+
vi.stubGlobal(
|
|
64
|
+
'fetch',
|
|
65
|
+
vi.fn(async () => new Response('<p>x</p>', { status: 200, headers: warningsHeader(['未定义变量 foo']) })),
|
|
66
|
+
)
|
|
67
|
+
const result = await renderHtml({ page: {}, body: [] } as never)
|
|
68
|
+
expect(result.value).toBe('<p>x</p>')
|
|
69
|
+
expect(result.warnings).toEqual(['未定义变量 foo'])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('头缺失 / 非法 base64 → 当作没有告警,**不能**让渲染失败', async () => {
|
|
73
|
+
vi.stubGlobal('fetch', vi.fn(async () => new Response(new Blob([new Uint8Array([1])]), { status: 200 })))
|
|
74
|
+
expect((await renderPdf({ page: {}, body: [] } as never)).warnings).toEqual([])
|
|
75
|
+
|
|
76
|
+
vi.stubGlobal(
|
|
77
|
+
'fetch',
|
|
78
|
+
vi.fn(
|
|
79
|
+
async () =>
|
|
80
|
+
new Response(new Blob([new Uint8Array([1])]), {
|
|
81
|
+
status: 200,
|
|
82
|
+
headers: { 'x-pdfwright-warnings': '!!!not-base64!!!' },
|
|
83
|
+
}),
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
expect((await renderPdf({ page: {}, body: [] } as never)).warnings).toEqual([])
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -96,6 +96,8 @@ export interface SemanticFont {
|
|
|
96
96
|
exact: boolean
|
|
97
97
|
/** 字体自然行高倍数(后端按字体度量算,前端据此还原行距) */
|
|
98
98
|
lineRatio?: number | null
|
|
99
|
+
/** 字体**排版**行高倍数(OS/2 win):判断文档网格吸附用(后端按字体度量算) */
|
|
100
|
+
lineRatioWin?: number | null
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
export interface SearchableFont {
|
|
@@ -129,7 +131,35 @@ export function validateDocument(
|
|
|
129
131
|
})
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
/** 从渲染响应头解出告警(``X-PdfWright-Warnings`` = base64url 的 JSON 数组)。
|
|
135
|
+
*
|
|
136
|
+
* ⚠️ 这层解包放在客户端而不是让调用方处理:``/api/render/*`` 返回二进制/HTML,
|
|
137
|
+
* 告警只能走响应头;解不出来(头缺失、旧服务、非法 base64)就当没有告警 —— 绝不能
|
|
138
|
+
* 因为一个提示头解码失败就让整个渲染失败。
|
|
139
|
+
*/
|
|
140
|
+
function decodeWarnings(response: Response): string[] {
|
|
141
|
+
const raw = response.headers.get('x-pdfwright-warnings')
|
|
142
|
+
if (!raw) return []
|
|
143
|
+
try {
|
|
144
|
+
const binary = atob(raw.replace(/-/g, '+').replace(/_/g, '/'))
|
|
145
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0))
|
|
146
|
+
const parsed: unknown = JSON.parse(new TextDecoder().decode(bytes))
|
|
147
|
+
return Array.isArray(parsed) ? parsed.map((item) => String(item)).slice(0, 20) : []
|
|
148
|
+
} catch {
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface RenderResult<T> {
|
|
154
|
+
value: T
|
|
155
|
+
/** 渲染期告警(图片没加载出来 / 变量未定义 / 字符被忽略…),已去重截断 */
|
|
156
|
+
warnings: string[]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function renderHtml(
|
|
160
|
+
document: DocumentSchema,
|
|
161
|
+
data?: FormData,
|
|
162
|
+
): Promise<RenderResult<string>> {
|
|
133
163
|
const response = await fetch('/api/render/html', {
|
|
134
164
|
method: 'POST',
|
|
135
165
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -140,10 +170,10 @@ export async function renderHtml(document: DocumentSchema, data?: FormData): Pro
|
|
|
140
170
|
// 早前这里只拼「HTTP xxx」,把后端给的具体原因(模板语法错误/非法配置项)丢了
|
|
141
171
|
throw new Error(await errorMessage(response, 'HTML 预览失败'))
|
|
142
172
|
}
|
|
143
|
-
return response.text()
|
|
173
|
+
return { value: await response.text(), warnings: decodeWarnings(response) }
|
|
144
174
|
}
|
|
145
175
|
|
|
146
|
-
export async function renderPdf(document: DocumentSchema, data?: FormData): Promise<Blob
|
|
176
|
+
export async function renderPdf(document: DocumentSchema, data?: FormData): Promise<RenderResult<Blob>> {
|
|
147
177
|
const response = await fetch('/api/render/pdf', {
|
|
148
178
|
method: 'POST',
|
|
149
179
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -153,7 +183,7 @@ export async function renderPdf(document: DocumentSchema, data?: FormData): Prom
|
|
|
153
183
|
if (!response.ok) {
|
|
154
184
|
throw new Error(await errorMessage(response, 'PDF 生成失败'))
|
|
155
185
|
}
|
|
156
|
-
return response.blob()
|
|
186
|
+
return { value: await response.blob(), warnings: decodeWarnings(response) }
|
|
157
187
|
}
|
|
158
188
|
|
|
159
189
|
/** 模板需要的字段(服务端从模板 AST + 变量声明推断,与 mock 数据同源)。 */
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** 数据面板(属性面板的「数据」tab):示例(Mock)数据、示例图样式、推断提示与字段清单。
|
|
3
|
+
*
|
|
4
|
+
* 从 `PropsPanel.vue` 拆出来的:这里读写的是运行态 `state.data` 与 `/api/mock`,
|
|
5
|
+
* 与「选中元素的属性」毫无关系。样式基元见 `panel-shared.css`,本面板专属样式见
|
|
6
|
+
* `data-panel.css`。
|
|
7
|
+
*/
|
|
8
|
+
import { ref } from 'vue'
|
|
9
|
+
import * as api from '@/api/client'
|
|
10
|
+
import { ElMessage } from 'element-plus'
|
|
11
|
+
import { exportSchema, refreshPreview, setMockImages, state } from '@/store/designer'
|
|
12
|
+
|
|
13
|
+
const dataText = ref('')
|
|
14
|
+
const mockBusy = ref(false)
|
|
15
|
+
/** 服务端返回的「无法推断」提示(函数调用/动态下标等) */
|
|
16
|
+
const mockWarnings = ref<string[]>([])
|
|
17
|
+
/** 服务端推断出的字段清单(类型 + 嵌套结构) */
|
|
18
|
+
const mockVariables = ref<api.MockVariable[]>([])
|
|
19
|
+
|
|
20
|
+
function syncDataText(): void {
|
|
21
|
+
dataText.value = JSON.stringify(state.data ?? {}, null, 2)
|
|
22
|
+
}
|
|
23
|
+
syncDataText()
|
|
24
|
+
|
|
25
|
+
/** 输入时**防抖自动应用**:改 mock 数据后预览立刻跟着变,不必先点别处再点刷新。 */
|
|
26
|
+
let dataTimer: ReturnType<typeof setTimeout> | undefined
|
|
27
|
+
|
|
28
|
+
function scheduleDataApply(): void {
|
|
29
|
+
if (dataTimer) clearTimeout(dataTimer)
|
|
30
|
+
dataTimer = setTimeout(() => {
|
|
31
|
+
dataTimer = undefined
|
|
32
|
+
applyData()
|
|
33
|
+
}, 400)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function applyData(): void {
|
|
37
|
+
try {
|
|
38
|
+
state.data = dataText.value.trim() ? JSON.parse(dataText.value) : {}
|
|
39
|
+
} catch {
|
|
40
|
+
ElMessage.error('数据 JSON 不合法')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 数据 tab 的「刷新」:应用 JSON 并重新渲染当前视图(HTML/PDF 预览或设计画布)。 */
|
|
45
|
+
function refreshData(): void {
|
|
46
|
+
applyData()
|
|
47
|
+
syncDataText()
|
|
48
|
+
void refreshPreview()
|
|
49
|
+
ElMessage.success('已刷新预览')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 示例图(Mock 假图)形状选项:签章与普通图片各一套默认。 */
|
|
53
|
+
const IMAGE_SHAPES = [
|
|
54
|
+
{ value: 'cross', label: '框 + X(图片占位)' },
|
|
55
|
+
{ value: 'rect', label: '纯色块' },
|
|
56
|
+
{ value: 'seal', label: '方章(框 + 星)' },
|
|
57
|
+
{ value: 'ring', label: '圆章(环 + 星)' },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
/** 示例图样式(签章图 / 图片):改了就重新生成示例图(见 setMockImages)。 */
|
|
61
|
+
function mockImageShape(which: 'seal' | 'image'): string {
|
|
62
|
+
const fallback = which === 'seal' ? 'seal' : 'cross'
|
|
63
|
+
return state.mockImages?.[which]?.shape ?? fallback
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mockImageLine(which: 'seal' | 'image'): string {
|
|
67
|
+
const fallback = which === 'seal' ? '#c1121f' : '#64748b'
|
|
68
|
+
return state.mockImages?.[which]?.line ?? fallback
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function onMockImage(which: 'seal' | 'image', patch: { shape?: string; line?: string }): void {
|
|
72
|
+
const clean = Object.fromEntries(Object.entries(patch).filter(([, value]) => value))
|
|
73
|
+
void setMockImages({ [which]: clean as never })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 一键生成 Mock 数据:调 `/api/mock`(库侧 `pdfwright.mock`,按模板语义推断)。
|
|
77
|
+
*
|
|
78
|
+
* 前端**不再自己猜**(按变量类型生成的扁平数组会让内层循环取不到数据 → 内容行整片
|
|
79
|
+
* 消失);变量提取/循环/嵌套/条件分支/下拉合法值都在库侧一处实现,与导入自带的
|
|
80
|
+
* 示例数据同源。已有数据只补缺失,手填的值不会被覆盖。
|
|
81
|
+
*/
|
|
82
|
+
async function fillMockData(): Promise<void> {
|
|
83
|
+
mockBusy.value = true
|
|
84
|
+
try {
|
|
85
|
+
const result = await api.generateMockData(exportSchema(), state.data, 2, state.mockImages)
|
|
86
|
+
state.data = result.data
|
|
87
|
+
mockWarnings.value = result.warnings
|
|
88
|
+
mockVariables.value = result.variables ?? []
|
|
89
|
+
syncDataText()
|
|
90
|
+
void refreshPreview()
|
|
91
|
+
if (result.warnings.length) {
|
|
92
|
+
ElMessage.warning(`已生成 Mock 数据,${result.warnings.length} 处无法推断(见下方提示)`)
|
|
93
|
+
} else {
|
|
94
|
+
ElMessage.success('已按模板结构生成 Mock 数据(变量/循环/嵌套/条件)')
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
ElMessage.error(`生成 Mock 数据失败:${(error as Error).message}`)
|
|
98
|
+
} finally {
|
|
99
|
+
mockBusy.value = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<template>
|
|
105
|
+
<div class="prop-group">
|
|
106
|
+
<div class="data-actions">
|
|
107
|
+
<el-button size="small" type="primary" plain :loading="mockBusy" @click="fillMockData">按模板生成 Mock</el-button>
|
|
108
|
+
<el-button size="small" @click="refreshData">刷新预览</el-button>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="data-hint">
|
|
111
|
+
用于画布/HTML/PDF 预览的示例数据(不影响导出配置);按模板里的循环/条件/控件声明推断,已有值只补不覆盖
|
|
112
|
+
</div>
|
|
113
|
+
<div class="mock-images">
|
|
114
|
+
<div class="mock-images-title">示例图(签章 / 图片字段的假图,base64 内联)</div>
|
|
115
|
+
<div class="mock-image-row">
|
|
116
|
+
<span class="mock-image-label">签章图</span>
|
|
117
|
+
<el-select
|
|
118
|
+
size="small"
|
|
119
|
+
:model-value="mockImageShape('seal')"
|
|
120
|
+
@update:model-value="onMockImage('seal', { shape: String($event) })"
|
|
121
|
+
>
|
|
122
|
+
<el-option
|
|
123
|
+
v-for="item in IMAGE_SHAPES"
|
|
124
|
+
:key="item.value"
|
|
125
|
+
:label="item.label"
|
|
126
|
+
:value="item.value"
|
|
127
|
+
/>
|
|
128
|
+
</el-select>
|
|
129
|
+
<el-color-picker
|
|
130
|
+
size="small"
|
|
131
|
+
:model-value="mockImageLine('seal')"
|
|
132
|
+
@update:model-value="onMockImage('seal', { line: String($event) })"
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="mock-image-row">
|
|
136
|
+
<span class="mock-image-label">图片</span>
|
|
137
|
+
<el-select
|
|
138
|
+
size="small"
|
|
139
|
+
:model-value="mockImageShape('image')"
|
|
140
|
+
@update:model-value="onMockImage('image', { shape: String($event) })"
|
|
141
|
+
>
|
|
142
|
+
<el-option
|
|
143
|
+
v-for="item in IMAGE_SHAPES"
|
|
144
|
+
:key="item.value"
|
|
145
|
+
:label="item.label"
|
|
146
|
+
:value="item.value"
|
|
147
|
+
/>
|
|
148
|
+
</el-select>
|
|
149
|
+
<el-color-picker
|
|
150
|
+
size="small"
|
|
151
|
+
:model-value="mockImageLine('image')"
|
|
152
|
+
@update:model-value="onMockImage('image', { line: String($event) })"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="mock-images-hint">改样式会重新生成示例图(覆盖当前数据里的 PNG 假图)</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div v-if="mockWarnings.length" class="mock-warnings">
|
|
158
|
+
<div v-for="message in mockWarnings" :key="message">⚠️ {{ message }}</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div v-if="mockVariables.length" class="mock-variables">
|
|
161
|
+
<div class="mock-variables-title">模板需要的字段({{ mockVariables.length }} 个根变量)</div>
|
|
162
|
+
<div v-for="variable in mockVariables" :key="variable.name" class="mock-variable">
|
|
163
|
+
<span class="mock-variable-name">{{ variable.name }}</span>
|
|
164
|
+
<span class="mock-variable-type">: {{ variable.type }}</span>
|
|
165
|
+
<span v-if="variable.declared" class="mock-variable-declared">已声明</span>
|
|
166
|
+
<span v-else class="mock-variable-undeclared">未声明</span>
|
|
167
|
+
<div v-if="variable.fields?.length" class="mock-variable-fields">
|
|
168
|
+
<div v-for="field in variable.fields" :key="field.name" class="mock-variable">
|
|
169
|
+
<span class="mock-variable-name">{{ field.name }}</span>
|
|
170
|
+
<span class="mock-variable-type">: {{ field.type }}</span>
|
|
171
|
+
<div v-if="field.fields?.length" class="mock-variable-fields">
|
|
172
|
+
<div v-for="nested in field.fields" :key="nested.name" class="mock-variable">
|
|
173
|
+
<span class="mock-variable-name">{{ nested.name }}</span>
|
|
174
|
+
<span class="mock-variable-type">: {{ nested.type }}</span>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<el-input
|
|
182
|
+
v-model="dataText"
|
|
183
|
+
type="textarea"
|
|
184
|
+
:rows="16"
|
|
185
|
+
class="json-area"
|
|
186
|
+
@blur="applyData"
|
|
187
|
+
@input="scheduleDataApply"
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
</template>
|
|
191
|
+
|
|
192
|
+
<style scoped src="./panel-shared.css"></style>
|
|
193
|
+
<style scoped src="./data-panel.css"></style>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「配置 JSON」面板的**同步接线**守卫(E2E 抓到的真 bug)。
|
|
3
|
+
*
|
|
4
|
+
* 症状:面板长期显示首次挂载时的快照 —— 用户看到/编辑的是过期配置,点「应用」会用
|
|
5
|
+
* 过期内容**覆盖**新编辑(实测:改了「本行独立列宽」开关,面板里读到的还是改前的 schema)。
|
|
6
|
+
* 根因:`JsonPanel` 的 `sync()` 只在 setup 与 `watch(state.mode)` 时跑,而 `el-drawer`
|
|
7
|
+
* 的内容首次打开后常驻挂载 → 再打开不会重新同步。
|
|
8
|
+
*
|
|
9
|
+
* 这里断言的是**接线**(`defineExpose` + drawer 的 `@open`),不是运行时行为 ——
|
|
10
|
+
* SSR 渲染不出 `el-input` 的 value,行为由 `runtime/e2e/browser2.mjs` 在真浏览器里验。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from 'node:fs'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
import { dirname, join } from 'node:path'
|
|
16
|
+
import { describe, expect, it } from 'vitest'
|
|
17
|
+
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
|
|
20
|
+
describe('JsonPanel 同步接线', () => {
|
|
21
|
+
const panel = readFileSync(join(here, 'JsonPanel.vue'), 'utf8')
|
|
22
|
+
const app = readFileSync(join(here, '..', 'App.vue'), 'utf8')
|
|
23
|
+
|
|
24
|
+
it('JsonPanel 通过 defineExpose 暴露 sync', () => {
|
|
25
|
+
expect(panel).toMatch(/defineExpose\(\{\s*sync\s*\}\)/)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('App.vue 在抽屉 open 时调用它', () => {
|
|
29
|
+
expect(app).toMatch(/@open="jsonPanelRef\?\.sync\(\)"/)
|
|
30
|
+
expect(app).toMatch(/<JsonPanel ref="jsonPanelRef"/)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('sync 真的从 exportSchema/state.data 取当前值(不是缓存的旧快照)', () => {
|
|
34
|
+
expect(panel).toMatch(/schemaText\.value = JSON\.stringify\(exportSchema\(\)/)
|
|
35
|
+
expect(panel).toMatch(/dataText\.value = JSON\.stringify\(state\.data/)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -14,6 +14,13 @@ function sync(): void {
|
|
|
14
14
|
sync()
|
|
15
15
|
watch(() => state.mode, sync)
|
|
16
16
|
|
|
17
|
+
// 抽屉每次打开都要显示**当前**文档:`sync()` 原先只在 setup 与 `watch(state.mode)`
|
|
18
|
+
// 时跑,而 el-drawer 的内容首次打开后常驻挂载 → 面板会一直显示首次挂载时的快照。
|
|
19
|
+
// 后果有二:① 用户看到/编辑的是过期配置;② 点「应用」会用过期内容**覆盖**新编辑
|
|
20
|
+
// (E2E 实测:改完表格列宽开关,面板里读到的还是改之前的 schema)。
|
|
21
|
+
// App.vue 在 drawer 的 ``open`` 事件里调这里暴露的 ``sync``。
|
|
22
|
+
defineExpose({ sync })
|
|
23
|
+
|
|
17
24
|
function applySchema(): void {
|
|
18
25
|
try {
|
|
19
26
|
const parsed = JSON.parse(schemaText.value) as Record<string, unknown>
|