x-print-designer 0.3.1 → 0.3.2

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.
package/README.md CHANGED
@@ -6,21 +6,151 @@
6
6
 
7
7
  ## 目录
8
8
 
9
- - [核心概念:模板模式 vs 报告模式](#核心概念模板模式-vs-报告模式)
9
+ - [前置依赖要求](#前置依赖要求)
10
10
  - [快速开始](#快速开始)
11
+ - [源码开发](#源码开发)
12
+ - [Vue 组件接入](#vue-组件接入)
13
+ - [Web Component 接入](#web-component-接入)
14
+ - [核心概念:模板模式 vs 报告模式](#核心概念模板模式-vs-报告模式)
11
15
  - [Vue 组件使用说明](#vue-组件使用说明)
12
16
  - [Web Component 使用说明](#web-component-使用说明)
13
17
  - [传入数据格式](#传入数据格式)
14
- - [保存回调 `onSave`](#保存回调-onsave)
15
- - [自动保存配置](#自动保存配置)
16
- - [模板数据结构(完整格式)](#模板数据结构完整格式)
17
18
  - [元素数据绑定](#元素数据绑定)
18
19
  - [图片元素使用说明](#图片元素使用说明)
20
+ - [保存回调 `onSave`](#保存回调-onsave)
21
+ - [自动保存配置](#自动保存配置)
22
+ - [打印与导出](#打印与导出)
23
+ - [模板数据结构](#模板数据结构)
19
24
  - [支持的元素类型](#支持的元素类型)
25
+ - [版本兼容性说明](#版本兼容性说明)
26
+ - [常见问题排查](#常见问题排查)
20
27
  - [技术栈](#技术栈)
21
28
 
22
29
  ---
23
30
 
31
+ ## 前置依赖要求
32
+
33
+ | 依赖 | 最低版本 | 说明 |
34
+ |------|---------|------|
35
+ | **Node.js** | `>= 18` | 运行时环境 |
36
+ | **pnpm** | `>= 8` | 推荐包管理器(项目使用 `pnpm-lock.yaml`) |
37
+ | **Vue** | `^3.5` | 核心框架 |
38
+
39
+ ### 浏览器兼容性
40
+
41
+ | 浏览器 | 最低版本 | 说明 |
42
+ |--------|---------|------|
43
+ | Chrome | >= 90 | 完全支持 |
44
+ | Edge | >= 90 | 完全支持 |
45
+ | Firefox | >= 90 | 完全支持 |
46
+ | Safari | >= 15 | 完全支持 |
47
+
48
+ > **说明:** 设计器基于现代浏览器 API(`ResizeObserver`、`CSS Grid`、`Shadow DOM`),不支持 IE 11 及以下版本。
49
+
50
+ ---
51
+
52
+ ## 快速开始
53
+
54
+ ### 源码开发
55
+
56
+ ```bash
57
+ # 克隆仓库
58
+ git clone https://github.com/0ldFive/Vue-Print-Designer.git
59
+ cd x-print-designer
60
+
61
+ # 安装依赖
62
+ pnpm install
63
+
64
+ # 启动开发服务器
65
+ pnpm dev
66
+
67
+ # 构建生产包
68
+ pnpm build
69
+
70
+ # 构建 Web Component 包
71
+ pnpm build:wc
72
+ ```
73
+
74
+ 启动后浏览器访问 `http://localhost:5173` 即可看到设计器 Demo 页面(`index.html`)。
75
+
76
+ ### Vue 组件接入
77
+
78
+ **适用于:** 将设计器作为子组件嵌入已有 Vue 3 项目。
79
+
80
+ ```vue
81
+ <template>
82
+ <PrintDesigner
83
+ mode="template"
84
+ :sample-data="sampleData"
85
+ :on-save="handleSave"
86
+ />
87
+ </template>
88
+
89
+ <script setup lang="ts">
90
+ import { ref } from 'vue'
91
+ import { PrintDesigner } from 'x-print-designer'
92
+ import 'x-print-designer/style.css'
93
+
94
+ const sampleData = ref({
95
+ id: 1,
96
+ title: '报告标题',
97
+ customerName: '张三',
98
+ items: [
99
+ { name: '产品A', price: 100, quantity: 2 }
100
+ ]
101
+ })
102
+
103
+ const handleSave = async (payload) => {
104
+ // payload: { id, name, data, isNew }
105
+ await fetch('/api/templates/save', {
106
+ method: 'POST',
107
+ headers: { 'Content-Type': 'application/json' },
108
+ body: JSON.stringify(payload)
109
+ })
110
+ }
111
+ </script>
112
+ ```
113
+
114
+ ### Web Component 接入
115
+
116
+ **适用于:** 任意前端技术栈(React / Angular / 原生 HTML),以自定义标签方式嵌入。
117
+
118
+ ```html
119
+ <!DOCTYPE html>
120
+ <html lang="zh-CN">
121
+ <head>
122
+ <meta charset="UTF-8" />
123
+ <script type="module" src="/dist/print-designer.es.js"></script>
124
+ <link rel="stylesheet" href="/dist/print-designer.css" />
125
+ </head>
126
+ <body>
127
+ <print-designer id="designer"></print-designer>
128
+
129
+ <script>
130
+ const designer = document.getElementById('designer')
131
+
132
+ designer.addEventListener('ready', () => {
133
+ designer.setMode('template')
134
+ designer.setTestData({
135
+ id: 1,
136
+ title: '报告标题',
137
+ items: [{ name: '产品A', price: 100 }]
138
+ })
139
+ designer.setOnSave((payload) => {
140
+ fetch('/api/templates/save', {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify(payload)
144
+ })
145
+ })
146
+ })
147
+ </script>
148
+ </body>
149
+ </html>
150
+ ```
151
+
152
+ ---
153
+
24
154
  ## 核心概念:模板模式 vs 报告模式
25
155
 
26
156
  设计器支持两种工作模式,通过 `mode` 属性/方法进行切换。
@@ -29,12 +159,12 @@
29
159
 
30
160
  | 概念 | 说明 |
31
161
  |------|------|
32
- | **模板 (Template)** | 页面布局设计,包含元素位置、样式、画布大小等,不包含具体数据 |
162
+ | **模板 (Template)** | 页面布局设计,包含元素位置、样式、画布大小等 |
33
163
  | **数据 (Data)** | 业务数据,如订单信息、报告内容、用户信息等 |
34
164
  | **模板 ID** | 模板的唯一标识,用于关联和加载模板 |
35
165
  | **数据 ID** | 业务数据的唯一标识,如订单 ID、报告 ID |
36
166
 
37
- ### 模板模式(`template`)— 默认模式
167
+ ### 模板模式(`template`)— 默认
38
168
 
39
169
  **设计理念:** 模板与数据分离,一个模板可绑定多个不同的数据。
40
170
 
@@ -43,14 +173,7 @@
43
173
  - 标签打印(同一标签模板,打印不同商品)
44
174
  - 快递单据(同一单据模板,打印不同收件人)
45
175
 
46
- **数据流向:**
47
- ```
48
- 模板(保存布局) + 数据(打印时注入) = 最终打印结果
49
- ```
50
-
51
- **保存行为:**
52
- - 保存时**只保存页面布局**(元素位置、样式、画布大小等)
53
- - **不保存** `testData`
176
+ **保存行为:** 保存时**只保存页面布局**(元素位置、样式、画布大小等),**不保存** `testData`。
54
177
 
55
178
  ### 报告模式(`report`)
56
179
 
@@ -61,14 +184,7 @@
61
184
  - 数据报表(当前统计数据的可视化呈现)
62
185
  - 档案文件(包含具体数据的完整文档)
63
186
 
64
- **数据流向:**
65
- ```
66
- 报告(模板 + 数据 一起保存) = 完整的报告文件
67
- ```
68
-
69
- **保存行为:**
70
- - 保存时**同时保存页面布局 + 测试数据**
71
- - `testData` 会被完整保存到模板数据中
187
+ **保存行为:** 保存时**同时保存页面布局 + testData**。
72
188
 
73
189
  ### 两种模式对比
74
190
 
@@ -79,46 +195,48 @@
79
195
  | 数据与模板关系 | 分离,运行时注入 | 绑定,一起保存 |
80
196
  | 模板复用性 | 高,可用于多个数据 | 低,针对当前数据 |
81
197
  | 典型用途 | 订单打印、标签 | 检测报告、数据报表 |
82
- | 数据持久化 | 数据需单独存储 | 数据与模板一起存储 |
83
198
 
84
199
  ### 使用示例
85
200
 
86
201
  **模板模式(推荐用于可复用模板):**
202
+
87
203
  ```javascript
88
- // 1. 创建/编辑模板
204
+ // 1. 编辑模板时传入预览数据
89
205
  const sampleData = {
90
- id: null, // 无需数据 ID,只是用于预览
91
- title: "@orderNo", // 绑定变量
92
- customerName: "@customerName"
93
- };
206
+ id: null, // 无需数据 ID,仅用于预览
207
+ orderNo: 'PREVIEW-001',
208
+ customerName: '预览客户'
209
+ }
94
210
 
95
- // 2. 保存模板(只保存布局)
96
- // payload.data 不包含 testData
211
+ // 2. 保存模板(只保存布局,不含 testData)
212
+ // payload.data 中不包含 testData
97
213
 
98
- // 3. 打印时动态注入数据
214
+ // 3. 打印时动态注入真实数据
215
+ designer.loadTemplate('tpl-order')
99
216
  designer.setVariables({
100
- orderNo: "ORD-2024-001",
101
- customerName: "张三"
102
- });
103
- designer.print();
217
+ orderNo: 'ORD-2024-001',
218
+ customerName: '张三'
219
+ })
220
+ designer.print({ mode: 'browser', options: { silent: true } })
104
221
  ```
105
222
 
106
223
  **报告模式(推荐用于完整报告):**
224
+
107
225
  ```javascript
108
- // 1. 创建/编辑报告
226
+ // 1. 编辑报告时传入完整数据
109
227
  const sampleData = {
110
- id: 8, // 报告 ID
111
- title: "2024年第一季度销售报告",
228
+ id: 8, // 报告 ID
229
+ title: '2024年第一季度销售报告',
112
230
  totalAmount: 50000,
113
- items: [...] // 报告数据
114
- };
231
+ items: [...]
232
+ }
115
233
 
116
234
  // 2. 保存报告(布局 + 数据一起保存)
117
- // payload.data 包含完整的 testData
235
+ // payload.data 中包含完整的 testData
118
236
 
119
237
  // 3. 加载后直接打印,无需再次注入数据
120
- designer.loadTemplate(8);
121
- designer.print();
238
+ designer.loadTemplate(8)
239
+ designer.print({ mode: 'browser', options: { silent: true } })
122
240
  ```
123
241
 
124
242
  ### 切换模式
@@ -131,66 +249,23 @@ designer.print();
131
249
 
132
250
  ```javascript
133
251
  // Web Component
134
- designer.setMode('template');
135
- designer.setMode('report');
136
- ```
137
-
138
- ---
139
-
140
- ## 快速开始
141
-
142
- ```bash
143
- npm install
144
- npm run dev
145
- ```
146
-
147
- ### 最简接入
148
-
149
- ```vue
150
- <template>
151
- <PrintDesigner
152
- mode="template"
153
- :sample-data="sampleData"
154
- :on-save="handleSave"
155
- />
156
- </template>
157
-
158
- <script setup>
159
- import { ref } from 'vue';
160
- import PrintDesigner from '@/components/PrintDesigner.vue';
161
-
162
- const sampleData = ref({
163
- id: 1,
164
- name: '测试报告',
165
- templateId: 'tpl_001',
166
- variables: {
167
- title: '报告标题',
168
- customerName: '张三',
169
- items: [
170
- { name: '产品A', price: 100, quantity: 2 }
171
- ]
172
- }
173
- });
174
-
175
- const handleSave = (payload) => {
176
- console.log('保存数据:', payload);
177
- // { id: '...', name: '测试报告', data: {...}, isNew: false }
178
- // 用户自行调用接口保存
179
- };
180
- </script>
252
+ designer.setMode('template')
253
+ designer.setMode('report')
181
254
  ```
182
255
 
183
256
  ---
184
257
 
185
258
  ## Vue 组件使用说明
186
259
 
187
- ### 组件 Props 一览
260
+ ### 组件 Props
188
261
 
189
262
  | 属性 | 类型 | 默认值 | 说明 |
190
263
  |------|------|--------|------|
191
- | `mode` | `'template'` \| `'report'` | `'template'` | 工作模式 |
192
- | `sample-data` | `object` | | 传入的示例数据 |
193
- | `on-save` | `(payload) => void \| Promise<void>` | | 保存回调,详见下方 |
264
+ | `mode` | `'template'` \| `'report'` | `'template'` | 工作模式,影响保存行为 |
265
+ | `view-mode` | `'edit'` \| `'preview'` | `'edit'` | 视图模式,`'preview'` 时只渲染打印内容 |
266
+ | `headless` | `boolean` | `false` | 无头模式,隐藏顶部工具栏和侧边栏 |
267
+ | `sample-data` | `Record<string, any>` | — | 传入的示例/预览数据,详见[传入数据格式](#传入数据格式) |
268
+ | `on-save` | `(payload) => void \| Promise<void>` | — | 保存回调,详见[保存回调](#保存回调-onsave) |
194
269
  | `auto-save-enabled` | `boolean` | `false` | 是否开启定时自动保存 |
195
270
  | `auto-save-interval-ms` | `number` | `120000` | 自动保存间隔(毫秒),默认 2 分钟 |
196
271
 
@@ -198,146 +273,178 @@ const handleSave = (payload) => {
198
273
 
199
274
  #### `mode`
200
275
 
201
- 工作模式。`'template'` 保存时只存布局,`'report'` 保存时同时存布局和 testData
276
+ 工作模式,决定保存时是否附带 `testData`。
202
277
 
203
278
  ```vue
204
279
  <PrintDesigner mode="report" />
205
280
  ```
206
281
 
282
+ #### `view-mode`
283
+
284
+ 切换编辑/预览视图。设为 `'preview'` 时隐藏所有编辑器 UI,仅渲染打印页面。适用于嵌入第三方页面时只展示打印效果。
285
+
286
+ ```vue
287
+ <PrintDesigner view-mode="preview" />
288
+ ```
289
+
290
+ #### `headless`
291
+
292
+ 无头模式,隐藏顶部工具栏和侧边属性面板。用于嵌入式场景仅保留画布区域。
293
+
294
+ ```vue
295
+ <PrintDesigner headless />
296
+ ```
297
+
207
298
  #### `sample-data`
208
299
 
209
- 传入设计器的示例数据。**最关键的属性**。详见下方 [传入数据格式](#传入数据格式)。
300
+ 传入设计器的预览数据。**最关键的属性**。详见 [传入数据格式](#传入数据格式)。
210
301
 
211
302
  ```vue
212
- <PrintDesigner :sample-data="{ id: 1, name: '报告', variables: {...} }" />
303
+ <PrintDesigner :sample-data="{ id: 1, title: '报告', items: [...] }" />
213
304
  ```
214
305
 
215
306
  #### `on-save`
216
307
 
217
- 保存回调函数。当用户点击保存按钮(或自动保存触发)时,设计器组装好数据后调用此回调,**不再自动调接口**,完全交由用户自行处理。
308
+ 保存回调函数。传入后设计器**不再执行内置保存逻辑**,完全由用户自行处理持久化。
218
309
 
219
310
  ```vue
220
- <PrintDesigner :on-save="(payload) => fetch('/api/save', { method: 'POST', body: JSON.stringify(payload) })" />
311
+ <PrintDesigner :on-save="(payload) => mySaveApi(payload)" />
221
312
  ```
222
313
 
223
- #### `auto-save-enabled`
314
+ > 不传 `onSave` 时,设计器使用内置 CRUD 逻辑(根据配置调远程接口或存 localStorage)。
224
315
 
225
- 是否开启定时自动保存。开启后每隔 `auto-save-interval-ms` 毫秒自动触发 `on-save` 回调。详见 [自动保存配置](#自动保存配置)。
316
+ #### `auto-save-enabled` / `auto-save-interval-ms`
226
317
 
227
- #### `auto-save-interval-ms`
228
-
229
- 自动保存间隔时间(毫秒),默认 `120000`(2 分钟)。
318
+ 详见 [自动保存配置](#自动保存配置)。
230
319
 
231
320
  ---
232
321
 
233
322
  ## Web Component 使用说明
234
323
 
235
- ### 标签方式
324
+ ### 加载方式
236
325
 
237
326
  ```html
238
- <print-designer id="designer"></print-designer>
327
+ <!-- ES Module 方式(推荐) -->
328
+ <script type="module" src="/dist/print-designer.es.js"></script>
329
+ <link rel="stylesheet" href="/dist/print-designer.css" />
239
330
 
240
- <script>
241
- const designer = document.getElementById('designer');
331
+ <!-- 或 UMD 方式 -->
332
+ <script src="/dist/print-designer.umd.js"></script>
333
+ <link rel="stylesheet" href="/dist/print-designer.css" />
334
+ ```
242
335
 
243
- designer.setMode('template');
244
- designer.setOnSave((payload) => {
245
- fetch('/api/save', { method: 'POST', body: JSON.stringify(payload) });
246
- });
247
- designer.setAutoSave(true, 120000);
248
- designer.setTestData({ orderNo: 'ORD-001', items: [...] });
249
- </script>
336
+ ```html
337
+ <print-designer id="designer"></print-designer>
250
338
  ```
251
339
 
252
- ### API 速览
340
+ ### 完整 API 参考
253
341
 
254
- | 方法 | 说明 |
255
- |------|------|
256
- | `setMode(mode)` | 设置模式:`'template'` / `'report'` |
257
- | `setViewMode(viewMode)` | 切换视图:`'edit'` / `'preview'` |
258
- | `setOnSave(handler)` | 设置保存回调 |
259
- | `setAutoSave(enabled, intervalMs?)` | 开启/关闭自动保存 |
260
- | `setTestData(data)` | 设置测试数据 |
261
- | `setVariables(vars)` | 打印/导出时注入真实变量 |
262
- | `setSampleData(data)` | 设置样本数据 |
263
- | `setTemplates(list, opts)` | 设置模板列表 |
264
- | `loadTemplate(id)` | 加载指定模板到画布 |
265
- | `loadTemplateData(data)` | 直接加载模板 JSON 数据到画布 |
266
- | `getTemplateData()` | 获取当前模板完整数据 |
267
- | `getPreviewHtml()` | 获取预览 HTML 字符串 |
268
- | `print(request)` | 执行打印 |
269
- | `export(request)` | 导出 PDF/图片/HTML |
270
- | `setPreviewMode(options)` | 🆕 进入独立预览面板模式 |
271
- | `exitPreviewMode()` | 🆕 退出预览模式,返回编辑器 |
342
+ 所有方法均通过 DOM 元素直接调用。**必须在 `ready` 事件触发后调用。**
343
+
344
+ | 方法 | 参数 | 返回值 | 说明 |
345
+ |------|------|--------|------|
346
+ | `setMode(mode)` | `'template'` \| `'report'` | `void` | 设置工作模式 |
347
+ | `setViewMode(viewMode)` | `'edit'` \| `'preview'` | `void` | 切换视图模式 |
348
+ | `setOnSave(handler)` | `(payload) => void \| Promise<void>` | `void` | 设置保存回调 |
349
+ | `setAutoSave(enabled, intervalMs?)` | `boolean, number?` | `void` | 开启/关闭自动保存,`intervalMs` 默认 120000 |
350
+ | `setTestData(data)` | `Record<string, any>` | `void` | 设置测试/预览数据(等价 Vue 组件的 `sample-data`) |
351
+ | `setSampleData(data)` | `Record<string, any>` | `void` | `setTestData` 的别名 |
352
+ | `setVariables(vars)` | `Record<string, any>` | `void` | 打印/导出时注入真实变量(覆盖 testData 中的同名字段) |
353
+ | `setTemplates(list, opts?)` | `Array<Template>, object?` | `void` | 设置预设模板列表。`opts` 可选:`{ currentTemplateId?: string }` |
354
+ | `loadTemplate(id)` | `string` | `Promise<void>` | 加载指定模板到画布 |
355
+ | `loadTemplateData(data)` | `Record<string, any>` | `boolean` | 直接加载模板 JSON 数据到画布,返回是否成功 |
356
+ | `getTemplateData()` | | `Record<string, any>` | 获取当前模板完整数据 |
357
+ | `getPreviewHtml()` | | `Promise<string>` | 获取预览 HTML 字符串 |
358
+ | `print(options?)` | `PrintOptions` | `Promise<void>` | 执行打印,详见[打印与导出](#打印与导出) |
359
+ | `export(options?)` | `ExportOptions` | `Promise<void>` | 导出 PDF / 图片 / HTML,详见[打印与导出](#打印与导出) |
360
+ | `setPreviewMode(options)` | `PreviewModeOptions` | `void` | 进入独立预览面板模式 |
361
+ | `exitPreviewMode()` | — | `void` | 退出预览模式,返回编辑器 |
362
+ | `setBranding(config)` | `{ title?, logoUrl?, showTitle?, showLogo? }` | `void` | 设置品牌标识 |
363
+ | `setLang(lang)` | `'zh'` \| `'en'` | `void` | 切换语言 |
364
+
365
+ ### 基本使用示例
366
+
367
+ ```javascript
368
+ const designer = document.getElementById('designer')
369
+
370
+ designer.addEventListener('ready', () => {
371
+ // 设置模式
372
+ designer.setMode('template')
373
+
374
+ // 设置数据
375
+ designer.setTestData({
376
+ id: 1,
377
+ orderNo: 'ORD-001',
378
+ customerName: '张三',
379
+ items: [
380
+ { name: '产品A', price: 100, quantity: 2 },
381
+ { name: '产品B', price: 200, quantity: 3 }
382
+ ]
383
+ })
384
+
385
+ // 保存回调
386
+ designer.setOnSave(async (payload) => {
387
+ console.log('保存:', payload)
388
+ await fetch('/api/templates/save', {
389
+ method: 'POST',
390
+ headers: { 'Content-Type': 'application/json' },
391
+ body: JSON.stringify(payload)
392
+ })
393
+ })
394
+
395
+ // 开启自动保存(每 2 分钟)
396
+ designer.setAutoSave(true, 120000)
397
+ })
398
+ ```
272
399
 
273
400
  ### 打印时注入真实数据
274
401
 
275
- ```js
276
- const designer = document.querySelector('print-designer');
402
+ 使用 `setVariables` 在打印/导出时覆盖预览数据中的字段:
277
403
 
278
- await designer.loadTemplate('tpl-order');
404
+ ```javascript
405
+ const designer = document.querySelector('print-designer')
279
406
 
407
+ // 加载模板
408
+ await designer.loadTemplate('tpl-order')
409
+
410
+ // 注入真实打印数据
280
411
  designer.setVariables({
281
412
  orderNo: 'REAL-2026-001',
282
413
  customerName: '李四',
283
414
  totalAmount: 5999.00
284
- });
415
+ })
285
416
 
286
- await designer.print({ mode: 'browser', options: { silent: true } });
417
+ // 静默打印
418
+ await designer.print({ mode: 'browser', options: { silent: true } })
287
419
  ```
288
420
 
289
- ### 独立预览面板 (`setPreviewMode` / `exitPreviewMode`) 🆕
290
-
291
- 当设计器以 Web Component 方式嵌入时,可切换到独立的**纯预览模式**——只渲染打印内容,不显示编辑器工具栏、属性面板、辅助线等 UI。
421
+ ### 独立预览面板
292
422
 
293
- **适用于:**
294
- - 插件/第三方页面嵌入时,单独展示打印预览效果
295
- - 用户填写表单后直接预览生成的打印内容
296
- - 无需编辑功能的只读预览场景
423
+ 当设计器嵌入第三方页面时,可切换到独立的**纯预览模式**——只渲染打印内容,不显示编辑器 UI。
297
424
 
298
- #### 切换方式
425
+ **适用于:** 插件/第三方页面嵌入时的打印预览、用户填写表单后的预览、无需编辑功能的只读场景。
299
426
 
300
- ```javascript
301
- const designer = document.querySelector('print-designer');
302
-
303
- // 进入预览模式,传入模板和数据
304
- designer.setPreviewMode({
305
- pages: templateData.pages, // 页面元素布局
306
- canvasSize: { width: 794, height: 1123 }, // 画布尺寸
307
- canvasBackground: '#ffffff', // 背景色
308
- testData: { // 测试数据
309
- name: '张三',
310
- amount: 1000,
311
- items: [{ name: '产品A', price: 100 }]
312
- },
313
- variables: {}, // 变量映射
314
- watermark: { enabled: false } // 水印设置
315
- });
427
+ #### `setPreviewMode(options)`
316
428
 
317
- // 退出预览,返回编辑器界面
318
- designer.exitPreviewMode();
429
+ ```typescript
430
+ interface PreviewModeOptions {
431
+ pages: Page[] // 页面元素布局(必填)
432
+ canvasSize: { width: number; height: number } // 画布尺寸(必填)
433
+ canvasBackground?: string // 画布背景色,默认 '#ffffff'
434
+ testData?: Record<string, any> // 测试数据
435
+ variables?: Record<string, any> // 额外变量映射
436
+ watermark?: WatermarkSettings // 水印配置
437
+ }
319
438
  ```
320
439
 
321
- #### 参数说明
322
-
323
- | 参数 | 类型 | 必填 | 说明 |
324
- |------|------|------|------|
325
- | `pages` | `Page[]` | ✅ 是 | 页面元素布局数组 |
326
- | `canvasSize` | `{ width, height }` | ✅ 是 | 画布尺寸(px) |
327
- | `canvasBackground` | `string` | 否 | 画布背景色,默认 `'#ffffff'` |
328
- | `testData` | `object` | 否 | 测试数据,模板中的变量会自动解析 |
329
- | `variables` | `object` | 否 | 额外变量映射 |
330
- | `watermark` | `object` | 否 | 水印配置 |
331
-
332
440
  #### 使用示例
333
441
 
334
442
  ```javascript
335
- // 场景:用户点击"预览"按钮,传入后端返回的模板和当前表单数据
336
443
  async function openPreview(reportId) {
337
- const designer = document.querySelector('print-designer');
444
+ const designer = document.querySelector('print-designer')
338
445
 
339
446
  // 从后端获取模板数据
340
- const template = await fetch(`/api/templates/${reportId}`).then(r => r.json());
447
+ const template = await fetch(`/api/templates/${reportId}`).then(r => r.json())
341
448
 
342
449
  // 进入预览模式
343
450
  designer.setPreviewMode({
@@ -350,16 +457,16 @@ async function openPreview(reportId) {
350
457
  items: getOrderItems()
351
458
  },
352
459
  watermark: template.data.watermark
353
- });
460
+ })
354
461
  }
355
462
 
356
463
  // 返回编辑
357
464
  function backToEdit() {
358
- designer.exitPreviewMode();
465
+ designer.exitPreviewMode()
359
466
  }
360
467
  ```
361
468
 
362
- > **注意:** `exitPreviewMode()` 会自动恢复进入预览前的编辑器状态(模板选择、编辑进度等)。
469
+ > `exitPreviewMode()` 会自动恢复进入预览前的编辑器状态(模板选择、编辑进度等)。
363
470
 
364
471
  ---
365
472
 
@@ -367,24 +474,20 @@ function backToEdit() {
367
474
 
368
475
  ### 基本要求
369
476
 
370
- `sample-data` 接收一个普通的 JSON 对象,**唯一必填字段是 `id`**,用于标识业务数据的唯一性。其他字段完全自由,可以根据业务需求灵活组织数据结构。
477
+ `sample-data` / `setTestData(data)` 接收一个普通的 JSON 对象。**唯一必填字段是 `id`**。
371
478
 
372
479
  | 字段 | 类型 | 必填 | 说明 |
373
480
  |------|------|------|------|
374
- | `id` | `number` / `string` | ✅ 是 | **业务数据的唯一标识**,如报告 ID、订单 ID |
375
- | `templateId` | `string` | 否 | 加载的打印模板 ID,不传则用当前已选模板 |
376
- | `name` | `string` | 否 | 数据名称,如报告标题、订单名称 |
377
- | `variables` | `object` | 否 | 业务变量对象(可选,用作组织数据的约定) |
378
- | 任意自定义字段 | `any` | 否 | `createTime`、`customerName`,也可在模板中直接引用 |
379
-
380
- ### 两种数据传入方式
481
+ | `id` | `number` \| `string` | ✅ 是 | 业务数据的唯一标识,如报告 ID、订单 ID |
482
+ | `templateId` | `string` | 否 | 绑定的模板 ID,不传则用当前已选模板 |
483
+ | `name` | `string` | 否 | 数据名称 |
484
+ | `variables` | `object` | 否 | 业务变量对象(推荐,用于组织数据) |
485
+ | 任意自定义字段 | `any` | 否 | 也可直接在根级别存放变量 |
381
486
 
382
- 组件支持两种数据组织方式,**可以任选其一**,组件会自动兼容处理:
487
+ ### 两种组织方式(任选其一,自动兼容)
383
488
 
384
489
  #### 方式一:带 `variables` 包装(推荐)
385
490
 
386
- 将所有业务变量放在 `variables` 对象中,结构清晰,适合复杂业务场景:
387
-
388
491
  ```json
389
492
  {
390
493
  "id": 8,
@@ -397,18 +500,15 @@ function backToEdit() {
397
500
  { "name": "产品A", "price": 100, "quantity": 2 }
398
501
  ],
399
502
  "chartData": [
400
- { "category": "一月", "value": 1200 },
401
- { "category": "二月", "value": 1500 }
503
+ { "category": "一月", "value": 1200 }
402
504
  ]
403
505
  }
404
506
  }
405
507
  ```
406
508
 
407
- 模板绑定写法:`@title`、`@customerName`、`@items`、`@chartData`
509
+ 模板绑定:`@title`、`@customerName`、`@items`、`@chartData`
408
510
 
409
- #### 方式二:直接扁平数据(简洁)
410
-
411
- 将所有字段直接放在根级别,无需 `variables` 包装,适合简单场景:
511
+ #### 方式二:扁平数据
412
512
 
413
513
  ```json
414
514
  {
@@ -420,29 +520,70 @@ function backToEdit() {
420
520
  { "name": "产品A", "price": 100, "quantity": 2 }
421
521
  ],
422
522
  "chartData": [
423
- { "category": "一月", "value": 1200 },
424
- { "category": "二月", "value": 1500 }
523
+ { "category": "一月", "value": 1200 }
425
524
  ]
426
525
  }
427
526
  ```
428
527
 
429
- 模板绑定写法:`@title`、`@customerName`、`@items`、`@chartData`
528
+ 模板绑定:`@title`、`@customerName`、`@items`、`@chartData`
430
529
 
431
- > 两种方式完全等效,组件内部会自动解析变量路径,无需担心格式不匹配。
530
+ > 两种方式等效。设计器内部优先在 `variables` 中查找,未找到则在根级别查找。
432
531
 
433
532
  ### 支持的数据类型
434
533
 
435
- 所有字段都可在模板元素中通过变量语法绑定,支持:
436
-
437
- | 类型 | 示例 | 在模板中绑定 |
438
- |------|------|-------------|
534
+ | 类型 | 示例 | 模板绑定 |
535
+ |------|------|---------|
439
536
  | 字符串 | `"张三"` | `@customerName` |
440
537
  | 数字 | `100` | `@totalAmount` |
441
538
  | 布尔值 | `true` | `@isPaid` |
442
539
  | 数组 | `[{...}, {...}]` | `@items` |
443
- | 嵌套对象 | `{user: {name: "李四"}}` | `@user.name` |
540
+ | 嵌套对象 | `{ user: { name: "李四" } }` | `@user.name` |
541
+
542
+ ### 表格数据
444
543
 
445
- ### 完整示例数据格式
544
+ 表格元素需要三个维度的变量绑定:
545
+
546
+ | 绑定属性 | 变量名示例 | 数据格式 | 说明 |
547
+ |---------|-----------|---------|------|
548
+ | `variable` | `@items` | `Array<Object>` | 表格行数据 |
549
+ | `columnsVariable` | `@tableCols` | `Array<{field, header, width}>` | 列定义 |
550
+ | `footerDataVariable` | `@tableFooter` | `Array<Object>` | 页脚汇总行 |
551
+
552
+ ```json
553
+ {
554
+ "items": [
555
+ { "name": "产品A", "price": 100, "quantity": 2 },
556
+ { "name": "产品B", "price": 200, "quantity": 3 }
557
+ ],
558
+ "tableCols": [
559
+ { "field": "name", "header": "产品名称", "width": 120 },
560
+ { "field": "price", "header": "单价", "width": 80 },
561
+ { "field": "quantity", "header": "数量", "width": 80 }
562
+ ],
563
+ "tableFooter": [
564
+ { "name": { "value": "合计" }, "price": { "value": "300" } }
565
+ ]
566
+ }
567
+ ```
568
+
569
+ ### 图表数据
570
+
571
+ ```json
572
+ {
573
+ "chartData": [
574
+ { "category": "一月", "value": 1200 },
575
+ { "category": "二月", "value": 1500 }
576
+ ]
577
+ }
578
+ ```
579
+
580
+ 图表元素需在属性面板中配置:
581
+ - **chartDataVariable**:`@chartData`
582
+ - **chartXField**:`category`(X 轴字段名)
583
+ - **chartYField**:`value`(Y 轴字段名)
584
+ - **chartNameField**:`category`(饼图名称字段)
585
+
586
+ ### 完整示例数据
446
587
 
447
588
  ```json
448
589
  {
@@ -466,249 +607,201 @@ function backToEdit() {
466
607
  "dockname": "宝安国际无人机机场",
467
608
  "distance": "12.5 km",
468
609
  "image": "https://picsum.photos/id/1/200/80"
469
- },
470
- {
471
- "time": "2026-04-08 14:00",
472
- "taskname": "西乡街道线路巡查",
473
- "dockname": "罗湖无人机机场",
474
- "distance": "10.2 km",
475
- "image": "https://picsum.photos/id/10/200/80"
476
- }
477
- ],
478
- "taskRecords": [
479
- {
480
- "time": "2026-04-02 09:30",
481
- "taskname": "宝安港区A线巡检",
482
- "dockname": "宝安国际无人机机场",
483
- "warn": 2
484
- },
485
- {
486
- "time": "2026-04-08 14:00",
487
- "taskname": "西乡街道线路巡查",
488
- "dockname": "罗湖无人机机场",
489
- "warn": 1
490
610
  }
491
611
  ],
492
612
  "chartData": [
493
613
  { "category": "一月", "value": 1200 },
494
- { "category": "二月", "value": 1500 },
495
- { "category": "三月", "value": 1800 }
614
+ { "category": "二月", "value": 1500 }
496
615
  ]
497
616
  }
498
617
  }
499
618
  ```
500
619
 
501
- ### 在模板中绑定变量
620
+ ### 注意事项
502
621
 
503
- 变量绑定使用 `@变量名` 或 `{#变量名}` 语法。绑定时只需使用变量名,无需关心数据是否在 `variables` 包装下:
622
+ 1. **`id` 字段**
623
+ - 业务数据的唯一标识,在保存回调中用于识别当前编辑的数据
624
+ - 不传则保存时 `payload.id` 为 `null`
504
625
 
505
- | 变量路径 | 绑定写法 | 应用元素 |
506
- |---------|---------|---------|
507
- | `variables.dockname` / `dockname` | `@dockname` | 文本 |
508
- | `variables.reportname` / `reportname` | `@reportname` | 文本 |
509
- | `variables.flightRecords` / `flightRecords` | `@flightRecords` | 表格、自定义表格 |
510
- | `variables.chartData` / `chartData` | `@chartData` | 图表 |
626
+ 2. **`id` `templateId` 的区别**
627
+ - `id`:业务数据的标识(报告中用作报告 ID)
628
+ - `templateId`:打印模板的标识,两者是完全不同的概念
511
629
 
512
- > 组件会自动解析变量路径,无论数据在 `variables.xxx` 还是直接在根级别,都能正确绑定。
630
+ 3. **变量命名**
631
+ - 使用 camelCase 命名,如 `customerName` 而非 `c`
632
+ - 避免特殊字符和空格
633
+ - 嵌套路径使用点号:`@user.profile.name`
513
634
 
514
- ### 表格数据
635
+ 4. **数组数据**
636
+ - 表格和图表需要数组类型
637
+ - 数组元素应包含一致的字段结构:`items: [{name: "A", price: 100}, {name: "B", price: 200}]`
515
638
 
516
- 表格需要三个维度的变量:
639
+ 5. **动态更新**
640
+ - Web Component 通过 `setSampleData()` / `setTestData()` 更新
641
+ - 数据更新后,画布中的变量绑定会自动重新解析
517
642
 
518
- | 绑定字段 | 变量名 | 格式 | 说明 |
519
- |---------|-------|------|------|
520
- | `variable` | `@items` | `Array<Object>` | 表格行数据 |
521
- | `columnsVariable` | `@tableCols` | `Array<{field, header, width}>` | 列定义 |
522
- | `footerDataVariable` | `@tableFooter` | `Array<Object>` | 页脚汇总行 |
643
+ ---
523
644
 
524
- ```json
525
- {
526
- "items": [
527
- { "name": "产品A", "price": 100, "quantity": 2 },
528
- { "name": "产品B", "price": 200, "quantity": 3 }
529
- ],
530
- "tableCols": [
531
- { "field": "name", "header": "产品名称", "width": 120 },
532
- { "field": "price", "header": "单价", "width": 80 },
533
- { "field": "quantity", "header": "数量", "width": 80 }
534
- ],
535
- "tableFooter": [
536
- { "name": { "value": "合计" }, "price": { "value": "300" } }
537
- ]
538
- }
539
- ```
645
+ ## 元素数据绑定
540
646
 
541
- ### 图表数据
647
+ ### 变量语法
542
648
 
543
- ```json
544
- {
545
- "chartData": [
546
- { "category": "一月", "value": 1200 },
547
- { "category": "二月", "value": 1500 }
548
- ]
549
- }
550
- ```
649
+ | 语法 | 示例 | 说明 |
650
+ |-----|------|------|
651
+ | `@变量名` | `@customerName` | 直接引用变量 |
652
+ | `{#变量路径}` | `{#order.items[0].name}` | 支持嵌套路径和数组索引 |
551
653
 
552
- 图表元素需配置:
553
- - **chartDataVariable:** `@chartData`
554
- - **chartXField:** `category`(X 轴字段名)
555
- - **chartYField:** `value`(Y 轴字段名)
556
- - **chartNameField:** `category`(饼图名称字段)
654
+ ### 各元素可绑定字段
557
655
 
558
- ### 使用注意事项
656
+ | 元素 | 绑定属性 | 示例 |
657
+ |------|---------|------|
658
+ | 文本 / 长文本 | `content` | `订单号: {#orderNo}` |
659
+ | 图片 | `variable` | `@imageUrl` |
660
+ | 表格 | `variable` + `columnsVariable` + `footerDataVariable` | `@items`, `@tableCols`, `@tableFooter` |
661
+ | 自定义表格 | `customTableCells[].field` | `@customerName`(单元格字段) |
662
+ | 条形码 | `variable` 或 `content` | `@barcode` |
663
+ | 二维码 | `variable` 或 `content` | `@url` |
664
+ | 图表 | `chartDataVariable` + `chartXField` + `chartYField` | `@chartData`, `category`, `value` |
665
+ | 数据卡片 | `variable` | `@totalAmount` |
666
+ | 进度条 | `variable` | `@progress` |
667
+ | 日期 | `variable` | `@createTime` |
559
668
 
560
- 1. **`id` 字段的重要性**
561
- - `id` 是业务数据的唯一标识,用于在保存回调中识别当前编辑的数据
562
- - 如果不传 `id`,保存时 `payload.id` 将为 `null`,表示新建数据
669
+ ---
563
670
 
564
- 2. **`id` 与 `templateId` 的区别**
565
- - `id`:业务数据(如订单、报告)的标识
566
- - `templateId`:打印模板的标识,两者是完全不同的概念
671
+ ## 图片元素使用说明
567
672
 
568
- 3. **变量命名建议**
569
- - 使用有意义的变量名,如 `customerName` 而非 `c`
570
- - 避免使用特殊字符和空格
571
- - 嵌套路径使用点号分隔,如 `@user.profile.name`
673
+ ### 方式 1:直接输入图片 URL
572
674
 
573
- 4. **数组数据的注意事项**
574
- - 表格和图表需要数组类型的数据
575
- - 数组元素应包含一致的字段结构
576
- - 示例:`items: [{name: "A", price: 100}, {name: "B", price: 200}]`
675
+ 在属性面板的"图片地址"输入框填写 URL(支持 HTTP/HTTPS)。
577
676
 
578
- 5. **动态更新数据**
579
- - 可以通过 Web Component 的 `setSampleData()` 方法动态更新数据
580
- - 数据更新后,模板中的变量绑定会自动重新解析
677
+ ```
678
+ https://example.com/logo.png
679
+ ```
581
680
 
582
- 6. **数据验证建议**
583
- - 确保传入的数据字段与模板中绑定的变量名一致
584
- - 如果变量找不到对应的数据,模板中会显示为空
681
+ ### 方式 2:本地上传(自动转 Base64)
585
682
 
586
- ---
683
+ 点击属性面板中的 **"上传图片"** 按钮或**双击画布上的图片元素**,从本地选择图片文件:
684
+ - 自动转为 Base64 并填入图片地址
685
+ - 限制最大 **2MB**
686
+ - 支持 PNG、JPG、GIF、WebP 等格式
587
687
 
588
- ## 保存回调 `onSave`
688
+ ### 方式 3:绑定变量
589
689
 
590
- 传入 `onSave` 后,用户点击保存按钮(`Ctrl+S` 或工具栏按钮)时,设计器不再自动调接口,而是将组装好的数据通过回调交给用户自行处理。
690
+ `variable` 绑定到变量名,图片地址从数据中动态读取。
591
691
 
592
- ### 回调参数
692
+ | 绑定方式 | 示例 | 数据值要求 |
693
+ |---------|------|-----------|
694
+ | 静态变量 | `@logoUrl` | 字符串 URL 或 Base64 |
695
+ | 数组元素 | `@flightRecords[0].image` | 该字段值为 URL 或 Base64 |
593
696
 
594
- ```ts
595
- interface SavePayload {
596
- id: string | null; // 模板 ID,null 表示新建
597
- name: string; // 模板名称
598
- data: Record<string, any>; // 模板数据(见下方详细说明)
599
- isNew: boolean; // 是否新建模板
697
+ **对应数据:**
698
+
699
+ ```json
700
+ {
701
+ "variables": {
702
+ "logoUrl": "https://example.com/logo.png",
703
+ "avatarBase64": "data:image/png;base64,iVBORw0KGgo...",
704
+ "flightRecords": [
705
+ { "image": "https://picsum.photos/id/1/200/80" }
706
+ ]
707
+ }
600
708
  }
601
709
  ```
602
710
 
603
- ### 不同模式下的保存数据差异
711
+ ### 方式对比
604
712
 
605
- | 数据字段 | 模板模式 | 报告模式 | 说明 |
606
- |---------|---------|---------|------|
607
- | `pages` | | | 页面元素布局 |
608
- | `canvasSize` | | | 画布尺寸 |
609
- | `watermark` | | | 水印设置 |
610
- | `guides` | ✅ | ✅ | 辅助线 |
611
- | `testData` | ❌ | ✅ | 传入的示例数据 |
612
- | `unit` | ✅ | ✅ | 单位设置 |
613
- | `showGrid` | ✅ | ✅ | 网格显示 |
614
- | ... | ... | ... | 其他配置 |
713
+ | 方式 | 适用场景 | 存储内容 |
714
+ |------|---------|---------|
715
+ | URL 输入 | 固定图片、CDN 资源 | URL 字符串 |
716
+ | 本地上传 | 无公网链接的图片 | Base64 编码 |
717
+ | 变量绑定 | 每条数据图片不同 | URL Base64 |
615
718
 
616
- ### 保存数据示例
719
+ ---
617
720
 
618
- **模板模式保存的数据:**
619
- ```javascript
620
- {
621
- id: "tpl_001", // 模板 ID
622
- name: "订单打印模板",
623
- data: {
624
- pages: [...], // 页面布局
625
- canvasSize: {...}, // 画布尺寸
626
- watermark: {...}, // 水印设置
627
- unit: "mm",
628
- showGrid: true,
629
- // ❌ 没有 testData 字段
630
- },
631
- isNew: false
632
- }
633
- ```
721
+ ## 保存回调 `onSave`
634
722
 
635
- **报告模式保存的数据:**
636
- ```javascript
637
- {
638
- id: "rpt_001", // 报告 ID
639
- name: "第一季度销售报告",
640
- data: {
641
- pages: [...], // 页面布局
642
- canvasSize: {...}, // 画布尺寸
643
- watermark: {...}, // 水印设置
644
- unit: "mm",
645
- showGrid: true,
646
- // ✅ 包含完整的 testData
647
- testData: {
648
- id: 8,
649
- name: "第一季度销售报告",
650
- totalAmount: 50000,
651
- items: [
652
- { name: "产品A", price: 100, quantity: 200 },
653
- { name: "产品B", price: 200, quantity: 150 }
654
- ],
655
- chartData: [
656
- { category: "一月", value: 1200 },
657
- { category: "二月", value: 1500 },
658
- { category: "三月", value: 1800 }
659
- ]
660
- }
661
- },
662
- isNew: false
723
+ 传入 `onSave` 后,用户保存(`Ctrl+S` 或工具栏保存按钮)时设计器不再自动持久化,而是将组装好的数据通过回调交给用户自行处理。
724
+
725
+ ### 回调参数
726
+
727
+ ```typescript
728
+ interface SavePayload {
729
+ id: string | null // 模板 ID,null 表示新建
730
+ name: string // 模板名称
731
+ data: Record<string, any> // 模板数据(含 pages、canvasSize、watermark 等)
732
+ isNew: boolean // 是否新建模板
663
733
  }
664
734
  ```
665
735
 
736
+ ### 不同模式下 data 内容差异
737
+
738
+ | 字段 | 模板模式 | 报告模式 |
739
+ |------|---------|---------|
740
+ | `pages` | ✅ | ✅ |
741
+ | `canvasSize` | ✅ | ✅ |
742
+ | `watermark` | ✅ | ✅ |
743
+ | `guides` | ✅ | ✅ |
744
+ | `unit` | ✅ | ✅ |
745
+ | `showGrid` | ✅ | ✅ |
746
+ | `headerHeight` / `footerHeight` | ✅ | ✅ |
747
+ | `canvasBackground` | ✅ | ✅ |
748
+ | `pageSpacingX` / `pageSpacingY` | ✅ | ✅ |
749
+ | `testData` | ❌ | ✅ |
750
+
666
751
  ### 使用示例
667
752
 
668
753
  ```vue
669
- <PrintDesigner
670
- :mode="mode"
671
- :on-save="handleSave"
672
- />
754
+ <template>
755
+ <PrintDesigner
756
+ :mode="mode"
757
+ :sample-data="sampleData"
758
+ :on-save="handleSave"
759
+ />
760
+ </template>
673
761
 
674
- <script setup>
675
- import { ref } from 'vue';
762
+ <script setup lang="ts">
763
+ import { ref } from 'vue'
676
764
 
677
- const mode = ref('template'); // 'report'
765
+ const mode = ref<'template' | 'report'>('template')
678
766
 
679
767
  const handleSave = async (payload) => {
680
- console.log('保存模式:', mode.value);
681
- console.log('保存的数据:', payload);
682
-
683
- // 根据模式决定如何处理
684
- if (mode.value === 'template') {
685
- // 模板模式:保存布局,数据单独处理
686
- await saveTemplate(payload.id, payload.name, payload.data);
687
- } else {
688
- // 报告模式:布局和数据一起保存
689
- await saveReport(payload.id, payload.name, payload.data);
768
+ // 根据模式决定 API 端点
769
+ const endpoint = mode.value === 'template'
770
+ ? '/api/templates/save'
771
+ : '/api/reports/save'
772
+
773
+ const res = await fetch(endpoint, {
774
+ method: 'POST',
775
+ headers: { 'Content-Type': 'application/json' },
776
+ body: JSON.stringify(payload)
777
+ })
778
+
779
+ if (!res.ok) throw new Error('保存失败')
780
+
781
+ // 新建时获取后端返回的 ID
782
+ const result = await res.json()
783
+ if (payload.isNew && result.id) {
784
+ console.log('新模板 ID:', result.id)
690
785
  }
691
- };
786
+ }
692
787
  </script>
693
788
  ```
694
789
 
695
- > 不传 `onSave` 时,设计器会走原有的默认保存逻辑(调用内置 CRUD 接口或存 localStorage),行为不变。
790
+ > 不传 `onSave` 时,设计器走内置默认保存逻辑(根据 CRUD 配置调远程接口或存 localStorage)。
696
791
 
697
792
  ---
698
793
 
699
794
  ## 自动保存配置
700
795
 
701
- 传入 `onSave` 后,可同时开启自动保存。开启后设计器会每隔固定时间调用 `onSave` 回调,右下角显示最近保存时间。
796
+ 传入 `onSave` 后可同时开启自动保存。右下角会显示"最近保存: HH:MM:SS"。
702
797
 
703
- ### Props 配置
798
+ ### Vue 组件
704
799
 
705
800
  | 属性 | 类型 | 默认值 | 说明 |
706
801
  |------|------|--------|------|
707
802
  | `auto-save-enabled` | `boolean` | `false` | 是否开启 |
708
803
  | `auto-save-interval-ms` | `number` | `120000` | 间隔(毫秒),默认 2 分钟 |
709
804
 
710
- ### 示例
711
-
712
805
  ```vue
713
806
  <PrintDesigner
714
807
  :on-save="handleSave"
@@ -717,19 +810,87 @@ const handleSave = async (payload) => {
717
810
  />
718
811
  ```
719
812
 
720
- ```js
721
- designer.setOnSave(handleSave);
722
- designer.setAutoSave(true, 120000); // 每 2 分钟自动保存
723
- designer.setAutoSave(false); // 关闭
813
+ ### Web Component
814
+
815
+ ```javascript
816
+ designer.setOnSave(handleSave)
817
+ designer.setAutoSave(true, 120000) // 每 2 分钟自动保存
818
+ designer.setAutoSave(false) // 关闭
724
819
  ```
725
820
 
726
- > 右下角会显示 "最近保存: HH:MM:SS",每次回调完成后自动更新时间。
821
+ > 自动保存仅在内容有变更时触发,连续未变更的周期内不会重复保存。
727
822
 
728
823
  ---
729
824
 
730
- ## 模板数据结构(完整格式)
825
+ ## 打印与导出
826
+
827
+ ### Web Component API
828
+
829
+ #### `print(options)`
830
+
831
+ ```typescript
832
+ interface PrintOptions {
833
+ mode: 'browser' | 'silent'
834
+ options?: {
835
+ silent?: boolean // 静默打印(跳过浏览器打印对话框)
836
+ printerName?: string // 指定打印机名称
837
+ copies?: number // 打印份数,默认 1
838
+ duplex?: boolean // 双面打印
839
+ pageRanges?: string // 页码范围,如 '1-3'
840
+ }
841
+ }
842
+ ```
843
+
844
+ ```javascript
845
+ // 浏览器打印
846
+ await designer.print({ mode: 'browser' })
847
+
848
+ // 静默打印到指定打印机
849
+ await designer.print({
850
+ mode: 'silent',
851
+ options: {
852
+ printerName: 'HP-LaserJet',
853
+ copies: 2
854
+ }
855
+ })
856
+ ```
857
+
858
+ #### `export(options)`
859
+
860
+ ```typescript
861
+ interface ExportOptions {
862
+ type: 'pdf' | 'image' | 'html'
863
+ filename?: string // 输出文件名(不含扩展名)
864
+ }
865
+ ```
866
+
867
+ ```javascript
868
+ // 导出 PDF
869
+ await designer.export({ type: 'pdf', filename: '订单-2024' })
870
+
871
+ // 导出图片
872
+ await designer.export({ type: 'image', filename: '订单-2024' })
873
+
874
+ // 导出 HTML
875
+ await designer.export({ type: 'html', filename: '订单-2024' })
876
+ ```
877
+
878
+ ### 快捷键
731
879
 
732
- 当用户直接保存/加载完整模板时,使用以下数据结构:
880
+ | 快捷键 | 功能 |
881
+ |--------|------|
882
+ | `Ctrl+S` | 保存 |
883
+ | `Ctrl+P` | 打印 |
884
+ | `Ctrl+Shift+P` | 预览 |
885
+ | `Ctrl+Shift+E` | 导出 PDF |
886
+
887
+ ---
888
+
889
+ ## 模板数据结构
890
+
891
+ 当需要直接保存/加载完整模板时,使用以下数据结构。
892
+
893
+ ### 完整 JSON 结构
733
894
 
734
895
  ```json
735
896
  {
@@ -825,158 +986,173 @@ designer.setAutoSave(false); // 关闭
825
986
 
826
987
  | 字段 | 类型 | 必填 | 说明 |
827
988
  |-----|------|-----|------|
828
- | `id` | `string` | 是 | 模板唯一 ID |
829
- | `name` | `string` | 是 | 模板名称 |
989
+ | `id` | `string` | 是 | 模板唯一 ID |
990
+ | `name` | `string` | 是 | 模板名称 |
830
991
  | `updatedAt` | `number` | 否 | 更新时间戳(毫秒) |
831
992
  | `permissions` | `object` | 否 | `{ editable, deletable, copyable }` |
832
- | `data` | `object` | 是 | 模板设计数据 |
993
+ | `data` | `object` | 是 | 模板设计数据 |
833
994
  | `ext` | `object` | 否 | 扩展数据(变量树等) |
834
995
 
835
- ### `data` 内部字段
996
+ ### `data` 字段
836
997
 
837
998
  | 字段 | 类型 | 必填 | 说明 |
838
999
  |-----|------|-----|------|
839
- | `pages` | `Page[]` | 是 | 页面列表,每页含 `elements` 数组 |
840
- | `canvasSize` | `{ width, height }` | 是 | 画布尺寸(默认 A4: 794×1123) |
841
- | `unit` | `'mm'`\|`'px'`\|`'pt'`\|`'in'`\|`'cm'` | 否 | 单位 |
1000
+ | `pages` | `Page[]` | 是 | 页面列表,每页含 `elements` 数组 |
1001
+ | `canvasSize` | `{ width, height }` | 是 | 画布尺寸 px(默认 A4: 794×1123) |
1002
+ | `unit` | `'mm' \| 'px' \| 'pt' \| 'in' \| 'cm'` | 否 | 单位,默认 `'mm'` |
842
1003
  | `watermark` | `WatermarkSettings` | 否 | 水印配置 |
843
- | `guides` | `Guide[]` | 否 | 参考线 |
844
- | `zoom` | `number` | 否 | 缩放比例 |
1004
+ | `guides` | `Guide[]` | 否 | 辅助参考线 |
1005
+ | `zoom` | `number` | 否 | 缩放比例,默认 `1` |
845
1006
  | `showGrid` | `boolean` | 否 | 显示网格 |
846
- | `headerHeight` | `number` | 否 | 页眉高度 |
847
- | `footerHeight` | `number` | 否 | 页脚高度 |
848
- | `pageSpacingX` | `number` | 否 | 水平边距 |
849
- | `pageSpacingY` | `number` | 否 | 垂直边距 |
1007
+ | `headerHeight` | `number` | 否 | 页眉高度 px |
1008
+ | `footerHeight` | `number` | 否 | 页脚高度 px |
1009
+ | `pageSpacingX` | `number` | 否 | 水平页边距 px |
1010
+ | `pageSpacingY` | `number` | 否 | 垂直页边距 px |
850
1011
  | `canvasBackground` | `string` | 否 | 画布背景色 |
851
- | `testData` | `object` | 否 | 测试数据(报告模式保存) |
852
-
853
- ### 元素基本字段
854
-
855
- ```ts
856
- interface PrintElement {
857
- id: string; // 元素唯一 ID
858
- type: ElementType; // 元素类型: 'text'|'image'|'table'|'customTable'|'barcode'|'qrcode'|'line'|'rect'|'circle'|'chart'|'pageNumber'|'longtext'
859
- x: number; // X 坐标
860
- y: number; // Y 坐标
861
- width: number; // 宽度
862
- height: number; // 高度
863
- content?: string; // 文本内容(支持变量占位符)
864
- variable?: string; // 绑定变量名(如 '@items')
865
- style: ElementStyle; // 样式对象
866
- }
867
- ```
1012
+ | `testData` | `object` | 否 | 测试数据(报告模式会保存,模板模式不保存) |
868
1013
 
869
1014
  ---
870
1015
 
871
- ## 元素数据绑定
1016
+ ## 支持的元素类型
872
1017
 
873
- ### 变量语法
1018
+ | 类型 | 标识 | 说明 |
1019
+ |------|------|------|
1020
+ | 文本 | `text` | 静态文本或变量内容 |
1021
+ | 长文本 | `longtext` | 多行自动扩展文本,支持彩色标记语法 `[color=#xxx]` |
1022
+ | 富文本 | `richtext` | HTML 富文本格式化内容 |
1023
+ | 图片 | `image` | 图片显示(URL / Base64 / 变量绑定) |
1024
+ | 表格 | `table` | 数据表格,支持自动分页、页脚汇总、自定义脚本 |
1025
+ | 自定义表格 | `customTable` | 自由合并单元格、单元格内嵌图片/字段、逐格自定义样式 |
1026
+ | 嵌套表格 | `nestedtable` | 分组嵌套数据展示(父级标签 + 子级行列表) |
1027
+ | 条形码 | `barcode` | 一维条形码(CODE128、EAN13、UPC 等) |
1028
+ | 二维码 | `qrcode` | 二维码(支持 L/M/Q/H 纠错级别) |
1029
+ | 图表 | `chart` | ECharts 图表(柱状图/折线图/饼图/环形图/面积图) |
1030
+ | 数据卡片 | `datacard` | 单个关键指标数据展示(含趋势箭头) |
1031
+ | 进度条 | `progress` | 百分比进度展示 |
1032
+ | 列表 | `list` | 编号列表数据展示(横向/纵向布局) |
1033
+ | 日期 | `date` | 日期时间显示(可绑定变量或显示当前时间) |
1034
+ | 页码 | `pageNumber` | 页码显示(支持多种格式) |
1035
+ | 签名/印章 | `signature` | 印章图片或二维码,支持透明度 |
1036
+ | 线条 | `line` | 直线(支持实线/虚线/点线) |
1037
+ | 矩形 | `rect` | 矩形(支持圆角) |
1038
+ | 圆形 | `circle` | 圆形 |
874
1039
 
875
- | 语法 | 示例 | 说明 |
876
- |-----|------|------|
877
- | `@变量名` | `@customerName` | 直接引用变量 |
878
- | `{#变量路径}` | `{#order.items[0].name}` | 支持嵌套路径和数组索引 |
1040
+ ---
879
1041
 
880
- ### 各元素可绑定字段
1042
+ ## 版本兼容性说明
881
1043
 
882
- | 元素 | 绑定的属性 | 示例 |
883
- |------|----------|------|
884
- | 文本 / 长文本 | `content` | `订单号: {#orderNo}` |
885
- | 图片 | `variable` | `@imageUrl` |
886
- | 表格 | `variable` + `columnsVariable` + `footerDataVariable` | `@items`, `@tableCols`, `@tableFooter` |
887
- | 条形码 | `variable` | `@barcode` |
888
- | 二维码 | `variable` | `@url` |
889
- | 图表 | `chartDataVariable` + `chartXField` + `chartYField` | `@chartData`, `category`, `value` |
1044
+ ### 依赖版本矩阵
1045
+
1046
+ | 组件包 | 要求版本 | 说明 |
1047
+ |--------|---------|------|
1048
+ | `vue` | `^3.5.0` | 核心框架 |
1049
+ | `pinia` | `^2.3.0` | 状态管理 |
1050
+ | `echarts` | `^6.0.0` | 图表库(按需引入) |
1051
+ | `lodash` | `^4.17.0` | 工具函数 |
1052
+ | `qrcode` | `^1.5.0` | 二维码生成 |
1053
+ | `jsbarcode` | `^3.12.0` | 条形码生成 |
1054
+ | `jspdf` | `4.2.x` | PDF 导出 |
1055
+ | `tailwindcss` | `^3.x` | CSS 框架(构建时依赖) |
1056
+
1057
+ ### 升级注意事项
1058
+
1059
+ - **从 `0.2.x` 升级到 `0.3.x`**:`onSave` 回调签名变化,`payload.data` 中 `testData` 的保存规则改为由 `mode` 决定
1060
+ - **Vue 版本**:仅支持 Vue 3.5+,不支持 Vue 2.x
1061
+ - **Node 版本**:`vite build` 和 `vue-tsc` 需要 Node >= 18
890
1062
 
891
1063
  ---
892
1064
 
893
- ## 图片元素使用说明
1065
+ ## 常见问题排查
894
1066
 
895
- 图片元素支持 **三种方式** 设置图片:
1067
+ ### 1. 设计器加载后空白 / 无渲染
896
1068
 
897
- ### 方式 1:直接输入图片 URL
1069
+ **原因:** 未引入样式文件或 `ready` 事件未触发。
898
1070
 
899
- 在图片元素的属性面板中,直接在"图片地址"输入框填写图片 URL(支持 HTTP/HTTPS 链接)。
1071
+ **排查步骤:**
1072
+ 1. 确认已引入 `x-print-designer/style.css` 或 `dist/print-designer.css`
1073
+ 2. Web Component 方式需在 `ready` 事件后调用 API
1074
+ 3. 检查浏览器控制台是否有报错
900
1075
 
901
- ```
902
- https://example.com/logo.png
903
- ```
1076
+ ### 2. 变量绑定不生效,显示为空或变量名
904
1077
 
905
- ### 方式 2:本地上传(自动转 Base64)
1078
+ **原因:** 变量路径与数据字段不匹配。
906
1079
 
907
- 点击属性面板中的 **"上传图片"** 按钮,从本地选择一张图片文件:
1080
+ **排查步骤:**
1081
+ 1. 确认 `sampleData` / `setTestData` 中包含对应字段
1082
+ 2. 检查变量名大小写是否一致(`@customerName` vs `customername`)
1083
+ 3. 嵌套路径使用点号分隔:`@user.name`
1084
+ 4. 确认数据格式:表格需要数组,图表需要 `Array<{category, value}>`
908
1085
 
909
- - 自动将图片转换为 **Base64** 字符串并填入图片地址
910
- - 限制最大 **2MB**,超出会提示错误
911
- - 支持所有常见图片格式(PNG、JPG、GIF、WebP 等)
1086
+ ### 3. 表格数据显示为空
912
1087
 
913
- > 也可以**双击画布上的图片元素**来快速触发上传(编辑模式下)。
1088
+ **原因:** 表格 `variable` 未绑定或数据格式不匹配。
914
1089
 
915
- ### 方式 3:绑定变量
1090
+ **排查步骤:**
1091
+ 1. 确认表格元素的 `variable` 属性绑定了变量名
1092
+ 2. 确认数据中该变量值为数组格式
1093
+ 3. 如果使用 `columnsVariable`,确保列定义数组字段存在且包含 `{field, header}`
916
1094
 
917
- 将图片元素的 `variable` 属性绑定到一个变量名,图片地址从数据中动态读取。
1095
+ ### 4. 打印/导出 PDF 时样式错乱
918
1096
 
919
- | 绑定方式 | 示例 | 数据值要求 |
920
- |---------|------|-----------|
921
- | 静态变量 | `@logoUrl` | 字符串:`"https://..."` 或 Base64 |
922
- | 数组元素 | `@flightRecords[0].image` | 该字段值需为 URL 或 Base64 |
1097
+ **原因:** 打印管线依赖 iframe 渲染 + 计算样式内联,部分样式可能未正确捕获。
923
1098
 
924
- **在 sampleData 中对应的数据:**
1099
+ **排查步骤:**
1100
+ 1. 优先使用预览(`Ctrl+Shift+P`)确认渲染效果
1101
+ 2. 确认 PDF 导出使用的是 `export({ type: 'pdf' })` 而非浏览器打印
1102
+ 3. 检查控制台是否有 `Failed to load` 相关错误
925
1103
 
926
- ```json
927
- {
928
- "variables": {
929
- "logoUrl": "https://example.com/logo.png",
930
- "avatarBase64": "data:image/png;base64,iVBORw0KGgo...",
931
- "flightRecords": [
932
- { "image": "https://picsum.photos/id/1/200/80" }
933
- ]
934
- }
935
- }
936
- ```
1104
+ ### 5. 自定义表格预览时样式丢失
937
1105
 
938
- ### 总结
1106
+ **原因:** 列宽未持久化或表格容器 `overflow: hidden` 约束未解锁。
939
1107
 
940
- | 方式 | 适用场景 | 存储内容 |
941
- |------|---------|---------|
942
- | URL 输入 | 固定图片、CDN 资源 | URL 字符串 |
943
- | 本地上传 | 无公网链接的图片 | Base64 编码字符串 |
944
- | 变量绑定 | 每条数据图片不同 | URL 或 Base64 |
1108
+ **排查步骤:**
1109
+ 1. 确认已保存模板(列宽需持久化到 `customTableColWidths`)
1110
+ 2. 升级到最新版本(v0.3.1+ 已修复预览模式下列宽持久化和容器高度问题)
945
1111
 
946
- ---
1112
+ ### 6. 图表在预览/导出时显示空白
947
1113
 
948
- ## 支持的元素类型
1114
+ **原因:** ECharts 异步渲染未完成,预览管线已截取快照。
949
1115
 
950
- | 类型 | 标识 | 说明 |
951
- |------|------|------|
952
- | 文本 | `text` | 静态文本或变量内容 |
953
- | 长文本 | `longtext` | 多行自动扩展文本 |
954
- | 富文本 | `richtext` | 富文本格式化内容 |
955
- | 图片 | `image` | 图片显示(URL / Base64 / 变量绑定) |
956
- | 表格 | `table` | 数据表格,支持自动分页、页脚脚本 |
957
- | 自定义表格 | `customTable` | 自由合并单元格、单元格内嵌图片/字段 |
958
- | 嵌套表格 | `nestedtable` | 分组嵌套数据展示 |
959
- | 条形码 | `barcode` | 一维条形码 |
960
- | 二维码 | `qrcode` | 二维码 |
961
- | 图表 | `chart` | ECharts(柱状图/折线图/饼图/环形图/面积图) |
962
- | 数据卡片 | `datacard` | 单个关键指标数据展示 |
963
- | 进度条 | `progress` | 百分比进度展示 |
964
- | 列表 | `list` | 多行列表数据展示 |
965
- | 日期 | `date` | 日期时间显示 |
966
- | 页码 | `pageNumber` | 页码显示 |
967
- | 签名/印章 | `signature` | 印章图片或二维码 |
968
- | 线条 | `line` | 直线 |
969
- | 矩形 | `rect` | 矩形 |
970
- | 圆形 | `circle` | 圆形 |
1116
+ **排查步骤:**
1117
+ 1. 升级到最新版本(已修复 ChartElement 异步渲染注册问题)
1118
+ 2. 确认 `chartDataVariable` 绑定的数据格式正确
1119
+ 3. 在编辑模式下确认图表正常显示后再预览
971
1120
 
972
- ---
1121
+ ### 7. `onSave` 回调未触发
973
1122
 
974
- ## 技术栈
1123
+ **原因:** 未正确传入 `onSave` 属性。
1124
+
1125
+ **排查步骤:**
1126
+ 1. Vue 组件确认 `:on-save="handleSave"` 正确绑定
1127
+ 2. Web Component 确认在 `ready` 事件后调用 `setOnSave`
1128
+ 3. 检查 `handleSave` 函数是否存在且无语法错误
1129
+ 4. 确认未同时传入自建的 CRUD 配置(`onSave` 会覆盖内置保存逻辑)
1130
+
1131
+ ### 8. 自动保存不生效
975
1132
 
976
- - Vue 3 / TypeScript
977
- - Pinia(状态管理)
978
- - Tailwind CSS
979
- - ECharts 5
980
- - Monaco Editor
1133
+ **原因:** 自动保存需要 `onSave` 回调。
1134
+
1135
+ **排查步骤:**
1136
+ 1. 确认同时传入了 `onSave` 和 `auto-save-enabled`
1137
+ 2. 自动保存仅在内容有变更时触发,未修改时不会保存
1138
+ 3. 检查 `auto-save-interval-ms` 是否设置过短(频繁保存可能被节流)
981
1139
 
982
1140
  ---
1141
+
1142
+ ## 技术栈
1143
+
1144
+ | 技术 | 用途 |
1145
+ |------|------|
1146
+ | Vue 3 + TypeScript | 核心框架 |
1147
+ | Pinia | 状态管理 |
1148
+ | Tailwind CSS | 原子化 CSS |
1149
+ | ECharts 6 | 图表渲染 |
1150
+ | Monaco Editor | 代码/JSON 编辑器 |
1151
+ | jsPDF | PDF 生成 |
1152
+ | jsBarcode | 条形码生成 |
1153
+ | qrcode | 二维码生成 |
1154
+ | canvg | SVG → Canvas 转换 |
1155
+ | dom-to-image-more | DOM → 图片导出 |
1156
+ | jszip | ZIP 打包 |
1157
+ | vue-i18n | 国际化(中文/English) |
1158
+ | Vite | 构建工具 |