pdfwright-web 0.2.2__tar.gz → 0.3.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.
Files changed (115) hide show
  1. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/.gitignore +2 -0
  2. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/PKG-INFO +2 -1
  3. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/package.json +1 -1
  4. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/App.vue +110 -21
  5. pdfwright_web-0.3.0/frontend/src/api/client.spec.ts +31 -0
  6. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/api/client.ts +79 -23
  7. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/components/UnitInput.vue +2 -0
  8. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/designer/CanvasArea.vue +214 -54
  9. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/designer/ComponentPalette.vue +8 -0
  10. pdfwright_web-0.3.0/frontend/src/designer/ElementTree.vue +94 -0
  11. pdfwright_web-0.3.0/frontend/src/designer/ElementTreeNode.vue +507 -0
  12. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/designer/JsonPanel.vue +4 -4
  13. pdfwright_web-0.3.0/frontend/src/designer/PropsPanel.spec.ts +542 -0
  14. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/designer/PropsPanel.vue +933 -52
  15. pdfwright_web-0.3.0/frontend/src/designer/drag-affordance.spec.ts +113 -0
  16. pdfwright_web-0.3.0/frontend/src/designer/dragState.ts +23 -0
  17. pdfwright_web-0.3.0/frontend/src/designer/table-resize-affordance.spec.ts +111 -0
  18. pdfwright_web-0.3.0/frontend/src/designer/treeCollapse.spec.ts +63 -0
  19. pdfwright_web-0.3.0/frontend/src/designer/treeCollapse.ts +62 -0
  20. pdfwright_web-0.3.0/frontend/src/designer/treeDrag.ts +31 -0
  21. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/fill/FillView.vue +65 -3
  22. pdfwright_web-0.3.0/frontend/src/lib/colors.spec.ts +67 -0
  23. pdfwright_web-0.3.0/frontend/src/lib/colors.ts +78 -0
  24. pdfwright_web-0.3.0/frontend/src/lib/convert.spec.ts +57 -0
  25. pdfwright_web-0.3.0/frontend/src/lib/convert.ts +44 -0
  26. pdfwright_web-0.3.0/frontend/src/lib/drop.spec.ts +140 -0
  27. pdfwright_web-0.3.0/frontend/src/lib/drop.ts +93 -0
  28. pdfwright_web-0.3.0/frontend/src/lib/expand.spec.ts +586 -0
  29. pdfwright_web-0.3.0/frontend/src/lib/expand.ts +837 -0
  30. pdfwright_web-0.3.0/frontend/src/lib/json-prop.spec.ts +52 -0
  31. pdfwright_web-0.3.0/frontend/src/lib/json-prop.ts +46 -0
  32. pdfwright_web-0.3.0/frontend/src/lib/layout.spec.ts +123 -0
  33. pdfwright_web-0.3.0/frontend/src/lib/layout.ts +100 -0
  34. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/markup.ts +12 -2
  35. pdfwright_web-0.3.0/frontend/src/lib/metadata.spec.ts +67 -0
  36. pdfwright_web-0.3.0/frontend/src/lib/metadata.ts +98 -0
  37. pdfwright_web-0.3.0/frontend/src/lib/pages.spec.ts +58 -0
  38. pdfwright_web-0.3.0/frontend/src/lib/pages.ts +60 -0
  39. pdfwright_web-0.3.0/frontend/src/lib/required.spec.ts +73 -0
  40. pdfwright_web-0.3.0/frontend/src/lib/required.ts +33 -0
  41. pdfwright_web-0.3.0/frontend/src/lib/snippet.spec.ts +67 -0
  42. pdfwright_web-0.3.0/frontend/src/lib/snippet.ts +56 -0
  43. pdfwright_web-0.3.0/frontend/src/lib/table.spec.ts +502 -0
  44. pdfwright_web-0.3.0/frontend/src/lib/table.ts +800 -0
  45. pdfwright_web-0.3.0/frontend/src/lib/tableResize.spec.ts +111 -0
  46. pdfwright_web-0.3.0/frontend/src/lib/tableResize.ts +122 -0
  47. pdfwright_web-0.3.0/frontend/src/lib/tree.spec.ts +152 -0
  48. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/tree.ts +73 -19
  49. pdfwright_web-0.3.0/frontend/src/renderer/ElementRenderer.spec.ts +593 -0
  50. pdfwright_web-0.3.0/frontend/src/renderer/ElementRenderer.vue +1658 -0
  51. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/renderer/WatermarkLayer.vue +16 -2
  52. pdfwright_web-0.3.0/frontend/src/store/cell-ops-daily.spec.ts +119 -0
  53. pdfwright_web-0.3.0/frontend/src/store/cell-ops.spec.ts +256 -0
  54. pdfwright_web-0.3.0/frontend/src/store/designer.spec.ts +1906 -0
  55. pdfwright_web-0.3.0/frontend/src/store/designer.ts +2191 -0
  56. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/types/schema.ts +48 -6
  57. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/pyproject.toml +8 -1
  58. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/__init__.py +1 -1
  59. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/app.py +97 -17
  60. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/meta.py +529 -34
  61. pdfwright_web-0.3.0/src/pdfwright_web/mock.py +1286 -0
  62. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/batch.py +73 -35
  63. pdfwright_web-0.3.0/src/pdfwright_web/routers/codes.py +75 -0
  64. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/convert.py +38 -66
  65. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/meta.py +2 -0
  66. pdfwright_web-0.3.0/src/pdfwright_web/routers/mock.py +37 -0
  67. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/render.py +32 -5
  68. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/verify.py +43 -25
  69. pdfwright_web-0.3.0/src/pdfwright_web/schemas.py +115 -0
  70. pdfwright_web-0.2.2/src/pdfwright_web/static/assets/index-DrWAVhY-.css → pdfwright_web-0.3.0/src/pdfwright_web/static/assets/index-CrZ4dGgx.css +1 -1
  71. pdfwright_web-0.3.0/src/pdfwright_web/static/assets/index-K7XXzc42.js +86 -0
  72. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/static/index.html +2 -2
  73. pdfwright_web-0.3.0/src/pdfwright_web/uploads.py +82 -0
  74. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/tests/test_api.py +432 -1
  75. pdfwright_web-0.3.0/tests/test_mock.py +814 -0
  76. pdfwright_web-0.2.2/frontend/src/designer/ElementTree.vue +0 -56
  77. pdfwright_web-0.2.2/frontend/src/designer/ElementTreeNode.vue +0 -115
  78. pdfwright_web-0.2.2/frontend/src/designer/PropsPanel.spec.ts +0 -115
  79. pdfwright_web-0.2.2/frontend/src/lib/expand.spec.ts +0 -104
  80. pdfwright_web-0.2.2/frontend/src/lib/expand.ts +0 -342
  81. pdfwright_web-0.2.2/frontend/src/lib/table.spec.ts +0 -159
  82. pdfwright_web-0.2.2/frontend/src/lib/table.ts +0 -223
  83. pdfwright_web-0.2.2/frontend/src/renderer/ElementRenderer.spec.ts +0 -214
  84. pdfwright_web-0.2.2/frontend/src/renderer/ElementRenderer.vue +0 -934
  85. pdfwright_web-0.2.2/frontend/src/store/designer.spec.ts +0 -584
  86. pdfwright_web-0.2.2/frontend/src/store/designer.ts +0 -1112
  87. pdfwright_web-0.2.2/src/pdfwright_web/routers/codes.py +0 -49
  88. pdfwright_web-0.2.2/src/pdfwright_web/schemas.py +0 -68
  89. pdfwright_web-0.2.2/src/pdfwright_web/static/assets/index-BgTOK7V4.js +0 -86
  90. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/README.md +0 -0
  91. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/env.d.ts +0 -0
  92. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/eslint.config.js +0 -0
  93. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/index.html +0 -0
  94. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/pnpm-lock.yaml +0 -0
  95. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/components/BoxInput.spec.ts +0 -0
  96. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/components/BoxInput.vue +0 -0
  97. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/clone.ts +0 -0
  98. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/download.ts +0 -0
  99. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/fonts.ts +0 -0
  100. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/format.spec.ts +0 -0
  101. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/format.ts +0 -0
  102. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/markup.spec.ts +0 -0
  103. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/template.ts +0 -0
  104. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/units.spec.ts +0 -0
  105. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/lib/units.ts +0 -0
  106. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/main.ts +0 -0
  107. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/renderer/InlineText.vue +0 -0
  108. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/src/styles/main.css +0 -0
  109. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/tsconfig.json +0 -0
  110. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/frontend/vite.config.ts +0 -0
  111. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/concurrency.py +0 -0
  112. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/__init__.py +0 -0
  113. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/examples.py +0 -0
  114. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/options.py +0 -0
  115. {pdfwright_web-0.2.2 → pdfwright_web-0.3.0}/src/pdfwright_web/security_defaults.py +0 -0
