标签模板编辑器 · 设计方案(代码实证版 v2)

给「设置 → 打印模板 · 商品价签」的「编辑」按钮做可视化模板编辑器 · 2026-08-28 · 本版所有结论已逐行核对源码(不采信二手调研)· 方案评审(未实现)
目标不变:店家自定义价签版式(选字段 / 调字号字体位置 / 实时预览 / 多样式命名 / 打印参数进高级)。 本版把上一版两处关键判断按源码订正了:① 二维码走的是位图不是原生指令; ② PDF / Web 两条路径用的是 flexbox 流式布局、根本没有绝对坐标——这直接改变了「统一渲染」的做法。 结论:真正要数据驱动的只有画布(预览) + 热敏(实打)这一条主线,PDF/Web 用「贴位图」收口即可, 比上一版设想的工作量更小、风险更低

1. 现状(逐行核对源码)

标签有四条输出路径,各自布局机制如下(已核对,非调研二手):

路径位置何时走布局机制条码 / 二维码
画布print_util_stub.dart:246 _renderLabelBitmap预览弹窗 + 热敏文字光栅(同一函数,drawQr/drawBarcode=false 供热敏)绝对坐标(逻辑 320×160,4× 超采样)条码 _drawBarcode 画黑条(:315);QR drawImageRect(:295)
热敏 TSPL同文件 _printFlatLabelThermal:351检测到 DL-888B 等热敏机(真实主力)文字=画布光栅 BITMAP(:394);绝对坐标条码=原生 BARCODE(:451);QR=位图 BITMAP(:427)
PDF 回退同文件 printProductLabelImpl:463只在检测不到热敏机时(:491-499)flexbox 流式pw.Column/Row/Expanded),无绝对坐标条码 pw.BarcodeWidget 矢量(:578);QR pw.Image(:593)
Web HTMLprint_util_web.dart:21Web 构建(window.print()CSS flexbox 流式(:82-124),无绝对坐标条码 toSvg 矢量(:64);QR base64 img(:143)
订正 ①(我上一版说错的):热敏路径里二维码是 BITMAP $qrX,$qrY,...(第 427 行)——位图,不是原生 QRCODE 指令。它「相对清晰」的原因是独立渲染:把 QR PNG 按目标尺寸 130×130 单独解码 + 1:1 逐像素 128 阈值二值化(:422-436),不经过文字那条 4×4 超采样降采样链路。只有条码是真·原生 TSPL BARCODE 指令(:451)。
订正 ②(影响架构):PDF 和 Web 不是绝对坐标布局,是 flexbox 流式(PDF 用 pw.Column/Row/Expanded,Web 用 CSS flex)。这意味着——如果编辑器让用户「把字段拖到某个 x/y」,这套坐标天然只对得上画布/热敏,对不上 PDF/Web 的 flex 流。要让 PDF/Web 认坐标,就得把它们的 flex 布局整个换掉。这正是下一节改方案的原因。

1.1 画布布局其实是「自适应」的,不是死坐标

关键细节(_renderLabelBitmap:319-340):品名字号 _fitFont 按宽度自适应 clamp(12,18);文字块(品名 + 两行副字段)在剩余竖直空间里自动均分留白居中;副字段 _labelSubFields:222 把「编号+系列」拼一行、「规格+日期」拼一行,空字段自动跳过、整行塌缩。也就是说当前版式不是一组固定像素位置,而是「几个区域 + 自动排版」。这点直接决定版式模型该怎么建(见 §3)。

1.2 实际渲染 / 未渲染的字段

参与渲染:shopName / name / code / series / spec / productionDate / qr / barcode已在 LabelData 但从不渲染batchNo / remark / shopAddress / shopPhone(label_data.dart:15-20) → 编辑器可直接把它们做成可选字段,数据层零改动

1.3 入口与存储(已核对,利好)

2. 渲染架构(按源码修订:画布为唯一权威 + PDF/Web 贴位图)

上一版我提「抽一层绘制指令 IR 给四路径各写 adapter」。读完代码后这是过度设计——因为 PDF/Web 是流式布局、且只是回退/次要面。更省的做法:

让「画布」成为唯一的几何权威。 模板 = 画布上的绝对布局;画布同时驱动预览热敏文字光栅(热敏文字本就是画布栅格化的)。PDF/Web 直接嵌入画布渲染出的位图,不再各自摆版。四套手工对齐的布局 → 收敛成一套画布渲染器 + 两个「贴位图」消费者 + 热敏既有的条码/QR 特判
改法条码/QR 清晰度
预览画布 → PNG(现状即是)屏幕显示,无所谓
热敏(主力)文字=画布光栅(现状);条码=原生 BARCODE、QR=独立 BITMAP都保留);唯一改动:它们的坐标从写死的 const 改成由模板算出不降级:条码原生矢量、QR 独立 1:1 阈值,扫码可靠性照旧
PDF 回退整个 pw.* 组件树换成单张 pw.Image(画布位图) 满页铺回退面向办公激打/喷墨(≥300dpi),画布 1280px≈812dpi 栅格化,条码可扫
WebHTML flex 换成 <img> 画布 PNG(40×20mm)同上,Web 打印走高 dpi,无虞

注:条码/QR 光栅化「扫不出」的风险只发生在热敏 203dpi——所以热敏那条坚决保留原生条码 + 独立 QR,不动。PDF/Web 是高 dpi 面,贴位图完全够用。

