diff --git a/client/lib/core/utils/label_bitmap.dart b/client/lib/core/utils/label_bitmap.dart index 437f127..140ac78 100644 --- a/client/lib/core/utils/label_bitmap.dart +++ b/client/lib/core/utils/label_bitmap.dart @@ -54,7 +54,8 @@ Future renderLabelImage({ lay.headerFontSize, w - 2 * lay.headerTextX, bold: lay.headerBold, textAlign: _uiAlign(lay.headerAlign), - color: cCream); + color: cCream, + centerInH: lay.headerHeight); // 店名在深蓝带内上下居中(textY 作微调) } // ── 右列二维码(正方形,绝对坐标)───────────────────────────── @@ -127,12 +128,15 @@ Future renderLabelPngBytes({ return bd?.buffer.asUint8List(); } -/// 在 canvas 上画一行文字(默认黑色,单行不换行,CJK 走系统字体回退) +/// 在 canvas 上画一行文字(默认黑色,单行不换行,CJK 走系统字体回退)。 +/// [centerInH] > 0 时:在 [y, y+centerInH] 区间内竖直居中(用紧排行高 1.0 量高后 +/// 上下留白均分),供抬头店名在深蓝带内上下居中;[y] 作为居中基础上的手动微调量。 void _drawText( ui.Canvas canvas, String s, double x, double y, double size, double maxW, {bool bold = false, ui.TextAlign textAlign = ui.TextAlign.left, - ui.Color color = const ui.Color(0xFF000000)}) // ds-ignore: 纸面固定色(默认黑) + ui.Color color = const ui.Color(0xFF000000), // ds-ignore: 纸面固定色(默认黑) + double centerInH = 0}) { if (s.isEmpty) return; final pb = ui.ParagraphBuilder(ui.ParagraphStyle( @@ -142,11 +146,13 @@ void _drawText( textAlign: textAlign, maxLines: 1, ellipsis: '', + height: centerInH > 0 ? 1.0 : null, // 居中时用紧排行高,量出的高≈字号,居中更准 )) ..pushStyle(ui.TextStyle(color: color)) // ds-ignore: 打印画布纸面固定色 ..addText(s); final p = pb.build()..layout(ui.ParagraphConstraints(width: maxW)); - canvas.drawParagraph(p, ui.Offset(x, y)); + final double dy = centerInH > 0 ? y + (centerInH - p.height) / 2 : y; + canvas.drawParagraph(p, ui.Offset(x, dy)); } /// 版式对齐 → 画布 TextAlign。 diff --git a/client/lib/core/utils/label_template.dart b/client/lib/core/utils/label_template.dart index d052ddb..c01de3a 100644 --- a/client/lib/core/utils/label_template.dart +++ b/client/lib/core/utils/label_template.dart @@ -147,7 +147,7 @@ class LabelHeader { this.show = true, this.height = 24, this.textX = 10, - this.textY = 4, + this.textY = 0, // 0=渲染器按 height 竖直居中;正负值作微调 this.fontSize = 14, this.bold = true, this.align = LabelAlign.left, @@ -167,7 +167,7 @@ class LabelHeader { show: j['show'] as bool? ?? true, height: (j['height'] as num?)?.toDouble() ?? 24, textX: (j['textX'] as num?)?.toDouble() ?? 10, - textY: (j['textY'] as num?)?.toDouble() ?? 4, + textY: (j['textY'] as num?)?.toDouble() ?? 0, fontSize: (j['fontSize'] as num?)?.toDouble() ?? 14, bold: j['bold'] as bool? ?? true, align: _alignFrom(j['align'] as String?), @@ -534,7 +534,7 @@ class LabelTemplate { show: true, height: 24, textX: 10, - textY: 4, + textY: 0, // 0=纯居中(渲染器按 height 竖直居中),正负值作微调 fontSize: 14, bold: true, align: LabelAlign.left), @@ -564,12 +564,41 @@ class LabelTemplate { align: LabelAlign.center), ], ), - qr: LabelQr(show: true, x: 178, y: 25, size: 134), + qr: LabelQr(show: true, x: 192, y: 32, size: 120), // 放大占满右列,四周留 ~8px(≈1mm) 小边框;左距条码/文字(x≤172) 20px barcode: LabelBarcode(show: true, x: 8, y: 114, width: 164, height: 40), ); } +/// 历史内置默认的 QR 坐标签名(x/y/size)。加载老店铺模板时,凡 QR 恰好等于某个历史默认, +/// 自动升级为当前 [LabelTemplate.builtinDefault] 的居中坐标——老模板无需手动「重置」即可拿到 +/// 二维码居中+四周留白的新版式。用户手动挪过的 QR 不匹配任一签名,保持不动。 +const List> _legacyDefaultQrXYS = [ + [178, 25, 134], // 最初默认(贴头顶、无留白) + [190, 34, 116], // 中间一版(缩小但仍偏上) +]; + +/// 就地把匹配历史默认签名的 QR 升级到当前默认坐标;返回同一实例便于链式 map。 +LabelTemplate _migrateLegacyDefaults(LabelTemplate t) { + final q = t.qr; + for (final s in _legacyDefaultQrXYS) { + if (q.x == s[0] && q.y == s[1] && q.size == s[2]) { + final def = LabelTemplate.builtinDefault().qr; + q.x = def.x; + q.y = def.y; + q.size = def.size; + break; + } + } + return t; +} + +/// 内置默认模板是「代码真相 + 只读锁」:无论存储里存了什么旧版式(旧 QR、品名被关等), +/// 加载时一律用当前 [LabelTemplate.builtinDefault] 覆盖,保证默认永远是最新基准。 +/// 用户的定制一律走「复制」出来的副本(另一 id),不受影响。 +LabelTemplate _forceBuiltinDefaultFromCode(LabelTemplate t) => + t.id == 'builtin-default' ? LabelTemplate.builtinDefault() : t; + /// 从 shop.custom_fields 读取店级模板列表;为空/损坏时回退单个 [LabelTemplate.builtinDefault]。 List readLabelTemplates(Map cf) { final raw = cf['label_templates']; @@ -577,6 +606,8 @@ List readLabelTemplates(Map cf) { final list = raw .whereType() .map((m) => LabelTemplate.fromJson(m.cast())) + .map(_migrateLegacyDefaults) + .map(_forceBuiltinDefaultFromCode) .toList(); if (list.isNotEmpty) return list; } diff --git a/client/lib/screens/devices/label_template_editor_screen.dart b/client/lib/screens/devices/label_template_editor_screen.dart index 1848882..5f042dc 100644 --- a/client/lib/screens/devices/label_template_editor_screen.dart +++ b/client/lib/screens/devices/label_template_editor_screen.dart @@ -41,8 +41,6 @@ class LabelTemplateEditorScreen extends ConsumerStatefulWidget { const List> _lineGroups = [ [LabelBinding.code, LabelBinding.series], // 行 0 [LabelBinding.spec, LabelBinding.productionDate], // 行 1 - [LabelBinding.batchNo], // 行 2 - [LabelBinding.remark], // 行 3 ]; enum _FKind { name, header, sub, qr, barcode } @@ -64,8 +62,6 @@ const List<_Fd> _fields = [ _Fd('spec', '版本 / 规格', _FKind.sub, line: 1, binding: LabelBinding.spec), _Fd('date', '生产日期', _FKind.sub, line: 1, binding: LabelBinding.productionDate), - _Fd('batch', '批次号', _FKind.sub, line: 2, binding: LabelBinding.batchNo), - _Fd('remark', '备注', _FKind.sub, line: 3, binding: LabelBinding.remark), _Fd('qr', '二维码', _FKind.qr), _Fd('barcode', '条形码', _FKind.barcode), ]; @@ -93,9 +89,15 @@ class _LabelTemplateEditorScreenState final Map _ctrls = {}; final TextEditingController _sizeCtrl = TextEditingController(); + /// 系统自带的默认模板 id(受保护基准版式)。 + static const String _builtinDefaultId = 'builtin-default'; + LabelTemplate get _t => _templates.firstWhere((t) => t.id == _editingId, orElse: () => _templates.first); + /// 默认模板只读:字段/字号/位置/打印参数一律不可改,要改必须先「复制」。 + bool get _isLocked => _t.id == _builtinDefaultId; + @override void initState() { super.initState(); @@ -179,21 +181,49 @@ class _LabelTemplateEditorScreenState } /// 预览/打印共用的示例数据(QR 已在 bootstrap 生成)。 + /// 预览示例的默认内容(仅用于左侧预览;真实出签永远取商品真实数据)。 + String _sampleDefault(String key) { + switch (key) { + case 'name': + return '贵州茅台酒'; + case 'code': + return 'P1001'; + case 'series': + return '飞天53度'; + case 'spec': + return '500ml'; + case 'date': + return '2024-05-18'; + case 'shop': + return ref.read(shopInfoProvider).valueOrNull?.name ?? '鼎晟酒行'; + } + return ''; + } + + /// 字段内容编辑框的控制器(复用 [_ctrls] 池,`sample/` 命名空间,随店级预览而非模板)。 + TextEditingController _contentCtrl(_Fd fd) => _ctrls.putIfAbsent( + 'sample/${fd.key}', + () => TextEditingController(text: _sampleDefault(fd.key))); + + /// 当前预览示例取值:编辑框非空用编辑框,否则回默认。 + String _sampleOf(String key) { + final v = _ctrls['sample/$key']?.text.trim() ?? ''; + return v.isNotEmpty ? v : _sampleDefault(key); + } + LabelData? _sampleLabel() { final qr = _qrBytes; if (qr == null) return null; - final shopName = - ref.read(shopInfoProvider).valueOrNull?.name ?? '鼎晟酒行'; return LabelData( qrBytes: qr, - name: '贵州茅台酒', - code: 'P1001', - series: '飞天53度', - spec: '500ml', - productionDate: '2024-05-18', + name: _sampleOf('name'), + code: _sampleOf('code'), + series: _sampleOf('series'), + spec: _sampleOf('spec'), + productionDate: _sampleOf('date'), batchNo: '20240518-01', remark: '整箱现货', - shopName: shopName, + shopName: _sampleOf('shop'), ); } @@ -282,6 +312,13 @@ class _LabelTemplateEditorScreenState _t.textStack.name.fontSize = v ? null : _t.textStack.name.fontMax; }); + /// 步进器 +1/-1:夹在 [6,48],回写模型与输入框(同原型 stepSize)。 + void _stepSize(_Fd fd, int d) { + final n = (_sizeOf(fd).round() + d).clamp(6, 48); + _setSize(fd, n.toDouble()); + _sizeCtrl.text = n.toString(); + } + bool _boldOf(_Fd fd) => switch (fd.kind) { _FKind.name => _t.textStack.name.bold, _FKind.header => _t.header.bold, @@ -531,7 +568,9 @@ class _LabelTemplateEditorScreenState style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), ), const Spacer(), - DsButton('重置', icon: LucideIcons.refreshCw, onPressed: _reset), + DsButton('重置', + icon: LucideIcons.refreshCw, + onPressed: _isLocked ? null : _reset), const SizedBox(width: 10), DsButton('打印', icon: LucideIcons.printer, onPressed: _print), const SizedBox(width: 10), @@ -681,6 +720,10 @@ class _LabelTemplateEditorScreenState ], onChanged: _switchEditing, ), + if (_isLocked) ...[ + const SizedBox(height: 10), + _lockNote(t), + ], const SizedBox(height: 10), Wrap( spacing: 8, @@ -695,7 +738,7 @@ class _LabelTemplateEditorScreenState DsButton('重命名', icon: LucideIcons.pencil, small: true, - onPressed: _renameTemplate), + onPressed: _isLocked ? null : _renameTemplate), DsButton('设默认', icon: LucideIcons.check, small: true, @@ -704,7 +747,7 @@ class _LabelTemplateEditorScreenState icon: LucideIcons.x, small: true, variant: DsBtnVariant.danger, - onPressed: _deleteTemplate), + onPressed: _isLocked ? null : _deleteTemplate), ], ), const SizedBox(height: 12), @@ -736,6 +779,19 @@ class _LabelTemplateEditorScreenState ); } + /// 默认模板只读提示条(点「复制」是可编辑逃生口)。 + Widget _lockNote(AppTokens t) => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: t.warnBg, + borderRadius: BorderRadius.circular(AppDims.rMd), + border: Border.all(color: t.warn), + ), + child: Text('系统默认模板不可修改,点「复制」生成可编辑副本', + style: TextStyle( + fontSize: AppDims.fsXs, color: t.warn, height: 1.5)), + ); + Widget _fieldSection(AppTokens t) => _sectionCard( t, '显示字段', @@ -752,22 +808,25 @@ class _LabelTemplateEditorScreenState Widget _fieldRow(AppTokens t, _Fd fd) { final on = _onOf(fd); - final open = _openField == fd.key; + final open = !_isLocked && _openField == fd.key; final graphic = fd.kind == _FKind.qr || fd.kind == _FKind.barcode; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ InkWell( borderRadius: BorderRadius.circular(AppDims.rMd), - onTap: () => setState(() { - if (open) { - _openField = null; - } else { - _openField = fd.key; - _sizeCtrl.text = - graphic || _isAuto(fd) ? '' : _sizeOf(fd).round().toString(); - } - }), + onTap: _isLocked + ? null + : () => setState(() { + if (open) { + _openField = null; + } else { + _openField = fd.key; + _sizeCtrl.text = graphic || _isAuto(fd) + ? '' + : _sizeOf(fd).round().toString(); + } + }), child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9), decoration: BoxDecoration( @@ -776,7 +835,9 @@ class _LabelTemplateEditorScreenState ), child: Row( children: [ - DsSwitch(value: on, onChanged: (v) => _setOn(fd, v)), + DsSwitch( + value: on, + onChanged: _isLocked ? null : (v) => _setOn(fd, v)), const SizedBox(width: 10), Expanded( child: Text(fd.label, @@ -822,6 +883,27 @@ class _LabelTemplateEditorScreenState return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // 内容(仅改左侧预览示例,真实打印永远取商品真实数据) + Row( + children: [ + SizedBox( + width: 44, + child: Text('内容', + style: TextStyle( + fontSize: AppDims.fsSm, color: t.muted))), + Expanded( + child: DsInput( + controller: _contentCtrl(fd), + onChanged: (_) => _scheduleRender(), + ), + ), + ], + ), + const SizedBox(height: 4), + Text('仅改预览示例,真实打印按商品数据填充', + style: TextStyle( + fontSize: AppDims.fsXs, color: t.muted, height: 1.6)), + const SizedBox(height: 12), // 字号(品名可切自动) Row( children: [ @@ -841,6 +923,7 @@ class _LabelTemplateEditorScreenState final n = int.tryParse(v.trim()); if (n != null && n > 0) _setSize(fd, n.toDouble()); }, + suffix: _sizeStepper(fd, t, auto), ), ), if (isName) ...[ @@ -889,6 +972,26 @@ class _LabelTemplateEditorScreenState ); } + /// 字号输入框右侧内嵌的上下箭头步进器(点击 +1/-1,同原型 .lt-steparr)。 + Widget _sizeStepper(_Fd fd, AppTokens t, bool auto) { + Widget arrow(IconData ic, int d) => InkWell( + onTap: auto ? null : () => _stepSize(fd, d), + child: SizedBox( + height: 15, + width: 18, + child: Icon(ic, size: 13, color: auto ? t.faint : t.muted), + ), + ); + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + arrow(LucideIcons.chevronUp, 1), + arrow(LucideIcons.chevronDown, -1), + ], + ); + } + // ── 高级 · 区域位置 ─────────────────────────────────────────────────── Widget _advancedSection(AppTokens t) { return Container( @@ -978,8 +1081,10 @@ class _LabelTemplateEditorScreenState DsSeg( items: const ['正向', '反向'], index: _t.print.direction == 0 ? 1 : 0, - onChanged: (i) => - _mutate(() => _t.print.direction = i == 0 ? 1 : 0), + onChanged: (i) { + if (_isLocked) return; + _mutate(() => _t.print.direction = i == 0 ? 1 : 0); + }, ), ], ), @@ -1016,6 +1121,7 @@ class _LabelTemplateEditorScreenState label, input: DsInput( controller: c, + enabled: !_isLocked, keyboardType: TextInputType.numberWithOptions(decimal: decimal), onChanged: (v) { final n = double.tryParse(v.trim()); diff --git a/client/test/golden/goldens/label_render_default.png b/client/test/golden/goldens/label_render_default.png index 9a276ee..60bc78b 100644 Binary files a/client/test/golden/goldens/label_render_default.png and b/client/test/golden/goldens/label_render_default.png differ diff --git a/client/test/label_template_storage_test.dart b/client/test/label_template_storage_test.dart index 36531ee..c502f7d 100644 --- a/client/test/label_template_storage_test.dart +++ b/client/test/label_template_storage_test.dart @@ -47,5 +47,69 @@ void main() { expect(list.map((t) => t.id), ['builtin-default', 'tpl-2']); expect(list[1].name, '双语抬头'); }); + + test('老店铺旧默认 QR 加载时自动升级为居中坐标', () { + final def = LabelTemplate.builtinDefault(); + // 构造两个历史默认签名的模板(贴头顶版 + 中间偏上版) + final legacyA = def.copyWith(id: 'tpl-legacy-a', name: '旧模板A'); + legacyA.qr + ..x = 178 + ..y = 25 + ..size = 134; + final legacyB = def.copyWith(id: 'tpl-legacy-b', name: '旧模板B'); + legacyB.qr + ..x = 190 + ..y = 34 + ..size = 116; + final cf = { + 'label_templates': [legacyA.toJson(), legacyB.toJson()], + }; + final list = readLabelTemplates(cf); + for (final t in list) { + expect(t.qr.x, def.qr.x, reason: '${t.name} QR.x 应升级到新默认'); + expect(t.qr.y, def.qr.y, reason: '${t.name} QR.y 应升级到新默认'); + expect(t.qr.size, def.qr.size, reason: '${t.name} QR.size 应升级到新默认'); + } + }); + + test('内置默认模板永远回代码基准:存储里被改脏也覆盖回 builtinDefault', () { + final def = LabelTemplate.builtinDefault(); + // 模拟老库存的 builtin-default:品名被关、QR 乱改、字号乱改 + final stale = def.copyWith(); + stale.textStack.name.show = false; + stale.qr + ..x = 10 + ..y = 10 + ..size = 200; + stale.textStack.subLines[0].fontSize = 99; + final cf = { + 'label_templates': [stale.toJson()], + }; + final t = + readLabelTemplates(cf).firstWhere((x) => x.id == 'builtin-default'); + expect(t.textStack.name.show, isTrue, reason: '品名默认必须展示'); + expect(t.qr.x, def.qr.x); + expect(t.qr.y, def.qr.y); + expect(t.qr.size, def.qr.size); + expect(t.textStack.subLines[0].fontSize, def.textStack.subLines[0].fontSize); + }); + + test('用户自定义过的 QR 不匹配旧签名,加载时保持不动', () { + final custom = LabelTemplate.builtinDefault().copyWith( + id: 'tpl-custom', + name: '自定义位置', + ); + custom.qr + ..x = 210 + ..y = 60 + ..size = 80; // 与任一历史默认都不同 + final cf = { + 'label_templates': [custom.toJson()], + }; + final t = readLabelTemplates(cf).first; + expect(t.qr.x, 210); + expect(t.qr.y, 60); + expect(t.qr.size, 80); + }); }); } diff --git a/design/prototype/screens/icons.js b/design/prototype/screens/icons.js index c579cd9..8b2256e 100644 --- a/design/prototype/screens/icons.js +++ b/design/prototype/screens/icons.js @@ -2,7 +2,7 @@ // 所有屏用 引用;改此处一个 symbol,全局生效。 // 共 108 个:100 个源自 lucide-static(含 i-printer),i-info/i-alert 为点改实心圆的清晰变体,,另有平台品牌图标 i-apple / i-android(simple-icons,填充式,path 自带 fill 覆盖描边样式)与 i-tablet-smartphone(lucide)。新增图标先在此登记 symbol,再用 use 引用。 (function(){ - var SPRITE=''; + var SPRITE=''; function inject(){ if(document.getElementById('ym-icon-sprite'))return; var d=document.createElement('div'); d.id='ym-icon-sprite'; d.style.cssText='position:absolute;width:0;height:0'; d.innerHTML=SPRITE; document.body.insertBefore(d,document.body.firstChild); } if(document.readyState!=='loading')inject(); else document.addEventListener('DOMContentLoaded',inject); // ---- 徽章图标化(2026-07-04 拍板:全系统圆点→代表图标)---- diff --git a/design/prototype/screens/label-template-editor.html b/design/prototype/screens/label-template-editor.html index 300645d..a2dc1e8 100644 --- a/design/prototype/screens/label-template-editor.html +++ b/design/prototype/screens/label-template-editor.html @@ -77,6 +77,17 @@ .lt-srow:last-child{margin-bottom:0;} .lt-srow .k{width:44px; flex:none; font-size:var(--fs-sm); color:var(--muted);} .lt-srow .input{width:80px;} + /* 字号步进器:输入框右侧内嵌上下箭头,点击 +1/-1 */ + .lt-stepper{position:relative; width:80px; flex:none;} + .lt-stepper .input{width:100%; box-sizing:border-box; padding-right:22px;} + .lt-steparr{position:absolute; right:1px; top:1px; bottom:1px; width:20px; + display:flex; flex-direction:column; border-left:1px solid var(--border);} + .lt-steparr button{flex:1; display:flex; align-items:center; justify-content:center; + border:0; background:transparent; cursor:pointer; color:var(--muted); padding:0;} + .lt-steparr button:hover{background:var(--row-hover); color:var(--text);} + .lt-steparr button:last-child{border-top:1px solid var(--border);} + .lt-steparr button:disabled{opacity:.4; cursor:default; background:transparent; color:var(--muted);} + .lt-steparr svg{width:11px; height:11px; stroke:currentColor; fill:none; stroke-width:2.5;} .lt-gnote{margin:0; font-size:var(--fs-xs); color:var(--muted); line-height:1.6;} /* 字体下拉(自包含浮层,每行独立) */ .lt-fontsel{position:relative; flex:1;} @@ -92,6 +103,11 @@ .lt-tplhint{display:flex; align-items:center; gap:6px; margin-top:12px; padding-top:12px; border-top:1px solid var(--border); font-size:var(--fs-xs); color:var(--muted);} .lt-tplhint svg{width:14px; height:14px; flex:none; stroke:currentColor; fill:none; stroke-width:2; color:var(--primary);} .lt-tplhint b{color:var(--primary); font-weight:600;} + /* 默认模板只读锁:提示条 + 受锁区域整体禁用(点「复制」为可编辑逃生口) */ + .lt-locknote{margin-top:10px; padding:10px 12px; background:var(--warn-bg); border:1px solid var(--warn); border-radius:var(--r-md); font-size:var(--fs-xs); color:var(--warn); line-height:1.5;} + .lt-lockoff{transition:opacity .15s;} + .dis{opacity:.45; pointer-events:none;} + .lt-panel.locked #fieldList, .lt-panel.locked .lt-adv, .lt-panel.locked #printSec{opacity:.5; pointer-events:none;} /* 高级折叠 */ .lt-adv{background:var(--surface); border:1px solid var(--border); border-radius:var(--r-lg); overflow:hidden;} .lt-adv>summary{list-style:none; cursor:pointer; padding:14px 16px; font-size:var(--fs-sm); font-weight:700; color:var(--title); display:flex; align-items:center; gap:8px;} @@ -116,7 +132,7 @@
返回

