pdfwright-web 0.2.2__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.2.2 → pdfwright_web-0.4.0}/.gitignore +2 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/PKG-INFO +2 -1
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/package.json +1 -1
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/App.vue +147 -25
- pdfwright_web-0.4.0/frontend/src/api/client.spec.ts +88 -0
- pdfwright_web-0.4.0/frontend/src/api/client.ts +270 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/components/UnitInput.vue +2 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/designer/CanvasArea.vue +214 -54
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/designer/ComponentPalette.vue +8 -0
- pdfwright_web-0.4.0/frontend/src/designer/DataPanel.vue +193 -0
- pdfwright_web-0.4.0/frontend/src/designer/ElementTree.vue +94 -0
- pdfwright_web-0.4.0/frontend/src/designer/ElementTreeNode.vue +507 -0
- pdfwright_web-0.4.0/frontend/src/designer/JsonPanel.spec.ts +37 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/designer/JsonPanel.vue +11 -4
- pdfwright_web-0.4.0/frontend/src/designer/PagePanel.vue +407 -0
- pdfwright_web-0.4.0/frontend/src/designer/PropsPanel.spec.ts +617 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/designer/PropsPanel.vue +589 -595
- pdfwright_web-0.4.0/frontend/src/designer/data-panel.css +82 -0
- pdfwright_web-0.4.0/frontend/src/designer/drag-affordance.spec.ts +113 -0
- pdfwright_web-0.4.0/frontend/src/designer/dragState.ts +23 -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.4.0/frontend/src/designer/table-resize-affordance.spec.ts +111 -0
- pdfwright_web-0.4.0/frontend/src/designer/treeCollapse.spec.ts +63 -0
- pdfwright_web-0.4.0/frontend/src/designer/treeCollapse.ts +62 -0
- pdfwright_web-0.4.0/frontend/src/designer/treeDrag.ts +31 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/fill/FillView.vue +69 -5
- pdfwright_web-0.4.0/frontend/src/lib/colors.spec.ts +67 -0
- pdfwright_web-0.4.0/frontend/src/lib/colors.ts +78 -0
- pdfwright_web-0.4.0/frontend/src/lib/convert.spec.ts +57 -0
- pdfwright_web-0.4.0/frontend/src/lib/convert.ts +44 -0
- pdfwright_web-0.4.0/frontend/src/lib/drop.spec.ts +140 -0
- pdfwright_web-0.4.0/frontend/src/lib/drop.ts +93 -0
- pdfwright_web-0.4.0/frontend/src/lib/expand.spec.ts +586 -0
- pdfwright_web-0.4.0/frontend/src/lib/expand.ts +833 -0
- 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/json-prop.spec.ts +52 -0
- pdfwright_web-0.4.0/frontend/src/lib/json-prop.ts +46 -0
- pdfwright_web-0.4.0/frontend/src/lib/layout.spec.ts +123 -0
- pdfwright_web-0.4.0/frontend/src/lib/layout.ts +100 -0
- pdfwright_web-0.4.0/frontend/src/lib/markup.spec.ts +116 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/lib/markup.ts +92 -14
- pdfwright_web-0.4.0/frontend/src/lib/metadata.spec.ts +67 -0
- pdfwright_web-0.4.0/frontend/src/lib/metadata.ts +98 -0
- pdfwright_web-0.4.0/frontend/src/lib/pages.spec.ts +58 -0
- pdfwright_web-0.4.0/frontend/src/lib/pages.ts +60 -0
- pdfwright_web-0.4.0/frontend/src/lib/required.spec.ts +73 -0
- pdfwright_web-0.4.0/frontend/src/lib/required.ts +33 -0
- pdfwright_web-0.4.0/frontend/src/lib/snippet.spec.ts +67 -0
- pdfwright_web-0.4.0/frontend/src/lib/snippet.ts +56 -0
- pdfwright_web-0.4.0/frontend/src/lib/table-vectors.spec.ts +54 -0
- pdfwright_web-0.4.0/frontend/src/lib/table.spec.ts +581 -0
- pdfwright_web-0.4.0/frontend/src/lib/table.ts +838 -0
- pdfwright_web-0.4.0/frontend/src/lib/tableResize.spec.ts +111 -0
- pdfwright_web-0.4.0/frontend/src/lib/tableResize.ts +114 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/lib/template.ts +0 -12
- pdfwright_web-0.4.0/frontend/src/lib/tree.spec.ts +152 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/lib/tree.ts +73 -19
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/lib/units.ts +0 -5
- pdfwright_web-0.4.0/frontend/src/renderer/ElementRenderer.spec.ts +650 -0
- pdfwright_web-0.4.0/frontend/src/renderer/ElementRenderer.vue +1699 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/renderer/InlineText.vue +7 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/renderer/WatermarkLayer.vue +16 -2
- pdfwright_web-0.4.0/frontend/src/store/cell-ops-daily.spec.ts +119 -0
- pdfwright_web-0.4.0/frontend/src/store/cell-ops.spec.ts +256 -0
- pdfwright_web-0.4.0/frontend/src/store/designer.spec.ts +2069 -0
- pdfwright_web-0.4.0/frontend/src/store/designer.ts +1982 -0
- 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.2.2 → pdfwright_web-0.4.0}/frontend/src/types/schema.ts +62 -6
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/pyproject.toml +13 -2
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/__init__.py +1 -1
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/app.py +156 -29
- pdfwright_web-0.2.2/src/pdfwright_web/meta.py → pdfwright_web-0.4.0/src/pdfwright_web/component_catalog.py +216 -358
- pdfwright_web-0.4.0/src/pdfwright_web/component_props.py +756 -0
- 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.4.0/src/pdfwright_web/mock.py +1399 -0
- pdfwright_web-0.4.0/src/pdfwright_web/routers/batch.py +184 -0
- pdfwright_web-0.4.0/src/pdfwright_web/routers/codes.py +121 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/convert.py +38 -66
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/meta.py +7 -2
- pdfwright_web-0.4.0/src/pdfwright_web/routers/mock.py +44 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/render.py +71 -6
- pdfwright_web-0.4.0/src/pdfwright_web/routers/verify.py +131 -0
- pdfwright_web-0.4.0/src/pdfwright_web/schemas.py +115 -0
- pdfwright_web-0.4.0/src/pdfwright_web/static/assets/index-C7PqDIDc.js +88 -0
- pdfwright_web-0.2.2/src/pdfwright_web/static/assets/index-DrWAVhY-.css → pdfwright_web-0.4.0/src/pdfwright_web/static/assets/index-ds5v6EJJ.css +1 -1
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/static/index.html +2 -2
- pdfwright_web-0.4.0/src/pdfwright_web/uploads.py +83 -0
- pdfwright_web-0.4.0/tests/conftest.py +45 -0
- pdfwright_web-0.4.0/tests/test_api.py +1265 -0
- pdfwright_web-0.4.0/tests/test_font_consistency.py +186 -0
- pdfwright_web-0.4.0/tests/test_mock.py +819 -0
- 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.2.2/frontend/src/api/client.ts +0 -184
- pdfwright_web-0.2.2/frontend/src/designer/ElementTree.vue +0 -56
- pdfwright_web-0.2.2/frontend/src/designer/ElementTreeNode.vue +0 -115
- pdfwright_web-0.2.2/frontend/src/designer/PropsPanel.spec.ts +0 -115
- pdfwright_web-0.2.2/frontend/src/lib/expand.spec.ts +0 -104
- pdfwright_web-0.2.2/frontend/src/lib/expand.ts +0 -342
- pdfwright_web-0.2.2/frontend/src/lib/fonts.ts +0 -63
- pdfwright_web-0.2.2/frontend/src/lib/markup.spec.ts +0 -58
- pdfwright_web-0.2.2/frontend/src/lib/table.spec.ts +0 -159
- pdfwright_web-0.2.2/frontend/src/lib/table.ts +0 -223
- pdfwright_web-0.2.2/frontend/src/renderer/ElementRenderer.spec.ts +0 -214
- pdfwright_web-0.2.2/frontend/src/renderer/ElementRenderer.vue +0 -934
- pdfwright_web-0.2.2/frontend/src/store/designer.spec.ts +0 -584
- pdfwright_web-0.2.2/frontend/src/store/designer.ts +0 -1112
- pdfwright_web-0.2.2/src/pdfwright_web/routers/batch.py +0 -139
- pdfwright_web-0.2.2/src/pdfwright_web/routers/codes.py +0 -49
- pdfwright_web-0.2.2/src/pdfwright_web/routers/verify.py +0 -87
- pdfwright_web-0.2.2/src/pdfwright_web/schemas.py +0 -68
- pdfwright_web-0.2.2/src/pdfwright_web/static/assets/index-BgTOK7V4.js +0 -86
- pdfwright_web-0.2.2/tests/test_api.py +0 -640
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/README.md +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/env.d.ts +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/eslint.config.js +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/index.html +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/pnpm-lock.yaml +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/components/BoxInput.spec.ts +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/components/BoxInput.vue +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/lib/clone.ts +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/lib/download.ts +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/lib/format.spec.ts +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/lib/format.ts +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/lib/units.spec.ts +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/main.ts +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/src/styles/main.css +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/tsconfig.json +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/frontend/vite.config.ts +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/concurrency.py +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/__init__.py +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/examples.py +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/routers/options.py +0 -0
- {pdfwright_web-0.2.2 → pdfwright_web-0.4.0}/src/pdfwright_web/security_defaults.py +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pdfwright-web
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: pdfwright 的无状态 Web 服务:可视化配置编辑器 + HTML/PDF 预览 + 对外渲染 API(不存储任何数据与内容)
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
7
|
Requires-Dist: fastapi>=0.115
|
|
8
|
+
Requires-Dist: jinja2>=3.1
|
|
8
9
|
Requires-Dist: pdfwright>=0.2.1
|
|
9
10
|
Requires-Dist: pydantic>=2.9
|
|
10
11
|
Requires-Dist: python-multipart>=0.0.9
|
|
@@ -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,31 +10,91 @@ 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, importSchema, initialize, redo, setMode, state, undo } from '@/store/designer'
|
|
13
|
+
import { downloadPdf, exportSchema, extractLegacyData, importSchema, importSchemaJson, initialize, redo, removeSelected, restoreDraft, peekDraft, clearDraft, setMode, state, undo } from '@/store/designer'
|
|
14
|
+
import { downloadBlob } from '@/lib/download'
|
|
14
15
|
import type { DocumentSchema } from '@/types/schema'
|
|
15
16
|
|
|
16
17
|
const fileInput = ref<HTMLInputElement | null>(null)
|
|
17
18
|
const docxInput = ref<HTMLInputElement | null>(null)
|
|
18
19
|
const xlsxInput = ref<HTMLInputElement | null>(null)
|
|
20
|
+
//: 导入 Word 时是否把「文字 + 内联命令标签」(`记录人:{{sign:…}}`)排在同一行。
|
|
21
|
+
//: 默认 **块级**(各自独立成块,可自由拖拽),需要保留 Word 的同行排版时从下拉菜单选。
|
|
22
|
+
const docxInline = ref(false)
|
|
19
23
|
const jsonOpen = ref(false)
|
|
24
|
+
/** JSON 抽屉里的面板(``@open`` 时要让它重新同步,见 JsonPanel.sync 的注释)。 */
|
|
25
|
+
const jsonPanelRef = ref<InstanceType<typeof import('./designer/JsonPanel.vue')['default']> | null>(null)
|
|
20
26
|
const leftTab = ref('palette')
|
|
21
27
|
const examples = ref<ExampleItem[]>([])
|
|
28
|
+
/** 新构建探测轮询的定时器句柄(`onUnmounted` 里要清)。 */
|
|
29
|
+
let buildTimer: number | undefined
|
|
22
30
|
|
|
23
31
|
const errorCount = computed(() => state.issues.filter((issue) => issue.level === 'error').length)
|
|
24
32
|
const warnCount = computed(() => state.issues.filter((issue) => issue.level === 'warning').length)
|
|
25
33
|
|
|
26
34
|
onMounted(() => {
|
|
27
|
-
void
|
|
28
|
-
// 前端资源版本守护:服务端重建后旧页面引用的 hash 资源已被删除(表现为「样式全丢」),
|
|
29
|
-
// 这里主动提示刷新,而不是让用户对着坏页面排查
|
|
35
|
+
void bootstrap()
|
|
30
36
|
void watchBuild()
|
|
31
|
-
setInterval(() => void watchBuild(), 30000)
|
|
32
|
-
document.addEventListener('visibilitychange',
|
|
33
|
-
|
|
34
|
-
})
|
|
37
|
+
buildTimer = window.setInterval(() => void watchBuild(), 30000)
|
|
38
|
+
document.addEventListener('visibilitychange', onVisibility)
|
|
39
|
+
window.addEventListener('keydown', onKeydown)
|
|
35
40
|
void loadExamples()
|
|
36
41
|
})
|
|
37
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
|
+
|
|
52
|
+
/** 启动:先尝试恢复上次的草稿(刷新/误关不丢工作),用户可选择放弃。 */
|
|
53
|
+
async function bootstrap(): Promise<void> {
|
|
54
|
+
await initialize()
|
|
55
|
+
if (!peekDraft()) return
|
|
56
|
+
try {
|
|
57
|
+
await ElMessageBox.confirm('检测到上次未导出的草稿,是否恢复?', '恢复草稿', {
|
|
58
|
+
confirmButtonText: '恢复',
|
|
59
|
+
cancelButtonText: '放弃',
|
|
60
|
+
closeOnClickModal: false,
|
|
61
|
+
type: 'info',
|
|
62
|
+
})
|
|
63
|
+
restoreDraft()
|
|
64
|
+
ElMessage.success('已恢复上次的草稿')
|
|
65
|
+
} catch {
|
|
66
|
+
clearDraft()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function onVisibility(): void {
|
|
71
|
+
if (document.visibilityState === 'visible') void watchBuild()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 键盘快捷键:设计器该有的那几个(撤销/重做/删除)。
|
|
75
|
+
*
|
|
76
|
+
* 不在输入框里抢键(正在打字时不触发),与常见编辑器一致。
|
|
77
|
+
*/
|
|
78
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
79
|
+
if (state.mode !== 'design') return
|
|
80
|
+
const target = event.target as HTMLElement | null
|
|
81
|
+
const typing =
|
|
82
|
+
target instanceof HTMLInputElement ||
|
|
83
|
+
target instanceof HTMLTextAreaElement ||
|
|
84
|
+
target?.isContentEditable === true
|
|
85
|
+
const mod = event.metaKey || event.ctrlKey
|
|
86
|
+
if (mod && event.key.toLowerCase() === 'z' && !typing) {
|
|
87
|
+
event.preventDefault()
|
|
88
|
+
if (event.shiftKey) redo()
|
|
89
|
+
else undo()
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
if ((event.key === 'Delete' || event.key === 'Backspace') && !typing && state.selectedId) {
|
|
93
|
+
event.preventDefault()
|
|
94
|
+
removeSelected()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
38
98
|
/** 前端资源版本守护:服务端重建后,旧页面引用的 hash 资源已删除
|
|
39
99
|
* (表现为「样式/脚本全丢」),这里提示刷新而不是让用户对着坏页面排查。 */
|
|
40
100
|
let knownBuildId = ''
|
|
@@ -74,26 +134,51 @@ async function loadExamples(): Promise<void> {
|
|
|
74
134
|
function loadExample(id: string): void {
|
|
75
135
|
const item = examples.value.find((example) => example.id === id)
|
|
76
136
|
if (!item) return
|
|
77
|
-
|
|
78
|
-
|
|
137
|
+
// 覆盖当前编辑内容前确认(有历史栈可撤销,但不该拿用户的草稿赌)
|
|
138
|
+
void ElMessageBox.confirm(`加载示例「${item.title}」会替换当前页面内容,继续?`, '加载示例', {
|
|
139
|
+
confirmButtonText: '加载',
|
|
140
|
+
cancelButtonText: '取消',
|
|
141
|
+
type: 'warning',
|
|
142
|
+
})
|
|
143
|
+
.then(() => {
|
|
144
|
+
importSchema(item.schema, item.data ?? {})
|
|
145
|
+
ElMessage.success(`已加载示例:${item.title}`)
|
|
146
|
+
})
|
|
147
|
+
.catch(() => undefined)
|
|
79
148
|
}
|
|
80
149
|
|
|
81
150
|
function exportJson(): void {
|
|
82
151
|
const blob = new Blob([JSON.stringify(exportSchema(), null, 2)], { type: 'application/json' })
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
152
|
+
// 用统一的下载工具:blob URL 必须**延迟回收**(立即 revoke 会取消尚未开始的下载)
|
|
153
|
+
downloadBlob(blob, `${(state.schema.meta?.title as string) || 'page'}.json`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 导入文档(转换接口只返回配置 + 字段)→ 再单独要一份示例数据,保证「导入即可见效果」。
|
|
157
|
+
*
|
|
158
|
+
* 导入是**纯解析**:造示例数据是预览辅助能力,由 `/api/mock` 提供;拿不到也不阻断
|
|
159
|
+
* 导入(只提示),用户仍可在「数据」tab 里手填或再点「按模板生成 Mock」。
|
|
160
|
+
*/
|
|
161
|
+
async function importConverted(document: DocumentSchema): Promise<void> {
|
|
162
|
+
let data: Record<string, unknown> = {}
|
|
163
|
+
try {
|
|
164
|
+
data = (await api.generateMockData(document)).data
|
|
165
|
+
} catch (error) {
|
|
166
|
+
ElMessage.warning(`已导入配置;示例数据生成失败:${(error as Error).message}`)
|
|
167
|
+
}
|
|
168
|
+
importSchema(document, data)
|
|
89
169
|
}
|
|
90
170
|
|
|
91
171
|
async function onJsonFile(event: Event): Promise<void> {
|
|
92
172
|
const file = (event.target as HTMLInputElement).files?.[0]
|
|
93
173
|
if (!file) return
|
|
94
174
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
175
|
+
const parsed = JSON.parse(await file.text()) as Record<string, unknown>
|
|
176
|
+
// 旧版导出把示例数据嵌在配置顶层:剥到运行态,否则整篇字段全部渲染为空
|
|
177
|
+
const legacy = extractLegacyData(parsed)
|
|
178
|
+
importSchemaJson(parsed)
|
|
179
|
+
ElMessage.success(
|
|
180
|
+
legacy ? `页面配置已导入(含 ${Object.keys(legacy).length} 个示例数据字段,已载入「数据」)` : '页面配置已导入',
|
|
181
|
+
)
|
|
97
182
|
} catch (error) {
|
|
98
183
|
ElMessage.error(`导入失败:${(error as Error).message}`)
|
|
99
184
|
}
|
|
@@ -113,7 +198,7 @@ async function importHtml(): Promise<void> {
|
|
|
113
198
|
htmlBusy.value = true
|
|
114
199
|
try {
|
|
115
200
|
const result = await api.convertHtml(htmlSource.value)
|
|
116
|
-
|
|
201
|
+
await importConverted(result.document)
|
|
117
202
|
ElMessage.success(`已转换:${result.fields.length} 个字段`)
|
|
118
203
|
htmlDialog.value = false
|
|
119
204
|
} catch (error) {
|
|
@@ -127,14 +212,23 @@ async function onOfficeFile(event: Event, kind: 'docx' | 'xlsx'): Promise<void>
|
|
|
127
212
|
const file = (event.target as HTMLInputElement).files?.[0]
|
|
128
213
|
if (!file) return
|
|
129
214
|
try {
|
|
130
|
-
const result =
|
|
131
|
-
|
|
215
|
+
const result =
|
|
216
|
+
kind === 'docx'
|
|
217
|
+
? await api.convertDocx(file, docxInline.value)
|
|
218
|
+
: await api.convertXlsx(file)
|
|
219
|
+
await importConverted(result.document)
|
|
132
220
|
ElMessage.success(`已转换:${result.fields.length} 个字段`)
|
|
133
221
|
} catch (error) {
|
|
134
222
|
ElMessage.error(`转换失败:${(error as Error).message}`)
|
|
135
223
|
}
|
|
136
224
|
;(event.target as HTMLInputElement).value = ''
|
|
137
225
|
}
|
|
226
|
+
|
|
227
|
+
/** 「导入 Word」下拉:`inline` 决定同一段落里的文字与图片/签章位是否排在同一行。 */
|
|
228
|
+
function pickDocxFile(inline: boolean): void {
|
|
229
|
+
docxInline.value = inline
|
|
230
|
+
docxInput.value?.click()
|
|
231
|
+
}
|
|
138
232
|
</script>
|
|
139
233
|
|
|
140
234
|
<template>
|
|
@@ -164,6 +258,7 @@ async function onOfficeFile(event: Event, kind: 'docx' | 'xlsx'): Promise<void>
|
|
|
164
258
|
<div class="toolbar-spacer" />
|
|
165
259
|
|
|
166
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>
|
|
167
262
|
<span v-else-if="warnCount" class="issue-badge warn">{{ warnCount }} 个提示</span>
|
|
168
263
|
|
|
169
264
|
<el-button size="small" :disabled="state.historyIndex <= 0" @click="undo">撤销</el-button>
|
|
@@ -171,7 +266,15 @@ async function onOfficeFile(event: Event, kind: 'docx' | 'xlsx'): Promise<void>
|
|
|
171
266
|
<el-button size="small" @click="jsonOpen = true">配置 JSON</el-button>
|
|
172
267
|
<el-button size="small" @click="exportJson">导出</el-button>
|
|
173
268
|
<el-button size="small" @click="fileInput?.click()">导入 JSON</el-button>
|
|
174
|
-
<el-button size="small" @click="
|
|
269
|
+
<el-dropdown split-button size="small" trigger="click" @click="pickDocxFile(false)" @command="pickDocxFile">
|
|
270
|
+
导入 Word
|
|
271
|
+
<template #dropdown>
|
|
272
|
+
<el-dropdown-menu>
|
|
273
|
+
<el-dropdown-item :command="false">块级(默认):文字与图片/签章各自成块</el-dropdown-item>
|
|
274
|
+
<el-dropdown-item :command="true">同排:文字与图片/签章排在同一行</el-dropdown-item>
|
|
275
|
+
</el-dropdown-menu>
|
|
276
|
+
</template>
|
|
277
|
+
</el-dropdown>
|
|
175
278
|
<el-button size="small" @click="xlsxInput?.click()">导入 Excel</el-button>
|
|
176
279
|
<el-button size="small" @click="htmlDialog = true">导入 HTML</el-button>
|
|
177
280
|
<el-button size="small" type="primary" :loading="state.busy" @click="downloadPdf">生成 PDF</el-button>
|
|
@@ -203,6 +306,12 @@ async function onOfficeFile(event: Event, kind: 'docx' | 'xlsx'): Promise<void>
|
|
|
203
306
|
ⓘ {{ issue.path }}:{{ issue.message }}
|
|
204
307
|
</span>
|
|
205
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>
|
|
206
315
|
|
|
207
316
|
<main class="main">
|
|
208
317
|
<template v-if="state.mode === 'design'">
|
|
@@ -222,8 +331,14 @@ async function onOfficeFile(event: Event, kind: 'docx' | 'xlsx'): Promise<void>
|
|
|
222
331
|
<FillView v-else />
|
|
223
332
|
</main>
|
|
224
333
|
|
|
225
|
-
<el-drawer
|
|
226
|
-
|
|
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" />
|
|
227
342
|
</el-drawer>
|
|
228
343
|
</div>
|
|
229
344
|
</template>
|
|
@@ -304,6 +419,13 @@ async function onOfficeFile(event: Event, kind: 'docx' | 'xlsx'): Promise<void>
|
|
|
304
419
|
color: #b45309;
|
|
305
420
|
border-bottom-color: #fde68a;
|
|
306
421
|
}
|
|
422
|
+
/* 渲染告警:用中性橙底与静态校验的黄色条区分(一个来自配置、一个来自这次渲染) */
|
|
423
|
+
.issue-bar.render-warn {
|
|
424
|
+
background: #fff7ed;
|
|
425
|
+
color: #c2410c;
|
|
426
|
+
border-bottom-color: #fed7aa;
|
|
427
|
+
align-items: center;
|
|
428
|
+
}
|
|
307
429
|
.example-item {
|
|
308
430
|
padding: 2px 0;
|
|
309
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
|
+
})
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/** 后端 API 客户端(同源 /api;开发模式下由 Vite 代理到 pdfwright-web)。 */
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ComponentsPayload,
|
|
5
|
+
DocumentSchema,
|
|
6
|
+
FormData,
|
|
7
|
+
MockImageStyles,
|
|
8
|
+
OptionItem,
|
|
9
|
+
OptionsConfig,
|
|
10
|
+
} from '@/types/schema'
|
|
11
|
+
|
|
12
|
+
export interface ValidationIssue {
|
|
13
|
+
level: 'error' | 'warning'
|
|
14
|
+
message: string
|
|
15
|
+
path: string
|
|
16
|
+
code: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ValidationResponse {
|
|
20
|
+
ok: boolean
|
|
21
|
+
issues: ValidationIssue[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ExampleItem {
|
|
25
|
+
id: string
|
|
26
|
+
title: string
|
|
27
|
+
description: string
|
|
28
|
+
variables: number
|
|
29
|
+
blocks: number
|
|
30
|
+
schema: DocumentSchema
|
|
31
|
+
/** 示例数据(与 schema 分离:schema 不再内嵌 data) */
|
|
32
|
+
data?: FormData | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ConvertResponse {
|
|
36
|
+
/** 纯解析结果:配置 + 提取到的字段(**不含示例数据**) */
|
|
37
|
+
document: DocumentSchema
|
|
38
|
+
fields: { name: string; label: string; kind: string }[]
|
|
39
|
+
warnings?: string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 请求超时(毫秒):长文档渲染/大文件转换也不例外 —— 没有超时时 UI 会一直转圈。 */
|
|
43
|
+
const REQUEST_TIMEOUT_MS = 120_000
|
|
44
|
+
|
|
45
|
+
/** 错误响应体里是否带可直接展示的消息(后端统一返回 {error_code, message, detail})。 */
|
|
46
|
+
async function errorMessage(response: Response, fallback: string): Promise<string> {
|
|
47
|
+
try {
|
|
48
|
+
const body = (await response.json()) as { message?: string }
|
|
49
|
+
if (body?.message) return body.message
|
|
50
|
+
} catch {
|
|
51
|
+
try {
|
|
52
|
+
const text = await response.text()
|
|
53
|
+
if (text.trim()) return text.trim().slice(0, 300)
|
|
54
|
+
} catch {
|
|
55
|
+
/* 忽略 */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return `${fallback}(HTTP ${response.status})`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function jsonRequest<T>(url: string, init?: RequestInit): Promise<T> {
|
|
62
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), ...init })
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(await errorMessage(response, '请求失败'))
|
|
65
|
+
}
|
|
66
|
+
return (await response.json()) as T
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function fetchComponents(): Promise<ComponentsPayload> {
|
|
70
|
+
return jsonRequest<ComponentsPayload>('/api/meta/components')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 服务端前端构建标识(用于提示「有新版本,请刷新」)。 */
|
|
74
|
+
export function fetchBuildId(): Promise<{ buildId: string }> {
|
|
75
|
+
return jsonRequest<{ buildId: string }>('/api/version')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function fetchExamples(): Promise<{ examples: ExampleItem[]; count: number }> {
|
|
79
|
+
return jsonRequest<{ examples: ExampleItem[]; count: number }>('/api/examples')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface FontAlias {
|
|
83
|
+
alias: string
|
|
84
|
+
family: string
|
|
85
|
+
resolved?: string | null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SemanticFont {
|
|
89
|
+
family: string
|
|
90
|
+
label: string
|
|
91
|
+
css?: string | null
|
|
92
|
+
category?: string | null
|
|
93
|
+
requiresCjk?: boolean
|
|
94
|
+
resolved?: string | null
|
|
95
|
+
available: boolean
|
|
96
|
+
exact: boolean
|
|
97
|
+
/** 字体自然行高倍数(后端按字体度量算,前端据此还原行距) */
|
|
98
|
+
lineRatio?: number | null
|
|
99
|
+
/** 字体**排版**行高倍数(OS/2 win):判断文档网格吸附用(后端按字体度量算) */
|
|
100
|
+
lineRatioWin?: number | null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface SearchableFont {
|
|
104
|
+
family: string
|
|
105
|
+
aliases: string[]
|
|
106
|
+
category: string
|
|
107
|
+
has_cjk: boolean
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface FontInfo {
|
|
111
|
+
families: string[]
|
|
112
|
+
count: number
|
|
113
|
+
aliases: FontAlias[]
|
|
114
|
+
semantic: SemanticFont[]
|
|
115
|
+
searchable: SearchableFont[]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function fetchFonts(): Promise<FontInfo> {
|
|
119
|
+
return jsonRequest<FontInfo>('/api/meta/fonts')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function validateDocument(
|
|
123
|
+
document: DocumentSchema,
|
|
124
|
+
data?: FormData,
|
|
125
|
+
): Promise<ValidationResponse> {
|
|
126
|
+
return jsonRequest<ValidationResponse>('/api/validate', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
// 传了 data 时后端会额外校验「必填项是否已填」(required_missing)
|
|
130
|
+
body: JSON.stringify({ document, data: data ?? null }),
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
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>> {
|
|
163
|
+
const response = await fetch('/api/render/html', {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: { 'Content-Type': 'application/json' },
|
|
166
|
+
body: JSON.stringify({ document, data: data ?? null }),
|
|
167
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
168
|
+
})
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
// 早前这里只拼「HTTP xxx」,把后端给的具体原因(模板语法错误/非法配置项)丢了
|
|
171
|
+
throw new Error(await errorMessage(response, 'HTML 预览失败'))
|
|
172
|
+
}
|
|
173
|
+
return { value: await response.text(), warnings: decodeWarnings(response) }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function renderPdf(document: DocumentSchema, data?: FormData): Promise<RenderResult<Blob>> {
|
|
177
|
+
const response = await fetch('/api/render/pdf', {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
body: JSON.stringify({ document, data: data ?? null }),
|
|
181
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
182
|
+
})
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
throw new Error(await errorMessage(response, 'PDF 生成失败'))
|
|
185
|
+
}
|
|
186
|
+
return { value: await response.blob(), warnings: decodeWarnings(response) }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** 模板需要的字段(服务端从模板 AST + 变量声明推断,与 mock 数据同源)。 */
|
|
190
|
+
export interface MockVariable {
|
|
191
|
+
name: string
|
|
192
|
+
type: string
|
|
193
|
+
/** 是否已在 `variables` 里声明 */
|
|
194
|
+
declared: boolean
|
|
195
|
+
label?: string
|
|
196
|
+
/** 数组/对象的字段(递归同结构) */
|
|
197
|
+
fields?: MockVariable[]
|
|
198
|
+
/** 标量数组的元素类型 */
|
|
199
|
+
items?: string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface MockResponse {
|
|
203
|
+
data: FormData
|
|
204
|
+
/** 无法推断的引用(函数调用/动态下标等)—— 提示用户而不是静默缺字段 */
|
|
205
|
+
warnings: string[]
|
|
206
|
+
/** 变量清单:类型 + 嵌套结构(设计器「数据」tab 展示,比前端正则扫描准) */
|
|
207
|
+
variables: MockVariable[]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 按模板语义生成示例数据(库侧 `pdfwright.mock`;设计器不再自己猜)。
|
|
211
|
+
*
|
|
212
|
+
* 已有数据会**只补缺失**:手填的值不会被覆盖。
|
|
213
|
+
*/
|
|
214
|
+
export function generateMockData(
|
|
215
|
+
document: DocumentSchema,
|
|
216
|
+
data?: FormData,
|
|
217
|
+
samples = 2,
|
|
218
|
+
images?: MockImageStyles,
|
|
219
|
+
): Promise<MockResponse> {
|
|
220
|
+
return jsonRequest<MockResponse>('/api/mock', {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: { 'Content-Type': 'application/json' },
|
|
223
|
+
body: JSON.stringify({ document, data: data ?? null, samples, images: images ?? null }),
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function fetchOptions(config: OptionsConfig): Promise<OptionItem[]> {
|
|
228
|
+
if (config.type === 'static') return config.items ?? []
|
|
229
|
+
const payload = await jsonRequest<{ items: OptionItem[] }>('/api/options/fetch', {
|
|
230
|
+
method: 'POST',
|
|
231
|
+
headers: { 'Content-Type': 'application/json' },
|
|
232
|
+
body: JSON.stringify({
|
|
233
|
+
url: config.url,
|
|
234
|
+
method: config.method ?? 'GET',
|
|
235
|
+
params: config.params ?? null,
|
|
236
|
+
headers: config.headers ?? null,
|
|
237
|
+
body: config.body ?? null,
|
|
238
|
+
path: config.path ?? null,
|
|
239
|
+
valueKey: config.valueKey ?? 'value',
|
|
240
|
+
labelKey: config.labelKey ?? 'label',
|
|
241
|
+
}),
|
|
242
|
+
})
|
|
243
|
+
return payload.items
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function uploadConvert(url: string, file: File): Promise<ConvertResponse> {
|
|
247
|
+
const form = new FormData()
|
|
248
|
+
form.append('file', file)
|
|
249
|
+
return jsonRequest<ConvertResponse>(url, { method: 'POST', body: form })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function convertDocx(file: File, inlineCommands = false): Promise<ConvertResponse> {
|
|
253
|
+
// ``inlineCommands``:同一段落里的「文字 + 内联命令标签」排在同一行
|
|
254
|
+
// (默认 false = 块级,各自独立成块;见 docs/word-import.md)
|
|
255
|
+
const query = inlineCommands ? '?inline_commands=true' : ''
|
|
256
|
+
return uploadConvert(`/api/convert/docx${query}`, file)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function convertXlsx(file: File): Promise<ConvertResponse> {
|
|
260
|
+
return uploadConvert('/api/convert/xlsx', file)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** HTML 富文本 → schema(贴 HTML 片段,不落盘)。 */
|
|
264
|
+
export function convertHtml(html: string, autoField = true): Promise<ConvertResponse> {
|
|
265
|
+
return jsonRequest<ConvertResponse>('/api/convert/html', {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
268
|
+
body: JSON.stringify({ html, auto_field: autoField }),
|
|
269
|
+
})
|
|
270
|
+
}
|