附带红利:因为 PDF/Web 只是回退/次要面,它们的模板落地可以延后——先做画布+热敏(覆盖真实打印 + 预览),PDF/Web 后面「贴位图」一句话的事。比上一版少写两个 adapter。

3. 版式模型:区域 + 自动栈(贴合现有自适应布局)

因为 §1.1 里现状是「区域 + 自动排版」而非死坐标,模型不宜做成纯自由画布(否则丢掉品名自适应、竖直自动居中这些现有优点,且短名不再放大、隐藏字段不再回填空间 → 反而变差)。建议区域(region) + 元素(element) 混合模型:

LabelTemplate { id, name // 可命名 / 重命名 paper: { width_mm:40, height_mm:20, dpi:203 } print: { density:10, speed:2, gap_mm:2, direction:1, copies:1 } // 高级设置 regions: { header: { visible, height_mm, bg, fields:[ text... ] } // 深蓝抬头 textStack:{ x,y,w,h, align, fields:[ text... ] } // 自动竖直均分(保留现有算法) qr: { visible, x, y, size } // 右列二维码 barcode: { visible, x, y, w, h, symbology:code128, showText } // 左下条码 } } text field { binding, staticText?, fontSize(或 auto), bold, align, color } binding ∈ shopName|shopAddress|shopPhone|name|code|series|spec| batchNo|productionDate|remark|static

4. 存储:shop.custom_fields(已被源码证实可行、零后端改动)

模板存 shop.custom_fields.label_templates(数组)+ label_template_active(默认 id)。依据:

5. 编辑器交互(桌面专属)

价签卡「编辑」→ 大对话框。左预览右属性,改动 debounce ~150ms 调 renderLabelPreview(sample, template) 重渲染(预览路径现成)。

┌─ 标签模板编辑器 ──────────────────────────────────────────┐ │ 模板:[商品价签(默认)▾] [新建][复制][重命名][删除][设默认] │ ├──────────────────────────┬──────────────────────────────┤ │ 实时预览(真实渲染位图) │ 字段 │ │ ┌──────────────────┐ │ ☑店名 ☑品名 ☑编号 ☑系列 │ │ │ 岩美酒行旗舰店 │ │ ☑规格 ☑生产日期 ☐批次 ☐备注 │ │ │ 茅台飞天五十三度 │▨ │ ☑二维码 ☑底部条码 │ │ │ P1001 飞天 │▨ │ ──────────────────────────── │ │ │ 53度 2024-06-01 │ │ 选中「品名」: │ │ │ ▐▌▐ ▌▐▌▌▐ ▌▐▌▐ │ │ 字号[自适应▾] □粗体 │ │ └──────────────────┘ │ 对齐[居中▾] 颜色[■] │ │ [示例▾][用真实商品▾] │ 选中「二维码」: X[178]Y[25] │ │ │ 尺寸[134] │ │ │ ──────────────────────────── │ │ │ ▸ 高级(纸张/DPI/密度/速度) │ ├──────────────────────────┴──────────────────────────────┤ │ [取消] [保存] │ └──────────────────────────────────────────────────────────┘

6. 两个需你定调的点(含源码约束)

决策 A:字段自由度 —— 纯自由画布会丢现有自适应

你要「可调位置」,但源码现状是自适应排版(品名按宽度放大、文字块自动竖直居中、空字段塌缩)。三档:

推荐 。若你更想要 ③ 的「每个字段都能拖」,我按纯坐标做,但要接受品名不再自动放大、隐藏字段不再自动回填空间。

决策 B:字体与颜色 —— 热敏是单色,有物理约束

7. 落地路线(design-first,主线先行)

内容验收
P0 建模定义 LabelTemplate(区域+自动栈)+ 画布渲染器读模型;内置默认模板=现 const;facade/Impl 加 template 参数字体探针 / golden:默认模板逐像素零回归
P1 原型design/prototype/ 新增编辑器桌面原型5180 serve URL 评审,过了再写代码
P2 编辑器编辑器 UI + custom_fields 存储 + 预览联动 + 多模板/重命名;热敏条码/QR 坐标改模板驱动桌面建/存/切模板,预览实时刷新,热敏按模板出签
P3 收口(可延后)PDF/Web 改「贴画布位图」→ 自定义模板四面一致同模板热敏/PDF/Web 观感一致

按铁律:P1 原型过审前不写实现代码;前端改动原型与代码同提交。本方案即 P1 前评审。

8. 待你拍板

#决策推荐
1模板存哪shop.custom_fields(源码证实零后端改动、店级同步)
2渲染统一画布为唯一权威 + PDF/Web 贴位图;热敏保留原生条码/独立 QR,仅坐标受模板驱动
3字段自由度② 区域+自动栈(可调位置又不丢自适应)
4字体MVP 只字号/粗体,多字体二期(增包体)
5颜色热敏无效 → 建议 MVP 不暴露颜色(或仅作预览观感),你定

给个方向(认同/改哪条),我就做 P0 建模并出 P1 编辑器原型。

核对源码:print_util_stub.dart(:158-617) / print_util_web.dart / label_data.dart / print_util.dart / label_preview_dialog.dart / device_management_screen.dart(:930-1035) / 后端 shop.go(:51-79)+shop_test.go · 本方案未改任何代码。