Files
label-designer/README.md
2026-06-13 13:51:02 +08:00

564 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Label Designer
标签批量设计与排版工具:可视化编辑标签模板、导入 Excel 数据、预览整页排版并导出 PDF。
模板以 JSON 保存,导出文件扩展名为 `.lad`Label Application Data
---
## 快速开始
```bash
npm install
npm run dev # 开发服务器,默认 http://localhost:3000
npm run build # 生产构建
npm run preview # 预览构建结果
```
---
## 模板文件概览
一个模板描述**单张标签**的外观与排版参数。批量数据Excel 行)在导出阶段关联,**不写入模板 JSON**。
### 最小合法结构
导入外部模板时JSON 至少包含:
| 字段 | 类型 | 说明 |
|------|------|------|
| `width` | number | 标签宽度 (mm) |
| `height` | number | 标签高度 (mm) |
| `elements` | array | 元素列表 |
### 完整根对象 `LabelTemplate`
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | string | 否 | 模板唯一 ID应用内自动生成 |
| `name` | string | 否 | 模板显示名称 |
| `width` | number | **是** | 标签画布宽度 (mm) |
| `height` | number | **是** | 标签画布高度 (mm) |
| `elements` | `TemplateElement[]` | **是** | 标签上的所有元素 |
| `variableList` | string[] | 否 | 声明的变量名列表(统一管理) |
| `variableDefaults` | Record<string, string> | 否 | 设计预览用默认值,`变量名 → 文本` |
| `paper` | `PaperConfig` | 否 | 纸张排版;缺省时使用 A4 默认配置 |
| `cutLine` | `CutLineConfig` | 否 | PDF 裁切参考线;缺省时启用灰色虚线 |
---
## 坐标与单位
- 所有尺寸、位置、边距、间距:**毫米 (mm)**
- 字号:**磅 (pt)**
- 元素坐标 `(x, y)` 为相对标签画布左上角的距离
- 旋转 `rotation`:顺时针角度(度),默认 `0`
---
## 变量与占位符
### 语法
在元素的 `content` 中使用 `{变量名}` 引用数据。导出时由 Excel 列映射或默认值填充。
表格元素的变量写在各单元格的 `tableCells["行,列"].content` 中(见下文 **表格** 一节),同样参与变量汇总与数据映射。
| 占位符 | 说明 |
|--------|------|
| `{变量名}` | 普通变量,如 `{货位编号}``{物料名称}` |
| `{#INDEX}` | 内置占位符:当前数据行在导入数据中的 **1-based 序号**(第 1 行 → `1` |
变量名规则:
- 写在花括号内,不含 `}`
- `#INDEX` reserved不要作为自定义变量名
- 可在 `variableList` 中预先声明;也会从 `content`、**表格单元格 content** 与显示条件中自动汇总
### 导出时的变量映射(非模板字段)
导出 Excel 时,每个变量需关联数据源,映射关系保存在浏览器本地,**不在模板 JSON 内**
| 映射值 | 含义 |
|--------|------|
| Excel 列名 | 取该列单元格值 |
| `__use_default__` | 使用模板 `variableDefaults` 中的默认值 |
| `__use_row_index__` | 使用数据序号(与 `{#INDEX}` 相同1-based |
列名与变量名相同时会自动匹配。
---
## 元素 `TemplateElement`
每个元素是标签上的一个图层,按数组顺序从下到上绘制。
### 通用字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 元素唯一 ID |
| `type` | `'text' \| 'barcode' \| 'qrcode' \| 'image' \| 'table'` | 元素类型 |
| `name` | string | 图层名称(设计器显示用) |
| `x`, `y` | number | 位置 (mm) |
| `width`, `height` | number | 尺寸 (mm) |
| `rotation` | number | 旋转角度,默认 `0` |
| `content` | string | 静态文本或 `{变量}` 组合 |
| `fontFamily` | `'sans' \| 'serif' \| 'mono'` | 字体族 |
| `backgroundColor` | string | 背景色,如 `#ffffff``transparent` |
| `backgroundOpacity` | number | 背景不透明度 0100 |
| `padding` | number | 内边距 (mm),四边统一 |
| `paddingTop/Right/Bottom/Left` | number | 分边内边距,覆盖 `padding` |
| `borderWidth` | number | 边框线宽 (mm)0 为无边框 |
| `borderColor` | string | 边框颜色 |
| `borderWidthTop/Right/Bottom/Left` | number | 分边边框宽度 |
| `borderColorTop/Right/Bottom/Left` | string | 分边边框颜色 |
| `borderRadius` | number | 圆角半径 (mm) |
| `borderRadiusTL/TR/BR/BL` | number | 分角圆角 |
| `borderRadiusCorners` | object | 指定哪些角圆角,如 `{ "tl": true, "tr": true }` |
| `locked` | boolean | 锁定后设计器不可拖动 |
### 值提取(`text` / `barcode` / `qrcode` / `image` / 表格单元格 均有效)
**变量替换完成后的完整 `content` 字符串** 做提取(先替换 `{变量}`,再提取)。
| 字段 | 类型 | 说明 |
|------|------|------|
| `textExtractMode` | `'none' \| 'split' \| 'prefix' \| 'suffix'` | 提取方式;缺省且存在 `textSplitDelimiter` 时视为 `split` |
| `textSplitDelimiter` | string | 分隔符,如 `-` |
| `textSplitIndex` | number | 按分隔符拆分后取第 N 段(**从 1 开始**),段数不足则为空 |
| `textExtractLength` | number | 前 N / 后 N 位时的位数 |
提取前会对完整内容 `trim()` 去除首尾空格。
**示例**`content: "ABC-{货位编号}"`,货位编号 = `A-1-2-3`
→ 替换后 `ABC-A-1-2-3` → 分隔符 `-`、第 3 段 → 显示 `1`
### 显示前后缀(仅 `type: "text"`
在值提取 **之后** 追加,仅影响显示,**不参与**「内容值」显示条件判断。
| 字段 | 类型 | 说明 |
|------|------|------|
| `textDisplayPrefix` | string | 前缀字符,如 `¥` |
| `textDisplaySuffix` | string | 后缀字符,如 `元` |
### 显示控制
| 字段 | 类型 | 说明 |
|------|------|------|
| `visibilityMode` | `'always' \| 'conditional'` | 始终显示 / 按条件显示 |
| `visibilityRules` | `VisibilityRule[]` | 条件列表,**全部满足**才显示 |
#### `VisibilityRule`
| 字段 | 类型 | 说明 |
|------|------|------|
| `target` | `'variable' \| 'content'` | 条件对象:变量值 或 本元素内容值(提取后、不含前后缀) |
| `variable` | string | `target=variable` 时的变量名 |
| `operator` | 见下表 | 比较运算符 |
| `value` | string | `gt` / `lt` / `eq` / `neq` 时的比较目标 |
| operator | 含义 |
|----------|------|
| `empty` | 为空 |
| `not_empty` | 不为空 |
| `gt` | 大于(数值优先比较,否则字符串) |
| `lt` | 小于 |
| `eq` | 等于 |
| `neq` | 不等于 |
---
### 文本元素 `type: "text"`
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `fontSize` | number | — | 字号 (pt) |
| `textAlign` | `'left' \| 'center' \| 'right'` | — | 水平对齐 |
| `verticalAlign` | `'top' \| 'middle' \| 'bottom'` | `middle` | 垂直对齐 |
| `wordWrap` | boolean | `true` | 自动换行 |
| `textWritingMode` | `'horizontal' \| 'vertical'` | `horizontal` | 排列方向 |
| `fontWeight` | `'normal' \| 'bold'` | — | 粗体 |
| `fontStyle` | `'normal' \| 'italic'` | `normal` | 斜体 |
| `textUnderline` | boolean | — | 下划线 |
| `textLineThrough` | boolean | — | 删除线 |
| `textScaleX`, `textScaleY` | number | `1` | 水平/垂直拉伸 |
| `letterSpacing` | number | — | 字间距 (mm) |
| `textColor` | string | — | 文字颜色 |
| `textFormat` | `'text' \| 'number' \| 'percent' \| 'currency'` | `text` | 显示格式 |
| `decimalPlaces` | number | `2` | 数值/百分比/货币的小数位 |
| `numberPartDisplay` | `'full' \| 'integer' \| 'fraction'` | `full` | 数值拆分显示(如价格拆成整数与小数两部分) |
数值格式作用于每个 `{变量}` 替换值;提取与前后缀见上文顺序。
---
### 条形码 `type: "barcode"`
| 字段 | 类型 | 说明 |
|------|------|------|
| `barcodeFormat` | `'CODE128' \| 'CODE39' \| 'EAN13' \| 'ITF'` | 码制 |
| `showText` | boolean | 是否在条码下方显示可读文本 |
| `content` | string | 条码数据,通常为 `{变量}` |
输出模式下内容为空则不渲染该元素。
---
### 二维码 `type: "qrcode"`
| 字段 | 类型 | 说明 |
|------|------|------|
| `content` | string | 二维码内容URL、文本等 |
二维码元素宽高相等(设计器约束)。内容为空时不渲染。
---
### 图片 `type: "image"`
| 字段 | 类型 | 说明 |
|------|------|------|
| `content` | string | 图片 URL、`data:` URI`{变量}` |
| `imageFit` | `'contain' \| 'cover' \| 'fill'` | 缩放方式,默认 `contain` |
---
### 表格 `type: "table"`
表格由行数、列数及各行高、各列宽定义网格;单元格可单独设置内容与文本样式,支持合并。
元素级 `content` 对表格通常留空;**单元格内容默认为空**,在 `tableCells` 中按坐标单独设置。
#### 表格结构字段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `tableRows` | number | `3` | 行数 (150) |
| `tableCols` | number | `3` | 列数 (150) |
| `tableRowHeights` | number[] | — | 各行高度 (mm),长度须等于 `tableRows` |
| `tableColWidths` | number[] | — | 各列宽度 (mm),长度须等于 `tableCols` |
| `tableBorderWidth` | number | `0.2` | 单元格网格线宽 (mm)`0` 时不绘制网格线 |
| `tableBorderColor` | string | `#374151` | 网格线颜色 |
| `tableCells` | Record<string, TableCellData> | — | 单元格数据,键为 `"行,列"`**从 0 开始** |
**尺寸关系**
- 内容区(网格区域)= 元素宽高减去 `padding`
- `sum(tableRowHeights)` = 内容区高度
- `sum(tableColWidths)` = 内容区宽度
- 增大内边距时,元素宽高会自动增大以保持行高/列宽不变
拖拽缩放表格四角时,各行高、各列宽会 **等比缩放**
#### `TableCellData``tableCells` 的值)
| 字段 | 类型 | 说明 |
|------|------|------|
| `content` | string | 单元格文本或 `{变量}`;缺省为空 |
| `mergeAnchor` | `{ row, col }` | 被合并单元格指向主单元格坐标 |
| `rowSpan` | number | 主单元格合并行数,默认 `1` |
| `colSpan` | number | 主单元格合并列数,默认 `1` |
| `fontSize` | number | 字号 (pt),缺省继承元素级 |
| `fontFamily` | `'sans' \| 'serif' \| 'mono'` | 字体 |
| `textAlign` | `'left' \| 'center' \| 'right'` | 水平对齐 |
| `verticalAlign` | `'top' \| 'middle' \| 'bottom'` | 垂直对齐 |
| `fontWeight` | `'normal' \| 'bold'` | 粗体 |
| `fontStyle` | `'normal' \| 'italic'` | 斜体 |
| `textUnderline` | boolean | 下划线 |
| `textLineThrough` | boolean | 删除线 |
| `textScaleX`, `textScaleY` | number | 水平/垂直拉伸 |
| `letterSpacing` | number | 字间距 (mm) |
| `textWritingMode` | `'horizontal' \| 'vertical'` | 排列方向 |
| `textColor` | string | 文字颜色 |
| `backgroundColor` | string | 单元格背景色 |
| `wordWrap` | boolean | 自动换行 |
| `textFormat` | 同文本元素 | 显示格式 |
| `decimalPlaces` | number | 小数位数 |
| `numberPartDisplay` | 同文本元素 | 数值拆分显示 |
| `textExtractMode` 等 | 同值提取字段 | 对该单元格 `content` 生效 |
| `textDisplayPrefix` / `textDisplaySuffix` | string | 显示前后缀 |
未在 `tableCells` 中声明的坐标视为空白单元格,使用元素级默认字体/对齐等绘制属性。
#### 合并单元格
- 主单元格(左上角)存 `rowSpan` / `colSpan`
- 被合并的从属单元格存 `mergeAnchor: { row, col }` 指向主单元格
- 合并区域内仅主单元格参与内容与样式;渲染时内部网格线自动省略
#### 表格示例片段
```json
{
"id": "el_table",
"type": "table",
"name": "明细表",
"x": 2,
"y": 2,
"width": 66,
"height": 20,
"content": "",
"fontSize": 9,
"textAlign": "center",
"verticalAlign": "middle",
"fontWeight": "normal",
"fontFamily": "sans",
"barcodeFormat": "CODE128",
"showText": false,
"tableRows": 2,
"tableCols": 3,
"tableRowHeights": [6, 6],
"tableColWidths": [22, 22, 22],
"tableBorderWidth": 0.2,
"tableBorderColor": "#374151",
"tableCells": {
"0,0": { "content": "物料", "fontWeight": "bold" },
"0,1": { "content": "数量", "fontWeight": "bold" },
"0,2": { "content": "货位", "fontWeight": "bold" },
"1,0": { "content": "{物料名称}" },
"1,1": { "content": "{数量}" },
"1,2": { "content": "{货位编号}" }
}
}
```
合并 2×2 区域示例(主单元格 `"0,0"``rowSpan: 2``colSpan: 2`
```json
"tableCells": {
"0,0": { "content": "标题", "rowSpan": 2, "colSpan": 2 },
"0,1": { "mergeAnchor": { "row": 0, "col": 0 } },
"1,0": { "mergeAnchor": { "row": 0, "col": 0 } },
"1,1": { "mergeAnchor": { "row": 0, "col": 0 } }
}
```
---
## 纸张排版 `PaperConfig`
控制标签如何排列到 PDF 页面上(每模板独立保存)。
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | `'A4' \| 'A5' \| 'A6' \| 'custom'` | 纸张规格 |
| `width`, `height` | number | 纸张尺寸 (mm);预设规格可省略自定义值 |
| `orientation` | `'portrait' \| 'landscape'` | 纵向 / 横向 |
| `marginTop/Right/Bottom/Left` | number | 页边距 (mm) |
| `columnGap`, `rowGap` | number | 标签横向 / 纵向间距 (mm) |
| `flow` | `'left-right-top-bottom' \| 'top-bottom-left-right'` | 标签填充顺序 |
**默认 `paper`(未指定时)**
```json
{
"type": "A4",
"width": 210,
"height": 297,
"orientation": "portrait",
"marginTop": 10,
"marginRight": 10,
"marginBottom": 10,
"marginLeft": 10,
"columnGap": 2,
"rowGap": 2,
"flow": "left-right-top-bottom"
}
```
---
## 裁切线 `CutLineConfig`
PDF 中标签之间的裁切参考线(预览与导出一致)。
| 字段 | 类型 | 说明 |
|------|------|------|
| `enabled` | boolean | 是否显示 |
| `style` | `'dashed' \| 'solid'` | 线型 |
| `width` | number | 线宽 (mm),默认 `0.1` |
| `color` | string | 颜色,默认 `#cccccc` |
---
## 完整示例
```json
{
"id": "demo_label_001",
"name": "货位标签示例",
"width": 70,
"height": 40,
"variableList": ["货位编号", "物料名称", "批次号"],
"variableDefaults": {
"货位编号": "A-01-05",
"物料名称": "示例物料",
"批次号": "2026061101"
},
"paper": {
"type": "A4",
"width": 210,
"height": 297,
"orientation": "portrait",
"marginTop": 10,
"marginRight": 10,
"marginBottom": 10,
"marginLeft": 10,
"columnGap": 2,
"rowGap": 2,
"flow": "left-right-top-bottom"
},
"cutLine": {
"enabled": true,
"style": "dashed",
"width": 0.1,
"color": "#cccccc"
},
"elements": [
{
"id": "el_title",
"type": "text",
"name": "货位前缀",
"x": 2,
"y": 2,
"width": 66,
"height": 8,
"content": "货位:",
"fontSize": 9,
"fontWeight": "bold",
"textAlign": "left",
"fontFamily": "sans",
"barcodeFormat": "CODE128",
"showText": false,
"textColor": "#333333"
},
{
"id": "el_location",
"type": "text",
"name": "货位段",
"x": 18,
"y": 2,
"width": 20,
"height": 8,
"content": "{货位编号}",
"fontSize": 10,
"fontWeight": "bold",
"textAlign": "left",
"fontFamily": "mono",
"barcodeFormat": "CODE128",
"showText": false,
"textExtractMode": "split",
"textSplitDelimiter": "-",
"textSplitIndex": 2,
"visibilityMode": "conditional",
"visibilityRules": [
{
"target": "content",
"variable": "",
"operator": "not_empty"
}
]
},
{
"id": "el_name",
"type": "text",
"name": "物料名称",
"x": 2,
"y": 12,
"width": 66,
"height": 10,
"content": "{物料名称}",
"fontSize": 8,
"fontWeight": "normal",
"textAlign": "left",
"fontFamily": "sans",
"barcodeFormat": "CODE128",
"showText": false,
"wordWrap": true
},
{
"id": "el_barcode",
"type": "barcode",
"name": "条码",
"x": 2,
"y": 24,
"width": 40,
"height": 14,
"content": "{货位编号}",
"fontSize": 8,
"textAlign": "center",
"fontWeight": "normal",
"fontFamily": "mono",
"barcodeFormat": "CODE128",
"showText": true,
"textExtractMode": "suffix",
"textExtractLength": 8
},
{
"id": "el_qr",
"type": "qrcode",
"name": "二维码",
"x": 48,
"y": 22,
"width": 18,
"height": 18,
"content": "https://example.com/{货位编号}",
"fontSize": 8,
"textAlign": "center",
"fontWeight": "normal",
"fontFamily": "sans",
"barcodeFormat": "CODE128",
"showText": false
}
]
}
```
> 说明:`barcodeFormat` 与 `showText` 在所有元素类型上为历史 schema 要求字段,文本/二维码/图片元素也需填写(常用 `CODE128` + `showText: false`)。
---
## 内容解析顺序(导出 / 预览)
对单个元素,最终显示值按以下顺序计算:
1.`content` 中的 `{变量}` 替换为数据行值(文本元素可对每个变量做数值格式化;**表格**对每格 `tableCells[*].content` 同样处理)
2. 对完整字符串做 **值提取**trim → 分隔符 / 前 N / 后 N
3. 文本元素及 **表格单元格** 追加 **显示前后缀**`textDisplayPrefix` / `textDisplaySuffix`
4. 根据 **显示控制** 决定是否绘制该元素
表格单元格 `content` 为空时不绘制该格文字;网格线仍按 `tableBorderWidth` 绘制(为 `0` 时不显示)。
---
## 导入 / 导出模板
| 操作 | 说明 |
|------|------|
| 应用内导出 | 生成 `.lad` JSON 文件 |
| 应用内导入 | 校验 `width``height``elements` 后追加到模板库 |
| 本地存储 | 浏览器 `localStorage``label_templates_list_v2` |
导入时会执行 `normalizeTemplate`:补全默认 `paper``cutLine`,并移除已废弃的数据源字段。
---
## 导出 PDF 流程(与模板的关系)
1. 选择模板 → 设计标签(编辑 JSON 中的 `elements` 等)
2. 导入 Excel → 配置 **变量列映射**(非模板部分)
3. 设置纸张排版、数据范围、裁切线(排版参数来自模板 `paper` / `cutLine`
4. 预览确认 → 导出 PDF
PDF 每页按 `paper` 网格排列标签,裁切线可选,标签图由 Canvas 渲染为 PNG/JPEG 嵌入 PDF。
---
## 类型定义源码
完整 TypeScript 类型见 [`src/types.ts`](src/types.ts)(含 `TableCellData`)。
表格工具函数见 [`src/tableUtils.ts`](src/tableUtils.ts)。
模板规范化逻辑见 [`src/utils.ts`](src/utils.ts) 中的 `normalizeTemplate`