标签模板编辑器

左侧实时预览 · 右侧属性面板
-
重置
+
重置
打印
保存
@@ -167,10 +183,11 @@
新建
复制
-
重命名
+
重命名
设默认
-
删除
+
删除
+
系统默认模板不可修改,点「复制」生成可编辑副本
出签打印使用:默认版式
@@ -186,9 +203,9 @@
二维码
-
-
-
+
+
+
条形码
@@ -205,7 +222,7 @@ -
+
打印参数
@@ -249,14 +266,12 @@ // 字体不做(热敏单色 MVP);仅「品名」支持自动字号。同物理行两字段共用字号/加粗/对齐(PAIR)。 const ALIGN_LABEL = {left:'左', center:'居中', right:'右'}; const FIELDS = [ - {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:'name', label:'品名', type:'text', on:true, auto:true, bold:true, align:'center', size:18, sample:'贵州茅台酒'}, + {f:'shop', label:'店名(抬头)', type:'text', on:true, auto:false, bold:true, align:'center', size:14, sample:'鼎晟酒行'}, + {f:'code', label:'编号', type:'text', on:true, auto:false, bold:false, align:'center', size:14, sample:'P1001'}, + {f:'series', label:'型号 / 度数', type:'text', on:true, auto:false, bold:false, align:'center', size:14, sample:'飞天53度'}, + {f:'spec', label:'版本 / 规格', type:'text', on:true, auto:false, bold:false, align:'center', size:14, sample:'500ml'}, + {f:'date', label:'生产日期', type:'text', on:true, auto:false, bold:false, align:'center', size:14, sample:'2024-05-18'}, {f:'qr', label:'二维码', type:'graphic', on:true}, {f:'barcode', label:'条形码', type:'graphic', on:true}, ]; @@ -276,8 +291,17 @@ ` : ''; return `
+
内容 +
+

