diff --git a/client/lib/core/utils/print_util_stub.dart b/client/lib/core/utils/print_util_stub.dart index d6f225d..6a33e87 100644 --- a/client/lib/core/utils/print_util_stub.dart +++ b/client/lib/core/utils/print_util_stub.dart @@ -213,19 +213,28 @@ double _fitFont(String s, double maxW, double maxH, bool bold, double cap) { return size; } -/// 组装标签底部平铺的三字段:度数(型号) / 规格(版本) / 生产日期。 -/// 各值为空则跳过;日期截断到 10 字符(YYYY-MM-DD)。四路渲染共用,口径一致。 +/// 组装标签左列名下两行副字段(居中排布): +/// 行1 = 编号 + 度数(型号) 行2 = 规格(版本) + 生产日期 +/// 2026-08-28 用户口径「编号和度数放一行、规格和日期放一行、居中」。 +/// 各字段为空则跳过(该行可只剩一个字段或整行消失);日期截断到 10 字符。四路渲染共用。 List _labelSubFields( - String? series, String? spec, String? productionDate) { + String code, String? series, String? spec, String? productionDate) { final date = (productionDate ?? '').isNotEmpty ? (productionDate!.length > 10 ? productionDate.substring(0, 10) : productionDate) : ''; - return [ + final line1 = [ + if (code.isNotEmpty) code, if ((series ?? '').isNotEmpty) series!, + ].join(' '); + final line2 = [ if ((spec ?? '').isNotEmpty) spec!, if (date.isNotEmpty) date, + ].join(' '); + return [ + if (line1.isNotEmpty) line1, + if (line2.isNotEmpty) line2, ]; } @@ -236,7 +245,7 @@ Future _renderLabelBitmap({ required String shop, required String name, required String code, - required List subs, // 底部平铺字段:度数 / 规格 / 日期(已过滤空) + required List subs, // 左列逐行副字段:型号规格 / 日期(已过滤空) required Uint8List qrBytes, bool drawBarcode = true, // 热敏光栅路径置 false:条码改用原生 TSPL BARCODE 保证扫码 bool drawQr = true, // 热敏光栅路径置 false:二维码改用原生 TSPL BITMAP 保证扫码 @@ -272,10 +281,11 @@ Future _renderLabelBitmap({ const padV = 6.0, padH = 8.0; // 收紧留白,给二维码腾空间 const double colGap = 6.0; // 左列与二维码列间距(原 10,收小) - // 右列:二维码占主体整高(正方形) - const double qrSize = (bodyBot - bodyTop) - 2 * padV; // 109 - const double qrTop = bodyTop + padV; // 30 - const double qrLeft = w - padH - qrSize; // 203 + // 右列:二维码略放大,占主体近满高(正方形),2026-08-28 用户口径「二维码调大一点」 + const double qrPad = 1.0; + const double qrSize = (bodyBot - bodyTop) - 2 * qrPad; // 134 + const double qrTop = bodyTop + qrPad; // 25 + const double qrLeft = w - padH - qrSize; // 178 if (drawQr) { try { final codec = await ui.instantiateImageCodec(qrBytes); @@ -293,79 +303,38 @@ Future _renderLabelBitmap({ // 左列几何 const double leftX = padH; // 8 - const double leftRight = qrLeft - colGap; // 197 - const double leftW = leftRight - leftX; // 189 + const double leftRight = qrLeft - colGap; // 172 + const double leftW = leftRight - leftX; // 164 // 左列底部一维条码(Code128,内容=商品编号),供扫码枪扫码出库 const double barH = 40.0; - const double barBottom = bodyBot - padV; // 137 - const double barTop = barBottom - barH; // 97 + const double barBottom = bodyBot - padV; // 154 + const double barTop = barBottom - barH; // 114 if (drawBarcode) _drawBarcode(canvas, code, leftX, barTop, leftW, barH); - // 左列文字块(品名+编号 一行居中;度数/规格/日期 平铺一行),在条码上方居中 - const double infoTop = bodyTop + padV; // 32 - const double infoBot = barTop - 4; // 93 - const double infoH = infoBot - infoTop; // 61 + // 左列文字块:品名 + 两行副字段(行1=编号+度数、行2=规格+日期),整体水平居中、 + // 竖直均分。2026-08-28 用户口径「编号和度数放一行、规格和日期放一行、字体在空白处居中」。 + const double infoTop = bodyTop + padV; // 30 + const double infoBot = barTop - 4; // 110 + const double infoH = infoBot - infoTop; // 80 - // 品名自适应字号(为编号留出约 28% 宽度),上限 17 + const double kSmallSize = 12.0; // 编号+度数 / 规格+日期 final double nameSize = - _fitFont(name, leftW * 0.72, 1000, true, 17.0).clamp(11.0, 17.0); - const double codeSize = 10.0; - const double subSize = 10.0; - final double nameLineH = nameSize * 1.2; - const double subLineH = subSize * 1.2; - const double interGap = 8.0; - final double blockH = nameLineH + interGap + subLineH; - double blockTop = infoTop + (infoH - blockH) / 2; - if (blockTop < infoTop) blockTop = infoTop; - - // 品名 + 编号:整体水平居中,基线对齐 - final double nameW = _measureWidth(name, nameSize, true); - final bool hasCode = code.isNotEmpty; - const double codeGap = 9.0; - final double codeW = hasCode ? _measureWidth(code, codeSize, false) : 0; - final double groupW = nameW + (hasCode ? codeGap + codeW : 0); - double gx = leftX + (leftW - groupW) / 2; - if (gx < leftX) gx = leftX; - _drawText(canvas, name, gx, blockTop, nameSize, leftW, - bold: true, color: cInk); - if (hasCode) { - _drawText(canvas, code, gx + nameW + codeGap, - blockTop + (nameSize - codeSize) * 0.82, codeSize, codeW + 4, - color: cCode); - } - - // 三字段平铺一行(space-between,两侧留内边距使外白 > 中白) - final double subY = blockTop + nameLineH + interGap; - const double innerPad = 10.0; // 对齐原型 .lbl-sub padding:0 10px - const double innerLeft = leftX + innerPad; // 18 - const double innerRight = leftRight - innerPad; // 187 - const double innerW = innerRight - innerLeft; // 169 - if (subs.length == 1) { - final double sw0 = _measureWidth(subs[0], subSize, false); - _drawText( - canvas, subs[0], innerLeft + (innerW - sw0) / 2, subY, subSize, innerW, - color: cCode); - } else if (subs.isNotEmpty) { - // 字段间必须留可见最小间隔;总宽超限时按比例缩字号,避免贴死 - const double minGap = 5.0; - double fs = subSize; - double sumW = subs.fold( - 0.0, (s, x) => s + _measureWidth(x, fs, false)); - final double avail = innerW - minGap * (subs.length - 1); - if (sumW > avail && sumW > 0) { - fs = (subSize * avail / sumW).clamp(7.0, subSize); - sumW = subs.fold(0.0, (s, x) => s + _measureWidth(x, fs, false)); - } - final double gap = subs.length > 1 - ? ((innerW - sumW) / (subs.length - 1)).clamp(minGap, innerW) - : 0.0; - double cx = innerLeft; - for (int i = 0; i < subs.length; i++) { - final double wi = _measureWidth(subs[i], fs, false); - _drawText(canvas, subs[i], cx, subY, fs, wi + 4, color: cCode); - cx += wi + gap; - } + _fitFont(name, leftW, 1000, true, 16.0).clamp(11.0, 16.0); + final rows = <(String, double, bool)>[ + if (name.isNotEmpty) (name, nameSize, true), // 品名粗体 + for (final s in subs) (s, kSmallSize, false), // 编号+度数 / 规格+日期 + ]; + const double kLineH = 1.3; // 行高倍数 + final double totalTextH = rows.fold(0.0, (s, r) => s + r.$2 * kLineH); + final int n = rows.length; + final double rawGap = n > 0 ? (infoH - totalTextH) / (n + 1) : 2.0; + final double gap = rawGap < 1.0 ? 1.0 : rawGap; + double yy = infoTop + gap; + for (final r in rows) { + _drawText(canvas, r.$1, leftX, yy, r.$2, leftW, + bold: r.$3, color: r.$3 ? cInk : cCode, center: true); + yy += r.$2 * kLineH + gap; } return recorder.endRecording().toImage(sw, sh); // 返回 2× 图(640×320) @@ -438,10 +407,10 @@ Future _printFlatLabelThermal({ buf.add(ascii.encode('\r\n')); // ── 右列二维码(原生 BITMAP,避免光栅降采样糊码):占主体整高、右对齐 ── - const qrW = 118, qrH = 118; - const qrX = 320 - 8 - qrW; // 194 + const qrW = 130, qrH = 130; // 2026-08-28 放大,对齐画布 QR(134) + const qrX = 320 - 8 - qrW; // 182 const bodyTop = 24, bodyBot = 160; // 无页脚:主体直抵纸底 - const qrY = bodyTop + ((bodyBot - bodyTop) - qrH) ~/ 2; // 33 + const qrY = bodyTop + ((bodyBot - bodyTop) - qrH) ~/ 2; // 27 const qrWBytes = (qrW + 7) >> 3; // 13 final qrCodec = await ui.instantiateImageCodec(qrBytes, targetWidth: qrW, targetHeight: qrH); @@ -509,7 +478,7 @@ Future printProductLabelImpl({ const headerH = labelH * 0.20; // 底部平铺三字段:度数(型号) / 规格(版本) / 日期(口径与预览、TSPL 一致) - final subs = _labelSubFields(series, spec, productionDate); + final subs = _labelSubFields(code, series, spec, productionDate); // 桌面端热敏机:直接画扁平标签位图 + TSPL 裸发(不走会多走纸的系统打印) if (await _printFlatLabelThermal( @@ -563,55 +532,34 @@ Future printProductLabelImpl({ child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.stretch, children: [ - // 文字块(品名+编号 居中 / 三字段平铺),条码上方居中 + // 文字块:品名 + 两行副字段(行1=编号+度数、行2=规格+日期),整体居中。 + // 2026-08-28 用户口径「编号和度数放一行、规格和日期放一行、居中」。 pw.Expanded( child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, - crossAxisAlignment: pw.CrossAxisAlignment.stretch, + crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ - // 品名 + 编号(整体居中,编号小字随后) - pw.RichText( - textAlign: pw.TextAlign.center, - maxLines: 1, - overflow: pw.TextOverflow.clip, - text: pw.TextSpan(children: [ - pw.TextSpan( - text: name, - style: pw.TextStyle( - font: font, - fontSize: nameFs, - fontWeight: pw.FontWeight.bold, - color: _ink, - letterSpacing: 0.3)), - if (code.isNotEmpty) - pw.TextSpan( - text: ' $code', - style: pw.TextStyle( - font: font, - fontSize: 5, - color: _footnote)), - ]), - ), - pw.SizedBox(height: 4), - // 三字段平铺一行(两侧留边距,外白 > 中白) - if (subs.isNotEmpty) - pw.Padding( - padding: const pw.EdgeInsets.symmetric( - horizontal: 4), - child: pw.Row( - mainAxisAlignment: subs.length == 1 - ? pw.MainAxisAlignment.center - : pw.MainAxisAlignment.spaceBetween, - children: [ - for (final s in subs) - pw.Text(s, - style: pw.TextStyle( - font: font, - fontSize: 5, - color: _footnote)), - ], - ), - ), + pw.Text(name, + textAlign: pw.TextAlign.center, + maxLines: 2, + overflow: pw.TextOverflow.clip, + style: pw.TextStyle( + font: font, + fontSize: nameFs, + fontWeight: pw.FontWeight.bold, + color: _ink, + letterSpacing: 0.3)), + for (final s in subs) ...[ + pw.SizedBox(height: 2), + pw.Text(s, + textAlign: pw.TextAlign.center, + maxLines: 1, + overflow: pw.TextOverflow.clip, + style: pw.TextStyle( + font: font, + fontSize: 5, + color: _footnote)), + ], ], ), ), @@ -668,7 +616,8 @@ Future renderLabelPreviewImpl(LabelData label) async { shop: label.shopName, name: label.name, code: label.code, - subs: _labelSubFields(label.series, label.spec, label.productionDate), + subs: _labelSubFields( + label.code, label.series, label.spec, label.productionDate), qrBytes: label.qrBytes!, ); final bd = await img.toByteData(format: ui.ImageByteFormat.png); diff --git a/client/lib/core/utils/print_util_web.dart b/client/lib/core/utils/print_util_web.dart index 7c96a17..2b005f2 100644 --- a/client/lib/core/utils/print_util_web.dart +++ b/client/lib/core/utils/print_util_web.dart @@ -41,13 +41,21 @@ Future printProductLabelImpl({ ? productionDate.substring(0, 10) : productionDate) : ''; - // 底部平铺三字段:度数(型号) / 规格(版本) / 日期(口径与预览、PDF、TSPL 一致) - final subs = [ + // 左列名下两行副字段(居中):行1=编号+度数、行2=规格+日期(口径与预览、PDF、TSPL 一致) + // 2026-08-28 用户口径「编号和度数放一行、规格和日期放一行、字体在空白处居中」。 + final line1 = [ + if (code.isNotEmpty) code, if (seriesVal.isNotEmpty) seriesVal, + ].join(' '); + final line2 = [ if (specVal.isNotEmpty) specVal, if (dateVal.isNotEmpty) dateVal, + ].join(' '); + final subs = [ + if (line1.isNotEmpty) line1, + if (line2.isNotEmpty) line2, ]; - final subSpans = subs.map((s) => '$s').join(); + final subLines = subs.map((s) => '
$s
').join(); // 商品名过长时缩小字号 final nameLen = name.runes.length; final nameFontPt = nameLen > 10 ? 7.0 : (nameLen > 7 ? 8.0 : 9.0); @@ -98,18 +106,16 @@ body { width: 40mm; height: 20mm; overflow: hidden; background: #fff; } .left { flex: 1; min-width: 0; display: flex; flex-direction: column; } .info { flex: 1; min-height: 0; display: flex; flex-direction: column; - justify-content: center; gap: 1mm; overflow: hidden; + justify-content: center; align-items: center; gap: 0.6mm; overflow: hidden; } .pname { - font-weight: 700; text-align: center; line-height: 1.1; + font-weight: 700; text-align: center; line-height: 1.1; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.pcode { font-size: 5pt; font-weight: 400; color: #333; margin-left: 1.2mm; } .sub { - font-size: 5pt; color: #333; display: flex; - justify-content: space-between; gap: 1mm; padding: 0 1.2mm; + font-size: 5pt; color: #333; line-height: 1.15; text-align: center; + max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.sub span { white-space: nowrap; } .bc { flex-shrink: 0; height: 4.6mm; } .bc svg { width: 100%; height: 100%; display: block; } @@ -128,8 +134,8 @@ body { width: 40mm; height: 20mm; overflow: hidden; background: #fff; }
-
$name$code
-
$subSpans
+
$name
+ $subLines
$barcodeSvg
diff --git a/design/prototype/screens/devices.html b/design/prototype/screens/devices.html index 2c832bd..04c44c1 100644 --- a/design/prototype/screens/devices.html +++ b/design/prototype/screens/devices.html @@ -68,10 +68,9 @@ .lbl-hdr{height:26px; flex:none; display:flex; align-items:center; padding:0 10px; font-size:13px; font-weight:700; letter-spacing:.05em; background:#1f2a3a; color:#f4ecd8;} /* ds-allow 打印纸面固定尺寸 */ .lbl-mid{flex:1; min-height:0; display:flex; gap:6px; padding:6px 8px;} .lbl-left{flex:1; min-width:0; display:flex; flex-direction:column;} - .lbl-info{flex:1; min-height:0; display:flex; flex-direction:column; justify-content:center; gap:9px;} - .lbl-name{font-size:18px; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; text-align:center;} - .lbl-code{font-size:11px; font-weight:400; color:#333; margin-left:10px;} /* ds-allow 打印纸面固定尺寸 */ - .lbl-sub{font-size:11px; color:#333; display:flex; justify-content:space-between; gap:6px; padding:0 10px;} /* ds-allow 打印纸面固定尺寸 */ + .lbl-info{flex:1; min-height:0; display:flex; flex-direction:column; justify-content:center; align-items:center; gap:5px;} + .lbl-name{font-size:18px; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; text-align:center; max-width:100%;} /* ds-allow 打印纸面固定尺寸 */ + .lbl-sub{font-size:11px; color:#333; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; line-height:1.15; text-align:center; max-width:100%;} /* ds-allow 打印纸面固定尺寸;名下两行居中 */ .lbl-bc{flex:none; height:46px; margin-top:4px;} .lbl-bc img{width:100%; height:100%; display:block; object-fit:fill;} .lbl-qr{flex:none; height:100%; aspect-ratio:1;} @@ -164,8 +163,9 @@
-
飞天茅台P1297
-
53°500ml2023-05-18
+
飞天茅台
+
P1297    53°
+
500ml    2023-05-18
一维条码 P1297