@@ -31,3 +31,5 @@ pdfwright/examples/output/
31
31
 
32
32
  # 本地发布凭据(scripts/publish.sh --config 默认读它)
33
33
  .publish.env
34
+
35
+ runtime/
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pdfwright-web
3
- Version: 0.2.2
3
+ Version: 0.3.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,7 +1,7 @@
1
1
  {
2
2
  "name": "pdfwright-designer",
3
3
  "private": true,
4
- "version": "0.2.2",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -10,12 +10,16 @@ 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, hasDraft, 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)
20
24
  const leftTab = ref('palette')
21
25
  const examples = ref<ExampleItem[]>([])
@@ -24,17 +28,60 @@ const errorCount = computed(() => state.issues.filter((issue) => issue.level ===
24
28
  const warnCount = computed(() => state.issues.filter((issue) => issue.level === 'warning').length)
25
29
 
26
30
  onMounted(() => {
27
- void initialize()
28
- // 前端资源版本守护:服务端重建后旧页面引用的 hash 资源已被删除(表现为「样式全丢」),
29
- // 这里主动提示刷新,而不是让用户对着坏页面排查
31
+ void bootstrap()
30
32
  void watchBuild()
31
33
  setInterval(() => void watchBuild(), 30000)
32
- document.addEventListener('visibilitychange', () => {
33
- if (document.visibilityState === 'visible') void watchBuild()
34
- })
34
+ document.addEventListener('visibilitychange', onVisibility)
35
+ window.addEventListener('keydown', onKeydown)
35
36
  void loadExamples()
36
37
  })
37
38
 
39
+ /** 启动:先尝试恢复上次的草稿(刷新/误关不丢工作),用户可选择放弃。 */
40
+ async function bootstrap(): Promise<void> {
41
+ await initialize()
42
+ if (!hasDraft()) return
43
+ try {
44
+ await ElMessageBox.confirm('检测到上次未导出的草稿,是否恢复?', '恢复草稿', {
45
+ confirmButtonText: '恢复',
46
+ cancelButtonText: '放弃',
47
+ closeOnClickModal: false,
48
+ type: 'info',
49
+ })
50
+ restoreDraft()
51
+ ElMessage.success('已恢复上次的草稿')
52
+ } catch {
53
+ clearDraft()
54
+ }
55
+ }
56
+
57
+ function onVisibility(): void {
58
+ if (document.visibilityState === 'visible') void watchBuild()
59
+ }
60
+
61
+ /** 键盘快捷键:设计器该有的那几个(撤销/重做/删除)。
62
+ *
63
+ * 不在输入框里抢键(正在打字时不触发),与常见编辑器一致。
64
+ */
65
+ function onKeydown(event: KeyboardEvent): void {
66
+ if (state.mode !== 'design') return
67
+ const target = event.target as HTMLElement | null
68
+ const typing =
69
+ target instanceof HTMLInputElement ||
70
+ target instanceof HTMLTextAreaElement ||
71
+ target?.isContentEditable === true
72
+ const mod = event.metaKey || event.ctrlKey
73
+ if (mod && event.key.toLowerCase() === 'z' && !typing) {
74
+ event.preventDefault()
75
+ if (event.shiftKey) redo()
76
+ else undo()
77
+ return
78
+ }
79
+ if ((event.key === 'Delete' || event.key === 'Backspace') && !typing && state.selectedId) {
80
+ event.preventDefault()
81
+ removeSelected()
82
+ }
83
+ }
84
+
38
85
  /** 前端资源版本守护:服务端重建后,旧页面引用的 hash 资源已删除
39
86
  * (表现为「样式/脚本全丢」),这里提示刷新而不是让用户对着坏页面排查。 */
40
87
  let knownBuildId = ''
@@ -74,26 +121,51 @@ async function loadExamples(): Promise<void> {
74
121
  function loadExample(id: string): void {
75
122
  const item = examples.value.find((example) => example.id === id)
76
123
  if (!item) return
77
- importSchema(item.schema, item.data ?? {})
78
- ElMessage.success(`已加载示例:${item.title}`)
124
+ // 覆盖当前编辑内容前确认(有历史栈可撤销,但不该拿用户的草稿赌)
125
+ void ElMessageBox.confirm(`加载示例「${item.title}」会替换当前页面内容,继续?`, '加载示例', {
126
+ confirmButtonText: '加载',
127
+ cancelButtonText: '取消',
128
+ type: 'warning',
129
+ })
130
+ .then(() => {
131
+ importSchema(item.schema, item.data ?? {})
132
+ ElMessage.success(`已加载示例:${item.title}`)
133
+ })
134
+ .catch(() => undefined)
79
135
  }
80
136
 
81
137
  function exportJson(): void {
82
138
  const blob = new Blob([JSON.stringify(exportSchema(), null, 2)], { type: 'application/json' })
83
- const url = URL.createObjectURL(blob)
84
- const anchor = document.createElement('a')
85
- anchor.href = url
86
- anchor.download = `${(state.schema.meta?.title as string) || 'page'}.json`
87
- anchor.click()
88
- URL.revokeObjectURL(url)
139
+ // 用统一的下载工具:blob URL 必须**延迟回收**(立即 revoke 会取消尚未开始的下载)
140
+ downloadBlob(blob, `${(state.schema.meta?.title as string) || 'page'}.json`)
141
+ }
142
+
143
+ /** 导入文档(转换接口只返回配置 + 字段)→ 再单独要一份示例数据,保证「导入即可见效果」。
144
+ *
145
+ * 导入是**纯解析**:造示例数据是预览辅助能力,由 `/api/mock` 提供;拿不到也不阻断
146
+ * 导入(只提示),用户仍可在「数据」tab 里手填或再点「按模板生成 Mock」。
147
+ */
148
+ async function importConverted(document: DocumentSchema): Promise<void> {
149
+ let data: Record<string, unknown> = {}
150
+ try {
151
+ data = (await api.generateMockData(document)).data
152
+ } catch (error) {
153
+ ElMessage.warning(`已导入配置;示例数据生成失败:${(error as Error).message}`)
154
+ }
155
+ importSchema(document, data)
89
156
  }
90
157
 
91
158
  async function onJsonFile(event: Event): Promise<void> {
92
159
  const file = (event.target as HTMLInputElement).files?.[0]
93
160
  if (!file) return
94
161
  try {
95
- importSchema(JSON.parse(await file.text()) as DocumentSchema)
96
- ElMessage.success('页面配置已导入')
162
+ const parsed = JSON.parse(await file.text()) as Record<string, unknown>
163
+ // 旧版导出把示例数据嵌在配置顶层:剥到运行态,否则整篇字段全部渲染为空
164
+ const legacy = extractLegacyData(parsed)
165
+ importSchemaJson(parsed)
166
+ ElMessage.success(
167
+ legacy ? `页面配置已导入(含 ${Object.keys(legacy).length} 个示例数据字段,已载入「数据」)` : '页面配置已导入',
168
+ )
97
169
  } catch (error) {
98
170
  ElMessage.error(`导入失败:${(error as Error).message}`)
99
171
  }
@@ -113,7 +185,7 @@ async function importHtml(): Promise<void> {
113
185
  htmlBusy.value = true
114
186
  try {
115
187
  const result = await api.convertHtml(htmlSource.value)
116
- importSchema(result.document, result.data ?? {})
188
+ await importConverted(result.document)
117
189
  ElMessage.success(`已转换:${result.fields.length} 个字段`)
118
190
  htmlDialog.value = false
119
191
  } catch (error) {
@@ -127,14 +199,23 @@ async function onOfficeFile(event: Event, kind: 'docx' | 'xlsx'): Promise<void>
127
199
  const file = (event.target as HTMLInputElement).files?.[0]
128
200
  if (!file) return
129
201
  try {
130
- const result = kind === 'docx' ? await api.convertDocx(file) : await api.convertXlsx(file)
131
- importSchema(result.document, result.data ?? {})
202
+ const result =
203
+ kind === 'docx'
204
+ ? await api.convertDocx(file, docxInline.value)
205
+ : await api.convertXlsx(file)
206
+ await importConverted(result.document)
132
207
  ElMessage.success(`已转换:${result.fields.length} 个字段`)
133
208
  } catch (error) {
134
209
  ElMessage.error(`转换失败:${(error as Error).message}`)
135
210
  }
136
211
  ;(event.target as HTMLInputElement).value = ''
137
212
  }
213
+
214
+ /** 「导入 Word」下拉:`inline` 决定同一段落里的文字与图片/签章位是否排在同一行。 */
215
+ function pickDocxFile(inline: boolean): void {
216
+ docxInline.value = inline
217
+ docxInput.value?.click()
218
+ }
138
219
  </script>
139
220
 
140
221
  <template>
@@ -171,7 +252,15 @@ async function onOfficeFile(event: Event, kind: 'docx' | 'xlsx'): Promise<void>
171
252
  <el-button size="small" @click="jsonOpen = true">配置 JSON</el-button>
172
253
  <el-button size="small" @click="exportJson">导出</el-button>
173
254
  <el-button size="small" @click="fileInput?.click()">导入 JSON</el-button>
174
- <el-button size="small" @click="docxInput?.click()">导入 Word</el-button>
255
+ <el-dropdown split-button size="small" trigger="click" @click="pickDocxFile(false)" @command="pickDocxFile">
256
+ 导入 Word
257
+ <template #dropdown>
258
+ <el-dropdown-menu>
259
+ <el-dropdown-item :command="false">块级(默认):文字与图片/签章各自成块</el-dropdown-item>
260
+ <el-dropdown-item :command="true">同排:文字与图片/签章排在同一行</el-dropdown-item>
261
+ </el-dropdown-menu>
262
+ </template>
263
+ </el-dropdown>
175
264
  <el-button size="small" @click="xlsxInput?.click()">导入 Excel</el-button>
176
265
  <el-button size="small" @click="htmlDialog = true">导入 HTML</el-button>
177
266
  <el-button size="small" type="primary" :loading="state.busy" @click="downloadPdf">生成 PDF</el-button>
@@ -0,0 +1,31 @@
1
+ /** API 客户端:导入 Word 的「同排」开关要走对 URL(默认块级 = 不带参数)。 */
2
+
3
+ import { afterEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { convertDocx } 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
+ })
@@ -4,6 +4,7 @@ import type {
4
4
  ComponentsPayload,
5
5
  DocumentSchema,
6
6
  FormData,
7
+ MockImageStyles,
7
8
  OptionItem,
8
9
  OptionsConfig,
9
10
  } from '@/types/schema'
@@ -32,23 +33,35 @@ export interface ExampleItem {
32
33
  }
33
34
 
34
35
  export interface ConvertResponse {
36
+ /** 纯解析结果:配置 + 提取到的字段(**不含示例数据**) */
35
37
  document: DocumentSchema
36
38
  fields: { name: string; label: string; kind: string }[]
37
- /** 转换时自动生成的示例数据(与 schema 分离,单独下发) */
38
- data?: FormData | null
39
+ warnings?: string[]
39
40
  }
40
41
 
41
- async function jsonRequest<T>(url: string, init?: RequestInit): Promise<T> {
42
- const response = await fetch(url, init)
43
- if (!response.ok) {
44
- let detail = ''
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 {
45
51
  try {
46
- const body = (await response.json()) as { message?: string }
47
- detail = body.message ?? ''
52
+ const text = await response.text()
53
+ if (text.trim()) return text.trim().slice(0, 300)
48
54
  } catch {
49
- detail = await response.text()
55
+ /* 忽略 */
50
56
  }
51
- throw new Error(detail || `请求失败(HTTP ${response.status})`)
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, '请求失败'))
52
65
  }
53
66
  return (await response.json()) as T
54
67
  }
@@ -104,11 +117,15 @@ export function fetchFonts(): Promise<FontInfo> {
104
117
  return jsonRequest<FontInfo>('/api/meta/fonts')
105
118
  }
106
119
 
107
- export function validateDocument(document: DocumentSchema): Promise<ValidationResponse> {
120
+ export function validateDocument(
121
+ document: DocumentSchema,
122
+ data?: FormData,
123
+ ): Promise<ValidationResponse> {
108
124
  return jsonRequest<ValidationResponse>('/api/validate', {
109
125
  method: 'POST',
110
126
  headers: { 'Content-Type': 'application/json' },
111
- body: JSON.stringify({ document }),
127
+ // 传了 data 时后端会额外校验「必填项是否已填」(required_missing)
128
+ body: JSON.stringify({ document, data: data ?? null }),
112
129
  })
113
130
  }
114
131
 
@@ -117,8 +134,12 @@ export async function renderHtml(document: DocumentSchema, data?: FormData): Pro
117
134
  method: 'POST',
118
135
  headers: { 'Content-Type': 'application/json' },
119
136
  body: JSON.stringify({ document, data: data ?? null }),
137
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
120
138
  })
121
- if (!response.ok) throw new Error(`HTML 预览失败(HTTP ${response.status})`)
139
+ if (!response.ok) {
140
+ // 早前这里只拼「HTTP xxx」,把后端给的具体原因(模板语法错误/非法配置项)丢了
141
+ throw new Error(await errorMessage(response, 'HTML 预览失败'))
142
+ }
122
143
  return response.text()
123
144
  }
124
145
 
@@ -127,20 +148,52 @@ export async function renderPdf(document: DocumentSchema, data?: FormData): Prom
127
148
  method: 'POST',
128
149
  headers: { 'Content-Type': 'application/json' },
129
150
  body: JSON.stringify({ document, data: data ?? null }),
151
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
130
152
  })
