feat(client): 标签模板编辑器 P2 · 编辑器屏+存储+打印联动(原型同提交)
设备管理「价签·编辑」进入独立编辑器屏 /devices/label-template: - 左实时预览走真实 renderLabelPreview 渲染路径(示例数据, debounce 重渲) - 右属性面板:多模板管理(编辑对象/打印默认分离)、显示字段手风琴、 高级区域位置、打印参数 - 存 shop.custom_fields.label_templates + label_template_active(零后端改) - Option A:同物理行两字段(编号+型号 / 版本+日期)共用字号·加粗·对齐, 各自独立开关;仅品名支持自动字号;字体不做(热敏单色) - 打印联动:LabelPreviewDialog 改 ConsumerStatefulWidget, 未显式传模板时 自动取本店 active 模板→所有打印入口(商品详情/出入库列表/编辑抽屉)一致生效 守护: - 默认输出逐像素零变化由 label_render_golden_test 守(改内置默认常量才触发) - 存储读写由 label_template_storage_test 单测守(纯内存, 不触真实库) - 编辑器挂载由 label_template_editor_smoke_test 守 - 本屏不入自动 fidelity/golden 闸(左预览 Picture.toImage 需 runAsync, pumpAndSettle 骨架驱动不了), 原因+替代守法记入 CONTRACT 块4 design-first:原型 label-template-editor.html 去字体选择器、加 Option A 配对 镜像, 与真实屏同提交。真机热敏打印坐标/条码可扫须用户在打印机验收。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bupi8Kdqkfx2N5acFsHTx5
This commit is contained in:
@@ -107,6 +107,7 @@ AppTokens 目前**仅颜色**。原型还驱动:
|
||||
| 出库列表(桌/移) | `stock-out-list.html` | `stock_out_list_screen.dart` | 同步 | ✅ 副标 + 共享 StatusPill;**2026-07-14 六项优化**:详情抽屉明细同增生产日期+批次号两列(抽屉 660)、状态筛选改多选 chip 且默认「草稿+待审核」、新建单默认填充默认仓库(原型 stock-out-list.html 同步);fidelity a 3.228% / b 2.217% / c 3.538%(均 ≤ 阈值 6%),移动 golden 自比全绿 | ✅ golden | ✅ golden |
|
||||
| 财务(桌/移) | `finance.html` | `finance_screen.dart` | 同步 | ✅ **Phase2 重建**:时间范围 chips(真实过滤)+KPI 4 卡(stock summary 环比)+收支趋势柱状图(DsBarChart, /finance/trend)+应收应付汇总(/finance/summary)+流水表+往来抽屉+登记收支(补往来单位);**2026-07-14 六项优化**:流水表筛选新增「往来单位」下拉、时间范围统一改用通用自定义时间组件(原型 finance.html/m-finance.html 同步);fidelity a 4.036% / b 3.131% / c 4.340%(均 ≤ 阈值 6%),移动 golden 自比全绿;**2026-07-14 追加**:应收/应付汇总卡头部新增「往来单位」搜索——本地即时过滤(`/finance/summary` 全量非分页,搜索口径=显示口径),桌面 `DsSearchBox`(`_secHead` trailing)/移动 应收·应付 分段头 `MSearchRow`(流水分段沿用既有 chip→sheet 单选,不重复),原型 finance.html `.sum-search` / m-finance.html 应收应付分段 `.m-search` 同步,finance 相关 6 个整屏 golden + m_finance 3 个 golden 已按新布局 `--update-goldens`;**2026-07-14 验收反馈调整**:桌面「往来单位」搜索由卡标题行 `DsSearchBox` 挪进汇总表表头「往来单位」列头右侧,改为下拉单选 `ComboSearchField`(同 `_flowTable` 往来单位筛选控件,选中精确匹配一行/× 清除恢复全量,候选=当前汇总行往来单位);原型 `.sec-head .sum-search` 移除,改 `.th-party-wrap` 内嵌 `.combo` + `openSearchMenu` 弹层;移动端未动。desktop `finance` golden 3 主题已 `--update-goldens`(3.11% 差异,目检确认新版式),`finance_mobile`/`m_finance` 无变化、原样通过;**2026-07-14 验收反馈2**(实机截图 3 处细节):①下拉候选由「仅当前汇总行往来单位」改为「全量往来单位」(`allPartnersProvider`,与 `_flowTable` toolbar 同源),选中一个汇总没有余额行的单位时走既有「没有匹配的往来单位」空态;②占位符叠词修复——`ComboSearchField.hint` 由 `'搜索往来单位'` 改 `'往来单位'`(内部会自动拼「搜索」前缀),弹层内搜索框恢复正常显示「搜索往来单位…」,收起态触发器文案对齐 `_flowTable` 写法同为「往来单位」;③表头行下拉垂直位置——`header` 外包 `SizedBox(height: 34)` 显式钉住 Row 高度消除潜在字体行高偏差,不改表头行 pad 11/14 既有行高口径。原型 finance.html 同步:`ALL_PARTNERS`(`SUMMARY` 全部单位 + 1 个无余额行的「鼎丰超市」演示空态)替换 `sumPartyId`/`SUMMARY` 候选、`#sumPartyInput` 收起态 placeholder 改「往来单位」;`.th-party-wrap{align-items:center}` + 浏览器 `th` 默认 `vertical-align:middle` 本就居中,未改 CSS。desktop `finance` golden 3 主题已 `--update-goldens`(0.04% 差异,目检确认新候选/占位文案,表头对齐无回归),`finance_mobile`/`m_finance` 无变化、原样通过 | ✅ fidelity | ✅ fidelity |
|
||||
| 设备管理(桌/移) | `devices.html` | `device_management_screen.dart` | 同步 | ✅ **Phase2 重建**:会话表(DsTable)+外设卡网格(custom_fields.peripherals 本地存档)+打印模板;fidelity 2.4–3.0%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 标签模板编辑器(桌/移) | `label-template-editor.html` | `label_template_editor_screen.dart`(`/devices/label-template`,设备管理「价签·编辑」进入) | 同步 | ✅ **2026-08 P2 落地**:左实时预览(走真实 `renderLabelPreview` 渲染路径,示例数据)+ 右属性面板(模板管理[编辑对象/打印默认分离]·显示字段手风琴·高级区域位置·打印参数);存 `shop.custom_fields.label_templates`+`label_template_active`(零后端改)。**Option A**:同物理行两字段(编号+型号 / 版本+日期)共用字号·加粗·对齐,各自独立开关;仅品名支持自动字号;字体不做(热敏单色)。默认输出逐像素零变化由 `label_render_golden_test.dart` 守(改内置默认常量才触发)。⚠️ **本屏不入自动 fidelity/golden 闸**:左预览用 `Picture.toImage` 直画光栅化(同 label_render,需 `runAsync`),`pumpAndSettle` 的 widget-golden 骨架无法驱动→整屏 golden 会把预览拍成占位。原型=代码一致由 serve 目检 + 本行保证;存储读写由 `label_template_storage_test.dart` 单测守;真机打印坐标/条码可扫须用户在热敏机验收 | ⚠️ 见左(不入闸) | ⚠️ 见左(不入闸) |
|
||||
| 系统设置(桌/移) | `settings.html` | `settings_screen.dart` | 同步 | ✅ **Phase2 重建**;假「系统参数」已删;fidelity 1.7–2.3%≤8%;**2026-07-10 授权管理面板迁出**(提升为独立一级屏 `license.html`),subnav 剩 门店信息/用户管理/偏好——**真实端已同步完成**(`settings_screen.dart` 子导航同步摘除授权管理项,golden 已按新态重生成,见授权管理两行) | ✅ fidelity | ✅ fidelity |
|
||||
| 用户管理(桌/移) | `users.html` | `users_screen.dart`(`/settings/users`) | 同步 | ✅ **Phase2 新独立页**:KPI 4 卡+搜索/角色筛选+rcard 弹窗,角色四级拉平;fidelity 1.3–2.1%≤8% | ✅ fidelity | ✅ fidelity |
|
||||
| 关于我们(桌/移) | `about.html` | `about_screen.dart` | 同步 | ✅ **Phase2 重建**:Hero/产品信息/更新日志 timeline(/public/release 同源);fidelity 2.2–2.7%≤8%;授权信息卡 2026-07-04 迁至 设置→授权管理 | ✅ fidelity | ✅ fidelity |
|
||||
|
||||
@@ -245,25 +245,22 @@
|
||||
/* ── 字段清单:样式内嵌每行,手风琴展开 ── */
|
||||
/* 1 期字体:仅系统字体 + 应用已内置思源黑体,不打包新字体(宋体已撤)。
|
||||
楷体/等宽蹭系统,缺失回退默认。 */
|
||||
const FONTS = [
|
||||
['系统默认', ''],
|
||||
['思源黑体(内置)', 'sans-serif'],
|
||||
['楷体', 'KaiTi, STKaiti, serif'],
|
||||
['等宽数字', 'ui-monospace, monospace'],
|
||||
];
|
||||
// 字体不做(热敏单色 MVP);仅「品名」支持自动字号。同物理行两字段共用字号/加粗/对齐(PAIR)。
|
||||
const ALIGN_LABEL = {left:'左', center:'居中', right:'右'};
|
||||
const FIELDS = [
|
||||
{f:'name', label:'品名', type:'text', on:true, auto:true, bold:true, align:'center', font:0, size:18},
|
||||
{f:'shop', label:'店名(抬头)', type:'text', on:true, auto:false, bold:true, align:'center', font:0, size:14},
|
||||
{f:'code', label:'编号', type:'text', on:true, auto:false, bold:false, align:'center', font:0, size:14},
|
||||
{f:'series', label:'型号 / 度数', type:'text', on:true, auto:false, bold:false, align:'center', font:0, size:14},
|
||||
{f:'spec', label:'版本 / 规格', type:'text', on:true, auto:false, bold:false, align:'center', font:0, size:14},
|
||||
{f:'date', label:'生产日期', type:'text', on:true, auto:false, bold:false, align:'center', font:0, size:14},
|
||||
{f:'batch', label:'批次号', type:'text', on:false, auto:false, bold:false, align:'center', font:0, size:14},
|
||||
{f:'remark', label:'备注', type:'text', on:false, auto:false, bold:false, align:'center', font:0, size:14},
|
||||
{f:'name', label:'品名', type:'text', on:true, auto:true, bold:true, align:'center', size:18},
|
||||
{f:'shop', label:'店名(抬头)', type:'text', on:true, auto:false, bold:true, align:'center', size:14},
|
||||
{f:'code', label:'编号', type:'text', on:true, auto:false, bold:false, align:'center', size:14},
|
||||
{f:'series', label:'型号 / 度数', type:'text', on:true, auto:false, bold:false, align:'center', size:14},
|
||||
{f:'spec', label:'版本 / 规格', type:'text', on:true, auto:false, bold:false, align:'center', size:14},
|
||||
{f:'date', label:'生产日期', type:'text', on:true, auto:false, bold:false, align:'center', size:14},
|
||||
{f:'batch', label:'批次号', type:'text', on:false, auto:false, bold:false, align:'center', size:14},
|
||||
{f:'remark', label:'备注', type:'text', on:false, auto:false, bold:false, align:'center', size:14},
|
||||
{f:'qr', label:'二维码', type:'graphic', on:true},
|
||||
{f:'barcode', label:'条形码', type:'graphic', on:true},
|
||||
];
|
||||
// 编号↔型号 同物理行、版本↔日期 同物理行:字号/加粗/对齐共用(Option A)
|
||||
const PAIR = {2:3, 3:2, 4:5, 5:4};
|
||||
|
||||
function metaText(fd){
|
||||
if(fd.type==='graphic') return '图形 · 位置见高级';
|
||||
@@ -273,18 +270,14 @@
|
||||
return parts.join(' · ');
|
||||
}
|
||||
function fontBody(fd, i){
|
||||
const opts = FONTS.map((ft,fi)=>
|
||||
`<div class="lt-fontitem${fi===fd.font?' on':''}" onclick="pickFont(event,${i},${fi})">${ft[0]}</div>`).join('');
|
||||
const autoCtrl = fd.f==='name'
|
||||
? `<span class="lt-fmeta">自动</span>
|
||||
<button class="switch${fd.auto?' on':''}" onclick="toggleAuto(event,${i})"></button>`
|
||||
: '';
|
||||
return `<div class="lt-fbody">
|
||||
<div class="lt-srow"><span class="k">字体</span>
|
||||
<div class="lt-fontsel">
|
||||
<div class="ctrl" onclick="toggleFont(event,${i})"><span class="lt-fontname">${FONTS[fd.font][0]}</span><svg width="14" height="14" viewBox="0 0 24 24" stroke-width="2"><use href="#i-chevron-down"/></svg></div>
|
||||
<div class="lt-fontpop">${opts}</div>
|
||||
</div></div>
|
||||
<div class="lt-srow"><span class="k">字号</span>
|
||||
<input class="input" value="${fd.size}" ${fd.auto?'disabled':''} oninput="setSize(${i},this.value)">
|
||||
<span class="lt-fmeta">自动</span>
|
||||
<button class="switch${fd.auto?' on':''}" onclick="toggleAuto(event,${i})"></button></div>
|
||||
${autoCtrl}</div>
|
||||
<div class="lt-srow"><span class="k">加粗</span>
|
||||
<button class="switch${fd.bold?' on':''}" onclick="toggleBold(event,${i})"></button></div>
|
||||
<div class="lt-srow"><span class="k">对齐</span>
|
||||
@@ -322,13 +315,20 @@
|
||||
previewEls(i).forEach(el=>{
|
||||
el.classList.toggle('lt-off', !fd.on);
|
||||
if(fd.type==='text'){
|
||||
el.style.fontFamily = FONTS[fd.font][1];
|
||||
el.style.fontWeight = fd.bold ? '700' : '400';
|
||||
el.style.textAlign = fd.align;
|
||||
el.style.fontSize = fd.auto ? '' : fd.size+'px';
|
||||
}
|
||||
});
|
||||
}
|
||||
/* 同物理行配对字段(编号↔型号、版本↔日期)共用字号/加粗/对齐 */
|
||||
function mirror(i){
|
||||
const p = PAIR[i]; if(p==null) return;
|
||||
FIELDS[p].size = FIELDS[i].size;
|
||||
FIELDS[p].bold = FIELDS[i].bold;
|
||||
FIELDS[p].align = FIELDS[i].align;
|
||||
applyStyle(p); refreshMeta(p);
|
||||
}
|
||||
/* 手风琴:一次只展开一行 */
|
||||
function expandField(i){
|
||||
const el = fieldEl(i), wasOpen = el.classList.contains('open');
|
||||
@@ -342,24 +342,9 @@
|
||||
fieldEl(i).classList.toggle('disabled', !FIELDS[i].on);
|
||||
applyStyle(i);
|
||||
}
|
||||
function toggleFont(e, i){
|
||||
e.stopPropagation();
|
||||
const pop = fieldEl(i).querySelector('.lt-fontpop'), open = pop.classList.contains('show');
|
||||
document.querySelectorAll('.lt-fontpop.show').forEach(p=>p.classList.remove('show'));
|
||||
if(!open) pop.classList.add('show');
|
||||
}
|
||||
function pickFont(e, i, fi){
|
||||
e.stopPropagation();
|
||||
FIELDS[i].font = fi;
|
||||
const el = fieldEl(i);
|
||||
el.querySelectorAll('.lt-fontitem').forEach((it,x)=>it.classList.toggle('on', x===fi));
|
||||
el.querySelector('.lt-fontname').textContent = FONTS[fi][0];
|
||||
el.querySelector('.lt-fontpop').classList.remove('show');
|
||||
applyStyle(i);
|
||||
}
|
||||
function setSize(i, v){
|
||||
const n = parseInt(v,10);
|
||||
if(!isNaN(n)){ FIELDS[i].size = n; applyStyle(i); refreshMeta(i); }
|
||||
if(!isNaN(n)){ FIELDS[i].size = n; applyStyle(i); refreshMeta(i); mirror(i); }
|
||||
}
|
||||
function toggleAuto(e, i){
|
||||
e.stopPropagation();
|
||||
@@ -372,14 +357,14 @@
|
||||
e.stopPropagation();
|
||||
FIELDS[i].bold = !FIELDS[i].bold;
|
||||
e.currentTarget.classList.toggle('on', FIELDS[i].bold);
|
||||
applyStyle(i); refreshMeta(i);
|
||||
applyStyle(i); refreshMeta(i); mirror(i);
|
||||
}
|
||||
function setAlign(e, i, a){
|
||||
e.stopPropagation();
|
||||
FIELDS[i].align = a;
|
||||
e.currentTarget.parentElement.querySelectorAll('button').forEach(b=>b.classList.remove('on'));
|
||||
e.currentTarget.classList.add('on');
|
||||
applyStyle(i); refreshMeta(i);
|
||||
applyStyle(i); refreshMeta(i); mirror(i);
|
||||
}
|
||||
|
||||
function pickSeg(btn){
|
||||
|
||||
Reference in New Issue
Block a user