# X-Print-Designer

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

---

## 目录

- [核心概念：模板模式 vs 报告模式](#核心概念模板模式-vs-报告模式)
- [快速开始](#快速开始)
- [Vue 组件使用说明](#vue-组件使用说明)
- [Web Component 使用说明](#web-component-使用说明)
- [传入数据格式](#传入数据格式)
- [保存回调 `onSave`](#保存回调-onsave)
- [自动保存配置](#自动保存配置)
- [模板数据结构（完整格式）](#模板数据结构完整格式)
- [元素数据绑定](#元素数据绑定)
- [图片元素使用说明](#图片元素使用说明)
- [支持的元素类型](#支持的元素类型)
- [技术栈](#技术栈)

---

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

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

### 概念说明

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

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

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

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

**数据流向：**
```
模板（保存布局） + 数据（打印时注入） = 最终打印结果
```

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

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

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

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

**数据流向：**
```
报告（模板 + 数据 一起保存） = 完整的报告文件
```

**保存行为：**
- 保存时**同时保存页面布局 + 测试数据**
- `testData` 会被完整保存到模板数据中

### 两种模式对比

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

### 使用示例

**模板模式（推荐用于可复用模板）：**
```javascript
// 1. 创建/编辑模板
const sampleData = {
  id: null,  // 无需数据 ID，只是用于预览
  title: "@orderNo",  // 绑定变量
  customerName: "@customerName"
};

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

// 3. 打印时动态注入数据
designer.setVariables({
  orderNo: "ORD-2024-001",
  customerName: "张三"
});
designer.print();
```

**报告模式（推荐用于完整报告）：**
```javascript
// 1. 创建/编辑报告
const sampleData = {
  id: 8,  // 报告 ID
  title: "2024年第一季度销售报告",
  totalAmount: 50000,
  items: [...]  // 报告数据
};

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

// 3. 加载后直接打印，无需再次注入数据
designer.loadTemplate(8);
designer.print();
```

### 切换模式

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

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

---

## 快速开始

```bash
npm install
npm run dev
```

### 最简接入

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

<script setup>
import { ref } from 'vue';
import PrintDesigner from '@/components/PrintDesigner.vue';

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

const handleSave = (payload) => {
  console.log('保存数据:', payload);
  // { id: '...', name: '测试报告', data: {...}, isNew: false }
  // 用户自行调用接口保存
};
</script>
```

---

## Vue 组件使用说明

### 组件 Props 一览

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `mode` | `'template'` \| `'report'` | `'template'` | 工作模式 |
| `sample-data` | `object` | — | 传入的示例数据 |
| `on-save` | `(payload) => void \| Promise<void>` | — | 保存回调，详见下方 |
| `auto-save-enabled` | `boolean` | `false` | 是否开启定时自动保存 |
| `auto-save-interval-ms` | `number` | `120000` | 自动保存间隔（毫秒），默认 2 分钟 |

### Props 详细说明

#### `mode`

工作模式。`'template'` 保存时只存布局，`'report'` 保存时同时存布局和 testData。

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

#### `sample-data`

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

```vue
<PrintDesigner :sample-data="{ id: 1, name: '报告', variables: {...} }" />
```

#### `on-save`

保存回调函数。当用户点击保存按钮（或自动保存触发）时，设计器组装好数据后调用此回调，**不再自动调接口**，完全交由用户自行处理。

```vue
<PrintDesigner :on-save="(payload) => fetch('/api/save', { method: 'POST', body: JSON.stringify(payload) })" />
```

#### `auto-save-enabled`

是否开启定时自动保存。开启后每隔 `auto-save-interval-ms` 毫秒自动触发 `on-save` 回调。详见 [自动保存配置](#自动保存配置)。

#### `auto-save-interval-ms`

自动保存间隔时间（毫秒），默认 `120000`（2 分钟）。

---

## Web Component 使用说明

### 标签方式

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

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

  designer.setMode('template');
  designer.setOnSave((payload) => {
    fetch('/api/save', { method: 'POST', body: JSON.stringify(payload) });
  });
  designer.setAutoSave(true, 120000);
  designer.setTestData({ orderNo: 'ORD-001', items: [...] });
</script>
```

### API 速览

| 方法 | 说明 |
|------|------|
| `setMode(mode)` | 设置模式：`'template'` / `'report'` |
| `setViewMode(viewMode)` | 切换视图：`'edit'` / `'preview'` |
| `setOnSave(handler)` | 设置保存回调 |
| `setAutoSave(enabled, intervalMs?)` | 开启/关闭自动保存 |
| `setTestData(data)` | 设置测试数据 |
| `setVariables(vars)` | 打印/导出时注入真实变量 |
| `setSampleData(data)` | 设置样本数据 |
| `setTemplates(list, opts)` | 设置模板列表 |
| `loadTemplate(id)` | 加载指定模板到画布 |
| `loadTemplateData(data)` | 直接加载模板 JSON 数据到画布 |
| `getTemplateData()` | 获取当前模板完整数据 |
| `getPreviewHtml()` | 获取预览 HTML 字符串 |
| `print(request)` | 执行打印 |
| `export(request)` | 导出 PDF/图片/HTML |
| `setPreviewMode(options)` | 🆕 进入独立预览面板模式 |
| `exitPreviewMode()` | 🆕 退出预览模式，返回编辑器 |

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

```js
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 } });
```

### 独立预览面板 (`setPreviewMode` / `exitPreviewMode`) 🆕

当设计器以 Web Component 方式嵌入时，可切换到独立的**纯预览模式**——只渲染打印内容，不显示编辑器工具栏、属性面板、辅助线等 UI。

**适用于：**
- 插件/第三方页面嵌入时，单独展示打印预览效果
- 用户填写表单后直接预览生成的打印内容
- 无需编辑功能的只读预览场景

#### 切换方式

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

// 进入预览模式，传入模板和数据
designer.setPreviewMode({
  pages: templateData.pages,                    // 页面元素布局
  canvasSize: { width: 794, height: 1123 },    // 画布尺寸
  canvasBackground: '#ffffff',                  // 背景色
  testData: {                                   // 测试数据
    name: '张三',
    amount: 1000,
    items: [{ name: '产品A', price: 100 }]
  },
  variables: {},                                // 变量映射
  watermark: { enabled: false }                 // 水印设置
});

// 退出预览，返回编辑器界面
designer.exitPreviewMode();
```

#### 参数说明

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pages` | `Page[]` | ✅ 是 | 页面元素布局数组 |
| `canvasSize` | `{ width, height }` | ✅ 是 | 画布尺寸（px） |
| `canvasBackground` | `string` | 否 | 画布背景色，默认 `'#ffffff'` |
| `testData` | `object` | 否 | 测试数据，模板中的变量会自动解析 |
| `variables` | `object` | 否 | 额外变量映射 |
| `watermark` | `object` | 否 | 水印配置 |

#### 使用示例

```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` 接收一个普通的 JSON 对象，**唯一必填字段是 `id`**，用于标识业务数据的唯一性。其他字段完全自由，可以根据业务需求灵活组织数据结构。

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | `number` / `string` | ✅ 是 | **业务数据的唯一标识**，如报告 ID、订单 ID |
| `templateId` | `string` | 否 | 加载的打印模板 ID，不传则用当前已选模板 |
| `name` | `string` | 否 | 数据名称，如报告标题、订单名称 |
| `variables` | `object` | 否 | 业务变量对象（可选，用作组织数据的约定） |
| 任意自定义字段 | `any` | 否 | 如 `createTime`、`customerName`，也可在模板中直接引用 |

### 两种数据传入方式

组件支持两种数据组织方式，**可以任选其一**，组件会自动兼容处理：

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

将所有业务变量放在 `variables` 对象中，结构清晰，适合复杂业务场景：

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

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

#### 方式二：直接扁平数据（简洁）

将所有字段直接放在根级别，无需 `variables` 包装，适合简单场景：

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

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

> 两种方式完全等效，组件内部会自动解析变量路径，无需担心格式不匹配。

### 支持的数据类型

所有字段都可在模板元素中通过变量语法绑定，支持：

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

### 完整示例数据格式

```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"
      },
      {
        "time": "2026-04-08 14:00",
        "taskname": "西乡街道线路巡查",
        "dockname": "罗湖无人机机场",
        "distance": "10.2 km",
        "image": "https://picsum.photos/id/10/200/80"
      }
    ],
    "taskRecords": [
      {
        "time": "2026-04-02 09:30",
        "taskname": "宝安港区A线巡检",
        "dockname": "宝安国际无人机机场",
        "warn": 2
      },
      {
        "time": "2026-04-08 14:00",
        "taskname": "西乡街道线路巡查",
        "dockname": "罗湖无人机机场",
        "warn": 1
      }
    ],
    "chartData": [
      { "category": "一月", "value": 1200 },
      { "category": "二月", "value": 1500 },
      { "category": "三月", "value": 1800 }
    ]
  }
}
```

### 在模板中绑定变量

变量绑定使用 `@变量名` 或 `{#变量名}` 语法。绑定时只需使用变量名，无需关心数据是否在 `variables` 包装下：

| 变量路径 | 绑定写法 | 应用元素 |
|---------|---------|---------|
| `variables.dockname` / `dockname` | `@dockname` | 文本 |
| `variables.reportname` / `reportname` | `@reportname` | 文本 |
| `variables.flightRecords` / `flightRecords` | `@flightRecords` | 表格、自定义表格 |
| `variables.chartData` / `chartData` | `@chartData` | 图表 |

> 组件会自动解析变量路径，无论数据在 `variables.xxx` 还是直接在根级别，都能正确绑定。

### 表格数据

表格需要三个维度的变量：

| 绑定字段 | 变量名 | 格式 | 说明 |
|---------|-------|------|------|
| `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`（饼图名称字段）

### 使用注意事项

1. **`id` 字段的重要性**
   - `id` 是业务数据的唯一标识，用于在保存回调中识别当前编辑的数据
   - 如果不传 `id`，保存时 `payload.id` 将为 `null`，表示新建数据

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

3. **变量命名建议**
   - 使用有意义的变量名，如 `customerName` 而非 `c`
   - 避免使用特殊字符和空格
   - 嵌套路径使用点号分隔，如 `@user.profile.name`

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

5. **动态更新数据**
   - 可以通过 Web Component 的 `setSampleData()` 方法动态更新数据
   - 数据更新后，模板中的变量绑定会自动重新解析

6. **数据验证建议**
   - 确保传入的数据字段与模板中绑定的变量名一致
   - 如果变量找不到对应的数据，模板中会显示为空

---

## 保存回调 `onSave`

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

### 回调参数

```ts
interface SavePayload {
  id: string | null;    // 模板 ID，null 表示新建
  name: string;         // 模板名称
  data: Record<string, any>;  // 模板数据（见下方详细说明）
  isNew: boolean;       // 是否新建模板
}
```

### 不同模式下的保存数据差异

| 数据字段 | 模板模式 | 报告模式 | 说明 |
|---------|---------|---------|------|
| `pages` | ✅ | ✅ | 页面元素布局 |
| `canvasSize` | ✅ | ✅ | 画布尺寸 |
| `watermark` | ✅ | ✅ | 水印设置 |
| `guides` | ✅ | ✅ | 辅助线 |
| `testData` | ❌ | ✅ | 传入的示例数据 |
| `unit` | ✅ | ✅ | 单位设置 |
| `showGrid` | ✅ | ✅ | 网格显示 |
| ... | ... | ... | 其他配置 |

### 保存数据示例

**模板模式保存的数据：**
```javascript
{
  id: "tpl_001",           // 模板 ID
  name: "订单打印模板",
  data: {
    pages: [...],          // 页面布局
    canvasSize: {...},     // 画布尺寸
    watermark: {...},     // 水印设置
    unit: "mm",
    showGrid: true,
    // ❌ 没有 testData 字段
  },
  isNew: false
}
```

**报告模式保存的数据：**
```javascript
{
  id: "rpt_001",           // 报告 ID
  name: "第一季度销售报告",
  data: {
    pages: [...],          // 页面布局
    canvasSize: {...},     // 画布尺寸
    watermark: {...},     // 水印设置
    unit: "mm",
    showGrid: true,
    // ✅ 包含完整的 testData
    testData: {
      id: 8,
      name: "第一季度销售报告",
      totalAmount: 50000,
      items: [
        { name: "产品A", price: 100, quantity: 200 },
        { name: "产品B", price: 200, quantity: 150 }
      ],
      chartData: [
        { category: "一月", value: 1200 },
        { category: "二月", value: 1500 },
        { category: "三月", value: 1800 }
      ]
    }
  },
  isNew: false
}
```

### 使用示例

```vue
<PrintDesigner
  :mode="mode"
  :on-save="handleSave"
/>

<script setup>
import { ref } from 'vue';

const mode = ref('template');  // 或 'report'

const handleSave = async (payload) => {
  console.log('保存模式:', mode.value);
  console.log('保存的数据:', payload);

  // 根据模式决定如何处理
  if (mode.value === 'template') {
    // 模板模式：保存布局，数据单独处理
    await saveTemplate(payload.id, payload.name, payload.data);
  } else {
    // 报告模式：布局和数据一起保存
    await saveReport(payload.id, payload.name, payload.data);
  }
};
</script>
```

> 不传 `onSave` 时，设计器会走原有的默认保存逻辑（调用内置 CRUD 接口或存 localStorage），行为不变。

---

## 自动保存配置

传入 `onSave` 后，可同时开启自动保存。开启后设计器会每隔固定时间调用 `onSave` 回调，右下角显示最近保存时间。

### Props 配置

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `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"
/>
```

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

> 右下角会显示 "最近保存: HH:MM:SS"，每次回调完成后自动更新时间。

---

## 模板数据结构（完整格式）

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

```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 }` | 是 | 画布尺寸（默认 A4: 794×1123） |
| `unit` | `'mm'`\|`'px'`\|`'pt'`\|`'in'`\|`'cm'` | 否 | 单位 |
| `watermark` | `WatermarkSettings` | 否 | 水印配置 |
| `guides` | `Guide[]` | 否 | 参考线 |
| `zoom` | `number` | 否 | 缩放比例 |
| `showGrid` | `boolean` | 否 | 显示网格 |
| `headerHeight` | `number` | 否 | 页眉高度 |
| `footerHeight` | `number` | 否 | 页脚高度 |
| `pageSpacingX` | `number` | 否 | 水平边距 |
| `pageSpacingY` | `number` | 否 | 垂直边距 |
| `canvasBackground` | `string` | 否 | 画布背景色 |
| `testData` | `object` | 否 | 测试数据（报告模式保存） |

### 元素基本字段

```ts
interface PrintElement {
  id: string;          // 元素唯一 ID
  type: ElementType;   // 元素类型: 'text'|'image'|'table'|'customTable'|'barcode'|'qrcode'|'line'|'rect'|'circle'|'chart'|'pageNumber'|'longtext'
  x: number;           // X 坐标
  y: number;           // Y 坐标
  width: number;       // 宽度
  height: number;      // 高度
  content?: string;    // 文本内容（支持变量占位符）
  variable?: string;   // 绑定变量名（如 '@items'）
  style: ElementStyle; // 样式对象
}
```

---

## 元素数据绑定

### 变量语法

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

### 各元素可绑定字段

| 元素 | 绑定的属性 | 示例 |
|------|----------|------|
| 文本 / 长文本 | `content` | `订单号: {#orderNo}` |
| 图片 | `variable` | `@imageUrl` |
| 表格 | `variable` + `columnsVariable` + `footerDataVariable` | `@items`, `@tableCols`, `@tableFooter` |
| 条形码 | `variable` | `@barcode` |
| 二维码 | `variable` | `@url` |
| 图表 | `chartDataVariable` + `chartXField` + `chartYField` | `@chartData`, `category`, `value` |

---

## 图片元素使用说明

图片元素支持 **三种方式** 设置图片：

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

在图片元素的属性面板中，直接在"图片地址"输入框填写图片 URL（支持 HTTP/HTTPS 链接）。

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

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

点击属性面板中的 **"上传图片"** 按钮，从本地选择一张图片文件：

- 自动将图片转换为 **Base64** 字符串并填入图片地址
- 限制最大 **2MB**，超出会提示错误
- 支持所有常见图片格式（PNG、JPG、GIF、WebP 等）

> 也可以**双击画布上的图片元素**来快速触发上传（编辑模式下）。

### 方式 3：绑定变量

将图片元素的 `variable` 属性绑定到一个变量名，图片地址从数据中动态读取。

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

**在 sampleData 中对应的数据：**

```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 |

---

## 支持的元素类型

| 类型 | 标识 | 说明 |
|------|------|------|
| 文本 | `text` | 静态文本或变量内容 |
| 长文本 | `longtext` | 多行自动扩展文本 |
| 富文本 | `richtext` | 富文本格式化内容 |
| 图片 | `image` | 图片显示（URL / Base64 / 变量绑定） |
| 表格 | `table` | 数据表格，支持自动分页、页脚脚本 |
| 自定义表格 | `customTable` | 自由合并单元格、单元格内嵌图片/字段 |
| 嵌套表格 | `nestedtable` | 分组嵌套数据展示 |
| 条形码 | `barcode` | 一维条形码 |
| 二维码 | `qrcode` | 二维码 |
| 图表 | `chart` | ECharts（柱状图/折线图/饼图/环形图/面积图） |
| 数据卡片 | `datacard` | 单个关键指标数据展示 |
| 进度条 | `progress` | 百分比进度展示 |
| 列表 | `list` | 多行列表数据展示 |
| 日期 | `date` | 日期时间显示 |
| 页码 | `pageNumber` | 页码显示 |
| 签名/印章 | `signature` | 印章图片或二维码 |
| 线条 | `line` | 直线 |
| 矩形 | `rect` | 矩形 |
| 圆形 | `circle` | 圆形 |

---

## 技术栈

- Vue 3 / TypeScript
- Pinia（状态管理）
- Tailwind CSS
- ECharts 5
- Monaco Editor

---
