# X-Print-Designer

基于 Vue 3 + TypeScript 开发的打印设计器，支持可视化拖拽设计打印模板，集成 ECharts 图表组件。

---

## 目录

- [前置依赖要求](#前置依赖要求)
- [快速开始](#快速开始)
  - [源码开发](#源码开发)
  - [Vue 组件接入](#vue-组件接入)
  - [Web Component 接入](#web-component-接入)
- [核心概念：模板模式 vs 报告模式](#核心概念模板模式-vs-报告模式)
- [Vue 组件使用说明](#vue-组件使用说明)
- [Web Component 使用说明](#web-component-使用说明)
- [传入数据格式](#传入数据格式)
- [元素数据绑定](#元素数据绑定)
- [图片元素使用说明](#图片元素使用说明)
- [保存回调 `onSave`](#保存回调-onsave)
- [自动保存配置](#自动保存配置)
- [打印与导出](#打印与导出)
- [模板数据结构](#模板数据结构)
- [支持的元素类型](#支持的元素类型)
- [版本兼容性说明](#版本兼容性说明)
- [常见问题排查](#常见问题排查)
- [技术栈](#技术栈)

---

## 前置依赖要求

| 依赖 | 最低版本 | 说明 |
|------|---------|------|
| **Node.js** | `>= 18` | 运行时环境 |
| **pnpm** | `>= 8` | 推荐包管理器（项目使用 `pnpm-lock.yaml`） |
| **Vue** | `^3.5` | 核心框架 |

### 浏览器兼容性

| 浏览器 | 最低版本 | 说明 |
|--------|---------|------|
| Chrome | >= 90 | 完全支持 |
| Edge | >= 90 | 完全支持 |
| Firefox | >= 90 | 完全支持 |
| Safari | >= 15 | 完全支持 |

> **说明：** 设计器基于现代浏览器 API（`ResizeObserver`、`CSS Grid`、`Shadow DOM`），不支持 IE 11 及以下版本。

---

## 快速开始

### 源码开发

```bash
# 克隆仓库
git clone https://github.com/0ldFive/Vue-Print-Designer.git
cd x-print-designer

# 安装依赖
pnpm install

# 启动开发服务器
pnpm dev

# 构建生产包
pnpm build

# 构建 Web Component 包
pnpm build:wc
```

启动后浏览器访问 `http://localhost:5173` 即可看到设计器 Demo 页面（`index.html`）。

### Vue 组件接入

**适用于：** 将设计器作为子组件嵌入已有 Vue 3 项目。

```vue
<template>
  <PrintDesigner
    mode="template"
    :sample-data="sampleData"
    :on-save="handleSave"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { PrintDesigner } from 'x-print-designer'
import 'x-print-designer/style.css'

const sampleData = ref({
  id: 1,
  title: '报告标题',
  customerName: '张三',
  items: [
    { name: '产品A', price: 100, quantity: 2 }
  ]
})

const handleSave = async (payload) => {
  // payload: { id, name, data, isNew }
  await fetch('/api/templates/save', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  })
}
</script>
```

### Web Component 接入

**适用于：** 任意前端技术栈（React / Angular / 原生 HTML），以自定义标签方式嵌入。

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <script type="module" src="/dist/print-designer.es.js"></script>
  <link rel="stylesheet" href="/dist/print-designer.css" />
</head>
<body>
  <print-designer id="designer"></print-designer>

  <script>
    const designer = document.getElementById('designer')

    designer.addEventListener('ready', () => {
      designer.setMode('template')
      designer.setTestData({
        id: 1,
        title: '报告标题',
        items: [{ name: '产品A', price: 100 }]
      })
      designer.setOnSave((payload) => {
        fetch('/api/templates/save', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload)
        })
      })
    })
  </script>
</body>
</html>
```

---

## 核心概念：模板模式 vs 报告模式

设计器支持两种工作模式，通过 `mode` 属性/方法进行切换。

### 概念说明

| 概念 | 说明 |
|------|------|
| **模板 (Template)** | 页面布局设计，包含元素位置、样式、画布大小等 |
| **数据 (Data)** | 业务数据，如订单信息、报告内容、用户信息等 |
| **模板 ID** | 模板的唯一标识，用于关联和加载模板 |
| **数据 ID** | 业务数据的唯一标识，如订单 ID、报告 ID |

### 模板模式（`template`）— 默认

**设计理念：** 模板与数据分离，一个模板可绑定多个不同的数据。

**适用场景：**
- 订单打印（同一订单模板，打印不同订单）
- 标签打印（同一标签模板，打印不同商品）
- 快递单据（同一单据模板，打印不同收件人）

**保存行为：** 保存时**只保存页面布局**（元素位置、样式、画布大小等），**不保存** `testData`。

### 报告模式（`report`）

**设计理念：** 数据与布局绑定，保存的是完整的报告快照。

**适用场景：**
- 检测报告（每次检测结果 + 固定格式的报告模板）
- 数据报表（当前统计数据的可视化呈现）
- 档案文件（包含具体数据的完整文档）

**保存行为：** 保存时**同时保存页面布局 + testData**。

### 两种模式对比

| 特性 | `template` 模板模式 | `report` 报告模式 |
|------|-------------------|------------------|
| 保存布局 | ✅ | ✅ |
| 保存 testData | ❌ | ✅ |
| 数据与模板关系 | 分离，运行时注入 | 绑定，一起保存 |
| 模板复用性 | 高，可用于多个数据 | 低，针对当前数据 |
| 典型用途 | 订单打印、标签 | 检测报告、数据报表 |

### 使用示例

**模板模式（推荐用于可复用模板）：**

```javascript
// 1. 编辑模板时传入预览数据
const sampleData = {
  id: null,               // 无需数据 ID，仅用于预览
  orderNo: 'PREVIEW-001',
  customerName: '预览客户'
}

// 2. 保存模板（只保存布局，不含 testData）
// payload.data 中不包含 testData

// 3. 打印时动态注入真实数据
designer.loadTemplate('tpl-order')
designer.setVariables({
  orderNo: 'ORD-2024-001',
  customerName: '张三'
})
designer.print({ mode: 'browser', options: { silent: true } })
```

**报告模式（推荐用于完整报告）：**

```javascript
// 1. 编辑报告时传入完整数据
const sampleData = {
  id: 8,                  // 报告 ID
  title: '2024年第一季度销售报告',
  totalAmount: 50000,
  items: [...]
}

// 2. 保存报告（布局 + 数据一起保存）
// payload.data 中包含完整的 testData

// 3. 加载后直接打印，无需再次注入数据
designer.loadTemplate(8)
designer.print({ mode: 'browser', options: { silent: true } })
```

### 切换模式

```vue
<!-- Vue 组件 -->
<PrintDesigner mode="template" />  <!-- 模板模式（默认） -->
<PrintDesigner mode="report" />    <!-- 报告模式 -->
```

```javascript
// Web Component
designer.setMode('template')
designer.setMode('report')
```

---

## Vue 组件使用说明

### 组件 Props

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `mode` | `'template'` \| `'report'` | `'template'` | 工作模式，影响保存行为 |
| `view-mode` | `'edit'` \| `'preview'` | `'edit'` | 视图模式，`'preview'` 时只渲染打印内容 |
| `headless` | `boolean` | `false` | 无头模式，隐藏顶部工具栏和侧边栏 |
| `sample-data` | `Record<string, any>` | — | 传入的示例/预览数据，详见[传入数据格式](#传入数据格式) |
| `on-save` | `(payload) => void \| Promise<void>` | — | 保存回调，详见[保存回调](#保存回调-onsave) |
| `auto-save-enabled` | `boolean` | `false` | 是否开启定时自动保存 |
| `auto-save-interval-ms` | `number` | `120000` | 自动保存间隔（毫秒），默认 2 分钟 |

### Props 详细说明

#### `mode`

工作模式，决定保存时是否附带 `testData`。

```vue
<PrintDesigner mode="report" />
```

#### `view-mode`

切换编辑/预览视图。设为 `'preview'` 时隐藏所有编辑器 UI，仅渲染打印页面。适用于嵌入第三方页面时只展示打印效果。

```vue
<PrintDesigner view-mode="preview" />
```

#### `headless`

无头模式，隐藏顶部工具栏和侧边属性面板。用于嵌入式场景仅保留画布区域。

```vue
<PrintDesigner headless />
```

#### `sample-data`

传入设计器的预览数据。**最关键的属性**。详见 [传入数据格式](#传入数据格式)。

```vue
<PrintDesigner :sample-data="{ id: 1, title: '报告', items: [...] }" />
```

#### `on-save`

保存回调函数。传入后设计器**不再执行内置保存逻辑**，完全由用户自行处理持久化。

```vue
<PrintDesigner :on-save="(payload) => mySaveApi(payload)" />
```

> 不传 `onSave` 时，设计器使用内置 CRUD 逻辑（根据配置调远程接口或存 localStorage）。

#### `auto-save-enabled` / `auto-save-interval-ms`

详见 [自动保存配置](#自动保存配置)。

---

## Web Component 使用说明

### 加载方式

```html
<!-- ES Module 方式（推荐） -->
<script type="module" src="/dist/print-designer.es.js"></script>
<link rel="stylesheet" href="/dist/print-designer.css" />

<!-- 或 UMD 方式 -->
<script src="/dist/print-designer.umd.js"></script>
<link rel="stylesheet" href="/dist/print-designer.css" />
```

```html
<print-designer id="designer"></print-designer>
```

### 完整 API 参考

所有方法均通过 DOM 元素直接调用。**必须在 `ready` 事件触发后调用。**

| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `setMode(mode)` | `'template'` \| `'report'` | `void` | 设置工作模式 |
| `setViewMode(viewMode)` | `'edit'` \| `'preview'` | `void` | 切换视图模式 |
| `setOnSave(handler)` | `(payload) => void \| Promise<void>` | `void` | 设置保存回调 |
| `setAutoSave(enabled, intervalMs?)` | `boolean, number?` | `void` | 开启/关闭自动保存，`intervalMs` 默认 120000 |
| `setTestData(data)` | `Record<string, any>` | `void` | 设置测试/预览数据（等价 Vue 组件的 `sample-data`） |
| `setSampleData(data)` | `Record<string, any>` | `void` | `setTestData` 的别名 |
| `setVariables(vars)` | `Record<string, any>` | `void` | 打印/导出时注入真实变量（覆盖 testData 中的同名字段） |
| `setTemplates(list, opts?)` | `Array<Template>, object?` | `void` | 设置预设模板列表。`opts` 可选：`{ currentTemplateId?: string }` |
| `loadTemplate(id)` | `string` | `Promise<void>` | 加载指定模板到画布 |
| `loadTemplateData(data)` | `Record<string, any>` | `boolean` | 直接加载模板 JSON 数据到画布，返回是否成功 |
| `getTemplateData()` | — | `Record<string, any>` | 获取当前模板完整数据 |
| `getPreviewHtml()` | — | `Promise<string>` | 获取预览 HTML 字符串 |
| `print(options?)` | `PrintOptions` | `Promise<void>` | 执行打印，详见[打印与导出](#打印与导出) |
| `export(options?)` | `ExportOptions` | `Promise<void>` | 导出 PDF / 图片 / HTML，详见[打印与导出](#打印与导出) |
| `setPreviewMode(options)` | `PreviewModeOptions` | `void` | 进入独立预览面板模式 |
| `exitPreviewMode()` | — | `void` | 退出预览模式，返回编辑器 |
| `setBranding(config)` | `{ title?, logoUrl?, showTitle?, showLogo? }` | `void` | 设置品牌标识 |
| `setLang(lang)` | `'zh'` \| `'en'` | `void` | 切换语言 |

### 基本使用示例

```javascript
const designer = document.getElementById('designer')

designer.addEventListener('ready', () => {
  // 设置模式
  designer.setMode('template')

  // 设置数据
  designer.setTestData({
    id: 1,
    orderNo: 'ORD-001',
    customerName: '张三',
    items: [
      { name: '产品A', price: 100, quantity: 2 },
      { name: '产品B', price: 200, quantity: 3 }
    ]
  })

  // 保存回调
  designer.setOnSave(async (payload) => {
    console.log('保存:', payload)
    await fetch('/api/templates/save', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    })
  })

  // 开启自动保存（每 2 分钟）
  designer.setAutoSave(true, 120000)
})
```

### 打印时注入真实数据

使用 `setVariables` 在打印/导出时覆盖预览数据中的字段：

```javascript
const designer = document.querySelector('print-designer')

// 加载模板
await designer.loadTemplate('tpl-order')

// 注入真实打印数据
designer.setVariables({
  orderNo: 'REAL-2026-001',
  customerName: '李四',
  totalAmount: 5999.00
})

// 静默打印
await designer.print({ mode: 'browser', options: { silent: true } })
```

### 独立预览面板

当设计器嵌入第三方页面时，可切换到独立的**纯预览模式**——只渲染打印内容，不显示编辑器 UI。

**适用于：** 插件/第三方页面嵌入时的打印预览、用户填写表单后的预览、无需编辑功能的只读场景。

#### `setPreviewMode(options)`

```typescript
interface PreviewModeOptions {
  pages: Page[]                              // 页面元素布局（必填）
  canvasSize: { width: number; height: number } // 画布尺寸（必填）
  canvasBackground?: string                  // 画布背景色，默认 '#ffffff'
  testData?: Record<string, any>            // 测试数据
  variables?: Record<string, any>           // 额外变量映射
  watermark?: WatermarkSettings             // 水印配置
}
```

#### 使用示例

```javascript
async function openPreview(reportId) {
  const designer = document.querySelector('print-designer')

  // 从后端获取模板数据
  const template = await fetch(`/api/templates/${reportId}`).then(r => r.json())

  // 进入预览模式
  designer.setPreviewMode({
    pages: template.data.pages,
    canvasSize: template.data.canvasSize,
    canvasBackground: template.data.canvasBackground || '#ffffff',
    testData: {
      customerName: document.getElementById('name').value,
      orderDate: document.getElementById('date').value,
      items: getOrderItems()
    },
    watermark: template.data.watermark
  })
}

// 返回编辑
function backToEdit() {
  designer.exitPreviewMode()
}
```

> `exitPreviewMode()` 会自动恢复进入预览前的编辑器状态（模板选择、编辑进度等）。

---

## 传入数据格式

### 基本要求

`sample-data` / `setTestData(data)` 接收一个普通的 JSON 对象。**唯一必填字段是 `id`**。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | `number` \| `string` | ✅ 是 | 业务数据的唯一标识，如报告 ID、订单 ID |
| `templateId` | `string` | 否 | 绑定的模板 ID，不传则用当前已选模板 |
| `name` | `string` | 否 | 数据名称 |
| `variables` | `object` | 否 | 业务变量对象（推荐，用于组织数据） |
| 任意自定义字段 | `any` | 否 | 也可直接在根级别存放变量 |

### 两种组织方式（任选其一，自动兼容）

#### 方式一：带 `variables` 包装（推荐）

```json
{
  "id": 8,
  "name": "测试报告",
  "templateId": "tpl_report",
  "variables": {
    "title": "报告标题",
    "customerName": "张三",
    "items": [
      { "name": "产品A", "price": 100, "quantity": 2 }
    ],
    "chartData": [
      { "category": "一月", "value": 1200 }
    ]
  }
}
```

模板绑定：`@title`、`@customerName`、`@items`、`@chartData`

#### 方式二：扁平数据

```json
{
  "id": 8,
  "name": "测试报告",
  "title": "报告标题",
  "customerName": "张三",
  "items": [
    { "name": "产品A", "price": 100, "quantity": 2 }
  ],
  "chartData": [
    { "category": "一月", "value": 1200 }
  ]
}
```

模板绑定：`@title`、`@customerName`、`@items`、`@chartData`

> 两种方式等效。设计器内部优先在 `variables` 中查找，未找到则在根级别查找。

### 支持的数据类型

| 类型 | 示例 | 模板绑定 |
|------|------|---------|
| 字符串 | `"张三"` | `@customerName` |
| 数字 | `100` | `@totalAmount` |
| 布尔值 | `true` | `@isPaid` |
| 数组 | `[{...}, {...}]` | `@items` |
| 嵌套对象 | `{ user: { name: "李四" } }` | `@user.name` |

### 表格数据

表格元素需要三个维度的变量绑定：

| 绑定属性 | 变量名示例 | 数据格式 | 说明 |
|---------|-----------|---------|------|
| `variable` | `@items` | `Array<Object>` | 表格行数据 |
| `columnsVariable` | `@tableCols` | `Array<{field, header, width}>` | 列定义 |
| `footerDataVariable` | `@tableFooter` | `Array<Object>` | 页脚汇总行 |

```json
{
  "items": [
    { "name": "产品A", "price": 100, "quantity": 2 },
    { "name": "产品B", "price": 200, "quantity": 3 }
  ],
  "tableCols": [
    { "field": "name", "header": "产品名称", "width": 120 },
    { "field": "price", "header": "单价", "width": 80 },
    { "field": "quantity", "header": "数量", "width": 80 }
  ],
  "tableFooter": [
    { "name": { "value": "合计" }, "price": { "value": "300" } }
  ]
}
```

### 图表数据

```json
{
  "chartData": [
    { "category": "一月", "value": 1200 },
    { "category": "二月", "value": 1500 }
  ]
}
```

图表元素需在属性面板中配置：
- **chartDataVariable**：`@chartData`
- **chartXField**：`category`（X 轴字段名）
- **chartYField**：`value`（Y 轴字段名）
- **chartNameField**：`category`（饼图名称字段）

### 完整示例数据

```json
{
  "id": 8,
  "name": "宝安机场飞行任务记录",
  "templateId": "tpl_report_drone",
  "createTime": "2026-05-01",
  "variables": {
    "dockname": "宝安国际无人机机场",
    "dronesn": "6EE5NC104E0PY6",
    "dronename": "M300 RTK",
    "reportname": "宝安机场飞行任务记录报告",
    "tasktype": "航线巡检",
    "totalcount": 4,
    "totaldistance": "48.6 km",
    "conclusion": "本轮飞行任务覆盖宝安区重点航线，数据采集完整。",
    "flightRecords": [
      {
        "time": "2026-04-02 09:30",
        "taskname": "宝安港区A线巡检",
        "dockname": "宝安国际无人机机场",
        "distance": "12.5 km",
        "image": "https://picsum.photos/id/1/200/80"
      }
    ],
    "chartData": [
      { "category": "一月", "value": 1200 },
      { "category": "二月", "value": 1500 }
    ]
  }
}
```

### 注意事项

1. **`id` 字段**
   - 业务数据的唯一标识，在保存回调中用于识别当前编辑的数据
   - 不传则保存时 `payload.id` 为 `null`

2. **`id` 与 `templateId` 的区别**
   - `id`：业务数据的标识（报告中用作报告 ID）
   - `templateId`：打印模板的标识，两者是完全不同的概念

3. **变量命名**
   - 使用 camelCase 命名，如 `customerName` 而非 `c`
   - 避免特殊字符和空格
   - 嵌套路径使用点号：`@user.profile.name`

4. **数组数据**
   - 表格和图表需要数组类型
   - 数组元素应包含一致的字段结构：`items: [{name: "A", price: 100}, {name: "B", price: 200}]`

5. **动态更新**
   - Web Component 通过 `setSampleData()` / `setTestData()` 更新
   - 数据更新后，画布中的变量绑定会自动重新解析

---

## 元素数据绑定

### 变量语法

| 语法 | 示例 | 说明 |
|-----|------|------|
| `@变量名` | `@customerName` | 直接引用变量 |
| `{#变量路径}` | `{#order.items[0].name}` | 支持嵌套路径和数组索引 |

### 各元素可绑定字段

| 元素 | 绑定属性 | 示例 |
|------|---------|------|
| 文本 / 长文本 | `content` | `订单号: {#orderNo}` |
| 图片 | `variable` | `@imageUrl` |
| 表格 | `variable` + `columnsVariable` + `footerDataVariable` | `@items`, `@tableCols`, `@tableFooter` |
| 自定义表格 | `customTableCells[].field` | `@customerName`（单元格字段） |
| 条形码 | `variable` 或 `content` | `@barcode` |
| 二维码 | `variable` 或 `content` | `@url` |
| 图表 | `chartDataVariable` + `chartXField` + `chartYField` | `@chartData`, `category`, `value` |
| 数据卡片 | `variable` | `@totalAmount` |
| 进度条 | `variable` | `@progress` |
| 日期 | `variable` | `@createTime` |

---

## 图片元素使用说明

### 方式 1：直接输入图片 URL

在属性面板的"图片地址"输入框填写 URL（支持 HTTP/HTTPS）。

```
https://example.com/logo.png
```

### 方式 2：本地上传（自动转 Base64）

点击属性面板中的 **"上传图片"** 按钮或**双击画布上的图片元素**，从本地选择图片文件：
- 自动转为 Base64 并填入图片地址
- 限制最大 **2MB**
- 支持 PNG、JPG、GIF、WebP 等格式

### 方式 3：绑定变量

将 `variable` 绑定到变量名，图片地址从数据中动态读取。

| 绑定方式 | 示例 | 数据值要求 |
|---------|------|-----------|
| 静态变量 | `@logoUrl` | 字符串 URL 或 Base64 |
| 数组元素 | `@flightRecords[0].image` | 该字段值为 URL 或 Base64 |

**对应数据：**

```json
{
  "variables": {
    "logoUrl": "https://example.com/logo.png",
    "avatarBase64": "data:image/png;base64,iVBORw0KGgo...",
    "flightRecords": [
      { "image": "https://picsum.photos/id/1/200/80" }
    ]
  }
}
```

### 方式对比

| 方式 | 适用场景 | 存储内容 |
|------|---------|---------|
| URL 输入 | 固定图片、CDN 资源 | URL 字符串 |
| 本地上传 | 无公网链接的图片 | Base64 编码 |
| 变量绑定 | 每条数据图片不同 | URL 或 Base64 |

---

## 保存回调 `onSave`

传入 `onSave` 后，用户保存（`Ctrl+S` 或工具栏保存按钮）时设计器不再自动持久化，而是将组装好的数据通过回调交给用户自行处理。

### 回调参数

```typescript
interface SavePayload {
  id: string | null      // 模板 ID，null 表示新建
  name: string           // 模板名称
  data: Record<string, any>  // 模板数据（含 pages、canvasSize、watermark 等）
  isNew: boolean         // 是否新建模板
}
```

### 不同模式下 data 内容差异

| 字段 | 模板模式 | 报告模式 |
|------|---------|---------|
| `pages` | ✅ | ✅ |
| `canvasSize` | ✅ | ✅ |
| `watermark` | ✅ | ✅ |
| `guides` | ✅ | ✅ |
| `unit` | ✅ | ✅ |
| `showGrid` | ✅ | ✅ |
| `headerHeight` / `footerHeight` | ✅ | ✅ |
| `canvasBackground` | ✅ | ✅ |
| `pageSpacingX` / `pageSpacingY` | ✅ | ✅ |
| `testData` | ❌ | ✅ |

### 使用示例

```vue
<template>
  <PrintDesigner
    :mode="mode"
    :sample-data="sampleData"
    :on-save="handleSave"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'

const mode = ref<'template' | 'report'>('template')

const handleSave = async (payload) => {
  // 根据模式决定 API 端点
  const endpoint = mode.value === 'template'
    ? '/api/templates/save'
    : '/api/reports/save'

  const res = await fetch(endpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  })

  if (!res.ok) throw new Error('保存失败')

  // 新建时获取后端返回的 ID
  const result = await res.json()
  if (payload.isNew && result.id) {
    console.log('新模板 ID:', result.id)
  }
}
</script>
```

> 不传 `onSave` 时，设计器走内置默认保存逻辑（根据 CRUD 配置调远程接口或存 localStorage）。

---

## 自动保存配置

传入 `onSave` 后可同时开启自动保存。右下角会显示"最近保存: HH:MM:SS"。

### Vue 组件

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `auto-save-enabled` | `boolean` | `false` | 是否开启 |
| `auto-save-interval-ms` | `number` | `120000` | 间隔（毫秒），默认 2 分钟 |

```vue
<PrintDesigner
  :on-save="handleSave"
  :auto-save-enabled="true"
  :auto-save-interval-ms="120000"
/>
```

### Web Component

```javascript
designer.setOnSave(handleSave)
designer.setAutoSave(true, 120000)   // 每 2 分钟自动保存
designer.setAutoSave(false)          // 关闭
```

> 自动保存仅在内容有变更时触发，连续未变更的周期内不会重复保存。

---

## 打印与导出

### Web Component API

#### `print(options)`

```typescript
interface PrintOptions {
  mode: 'browser' | 'silent'
  options?: {
    silent?: boolean            // 静默打印（跳过浏览器打印对话框）
    printerName?: string        // 指定打印机名称
    copies?: number             // 打印份数，默认 1
    duplex?: boolean            // 双面打印
    pageRanges?: string         // 页码范围，如 '1-3'
  }
}
```

```javascript
// 浏览器打印
await designer.print({ mode: 'browser' })

// 静默打印到指定打印机
await designer.print({
  mode: 'silent',
  options: {
    printerName: 'HP-LaserJet',
    copies: 2
  }
})
```

#### `export(options)`

```typescript
interface ExportOptions {
  type: 'pdf' | 'image' | 'html'
  filename?: string            // 输出文件名（不含扩展名）
}
```

```javascript
// 导出 PDF
await designer.export({ type: 'pdf', filename: '订单-2024' })

// 导出图片
await designer.export({ type: 'image', filename: '订单-2024' })

// 导出 HTML
await designer.export({ type: 'html', filename: '订单-2024' })
```

### 快捷键

| 快捷键 | 功能 |
|--------|------|
| `Ctrl+S` | 保存 |
| `Ctrl+P` | 打印 |
| `Ctrl+Shift+P` | 预览 |
| `Ctrl+Shift+E` | 导出 PDF |

---

## 模板数据结构

当需要直接保存/加载完整模板时，使用以下数据结构。

### 完整 JSON 结构

```json
{
  "id": "tpl_001",
  "name": "A4 订单模板",
  "updatedAt": 1715251200000,
  "permissions": {
    "editable": true,
    "deletable": true,
    "copyable": true
  },
  "data": {
    "pages": [
      {
        "id": "page_1",
        "elements": [
          {
            "id": "el_text_1",
            "type": "text",
            "x": 40,
            "y": 40,
            "width": 200,
            "height": 24,
            "content": "订单号: {#orderNo}",
            "style": { "fontSize": 14, "fontWeight": "bold", "color": "#111827" }
          },
          {
            "id": "el_table_1",
            "type": "table",
            "x": 40,
            "y": 100,
            "width": 500,
            "height": 200,
            "variable": "@items",
            "columnsVariable": "@tableCols",
            "showFooter": true,
            "footerDataVariable": "@tableFooter",
            "autoPaginate": true,
            "style": {
              "fontSize": 12,
              "color": "#000000",
              "headerBackgroundColor": "#f3f4f6",
              "borderColor": "#d1d5db",
              "borderWidth": 1
            }
          }
        ]
      }
    ],
    "canvasSize": { "width": 794, "height": 1123 },
    "guides": [],
    "zoom": 1,
    "showGrid": true,
    "headerHeight": 0,
    "footerHeight": 0,
    "showHeaderLine": false,
    "showFooterLine": false,
    "showMinimap": false,
    "canvasBackground": "#ffffff",
    "pageSpacingX": 0,
    "pageSpacingY": 0,
    "unit": "mm",
    "watermark": {
      "enabled": false,
      "text": "",
      "angle": -30,
      "color": "#000000",
      "opacity": 0.1,
      "size": 24,
      "density": 160
    },
    "testData": {
      "orderNo": "A001",
      "items": [{ "name": "产品A", "price": 100, "quantity": 2 }]
    }
  },
  "ext": {
    "availableVariables": [
      {
        "id": "order",
        "label": "订单信息",
        "children": [
          { "id": "orderNo", "label": "订单号" },
          { "id": "items", "label": "订单明细", "isArray": true }
        ]
      }
    ]
  }
}
```

### 顶层字段

| 字段 | 类型 | 必填 | 说明 |
|-----|------|-----|------|
| `id` | `string` | ✅ 是 | 模板唯一 ID |
| `name` | `string` | ✅ 是 | 模板名称 |
| `updatedAt` | `number` | 否 | 更新时间戳（毫秒） |
| `permissions` | `object` | 否 | `{ editable, deletable, copyable }` |
| `data` | `object` | ✅ 是 | 模板设计数据 |
| `ext` | `object` | 否 | 扩展数据（变量树等） |

### `data` 字段

| 字段 | 类型 | 必填 | 说明 |
|-----|------|-----|------|
| `pages` | `Page[]` | ✅ 是 | 页面列表，每页含 `elements` 数组 |
| `canvasSize` | `{ width, height }` | ✅ 是 | 画布尺寸 px（默认 A4: 794×1123） |
| `unit` | `'mm' \| 'px' \| 'pt' \| 'in' \| 'cm'` | 否 | 单位，默认 `'mm'` |
| `watermark` | `WatermarkSettings` | 否 | 水印配置 |
| `guides` | `Guide[]` | 否 | 辅助参考线 |
| `zoom` | `number` | 否 | 缩放比例，默认 `1` |
| `showGrid` | `boolean` | 否 | 显示网格 |
| `headerHeight` | `number` | 否 | 页眉高度 px |
| `footerHeight` | `number` | 否 | 页脚高度 px |
| `pageSpacingX` | `number` | 否 | 水平页边距 px |
| `pageSpacingY` | `number` | 否 | 垂直页边距 px |
| `canvasBackground` | `string` | 否 | 画布背景色 |
| `testData` | `object` | 否 | 测试数据（报告模式会保存，模板模式不保存） |

---

## 支持的元素类型

| 类型 | 标识 | 说明 |
|------|------|------|
| 文本 | `text` | 静态文本或变量内容 |
| 长文本 | `longtext` | 多行自动扩展文本，支持彩色标记语法 `[color=#xxx]` |
| 富文本 | `richtext` | HTML 富文本格式化内容 |
| 图片 | `image` | 图片显示（URL / Base64 / 变量绑定） |
| 表格 | `table` | 数据表格，支持自动分页、页脚汇总、自定义脚本 |
| 自定义表格 | `customTable` | 自由合并单元格、单元格内嵌图片/字段、逐格自定义样式 |
| 嵌套表格 | `nestedtable` | 分组嵌套数据展示（父级标签 + 子级行列表） |
| 条形码 | `barcode` | 一维条形码（CODE128、EAN13、UPC 等） |
| 二维码 | `qrcode` | 二维码（支持 L/M/Q/H 纠错级别） |
| 图表 | `chart` | ECharts 图表（柱状图/折线图/饼图/环形图/面积图） |
| 数据卡片 | `datacard` | 单个关键指标数据展示（含趋势箭头） |
| 进度条 | `progress` | 百分比进度展示 |
| 列表 | `list` | 编号列表数据展示（横向/纵向布局） |
| 日期 | `date` | 日期时间显示（可绑定变量或显示当前时间） |
| 页码 | `pageNumber` | 页码显示（支持多种格式） |
| 签名/印章 | `signature` | 印章图片或二维码，支持透明度 |
| 线条 | `line` | 直线（支持实线/虚线/点线） |
| 矩形 | `rect` | 矩形（支持圆角） |
| 圆形 | `circle` | 圆形 |

---

## 版本兼容性说明

### 依赖版本矩阵

| 组件包 | 要求版本 | 说明 |
|--------|---------|------|
| `vue` | `^3.5.0` | 核心框架 |
| `pinia` | `^2.3.0` | 状态管理 |
| `echarts` | `^6.0.0` | 图表库（按需引入） |
| `lodash` | `^4.17.0` | 工具函数 |
| `qrcode` | `^1.5.0` | 二维码生成 |
| `jsbarcode` | `^3.12.0` | 条形码生成 |
| `jspdf` | `4.2.x` | PDF 导出 |
| `tailwindcss` | `^3.x` | CSS 框架（构建时依赖） |

### 升级注意事项

- **从 `0.2.x` 升级到 `0.3.x`**：`onSave` 回调签名变化，`payload.data` 中 `testData` 的保存规则改为由 `mode` 决定
- **Vue 版本**：仅支持 Vue 3.5+，不支持 Vue 2.x
- **Node 版本**：`vite build` 和 `vue-tsc` 需要 Node >= 18

---

## 常见问题排查

### 1. 设计器加载后空白 / 无渲染

**原因：** 未引入样式文件或 `ready` 事件未触发。

**排查步骤：**
1. 确认已引入 `x-print-designer/style.css` 或 `dist/print-designer.css`
2. Web Component 方式需在 `ready` 事件后调用 API
3. 检查浏览器控制台是否有报错

### 2. 变量绑定不生效，显示为空或变量名

**原因：** 变量路径与数据字段不匹配。

**排查步骤：**
1. 确认 `sampleData` / `setTestData` 中包含对应字段
2. 检查变量名大小写是否一致（`@customerName` vs `customername`）
3. 嵌套路径使用点号分隔：`@user.name`
4. 确认数据格式：表格需要数组，图表需要 `Array<{category, value}>`

### 3. 表格数据显示为空

**原因：** 表格 `variable` 未绑定或数据格式不匹配。

**排查步骤：**
1. 确认表格元素的 `variable` 属性绑定了变量名
2. 确认数据中该变量值为数组格式
3. 如果使用 `columnsVariable`，确保列定义数组字段存在且包含 `{field, header}`

### 4. 打印/导出 PDF 时样式错乱

**原因：** 打印管线依赖 iframe 渲染 + 计算样式内联，部分样式可能未正确捕获。

**排查步骤：**
1. 优先使用预览（`Ctrl+Shift+P`）确认渲染效果
2. 确认 PDF 导出使用的是 `export({ type: 'pdf' })` 而非浏览器打印
3. 检查控制台是否有 `Failed to load` 相关错误

### 5. 自定义表格预览时样式丢失

**原因：** 列宽未持久化或表格容器 `overflow: hidden` 约束未解锁。

**排查步骤：**
1. 确认已保存模板（列宽需持久化到 `customTableColWidths`）
2. 升级到最新版本（v0.3.1+ 已修复预览模式下列宽持久化和容器高度问题）

### 6. 图表在预览/导出时显示空白

**原因：** ECharts 异步渲染未完成，预览管线已截取快照。

**排查步骤：**
1. 升级到最新版本（已修复 ChartElement 异步渲染注册问题）
2. 确认 `chartDataVariable` 绑定的数据格式正确
3. 在编辑模式下确认图表正常显示后再预览

### 7. `onSave` 回调未触发

**原因：** 未正确传入 `onSave` 属性。

**排查步骤：**
1. Vue 组件确认 `:on-save="handleSave"` 正确绑定
2. Web Component 确认在 `ready` 事件后调用 `setOnSave`
3. 检查 `handleSave` 函数是否存在且无语法错误
4. 确认未同时传入自建的 CRUD 配置（`onSave` 会覆盖内置保存逻辑）

### 8. 自动保存不生效

**原因：** 自动保存需要 `onSave` 回调。

**排查步骤：**
1. 确认同时传入了 `onSave` 和 `auto-save-enabled`
2. 自动保存仅在内容有变更时触发，未修改时不会保存
3. 检查 `auto-save-interval-ms` 是否设置过短（频繁保存可能被节流）

---

## 技术栈

| 技术 | 用途 |
|------|------|
| Vue 3 + TypeScript | 核心框架 |
| Pinia | 状态管理 |
| Tailwind CSS | 原子化 CSS |
| ECharts 6 | 图表渲染 |
| Monaco Editor | 代码/JSON 编辑器 |
| jsPDF | PDF 生成 |
| jsBarcode | 条形码生成 |
| qrcode | 二维码生成 |
| canvg | SVG → Canvas 转换 |
| dom-to-image-more | DOM → 图片导出 |
| jszip | ZIP 打包 |
| vue-i18n | 国际化（中文/English） |
| Vite | 构建工具 |
