d67295234e
价签版式原 100% 硬编码在四条渲染路径。引入 label_template.dart 声明式模型(纸张/打印/抬头/文字栈/QR/条码),builtinDefault() 逐值冻结 重构前常量;画布渲染器 _renderLabelBitmap 与热敏 _printFlatLabelThermal 改为纯读模型,坐标统一到画布权威。facade/web stub 加 template 可选参 (null→默认,现有调用点行为不变)。 新增 label_render_golden 零回归基准:默认模板渲染与重构前逐像素零 diff。 附评审通过的设计文档 label-template-editor.html(5 项决策)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bupi8Kdqkfx2N5acFsHTx5
206 lines
20 KiB
HTML
206 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>标签模板编辑器 设计方案 — 酒库管理系统</title>
|
||
<style>
|
||
:root{
|
||
--primary:#2563AC; --primary-dark:#154072; --danger:#D14343; --danger-bg:#FDECEC;
|
||
--success:#2E8B57; --warn:#B45309; --accent:#8B2331;
|
||
--ink:#232934; --muted:#6E7888; --border:#DCE2EB; --paper:#F5F7FA; --head:#F0F4FF;
|
||
}
|
||
*{box-sizing:border-box;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;}
|
||
body{margin:0;background:var(--paper);color:var(--ink);padding:28px;line-height:1.65;}
|
||
h1{font-size:20px;margin:0 0 4px;}
|
||
h2{font-size:16px;margin:26px 0 8px;color:var(--primary-dark);border-left:4px solid var(--primary);padding-left:10px;}
|
||
h3{font-size:14px;margin:18px 0 6px;color:var(--accent);}
|
||
.sub{color:var(--muted);font-size:13px;margin-bottom:18px;}
|
||
.card{background:#fff;border:1px solid var(--border);border-radius:10px;padding:16px 20px;max-width:1000px;margin-bottom:16px;}
|
||
p{font-size:14px;margin:6px 0;}
|
||
code{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;background:#EEF2F8;padding:1px 5px;border-radius:4px;color:var(--primary-dark);}
|
||
ol,ul{font-size:14px;margin:6px 0;padding-left:22px;}
|
||
li{margin:5px 0;}
|
||
table{width:100%;border-collapse:collapse;font-size:13px;max-width:1000px;margin:8px 0;}
|
||
th{background:var(--head);color:var(--primary-dark);font-weight:600;font-size:12px;text-align:left;padding:9px 10px;border:1px solid var(--border);}
|
||
td{padding:9px 10px;border:1px solid #EEF1F5;vertical-align:top;}
|
||
.tag{font-size:11px;padding:2px 8px;border-radius:10px;display:inline-block;}
|
||
.tag.ok{background:#E6F3EC;color:var(--success);}
|
||
.tag.warn{background:#FFF4E5;color:var(--warn);}
|
||
.tag.no{background:var(--danger-bg);color:var(--danger);}
|
||
.tag.rec{background:#E7F0FB;color:var(--primary-dark);}
|
||
.lead{font-size:14px;background:#F0F6FF;border-left:3px solid var(--primary);padding:10px 14px;border-radius:4px;max-width:1000px;}
|
||
.flow{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;background:#1d2430;color:#e6edf6;padding:14px 18px;border-radius:8px;max-width:1000px;overflow:auto;line-height:1.7;}
|
||
.decision{background:#FFF9EC;border:1px solid #F0DDB0;border-radius:10px;padding:14px 18px;max-width:1000px;margin-bottom:16px;}
|
||
.decision h3{color:var(--warn);margin-top:0;}
|
||
.rec-box{background:#EEF6F0;border:1px solid #BFE0CB;border-radius:8px;padding:8px 12px;margin:8px 0;font-size:13px;}
|
||
.rec-box b{color:var(--success);}
|
||
.fix{background:#FDECEC;border:1px solid #F3C6C6;border-radius:8px;padding:8px 12px;margin:8px 0;font-size:13px;}
|
||
.fix b{color:var(--danger);}
|
||
.wire{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#fbfcfe;border:1px solid var(--border);border-radius:8px;padding:14px 16px;white-space:pre;overflow:auto;max-width:1000px;color:#333;line-height:1.5;}
|
||
.fl{color:var(--muted);font-size:11.5px;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>标签模板编辑器 · 设计方案(代码实证版 v2)</h1>
|
||
<div class="sub">给「设置 → 打印模板 · 商品价签」的「编辑」按钮做可视化模板编辑器 · 2026-08-28 · 本版所有结论已逐行核对源码(不采信二手调研)· 方案评审(未实现)</div>
|
||
|
||
<div class="lead">
|
||
目标不变:店家自定义价签版式(选字段 / 调字号字体位置 / 实时预览 / 多样式命名 / 打印参数进高级)。
|
||
<b>本版把上一版两处关键判断按源码订正了</b>:① 二维码走的是<b>位图</b>不是原生指令;
|
||
② PDF / Web 两条路径用的是 <b>flexbox 流式布局、根本没有绝对坐标</b>——这直接改变了「统一渲染」的做法。
|
||
结论:真正要数据驱动的只有<b>画布(预览) + 热敏(实打)</b>这一条主线,PDF/Web 用「贴位图」收口即可,
|
||
<b>比上一版设想的工作量更小、风险更低</b>。
|
||
</div>
|
||
|
||
<h2>1. 现状(逐行核对源码)</h2>
|
||
<div class="card">
|
||
<p>标签有<b>四条输出路径</b>,各自布局机制如下(已核对,非调研二手):</p>
|
||
<table>
|
||
<tr><th>路径</th><th>位置</th><th>何时走</th><th>布局机制</th><th>条码 / 二维码</th></tr>
|
||
<tr><td><b>画布</b></td><td><code>print_util_stub.dart:246</code> <code>_renderLabelBitmap</code></td><td>预览弹窗 + 热敏文字光栅(同一函数,<code>drawQr/drawBarcode=false</code> 供热敏)</td><td><b>绝对坐标</b>(逻辑 320×160,4× 超采样)</td><td>条码 <code>_drawBarcode</code> 画黑条(:315);QR <code>drawImageRect</code>(:295)</td></tr>
|
||
<tr><td><b>热敏 TSPL</b></td><td>同文件 <code>_printFlatLabelThermal:351</code></td><td>检测到 DL-888B 等热敏机(真实主力)</td><td>文字=画布光栅 <code>BITMAP</code>(:394);<b>绝对坐标</b></td><td>条码=原生 <code>BARCODE</code>(:451);QR=<b>位图</b> <code>BITMAP</code>(:427)</td></tr>
|
||
<tr><td>PDF 回退</td><td>同文件 <code>printProductLabelImpl:463</code></td><td><b>只在检测不到热敏机时</b>(:491-499)</td><td><b>flexbox 流式</b>(<code>pw.Column/Row/Expanded</code>),<b>无绝对坐标</b></td><td>条码 <code>pw.BarcodeWidget</code> 矢量(:578);QR <code>pw.Image</code>(:593)</td></tr>
|
||
<tr><td>Web HTML</td><td><code>print_util_web.dart:21</code></td><td>Web 构建(<code>window.print()</code>)</td><td><b>CSS flexbox 流式</b>(:82-124),<b>无绝对坐标</b></td><td>条码 <code>toSvg</code> 矢量(:64);QR base64 img(:143)</td></tr>
|
||
</table>
|
||
|
||
<div class="fix"><b>订正 ①(我上一版说错的)</b>:热敏路径里二维码是 <code>BITMAP $qrX,$qrY,...</code>(第 427 行)——<b>位图,不是原生 QRCODE 指令</b>。它「相对清晰」的原因是<b>独立渲染</b>:把 QR PNG 按目标尺寸 130×130 单独解码 + 1:1 逐像素 128 阈值二值化(:422-436),<b>不经过文字那条 4×4 超采样降采样链路</b>。只有<b>条码</b>是真·原生 TSPL <code>BARCODE</code> 指令(:451)。</div>
|
||
|
||
<div class="fix"><b>订正 ②(影响架构)</b>:PDF 和 Web <b>不是绝对坐标布局,是 flexbox 流式</b>(PDF 用 <code>pw.Column/Row/Expanded</code>,Web 用 CSS flex)。这意味着——如果编辑器让用户「把字段拖到某个 x/y」,这套坐标<b>天然只对得上画布/热敏,对不上 PDF/Web 的 flex 流</b>。要让 PDF/Web 认坐标,就得把它们的 flex 布局整个换掉。这正是下一节改方案的原因。</div>
|
||
|
||
<h3>1.1 画布布局其实是「自适应」的,不是死坐标</h3>
|
||
<p>关键细节(<code>_renderLabelBitmap:319-340</code>):品名字号 <code>_fitFont</code> 按宽度<b>自适应</b> clamp(12,18);文字块(品名 + 两行副字段)在剩余竖直空间里<b>自动均分留白居中</b>;副字段 <code>_labelSubFields:222</code> 把「编号+系列」拼一行、「规格+日期」拼一行,<b>空字段自动跳过、整行塌缩</b>。也就是说当前版式<b>不是一组固定像素位置</b>,而是「几个区域 + 自动排版」。这点直接决定版式模型该怎么建(见 §3)。</p>
|
||
|
||
<h3>1.2 实际渲染 / 未渲染的字段</h3>
|
||
<p>参与渲染:<code>shopName / name / code / series / spec / productionDate / qr / barcode</code>。<b>已在 <code>LabelData</code> 但从不渲染</b>:<code>batchNo / remark / shopAddress / shopPhone</code>(<code>label_data.dart:15-20</code>) → 编辑器可直接把它们做成可选字段,<b>数据层零改动</b>。</p>
|
||
|
||
<h3>1.3 入口与存储(已核对,利好)</h3>
|
||
<ul>
|
||
<li><b>「编辑」是空实现</b>:<code>device_management_screen.dart:991</code> → <code>_snack('模板编辑即将上线')</code>;「预览」<code>_previewLabelTemplate:1002</code> 造示例 <code>LabelData</code> 弹 <code>LabelPreviewDialog</code>。<b>打印模板卡仅桌面渲染</b> → 编辑器只做桌面。</li>
|
||
<li><b>打印入口是扁平参数</b>:<code>print_util.dart:15</code> <code>printProductLabel(...)</code> 和 <code>:45</code> <code>renderLabelPreview(label)</code> 都不带模板概念 → 要加一个 <code>template</code> 参数贯穿下去(改动集中在 facade + 两个 Impl)。</li>
|
||
<li><b>后端 custom_fields 已支持增量 merge</b>:<code>shop.go:68-79</code> 读现有 custom_fields → 合并新键 → 保留旧键、店隔离,还有钉死契约的测试 <code>shop_test.go:31</code>。前端 <code>_savePeripherals:1031</code> 就是这么存外设的。<b>→ 模板存 custom_fields 零后端改动、且不会冲掉外设等已有键。</b></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>2. 渲染架构(按源码修订:画布为唯一权威 + PDF/Web 贴位图)</h2>
|
||
<div class="card">
|
||
<p>上一版我提「抽一层绘制指令 IR 给四路径各写 adapter」。读完代码后<b>这是过度设计</b>——因为 PDF/Web 是流式布局、且只是回退/次要面。更省的做法:</p>
|
||
<div class="rec-box"><b>让「画布」成为唯一的几何权威。</b> 模板 = 画布上的绝对布局;画布同时驱动<b>预览</b>和<b>热敏文字光栅</b>(热敏文字本就是画布栅格化的)。PDF/Web 直接<b>嵌入画布渲染出的位图</b>,不再各自摆版。四套手工对齐的布局 → 收敛成<b>一套画布渲染器 + 两个「贴位图」消费者 + 热敏既有的条码/QR 特判</b>。</div>
|
||
<table>
|
||
<tr><th>面</th><th>改法</th><th>条码/QR 清晰度</th></tr>
|
||
<tr><td>预览</td><td>画布 → PNG(现状即是)</td><td>屏幕显示,无所谓</td></tr>
|
||
<tr><td><b>热敏(主力)</b></td><td>文字=画布光栅(现状);条码=原生 <code>BARCODE</code>、QR=独立 <code>BITMAP</code>(<b>都保留</b>);<b>唯一改动</b>:它们的坐标从写死的 <code>const</code> 改成<b>由模板算出</b></td><td><b>不降级</b>:条码原生矢量、QR 独立 1:1 阈值,扫码可靠性照旧</td></tr>
|
||
<tr><td>PDF 回退</td><td><b>整个 <code>pw.*</code> 组件树换成单张 <code>pw.Image(画布位图)</code></b> 满页铺</td><td>回退面向办公激打/喷墨(≥300dpi),画布 1280px≈812dpi 栅格化,条码可扫</td></tr>
|
||
<tr><td>Web</td><td><b>HTML flex 换成 <code><img></code> 画布 PNG</b>(40×20mm)</td><td>同上,Web 打印走高 dpi,无虞</td></tr>
|
||
</table>
|
||
<p class="fl">注:条码/QR 光栅化「扫不出」的风险<b>只发生在热敏 203dpi</b>——所以热敏那条<b>坚决保留</b>原生条码 + 独立 QR,不动。PDF/Web 是高 dpi 面,贴位图完全够用。</p>
|
||
<div class="rec-box">附带红利:因为 PDF/Web 只是回退/次要面,<b>它们的模板落地可以延后</b>——先做画布+热敏(覆盖真实打印 + 预览),PDF/Web 后面「贴位图」一句话的事。<b>比上一版少写两个 adapter。</b></div>
|
||
</div>
|
||
|
||
<h2>3. 版式模型:区域 + 自动栈(贴合现有自适应布局)</h2>
|
||
<div class="card">
|
||
<p>因为 §1.1 里现状是「区域 + 自动排版」而非死坐标,模型<b>不宜做成纯自由画布</b>(否则丢掉品名自适应、竖直自动居中这些现有优点,且短名不再放大、隐藏字段不再回填空间 → 反而变差)。建议<b>区域(region) + 元素(element)</b> 混合模型:</p>
|
||
<div class="wire">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</div>
|
||
<ul>
|
||
<li><b>字段开关</b>:勾选控制 field 是否进 textStack / 是否显示 QR/条码。</li>
|
||
<li><b>调样式</b>:字号(或 auto 自适应)、粗体、对齐、颜色。</li>
|
||
<li><b>调位置</b>:QR / 条码 / 抬头 = <b>可移动可缩放的区域</b>(覆盖「调位置」诉求最实用的部分);textStack 内字段 = <b>调顺序 + 可选手动微调</b>,默认仍走现有自动均分。</li>
|
||
<li><b>零回归</b>:内置「默认模板」= 把现有 <code>const</code>(headerH=24 / qrSize=134 / barTop=114 …)原样编码;渲染器保留自动栈算法。用字体探针 / golden 位图对照守逐像素一致。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>4. 存储:shop.custom_fields(已被源码证实可行、零后端改动)</h2>
|
||
<div class="card">
|
||
<p>模板存 <code>shop.custom_fields.label_templates</code>(数组)+ <code>label_template_active</code>(默认 id)。依据:</p>
|
||
<ul>
|
||
<li>后端 <code>shop.go:68-79</code> 对 custom_fields 做<b>增量 merge</b> 并有测试钉死 → 写 <code>label_templates</code> 不碰 <code>peripherals</code> 等旧键,店隔离。<b>无需新表/迁移/接口。</b></li>
|
||
<li>前端照抄 <code>_savePeripherals:1031</code>:读 <code>shopInfoProvider</code> → 改 <code>customFields</code> map → <code>shopRepositoryProvider.updateInfo({...})</code>。</li>
|
||
<li>店级 → 跨设备/员工同步;打印机选择继续留本地 <code>label_printer</code>(<code>label_preview_dialog.dart:11</code>) 不动。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>5. 编辑器交互(桌面专属)</h2>
|
||
<div class="card">
|
||
<p>价签卡「编辑」→ 大对话框。左预览右属性,改动 debounce ~150ms 调 <code>renderLabelPreview(sample, template)</code> 重渲染(预览路径现成)。</p>
|
||
<div class="wire">┌─ 标签模板编辑器 ──────────────────────────────────────────┐
|
||
│ 模板:[商品价签(默认)▾] [新建][复制][重命名][删除][设默认] │
|
||
├──────────────────────────┬──────────────────────────────┤
|
||
│ 实时预览(真实渲染位图) │ 字段 │
|
||
│ ┌──────────────────┐ │ ☑店名 ☑品名 ☑编号 ☑系列 │
|
||
│ │ 岩美酒行旗舰店 │ │ ☑规格 ☑生产日期 ☐批次 ☐备注 │
|
||
│ │ 茅台飞天五十三度 │▨ │ ☑二维码 ☑底部条码 │
|
||
│ │ P1001 飞天 │▨ │ ──────────────────────────── │
|
||
│ │ 53度 2024-06-01 │ │ 选中「品名」: │
|
||
│ │ ▐▌▐ ▌▐▌▌▐ ▌▐▌▐ │ │ 字号[自适应▾] □粗体 │
|
||
│ └──────────────────┘ │ 对齐[居中▾] 颜色[■] │
|
||
│ [示例▾][用真实商品▾] │ 选中「二维码」: X[178]Y[25] │
|
||
│ │ 尺寸[134] │
|
||
│ │ ──────────────────────────── │
|
||
│ │ ▸ 高级(纸张/DPI/密度/速度) │
|
||
├──────────────────────────┴──────────────────────────────┤
|
||
│ [取消] [保存] │
|
||
└──────────────────────────────────────────────────────────┘</div>
|
||
</div>
|
||
|
||
<h2>6. 两个需你定调的点(含源码约束)</h2>
|
||
<div class="decision">
|
||
<h3>决策 A:字段自由度 —— 纯自由画布会丢现有自适应</h3>
|
||
<p>你要「可调位置」,但源码现状是<b>自适应排版</b>(品名按宽度放大、文字块自动竖直居中、空字段塌缩)。三档:</p>
|
||
<ul>
|
||
<li>① 槽位式:只开关+调样式不能移位。稳,但不满足「调位置」。</li>
|
||
<li><b>② 区域+自动栈</b> <span class="tag rec">推荐</span>:QR/条码/抬头做成<b>可移动缩放的区域</b>(满足「调位置」最实用部分);文字块内保留自动均分。<b>既能调位置又不丢现有优点。</b></li>
|
||
<li>③ 纯自由画布:每字段任意 x/y。最强,但<b>丢掉品名自适应/自动居中</b>,40×20mm 上还易重叠溢出。</li>
|
||
</ul>
|
||
<p>推荐 <b>②</b>。若你更想要 ③ 的「每个字段都能拖」,我按纯坐标做,但要接受品名不再自动放大、隐藏字段不再自动回填空间。</p>
|
||
</div>
|
||
|
||
<div class="decision">
|
||
<h3>决策 B:字体与颜色 —— 热敏是单色,有物理约束</h3>
|
||
<ul>
|
||
<li><b>颜色</b>:热敏机是<b>单色介质</b>,文字二值化后全是黑(抬头深蓝也退成黑)。所以「字段颜色」<b>只对预览/PDF/Web 有视觉意义,对热敏实打无效</b>。要不要暴露颜色,取决于你在不在乎屏幕/PDF 上的彩色观感。</li>
|
||
<li><b>字体</b>:<code>_drawText:166</code> 写死 <code>fontFamily:'NotoSansSC'</code>,且热敏要求打包字体。支持「换字体」= 再打包字体(如宋体,<b>每款中文约 4–8MB</b> 增包体)+ 参数化 fontFamily。<b>MVP 建议只做字号/粗体</b>(零增包),多字体二期按需。</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<h2>7. 落地路线(design-first,主线先行)</h2>
|
||
<div class="card">
|
||
<table>
|
||
<tr><th>期</th><th>内容</th><th>验收</th></tr>
|
||
<tr><td>P0 建模</td><td>定义 <code>LabelTemplate</code>(区域+自动栈)+ 画布渲染器读模型;内置默认模板=现 const;facade/Impl 加 <code>template</code> 参数</td><td>字体探针 / golden:默认模板逐像素零回归</td></tr>
|
||
<tr><td>P1 原型</td><td><code>design/prototype/</code> 新增编辑器桌面原型</td><td>5180 serve URL 评审,过了再写代码</td></tr>
|
||
<tr><td>P2 编辑器</td><td>编辑器 UI + custom_fields 存储 + 预览联动 + 多模板/重命名;<b>热敏</b>条码/QR 坐标改模板驱动</td><td>桌面建/存/切模板,预览实时刷新,热敏按模板出签</td></tr>
|
||
<tr><td>P3 收口(可延后)</td><td>PDF/Web 改「贴画布位图」→ 自定义模板四面一致</td><td>同模板热敏/PDF/Web 观感一致</td></tr>
|
||
</table>
|
||
<p class="tag warn" style="display:block;max-width:980px;padding:8px 12px;">按铁律:P1 原型过审前不写实现代码;前端改动原型与代码同提交。本方案即 P1 前评审。</p>
|
||
</div>
|
||
|
||
<h2>8. 待你拍板</h2>
|
||
<div class="card">
|
||
<table>
|
||
<tr><th>#</th><th>决策</th><th>推荐</th></tr>
|
||
<tr><td>1</td><td>模板存哪</td><td><b>shop.custom_fields</b>(源码证实零后端改动、店级同步)</td></tr>
|
||
<tr><td>2</td><td>渲染统一</td><td><b>画布为唯一权威 + PDF/Web 贴位图</b>;热敏保留原生条码/独立 QR,仅坐标受模板驱动</td></tr>
|
||
<tr><td>3</td><td>字段自由度</td><td><b>② 区域+自动栈</b>(可调位置又不丢自适应)</td></tr>
|
||
<tr><td>4</td><td>字体</td><td>MVP <b>只字号/粗体</b>,多字体二期(增包体)</td></tr>
|
||
<tr><td>5</td><td>颜色</td><td>热敏无效 → 建议 MVP <b>不暴露颜色</b>(或仅作预览观感),你定</td></tr>
|
||
</table>
|
||
<p>给个方向(认同/改哪条),我就做 P0 建模并出 P1 编辑器原型。</p>
|
||
</div>
|
||
|
||
<div class="sub" style="margin-top:24px;">核对源码:<code>print_util_stub.dart</code>(:158-617) / <code>print_util_web.dart</code> / <code>label_data.dart</code> / <code>print_util.dart</code> / <code>label_preview_dialog.dart</code> / <code>device_management_screen.dart</code>(:930-1035) / 后端 <code>shop.go</code>(:51-79)+<code>shop_test.go</code> · 本方案未改任何代码。</div>
|
||
</body>
|
||
</html>
|