pdfwright-web 0.2.1__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.
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/.gitignore +2 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/PKG-INFO +2 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/package.json +1 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/App.vue +110 -21
- pdfwright_web-0.3.0/frontend/src/api/client.spec.ts +31 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/api/client.ts +81 -21
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/components/UnitInput.vue +2 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/designer/CanvasArea.vue +214 -54
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/designer/ComponentPalette.vue +8 -0
- pdfwright_web-0.3.0/frontend/src/designer/ElementTree.vue +94 -0
- pdfwright_web-0.3.0/frontend/src/designer/ElementTreeNode.vue +507 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/designer/JsonPanel.vue +6 -3
- pdfwright_web-0.3.0/frontend/src/designer/PropsPanel.spec.ts +542 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/designer/PropsPanel.vue +933 -52
- pdfwright_web-0.3.0/frontend/src/designer/drag-affordance.spec.ts +113 -0
- pdfwright_web-0.3.0/frontend/src/designer/dragState.ts +23 -0
- pdfwright_web-0.3.0/frontend/src/designer/table-resize-affordance.spec.ts +111 -0
- pdfwright_web-0.3.0/frontend/src/designer/treeCollapse.spec.ts +63 -0
- pdfwright_web-0.3.0/frontend/src/designer/treeCollapse.ts +62 -0
- pdfwright_web-0.3.0/frontend/src/designer/treeDrag.ts +31 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/fill/FillView.vue +67 -5
- pdfwright_web-0.3.0/frontend/src/lib/colors.spec.ts +67 -0
- pdfwright_web-0.3.0/frontend/src/lib/colors.ts +78 -0
- pdfwright_web-0.3.0/frontend/src/lib/convert.spec.ts +57 -0
- pdfwright_web-0.3.0/frontend/src/lib/convert.ts +44 -0
- pdfwright_web-0.3.0/frontend/src/lib/drop.spec.ts +140 -0
- pdfwright_web-0.3.0/frontend/src/lib/drop.ts +93 -0
- pdfwright_web-0.3.0/frontend/src/lib/expand.spec.ts +586 -0
- pdfwright_web-0.3.0/frontend/src/lib/expand.ts +837 -0
- pdfwright_web-0.3.0/frontend/src/lib/json-prop.spec.ts +52 -0
- pdfwright_web-0.3.0/frontend/src/lib/json-prop.ts +46 -0
- pdfwright_web-0.3.0/frontend/src/lib/layout.spec.ts +123 -0
- pdfwright_web-0.3.0/frontend/src/lib/layout.ts +100 -0
- pdfwright_web-0.3.0/frontend/src/lib/markup.spec.ts +58 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/lib/markup.ts +32 -4
- pdfwright_web-0.3.0/frontend/src/lib/metadata.spec.ts +67 -0
- pdfwright_web-0.3.0/frontend/src/lib/metadata.ts +98 -0
- pdfwright_web-0.3.0/frontend/src/lib/pages.spec.ts +58 -0
- pdfwright_web-0.3.0/frontend/src/lib/pages.ts +60 -0
- pdfwright_web-0.3.0/frontend/src/lib/required.spec.ts +73 -0
- pdfwright_web-0.3.0/frontend/src/lib/required.ts +33 -0
- pdfwright_web-0.3.0/frontend/src/lib/snippet.spec.ts +67 -0
- pdfwright_web-0.3.0/frontend/src/lib/snippet.ts +56 -0
- pdfwright_web-0.3.0/frontend/src/lib/table.spec.ts +502 -0
- pdfwright_web-0.3.0/frontend/src/lib/table.ts +800 -0
- pdfwright_web-0.3.0/frontend/src/lib/tableResize.spec.ts +111 -0
- pdfwright_web-0.3.0/frontend/src/lib/tableResize.ts +122 -0
- pdfwright_web-0.3.0/frontend/src/lib/tree.spec.ts +152 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/lib/tree.ts +73 -19
- pdfwright_web-0.3.0/frontend/src/renderer/ElementRenderer.spec.ts +593 -0
- pdfwright_web-0.3.0/frontend/src/renderer/ElementRenderer.vue +1658 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/renderer/WatermarkLayer.vue +16 -2
- pdfwright_web-0.3.0/frontend/src/store/cell-ops-daily.spec.ts +119 -0
- pdfwright_web-0.3.0/frontend/src/store/cell-ops.spec.ts +256 -0
- pdfwright_web-0.3.0/frontend/src/store/designer.spec.ts +1906 -0
- pdfwright_web-0.3.0/frontend/src/store/designer.ts +2191 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/types/schema.ts +49 -7
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/pyproject.toml +8 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/__init__.py +1 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/app.py +97 -17
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/meta.py +529 -34
- pdfwright_web-0.3.0/src/pdfwright_web/mock.py +1286 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/batch.py +73 -35
- pdfwright_web-0.3.0/src/pdfwright_web/routers/codes.py +75 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/convert.py +38 -63
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/examples.py +5 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/meta.py +2 -0
- pdfwright_web-0.3.0/src/pdfwright_web/routers/mock.py +37 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/render.py +32 -5
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/verify.py +43 -25
- pdfwright_web-0.3.0/src/pdfwright_web/schemas.py +115 -0
- pdfwright_web-0.2.1/src/pdfwright_web/static/assets/index-z4cHEGId.css → pdfwright_web-0.3.0/src/pdfwright_web/static/assets/index-CrZ4dGgx.css +1 -1
- pdfwright_web-0.3.0/src/pdfwright_web/static/assets/index-K7XXzc42.js +86 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/static/index.html +2 -2
- pdfwright_web-0.3.0/src/pdfwright_web/uploads.py +82 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/tests/test_api.py +435 -3
- pdfwright_web-0.3.0/tests/test_mock.py +814 -0
- pdfwright_web-0.2.1/frontend/src/designer/ElementTree.vue +0 -56
- pdfwright_web-0.2.1/frontend/src/designer/ElementTreeNode.vue +0 -115
- pdfwright_web-0.2.1/frontend/src/designer/PropsPanel.spec.ts +0 -115
- pdfwright_web-0.2.1/frontend/src/lib/expand.spec.ts +0 -104
- pdfwright_web-0.2.1/frontend/src/lib/expand.ts +0 -340
- pdfwright_web-0.2.1/frontend/src/lib/markup.spec.ts +0 -14
- pdfwright_web-0.2.1/frontend/src/lib/table.spec.ts +0 -159
- pdfwright_web-0.2.1/frontend/src/lib/table.ts +0 -223
- pdfwright_web-0.2.1/frontend/src/renderer/ElementRenderer.spec.ts +0 -214
- pdfwright_web-0.2.1/frontend/src/renderer/ElementRenderer.vue +0 -934
- pdfwright_web-0.2.1/frontend/src/store/designer.spec.ts +0 -547
- pdfwright_web-0.2.1/frontend/src/store/designer.ts +0 -1098
- pdfwright_web-0.2.1/src/pdfwright_web/routers/codes.py +0 -49
- pdfwright_web-0.2.1/src/pdfwright_web/schemas.py +0 -58
- pdfwright_web-0.2.1/src/pdfwright_web/static/assets/index-CGEB_naM.js +0 -86
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/README.md +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/env.d.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/eslint.config.js +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/index.html +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/pnpm-lock.yaml +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/components/BoxInput.spec.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/components/BoxInput.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/lib/clone.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/lib/download.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/lib/fonts.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/lib/format.spec.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/lib/format.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/lib/template.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/lib/units.spec.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/lib/units.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/main.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/renderer/InlineText.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/src/styles/main.css +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/tsconfig.json +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/frontend/vite.config.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/concurrency.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/__init__.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.0}/src/pdfwright_web/routers/options.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.3.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.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
|
|
@@ -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
|
|
28
|
-
// 前端资源版本守护:服务端重建后旧页面引用的 hash 资源已被删除(表现为「样式全丢」),
|
|
29
|
-
// 这里主动提示刷新,而不是让用户对着坏页面排查
|
|
31
|
+
void bootstrap()
|
|
30
32
|
void watchBuild()
|
|
31
33
|
setInterval(() => void watchBuild(), 30000)
|
|
32
|
-
document.addEventListener('visibilitychange',
|
|
33
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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 =
|
|
131
|
-
|
|
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="
|
|
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'
|
|
@@ -27,24 +28,40 @@ export interface ExampleItem {
|
|
|
27
28
|
variables: number
|
|
28
29
|
blocks: number
|
|
29
30
|
schema: DocumentSchema
|
|
31
|
+
/** 示例数据(与 schema 分离:schema 不再内嵌 data) */
|
|
32
|
+
data?: FormData | null
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export interface ConvertResponse {
|
|
36
|
+
/** 纯解析结果:配置 + 提取到的字段(**不含示例数据**) */
|
|
33
37
|
document: DocumentSchema
|
|
34
38
|
fields: { name: string; label: string; kind: string }[]
|
|
39
|
+
warnings?: string[]
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 {
|
|
41
51
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
52
|
+
const text = await response.text()
|
|
53
|
+
if (text.trim()) return text.trim().slice(0, 300)
|
|
44
54
|
} catch {
|
|
45
|
-
|
|
55
|
+
/* 忽略 */
|
|
46
56
|
}
|
|
47
|
-
|
|
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, '请求失败'))
|
|
48
65
|
}
|
|
49
66
|
return (await response.json()) as T
|
|
50
67
|
}
|
|
@@ -100,11 +117,15 @@ export function fetchFonts(): Promise<FontInfo> {
|
|
|
100
117
|
return jsonRequest<FontInfo>('/api/meta/fonts')
|
|
101
118
|
}
|
|
102
119
|
|
|
103
|
-
export function validateDocument(
|
|
120
|
+
export function validateDocument(
|
|
121
|
+
document: DocumentSchema,
|
|
122
|
+
data?: FormData,
|
|
123
|
+
): Promise<ValidationResponse> {
|
|
104
124
|
return jsonRequest<ValidationResponse>('/api/validate', {
|
|
105
125
|
method: 'POST',
|
|
106
126
|
headers: { 'Content-Type': 'application/json' },
|
|
107
|
-
|
|
127
|
+
// 传了 data 时后端会额外校验「必填项是否已填」(required_missing)
|
|
128
|
+
body: JSON.stringify({ document, data: data ?? null }),
|
|
108
129
|
})
|
|
109
130
|
}
|
|
110
131
|
|
|
@@ -113,8 +134,12 @@ export async function renderHtml(document: DocumentSchema, data?: FormData): Pro
|
|
|
113
134
|
method: 'POST',
|
|
114
135
|
headers: { 'Content-Type': 'application/json' },
|
|
115
136
|
body: JSON.stringify({ document, data: data ?? null }),
|
|
137
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
116
138
|
})
|
|
117
|
-
if (!response.ok)
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
// 早前这里只拼「HTTP xxx」,把后端给的具体原因(模板语法错误/非法配置项)丢了
|
|
141
|
+
throw new Error(await errorMessage(response, 'HTML 预览失败'))
|
|
142
|
+
}
|
|
118
143
|
return response.text()
|
|
119
144
|
}
|
|
120
145
|
|
|
@@ -123,20 +148,52 @@ export async function renderPdf(document: DocumentSchema, data?: FormData): Prom
|
|
|
123
148
|
method: 'POST',
|
|
124
149
|
headers: { 'Content-Type': 'application/json' },
|
|
125
150
|
body: JSON.stringify({ document, data: data ?? null }),
|
|
151
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
126
152
|
})
|
|
127
153
|
if (!response.ok) {
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const body = (await response.json()) as { message?: string }
|
|
131
|
-
if (body.message) message = body.message
|
|
132
|
-
} catch {
|
|
133
|
-
/* 忽略解析失败 */
|
|
134
|
-
}
|
|
135
|
-
throw new Error(message)
|
|
154
|
+
throw new Error(await errorMessage(response, 'PDF 生成失败'))
|
|
136
155
|
}
|
|
137
156
|
return response.blob()
|
|
138
157
|
}
|
|
139
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
|
+
|
|
140
197
|
export async function fetchOptions(config: OptionsConfig): Promise<OptionItem[]> {
|
|
141
198
|
if (config.type === 'static') return config.items ?? []
|
|
142
199
|
const payload = await jsonRequest<{ items: OptionItem[] }>('/api/options/fetch', {
|
|
@@ -162,8 +219,11 @@ async function uploadConvert(url: string, file: File): Promise<ConvertResponse>
|
|
|
162
219
|
return jsonRequest<ConvertResponse>(url, { method: 'POST', body: form })
|
|
163
220
|
}
|
|
164
221
|
|
|
165
|
-
export function convertDocx(file: File): Promise<ConvertResponse> {
|
|
166
|
-
|
|
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)
|
|
167
227
|
}
|
|
168
228
|
|
|
169
229
|
export function convertXlsx(file: File): Promise<ConvertResponse> {
|