仅改预览示例,真实打印按商品数据填充

字号 - +
+ +
+ + +
+
${autoCtrl}
加粗
@@ -347,11 +371,27 @@ const n = parseInt(v,10); if(!isNaN(n)){ FIELDS[i].size = n; applyStyle(i); refreshMeta(i); mirror(i); } } + /* 内容:仅改左侧预览示例文字,不影响真实打印(真实出签永远取商品数据) */ + function setSample(i, v){ + FIELDS[i].sample = v; + previewEls(i).forEach(el=>{ el.textContent = v; }); + } + /* 步进器 +1/-1:夹在 [6,48],回写输入框并刷新预览 */ + function stepSize(i, d){ + if(FIELDS[i].auto) return; + let n = parseInt(FIELDS[i].size,10); if(isNaN(n)) n = 14; + n = Math.max(6, Math.min(48, n + d)); + FIELDS[i].size = n; + const inp = fieldEl(i).querySelector('.lt-stepper .input'); + if(inp) inp.value = n; + applyStyle(i); refreshMeta(i); mirror(i); + } function toggleAuto(e, i){ e.stopPropagation(); FIELDS[i].auto = !FIELDS[i].auto; e.currentTarget.classList.toggle('on', FIELDS[i].auto); fieldEl(i).querySelector('.lt-srow .input').disabled = FIELDS[i].auto; + fieldEl(i).querySelectorAll('.lt-steparr button').forEach(b=>b.disabled = FIELDS[i].auto); applyStyle(i); refreshMeta(i); } function toggleBold(e, i){ @@ -402,8 +442,18 @@ document.getElementById('renameInput').value = name; document.getElementById('tplPop').classList.remove('show'); renderTplPop(); + applyLock(); toast('正在编辑「'+name+'」'); } + /* 默认模板('默认版式')受保护只读:显示锁提示、禁用字段/高级/打印/重命名/删除/重置, + 其余(下拉/新建/复制/设默认)保持可用;点「复制」生成可编辑副本 */ + function isLockedTpl(){ return tplEditing === '默认版式'; } + function applyLock(){ + const on = isLockedTpl(); + document.getElementById('lockNote').style.display = on ? '' : 'none'; + document.querySelector('.lt-panel').classList.toggle('locked', on); + document.querySelectorAll('.lt-lockoff').forEach(b=>b.classList.toggle('dis', on)); + } function setDefaultTpl(){ tplActive = tplEditing; document.getElementById('tplActive').textContent = tplActive; @@ -413,6 +463,7 @@ renderTplPop(); document.addEventListener('click', ()=>document.querySelectorAll('.lt-fontpop.show').forEach(p=>p.classList.remove('show'))); renderFields(); + applyLock(); function openRename(){ document.getElementById('ovRename').classList.add('show'); } function closeRename(){ document.getElementById('ovRename').classList.remove('show'); } function doRename(){ closeRename(); toast('已重命名 ✓'); } diff --git a/web/_includes/icons.njk b/web/_includes/icons.njk index 25c1b81..e681fc5 100644 --- a/web/_includes/icons.njk +++ b/web/_includes/icons.njk @@ -1,3 +1,3 @@ {# 图标雪碧图 — 单一真相源:自 design/prototype/screens/icons.js 提取(lucide 同源), 页面用 引用;修改图标请先改原型再同步 #} - +