pdfwright-web 0.2.1__tar.gz → 0.2.2__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.2.2}/PKG-INFO +1 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/package.json +1 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/App.vue +3 -3
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/api/client.ts +4 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/designer/JsonPanel.vue +4 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/fill/FillView.vue +2 -2
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/expand.ts +4 -2
- pdfwright_web-0.2.2/frontend/src/lib/markup.spec.ts +58 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/markup.ts +20 -2
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/store/designer.spec.ts +37 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/store/designer.ts +17 -3
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/types/schema.ts +1 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/pyproject.toml +1 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/__init__.py +1 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/app.py +1 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/routers/convert.py +3 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/routers/examples.py +5 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/schemas.py +14 -4
- pdfwright_web-0.2.1/src/pdfwright_web/static/assets/index-CGEB_naM.js → pdfwright_web-0.2.2/src/pdfwright_web/static/assets/index-BgTOK7V4.js +16 -16
- pdfwright_web-0.2.1/src/pdfwright_web/static/assets/index-z4cHEGId.css → pdfwright_web-0.2.2/src/pdfwright_web/static/assets/index-DrWAVhY-.css +1 -1
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/static/index.html +2 -2
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/tests/test_api.py +3 -2
- pdfwright_web-0.2.1/frontend/src/lib/markup.spec.ts +0 -14
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/.gitignore +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/README.md +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/env.d.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/eslint.config.js +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/index.html +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/pnpm-lock.yaml +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/components/BoxInput.spec.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/components/BoxInput.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/components/UnitInput.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/designer/CanvasArea.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/designer/ComponentPalette.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/designer/ElementTree.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/designer/ElementTreeNode.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/designer/PropsPanel.spec.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/designer/PropsPanel.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/clone.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/download.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/expand.spec.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/fonts.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/format.spec.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/format.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/table.spec.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/table.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/template.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/tree.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/units.spec.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/lib/units.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/main.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/renderer/ElementRenderer.spec.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/renderer/ElementRenderer.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/renderer/InlineText.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/renderer/WatermarkLayer.vue +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/src/styles/main.css +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/tsconfig.json +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/frontend/vite.config.ts +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/concurrency.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/meta.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/routers/__init__.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/routers/batch.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/routers/codes.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/routers/meta.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/routers/options.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/routers/render.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/routers/verify.py +0 -0
- {pdfwright_web-0.2.1 → pdfwright_web-0.2.2}/src/pdfwright_web/security_defaults.py +0 -0
|
@@ -74,7 +74,7 @@ async function loadExamples(): Promise<void> {
|
|
|
74
74
|
function loadExample(id: string): void {
|
|
75
75
|
const item = examples.value.find((example) => example.id === id)
|
|
76
76
|
if (!item) return
|
|
77
|
-
importSchema(item.schema)
|
|
77
|
+
importSchema(item.schema, item.data ?? {})
|
|
78
78
|
ElMessage.success(`已加载示例:${item.title}`)
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -113,7 +113,7 @@ async function importHtml(): Promise<void> {
|
|
|
113
113
|
htmlBusy.value = true
|
|
114
114
|
try {
|
|
115
115
|
const result = await api.convertHtml(htmlSource.value)
|
|
116
|
-
importSchema(result.document)
|
|
116
|
+
importSchema(result.document, result.data ?? {})
|
|
117
117
|
ElMessage.success(`已转换:${result.fields.length} 个字段`)
|
|
118
118
|
htmlDialog.value = false
|
|
119
119
|
} catch (error) {
|
|
@@ -128,7 +128,7 @@ async function onOfficeFile(event: Event, kind: 'docx' | 'xlsx'): Promise<void>
|
|
|
128
128
|
if (!file) return
|
|
129
129
|
try {
|
|
130
130
|
const result = kind === 'docx' ? await api.convertDocx(file) : await api.convertXlsx(file)
|
|
131
|
-
importSchema(result.document)
|
|
131
|
+
importSchema(result.document, result.data ?? {})
|
|
132
132
|
ElMessage.success(`已转换:${result.fields.length} 个字段`)
|
|
133
133
|
} catch (error) {
|
|
134
134
|
ElMessage.error(`转换失败:${(error as Error).message}`)
|
|
@@ -27,11 +27,15 @@ export interface ExampleItem {
|
|
|
27
27
|
variables: number
|
|
28
28
|
blocks: number
|
|
29
29
|
schema: DocumentSchema
|
|
30
|
+
/** 示例数据(与 schema 分离:schema 不再内嵌 data) */
|
|
31
|
+
data?: FormData | null
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export interface ConvertResponse {
|
|
33
35
|
document: DocumentSchema
|
|
34
36
|
fields: { name: string; label: string; kind: string }[]
|
|
37
|
+
/** 转换时自动生成的示例数据(与 schema 分离,单独下发) */
|
|
38
|
+
data?: FormData | null
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
async function jsonRequest<T>(url: string, init?: RequestInit): Promise<T> {
|
|
@@ -17,7 +17,10 @@ watch(() => state.mode, sync)
|
|
|
17
17
|
|
|
18
18
|
function applySchema(): void {
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
const parsed = JSON.parse(schemaText.value) as DocumentSchema
|
|
21
|
+
// 配置 JSON 不含数据(data 是运行态):应用配置时保留当前示例数据,
|
|
22
|
+
// 否则编辑一下 schema 就顺手把「数据」tab / 右栏里的 mock 清空了。
|
|
23
|
+
importSchema(parsed, state.sampleData)
|
|
21
24
|
ElMessage.success('页面配置已应用')
|
|
22
25
|
void refreshPreview()
|
|
23
26
|
} catch (error) {
|
|
@@ -26,13 +26,13 @@ const margin = computed(() => {
|
|
|
26
26
|
return boxToPt(raw)
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
/** 初值:变量默认值 +
|
|
29
|
+
/** 初值:变量默认值 + 导入时携带的示例数据快照(填表过程中的编辑不回写快照,故「重置」可恢复)。 */
|
|
30
30
|
function initialData(): Record<string, unknown> {
|
|
31
31
|
const out: Record<string, unknown> = {}
|
|
32
32
|
for (const variable of state.schema.variables ?? []) {
|
|
33
33
|
if (variable.default !== undefined && variable.default !== null) out[variable.name] = variable.default
|
|
34
34
|
}
|
|
35
|
-
return { ...out, ...(state.
|
|
35
|
+
return { ...out, ...(state.sampleData ?? {}) }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const scope = computed(() => buildContext(state.schema, { ...state.data, ...formData }))
|
|
@@ -10,7 +10,10 @@ export interface ResolvedValue {
|
|
|
10
10
|
hasValue: boolean
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/** 构建作用域:变量默认值 <
|
|
13
|
+
/** 构建作用域:变量默认值 < 填写数据 data < extra。
|
|
14
|
+
*
|
|
15
|
+
* 配置与数据分离:数据只从 ``data`` 参数进,schema 不携带任何示例/mock 数据。
|
|
16
|
+
*/
|
|
14
17
|
export function buildContext(
|
|
15
18
|
schema: DocumentSchema,
|
|
16
19
|
data?: Record<string, unknown> | null,
|
|
@@ -22,7 +25,6 @@ export function buildContext(
|
|
|
22
25
|
scope[variable.name] = variable.default
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
|
-
if (schema.data) Object.assign(scope, schema.data)
|
|
26
28
|
if (data) Object.assign(scope, data)
|
|
27
29
|
if (extra) Object.assign(scope, extra)
|
|
28
30
|
return scope
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { markupToHtml, plainText } from './markup'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
describe('plainText', () => {
|
|
7
|
+
it('剥离内联标记但保留 {{ 变量 }}', () => {
|
|
8
|
+
expect(plainText('<font color="#000" size="10.5" face="cn-song">一、项目部</font>')).toBe('一、项目部')
|
|
9
|
+
expect(plainText('{{project_name}}日报')).toBe('{{project_name}}日报')
|
|
10
|
+
expect(plainText('<b>{{a}}</b> 与 <i>b</i>')).toBe('{{a}} 与 b')
|
|
11
|
+
expect(plainText('<font face="x">甲<br/>乙</font>')).toBe('甲 乙')
|
|
12
|
+
expect(plainText(null)).toBe('')
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('markupToHtml:安全硬化', () => {
|
|
17
|
+
it('<a href> 只放行 scheme 白名单(javascript: 被剥成无 href 的 <a>)', () => {
|
|
18
|
+
const html = markupToHtml('<a href="javascript:alert(1)">坏</a><a href="https://ok.example">好</a>', {
|
|
19
|
+
mode: 'preview',
|
|
20
|
+
})
|
|
21
|
+
expect(html).not.toContain('javascript:')
|
|
22
|
+
expect(html).toContain('<a>坏</a>')
|
|
23
|
+
expect(html).toContain('href="https://ok.example"')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('文本转义:未知标签按文本输出,不执行', () => {
|
|
27
|
+
const html = markupToHtml('<script>alert(1)</script>正文', { mode: 'preview' })
|
|
28
|
+
expect(html).not.toContain('<script>')
|
|
29
|
+
expect(html).toContain('<script>')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('未知属性被忽略(onmouseover 无法注入)', () => {
|
|
33
|
+
const html = markupToHtml('<font color="red" onmouseover="x">甲</font>', { mode: 'preview' })
|
|
34
|
+
expect(html).not.toContain('onmouseover')
|
|
35
|
+
expect(html).toContain('color:red')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('字号夹到合法范围(font size 与 span font-size 同规则)', () => {
|
|
39
|
+
expect(markupToHtml('<font size="5000">甲</font>', { mode: 'preview' })).toContain(
|
|
40
|
+
'font-size:500pt',
|
|
41
|
+
)
|
|
42
|
+
expect(markupToHtml('<font size="0.0001">甲</font>', { mode: 'preview' })).toContain(
|
|
43
|
+
'font-size:1pt',
|
|
44
|
+
)
|
|
45
|
+
expect(
|
|
46
|
+
markupToHtml('<span style="font-size:9999pt;color:red">甲</span>', { mode: 'preview' }),
|
|
47
|
+
).toContain('font-size:500pt')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('占位符值按文本转义(预览态值不成为标记)', () => {
|
|
51
|
+
const html = markupToHtml('{{ v }}', {
|
|
52
|
+
mode: 'preview',
|
|
53
|
+
scope: { v: '<b>注入</b>' },
|
|
54
|
+
})
|
|
55
|
+
expect(html).not.toContain('<b>')
|
|
56
|
+
expect(html).toContain('<b>注入</b>')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -39,6 +39,9 @@ const SAFE_CSS_PROPS = new Set([
|
|
|
39
39
|
'text-decoration',
|
|
40
40
|
])
|
|
41
41
|
const SAFE_URL_SCHEMES = ['http://', 'https://', 'mailto:', '/', '#']
|
|
42
|
+
// 内联标记字号范围(pt):与后端 `render/markup.py` 同值,挡住 `size="5000"` 这类病态值
|
|
43
|
+
const MIN_MARKUP_FONT_SIZE = 1
|
|
44
|
+
const MAX_MARKUP_FONT_SIZE = 500
|
|
42
45
|
const TAG_RE = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s[^<>]*)?)(\/?)>/g
|
|
43
46
|
const ATTR_RE = /([a-zA-Z-]+)\s*=\s*"([^"]*)"/g
|
|
44
47
|
const PLACEHOLDER_RE = /\{\{\s*([A-Za-z_][A-Za-z0-9_.\]]*)\s*\}\}/g
|
|
@@ -78,6 +81,20 @@ function escapeHtml(value: string): string {
|
|
|
78
81
|
.replace(/"/g, '"')
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
/** 字号夹到合法范围(与后端 `clamp_font_size` 同规则)。 */
|
|
85
|
+
function clampFontSize(size: number): number {
|
|
86
|
+
return Math.min(Math.max(size, MIN_MARKUP_FONT_SIZE), MAX_MARKUP_FONT_SIZE)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** CSS `font-size` 的数值部分夹到合法范围(保留原单位)。 */
|
|
90
|
+
function clampCssFontSize(value: string): string {
|
|
91
|
+
const match = /^\s*(-?[\d.]+)/.exec(value)
|
|
92
|
+
if (!match) return value
|
|
93
|
+
const size = Number(match[1])
|
|
94
|
+
if (!Number.isFinite(size)) return value
|
|
95
|
+
return `${clampFontSize(size)}${value.slice(match[0].trimEnd().length)}`
|
|
96
|
+
}
|
|
97
|
+
|
|
81
98
|
/** 占位符显示名:绝对路径里的数组下标压缩成 `[]`(person.0.person.0.name → person[].person[].name)。 */
|
|
82
99
|
export function displayName(name: string): string {
|
|
83
100
|
return name.replace(/\.\d+(?=\.|$)/g, '[]')
|
|
@@ -135,7 +152,8 @@ export function markupToHtml(markup: string | null | undefined, options: MarkupO
|
|
|
135
152
|
const parsed = parseStyleAttr(attrsOf(match[3] ?? '').style ?? '')
|
|
136
153
|
const styles: string[] = []
|
|
137
154
|
for (const [prop, value] of Object.entries(parsed)) {
|
|
138
|
-
if (SAFE_CSS_PROPS.has(prop))
|
|
155
|
+
if (!SAFE_CSS_PROPS.has(prop)) continue
|
|
156
|
+
styles.push(`${prop}:${prop === 'font-size' ? clampCssFontSize(value) : value}`)
|
|
139
157
|
}
|
|
140
158
|
parts.push(styles.length ? `<span style="${styles.join(';')}">` : '<span>')
|
|
141
159
|
} else if (PASSTHROUGH_TAGS[tag]) {
|
|
@@ -146,7 +164,7 @@ export function markupToHtml(markup: string | null | undefined, options: MarkupO
|
|
|
146
164
|
const family = attrs.face ? fontFamilyCss(attrs.face) : undefined
|
|
147
165
|
if (family) styles.push(`font-family:${family}`)
|
|
148
166
|
const size = Number(attrs.size)
|
|
149
|
-
if (Number.isFinite(size)
|
|
167
|
+
if (Number.isFinite(size)) styles.push(`font-size:${clampFontSize(size)}pt`)
|
|
150
168
|
if (attrs.color) styles.push(`color:${attrs.color}`)
|
|
151
169
|
if (attrs.backcolor) styles.push(`background-color:${attrs.backcolor}`)
|
|
152
170
|
const spacing = Number(attrs.spacing)
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
duplicateSelected,
|
|
26
26
|
ensureDrop,
|
|
27
27
|
exportSchema,
|
|
28
|
+
importSchema,
|
|
28
29
|
moveSelected,
|
|
29
30
|
moveNodeTo,
|
|
30
31
|
removeSelected,
|
|
@@ -68,6 +69,7 @@ function reset(): void {
|
|
|
68
69
|
state.selectedId = null
|
|
69
70
|
state.components = { groups: [], components: [headingMeta, sectionMeta], styleProps: [], fontSizes: { cn: [] }, pageSizes: [], fontFamilies: [] }
|
|
70
71
|
state.data = {}
|
|
72
|
+
state.sampleData = {}
|
|
71
73
|
state.issues = []
|
|
72
74
|
state.history = []
|
|
73
75
|
state.historyIndex = -1
|
|
@@ -545,3 +547,38 @@ describe('档案模板预设 / 变量自动补全', () => {
|
|
|
545
547
|
expect(syncVariables()).toBe(0)
|
|
546
548
|
})
|
|
547
549
|
})
|
|
550
|
+
|
|
551
|
+
describe('导入导出:配置只服务渲染', () => {
|
|
552
|
+
beforeEach(() => {
|
|
553
|
+
reset()
|
|
554
|
+
vi.spyOn(api, 'validateDocument').mockResolvedValue({ ok: true, issues: [] })
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
it('导出的页面配置不含 data(配置与数据分离)', () => {
|
|
558
|
+
const exported = exportSchema()
|
|
559
|
+
expect('data' in exported).toBe(false)
|
|
560
|
+
expect(JSON.stringify(exported)).not.toContain('"data"')
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
it('导入无 pdf 节的配置时 PDF/A 档位落地为默认 2b', () => {
|
|
564
|
+
importSchema({ version: '1.0', body: [{ type: 'paragraph', text: '正文' }] }, { a: 1 })
|
|
565
|
+
expect(exportSchema().pdf?.pdfa).toBe('2b')
|
|
566
|
+
// 示例数据与配置分离:只进运行态,供设计器内 mock 预览
|
|
567
|
+
expect(state.data).toEqual({ a: 1 })
|
|
568
|
+
expect(state.sampleData).toEqual({ a: 1 })
|
|
569
|
+
expect('data' in exportSchema()).toBe(false)
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
it('导入显式 pdfa=false 时不被默认值覆盖', () => {
|
|
573
|
+
importSchema({ version: '1.0', body: [], pdf: { pdfa: false, numericFont: 'serif' } })
|
|
574
|
+
const pdf = exportSchema().pdf
|
|
575
|
+
expect(pdf?.pdfa).toBe(false)
|
|
576
|
+
expect(pdf?.numericFont).toBe('serif')
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
it('不传 data 时运行态数据为空对象', () => {
|
|
580
|
+
importSchema({ version: '1.0', body: [] })
|
|
581
|
+
expect(state.data).toEqual({})
|
|
582
|
+
expect(state.sampleData).toEqual({})
|
|
583
|
+
})
|
|
584
|
+
})
|
|
@@ -62,6 +62,8 @@ export const DEFAULT_SCHEMA: DocumentSchema = {
|
|
|
62
62
|
interface DesignerState {
|
|
63
63
|
schema: DocumentSchema
|
|
64
64
|
data: FormData
|
|
65
|
+
/** 导入/转换带来的示例数据快照(填写态「重置」的不可变初值;不进 schema、不导出) */
|
|
66
|
+
sampleData: FormData
|
|
65
67
|
selectedId: string | null
|
|
66
68
|
components: ComponentsPayload | null
|
|
67
69
|
fonts: string[]
|
|
@@ -86,6 +88,7 @@ interface DesignerState {
|
|
|
86
88
|
export const state = reactive<DesignerState>({
|
|
87
89
|
schema: deepClone(DEFAULT_SCHEMA),
|
|
88
90
|
data: {},
|
|
91
|
+
sampleData: {},
|
|
89
92
|
selectedId: null,
|
|
90
93
|
components: null,
|
|
91
94
|
fonts: [],
|
|
@@ -987,6 +990,11 @@ export function setDataValue(name: string, value: unknown): void {
|
|
|
987
990
|
}
|
|
988
991
|
|
|
989
992
|
// ---- 导入导出 ----
|
|
993
|
+
/** 导出「页面配置」:只服务 PDF / 页面渲染。
|
|
994
|
+
*
|
|
995
|
+
* schema 本身就不含示例/mock 数据(配置与数据分离,见 ``importSchema``):这里只剔除
|
|
996
|
+
* 编辑器内部使用的 ``_id``。设计器的 mock 数据存在运行态 ``state.data``,不随配置分发。
|
|
997
|
+
*/
|
|
990
998
|
export function exportSchema(): DocumentSchema {
|
|
991
999
|
return {
|
|
992
1000
|
...state.schema,
|
|
@@ -994,16 +1002,22 @@ export function exportSchema(): DocumentSchema {
|
|
|
994
1002
|
}
|
|
995
1003
|
}
|
|
996
1004
|
|
|
997
|
-
|
|
1005
|
+
/** 导入页面配置。``data`` 是与配置**分离**的示例/填写数据(缺省为空),不会写进 schema。 */
|
|
1006
|
+
export function importSchema(schema: DocumentSchema, data?: FormData | null): void {
|
|
998
1007
|
state.schema = {
|
|
999
1008
|
...schema,
|
|
1009
|
+
// PDF/A 档位显式落地:属性面板在 `pdf` 缺失时会 UI 兜底显示 2b
|
|
1010
|
+
// (PropsPanel.pdfaLevel),若 JSON 不写出来,「标准」就只存在于隐式默认里
|
|
1011
|
+
// (导出看不到、与后端 effective_pdf_options 的默认也可能悄悄分叉)。
|
|
1012
|
+
pdf: { pdfa: '2b', ...(schema.pdf ?? {}) },
|
|
1000
1013
|
body: schema.body ?? [],
|
|
1001
1014
|
variables: schema.variables ?? [],
|
|
1002
1015
|
}
|
|
1003
1016
|
ensureIds(state.schema.body)
|
|
1004
1017
|
state.selectedId = null
|
|
1005
|
-
//
|
|
1006
|
-
state.
|
|
1018
|
+
// 示例数据(Word/Excel/HTML 转换器或示例文件带来)与配置分离:只进运行态,供预览使用
|
|
1019
|
+
state.sampleData = deepClone(data ?? {})
|
|
1020
|
+
state.data = deepClone(state.sampleData)
|
|
1007
1021
|
pushHistory()
|
|
1008
1022
|
void refreshPreview()
|
|
1009
1023
|
}
|
|
@@ -169,7 +169,7 @@ export interface DocumentSchema {
|
|
|
169
169
|
page?: PageConfig
|
|
170
170
|
variables?: VariableDecl[]
|
|
171
171
|
body?: ElementNode[]
|
|
172
|
-
data
|
|
172
|
+
// 配置与数据分离:schema **不含 data**(示例/mock 数据是运行态 state.data,渲染时单独传)
|
|
173
173
|
html?: Record<string, unknown>
|
|
174
174
|
pdf?: Record<string, unknown>
|
|
175
175
|
meta?: Record<string, unknown>
|
|
@@ -86,6 +86,7 @@ async def convert_docx(file: UploadFile = File(...)) -> ConvertResponse:
|
|
|
86
86
|
return ConvertResponse(
|
|
87
87
|
document=with_service_security(result.document).to_dict(),
|
|
88
88
|
fields=[{"name": f.name, "label": f.label, "kind": f.kind} for f in result.fields],
|
|
89
|
+
data=result.data,
|
|
89
90
|
)
|
|
90
91
|
|
|
91
92
|
|
|
@@ -124,6 +125,7 @@ async def convert_html(payload: HtmlConvertRequest) -> ConvertResponse:
|
|
|
124
125
|
return ConvertResponse(
|
|
125
126
|
document=result.document.to_dict(),
|
|
126
127
|
fields=[{"name": f.name, "label": f.label, "kind": f.kind} for f in result.fields],
|
|
128
|
+
data=result.data,
|
|
127
129
|
)
|
|
128
130
|
|
|
129
131
|
|
|
@@ -139,4 +141,5 @@ async def convert_html_file(file: UploadFile = File(...)) -> ConvertResponse:
|
|
|
139
141
|
return ConvertResponse(
|
|
140
142
|
document=with_service_security(result.document).to_dict(),
|
|
141
143
|
fields=[{"name": f.name, "label": f.label, "kind": f.kind} for f in result.fields],
|
|
144
|
+
data=result.data,
|
|
142
145
|
)
|
|
@@ -76,6 +76,10 @@ def _load_examples(directory: str) -> dict[str, Any]:
|
|
|
76
76
|
except (OSError, json.JSONDecodeError):
|
|
77
77
|
continue
|
|
78
78
|
meta = schema.get("meta") or {}
|
|
79
|
+
# 示例文件为了「单文件即完整演示」把 data 写在同一个 JSON 里;
|
|
80
|
+
# 但配置与数据分离 —— 对外只把 schema(不含 data)与 data 分开下发。
|
|
81
|
+
data = schema.get("data") if isinstance(schema.get("data"), dict) else None
|
|
82
|
+
schema = {k: v for k, v in schema.items() if k != "data"}
|
|
79
83
|
items.append(
|
|
80
84
|
{
|
|
81
85
|
"id": path.stem,
|
|
@@ -84,6 +88,7 @@ def _load_examples(directory: str) -> dict[str, Any]:
|
|
|
84
88
|
"variables": len(schema.get("variables") or []),
|
|
85
89
|
"blocks": len(schema.get("body") or []),
|
|
86
90
|
"schema": schema,
|
|
91
|
+
"data": data,
|
|
87
92
|
}
|
|
88
93
|
)
|
|
89
94
|
return {"examples": items, "count": len(items)}
|
|
@@ -4,16 +4,24 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic import BaseModel, Field, field_validator
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class RenderRequest(BaseModel):
|
|
11
|
-
"""渲染请求:schema +
|
|
11
|
+
"""渲染请求:schema + 填写数据(**配置与数据分离**)。"""
|
|
12
12
|
|
|
13
13
|
document: dict[str, Any] = Field(..., description="页面 JSON 配置(schema)")
|
|
14
|
-
data: dict[str, Any]
|
|
14
|
+
data: dict[str, Any] = Field(
|
|
15
|
+
default_factory=dict, description="表单填写数据(独立于 schema;缺省为空对象)"
|
|
16
|
+
)
|
|
15
17
|
filename: str | None = Field(default=None, description="下载文件名(PDF)")
|
|
16
18
|
|
|
19
|
+
@field_validator("data", mode="before")
|
|
20
|
+
@classmethod
|
|
21
|
+
def _none_to_empty(cls, value: Any) -> Any:
|
|
22
|
+
"""未传 / 传 null 一律视为空对象(缺失数据等价于空数据)。"""
|
|
23
|
+
return value or {}
|
|
24
|
+
|
|
17
25
|
|
|
18
26
|
class ValidateRequest(BaseModel):
|
|
19
27
|
"""校验请求。"""
|
|
@@ -50,9 +58,11 @@ class OptionsFetchResponse(BaseModel):
|
|
|
50
58
|
class ConvertResponse(BaseModel):
|
|
51
59
|
"""文档转换响应:返回可直接用于编辑器的 schema。
|
|
52
60
|
|
|
53
|
-
``warnings`` =
|
|
61
|
+
``data`` 是与配置分离的**示例数据**(不是 schema 字段);``warnings`` = 源文件里有、
|
|
62
|
+
但本次转换没能搬过来的特性(不静默丢弃)。
|
|
54
63
|
"""
|
|
55
64
|
|
|
56
65
|
document: dict[str, Any]
|
|
57
66
|
fields: list[dict[str, Any]] = []
|
|
58
67
|
warnings: list[str] = []
|
|
68
|
+
data: dict[str, Any] | None = None
|