This commit is contained in:
iqudoo
2026-06-13 13:01:12 +08:00
parent 8b09bcaa14
commit 318569c757
3 changed files with 445 additions and 11 deletions

447
README.md
View File

@@ -1,3 +1,446 @@
# Label Designer
# label-designer 标签批量设计与排版工具:可视化编辑标签模板、导入 Excel 数据、预览整页排版并导出 PDF。
> 标签批量设置排版并生成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 列映射或默认值填充。
| 占位符 | 说明 |
|--------|------|
| `{变量名}` | 普通变量,如 `{货位编号}``{物料名称}` |
| `{#INDEX}` | 内置占位符:当前数据行在导入数据中的 **1-based 序号**(第 1 行 → `1` |
变量名规则:
- 写在花括号内,不含 `}`
- `#INDEX` reserved不要作为自定义变量名
- 可在 `variableList` 中预先声明;也会从 `content` 与显示条件中自动汇总
### 导出时的变量映射(非模板字段)
导出 Excel 时,每个变量需关联数据源,映射关系保存在浏览器本地,**不在模板 JSON 内**
| 映射值 | 含义 |
|--------|------|
| Excel 列名 | 取该列单元格值 |
| `__use_default__` | 使用模板 `variableDefaults` 中的默认值 |
| `__use_row_index__` | 使用数据序号(与 `{#INDEX}` 相同1-based |
列名与变量名相同时会自动匹配。
---
## 元素 `TemplateElement`
每个元素是标签上的一个图层,按数组顺序从下到上绘制。
### 通用字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | string | 元素唯一 ID |
| `type` | `'text' \| 'barcode' \| 'qrcode' \| 'image'` | 元素类型 |
| `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` |
---
## 纸张排版 `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` 中的 `{变量}` 替换为数据行值(文本元素可对每个变量做数值格式化)
2. 对完整字符串做 **值提取**trim → 分隔符 / 前 N / 后 N
3. 文本元素追加 **显示前后缀**`textDisplayPrefix` / `textDisplaySuffix`
4. 根据 **显示控制** 决定是否绘制该元素
---
## 导入 / 导出模板
| 操作 | 说明 |
|------|------|
| 应用内导出 | 生成 `.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)。
模板规范化逻辑见 [`src/utils.ts`](src/utils.ts) 中的 `normalizeTemplate`

View File

@@ -442,7 +442,6 @@ export default function App() {
variableDefaults: parsed.variableDefaults, variableDefaults: parsed.variableDefaults,
paper: parsed.paper, paper: parsed.paper,
cutLine: parsed.cutLine, cutLine: parsed.cutLine,
showPdfCutLines: parsed.showPdfCutLines,
}); });
const list = [...templates, newTmpl]; const list = [...templates, newTmpl];
saveTemplatesToStorage(list); saveTemplatesToStorage(list);

View File

@@ -179,14 +179,6 @@ export interface LabelTemplate {
paper?: PaperConfig; paper?: PaperConfig;
/** PDF 裁切参考线配置 */ /** PDF 裁切参考线配置 */
cutLine?: CutLineConfig; cutLine?: CutLineConfig;
/** @deprecated 由 cutLine.enabled 替代 */
showPdfCutLines?: boolean;
/** @deprecated 数据范围仅存在于导出会话,不再写入模板 */
exportRange?: ExportRangeConfig;
/** @deprecated 仅用于迁移旧数据 */
dataSource?: ImportData;
/** @deprecated 模板与数据源已解耦,不再写入 */
dataSourceMeta?: DataSourceMeta;
} }
export type PaperType = 'A4' | 'A5' | 'A6' | 'custom'; export type PaperType = 'A4' | 'A5' | 'A6' | 'custom';