131
153
  if (!response.ok) {
132
- let message = `PDF 生成失败(HTTP ${response.status})`
133
- try {
134
- const body = (await response.json()) as { message?: string }
135
- if (body.message) message = body.message
136
- } catch {
137
- /* 忽略解析失败 */
138
- }
139
- throw new Error(message)
154
+ throw new Error(await errorMessage(response, 'PDF 生成失败'))
140
155
  }
141
156
  return response.blob()
142
157
  }
143
158
 
159
+ /** 模板需要的字段(服务端从模板 AST + 变量声明推断,与 mock 数据同源)。 */
160
+ export interface MockVariable {
161
+ name: string
162
+ type: string
163
+ /** 是否已在 `variables` 里声明 */
164
+ declared: boolean
165
+ label?: string
166
+ /** 数组/对象的字段(递归同结构) */
167
+ fields?: MockVariable[]
168
+ /** 标量数组的元素类型 */
169
+ items?: string
170
+ }
171
+
172
+ export interface MockResponse {
173
+ data: FormData
174
+ /** 无法推断的引用(函数调用/动态下标等)—— 提示用户而不是静默缺字段 */
175
+ warnings: string[]
176
+ /** 变量清单:类型 + 嵌套结构(设计器「数据」tab 展示,比前端正则扫描准) */
177
+ variables: MockVariable[]
178
+ }
179
+
180
+ /** 按模板语义生成示例数据(库侧 `pdfwright.mock`;设计器不再自己猜)。
181
+ *
182
+ * 已有数据会**只补缺失**:手填的值不会被覆盖。
183
+ */
184
+ export function generateMockData(
185
+ document: DocumentSchema,
186
+ data?: FormData,
187
+ samples = 2,
188
+ images?: MockImageStyles,
189
+ ): Promise<MockResponse> {
190
+ return jsonRequest<MockResponse>('/api/mock', {
191
+ method: 'POST',
192
+ headers: { 'Content-Type': 'application/json' },
193
+ body: JSON.stringify({ document, data: data ?? null, samples, images: images ?? null }),
194
+ })
195
+ }
196
+
144
197
  export async function fetchOptions(config: OptionsConfig): Promise<OptionItem[]> {
145
198
  if (config.type === 'static') return config.items ?? []
146
199
  const payload = await jsonRequest<{ items: OptionItem[] }>('/api/options/fetch', {
@@ -166,8 +219,11 @@ async function uploadConvert(url: string, file: File): Promise<ConvertResponse>
166
219
  return jsonRequest<ConvertResponse>(url, { method: 'POST', body: form })
167
220
  }
168
221
 
169
- export function convertDocx(file: File): Promise<ConvertResponse> {
170
- return uploadConvert('/api/convert/docx', file)
222
+ export function convertDocx(file: File, inlineCommands = false): Promise<ConvertResponse> {
223
+ // ``inlineCommands``:同一段落里的「文字 + 内联命令标签」排在同一行
224
+ // (默认 false = 块级,各自独立成块;见 docs/word-import.md)
225
+ const query = inlineCommands ? '?inline_commands=true' : ''
226
+ return uploadConvert(`/api/convert/docx${query}`, file)
171
227
  }
172
228
 
173
229
  export function convertXlsx(file: File): Promise<ConvertResponse> {
@@ -17,6 +17,8 @@ const props = withDefaults(
17
17
  {
18
18
  units: () => UNITS,
19
19
  defaultUnit: 'pt',
20
+ min: undefined,
21
+ max: undefined,
20
22
  step: 1,
21
23
  placeholder: '',
22
24
  size: 'small',