# 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 | 否 | 设计预览用默认值,`变量名 → 文本` | | `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 | 背景不透明度 0–100 | | `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` | 行数 (1–50) | | `tableCols` | number | `3` | 列数 (1–50) | | `tableRowHeights` | number[] | — | 各行高度 (mm),长度须等于 `tableRows` | | `tableColWidths` | number[] | — | 各列宽度 (mm),长度须等于 `tableCols` | | `tableBorderWidth` | number | `0.2` | 单元格网格线宽 (mm);`0` 时不绘制网格线 | | `tableBorderColor` | string | `#374151` | 网格线颜色 | | `tableCells` | Record | — | 单元格数据,键为 `"行,列"`(**从 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`。