chore: release client-v1.1.13
Deploy Client / build-windows (push) Successful in 4m43s
Deploy Client / build-client-web (push) Successful in 6m22s
Deploy Client / build-macos (push) Successful in 7m36s
Deploy Client / build-android (push) Successful in 7m54s
Deploy Client / build-ios (push) Failing after 13m2s
Deploy Client / release-deploy-client (push) Has been skipped

扫码出库免回车(120ms 防抖自动加入明细)+ 热敏标签重写为光栅化,
消除字体重叠/与预览失真/底部重复店名(原型+画布+PDF+Web 同步)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bupi8Kdqkfx2N5acFsHTx5
This commit is contained in:
wangjia
2026-08-27 21:48:47 +08:00
parent c3a127485f
commit 7025ccd1b5
5 changed files with 100 additions and 161 deletions
+8
View File
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.13] - 2026-08-27
### 改进
- 扫码出库免按回车:扫码枪扫完商品二维码后自动加入明细,无需再手动敲一下回车。
### 修复
- 修复热敏打印机(得力等 TSPL 标签机)打印商品价签时字体重叠、且底部多出一行酒行名称的问题;现在热敏打印效果与屏幕预览完全一致,不再失真。
## [1.1.12] - 2026-08-27
### 改进
+77 -158
View File
@@ -4,7 +4,6 @@ import 'dart:typed_data';
import 'dart:ui' as ui;
import 'dart:ffi';
import 'package:barcode/barcode.dart' as bc;
import 'package:charset_converter/charset_converter.dart';
import 'package:ffi/ffi.dart';
import 'package:win32/win32.dart'
show
@@ -15,8 +14,7 @@ import 'package:win32/win32.dart'
EndPagePrinter,
EndDocPrinter,
ClosePrinter,
DOC_INFO_1,
WideCharToMultiByte;
DOC_INFO_1;
import 'package:flutter/foundation.dart'
show kIsWeb, debugPrint, visibleForTesting;
import 'package:flutter/services.dart';
@@ -240,6 +238,8 @@ Future<ui.Image> _renderLabelBitmap({
required String code,
required List<String> subs, // 底部平铺字段:度数 / 规格 / 日期(已过滤空)
required Uint8List qrBytes,
bool drawBarcode = true, // 热敏光栅路径置 false:条码改用原生 TSPL BARCODE 保证扫码
bool drawQr = true, // 热敏光栅路径置 false:二维码改用原生 TSPL BITMAP 保证扫码
}) async {
const w = 320, h = 160; // 40×20mm @203dpi(逻辑尺寸)
const scale = 2; // 2× 超采样,内部以 640×320 绘制
@@ -272,7 +272,7 @@ Future<ui.Image> _renderLabelBitmap({
const footerTop = h - footerH; // 145
canvas.drawRect(ui.Rect.fromLTWH(0, footerTop, w.toDouble(), footerH),
ui.Paint()..color = cCream);
_drawText(canvas, shop, 10, footerTop + 3.5, 8, 180, color: cFoot);
// 页脚只留右侧尺寸标注:店名已在顶部抬头,页脚不再重复(2026-08-27 去冗余)
final szW = _measureWidth('40×20mm', 8, false);
_drawText(canvas, '40×20mm', w - 10 - szW, footerTop + 3.5, 8, szW,
color: cFoot);
@@ -287,16 +287,19 @@ Future<ui.Image> _renderLabelBitmap({
const double qrSize = (bodyBot - bodyTop) - 2 * padV; // 109
const double qrTop = bodyTop + padV; // 30
const double qrLeft = w - padH - qrSize; // 203
try {
final codec = await ui.instantiateImageCodec(qrBytes);
final qrImg = (await codec.getNextFrame()).image;
canvas.drawImageRect(
qrImg,
ui.Rect.fromLTWH(0, 0, qrImg.width.toDouble(), qrImg.height.toDouble()),
const ui.Rect.fromLTWH(qrLeft, qrTop, qrSize, qrSize),
ui.Paint());
} catch (e) {
debugPrint('[label] QR 解码失败: $e');
if (drawQr) {
try {
final codec = await ui.instantiateImageCodec(qrBytes);
final qrImg = (await codec.getNextFrame()).image;
canvas.drawImageRect(
qrImg,
ui.Rect.fromLTWH(
0, 0, qrImg.width.toDouble(), qrImg.height.toDouble()),
const ui.Rect.fromLTWH(qrLeft, qrTop, qrSize, qrSize),
ui.Paint());
} catch (e) {
debugPrint('[label] QR 解码失败: $e');
}
}
// 左列几何
@@ -308,7 +311,7 @@ Future<ui.Image> _renderLabelBitmap({
const double barH = 40.0;
const double barBottom = bodyBot - padV; // 137
const double barTop = barBottom - barH; // 97
_drawBarcode(canvas, code, leftX, barTop, leftW, barH);
if (drawBarcode) _drawBarcode(canvas, code, leftX, barTop, leftW, barH);
// 左列文字块(品名+编号 一行居中;度数/规格/日期 平铺一行),在条码上方居中
const double infoTop = bodyTop + padV; // 32
@@ -379,64 +382,10 @@ Future<ui.Image> _renderLabelBitmap({
return recorder.endRecording().toImage(sw, sh); // 返回 2× 图(640×320
}
/// UTF-8 → GBK 字节:
/// Windows 直接调 WideCharToMultiByte(936) FFI,无需插件;
/// macOS 走 charset_converterCoreFoundation 原生支持 GBK)。
Uint8List _encodeGbk(String text) {
if (text.isEmpty) return Uint8List(0);
if (Platform.isWindows) {
final wideStr = text.toNativeUtf16();
try {
final needed = WideCharToMultiByte(
936, 0, wideStr, -1, nullptr, 0, nullptr, nullptr);
if (needed <= 1) return Uint8List(0);
final buf = calloc<Uint8>(needed);
try {
WideCharToMultiByte(
936, 0, wideStr, -1, buf.cast(), needed, nullptr, nullptr);
return Uint8List.fromList(buf.asTypedList(needed - 1)); // 不含 null 终止符
} finally {
calloc.free(buf);
}
} finally {
calloc.free(wideStr);
}
}
// macOS: charset_converter(同步 CoreFoundation
// encode() 在 macOS 上是同步 channel 调用,此处以 Future.value 包装保持接口一致
throw UnsupportedError('call _encodeGbkAsync on macOS');
}
Future<Uint8List> _encodeGbkAsync(String text) async {
if (text.isEmpty) return Uint8List(0);
if (Platform.isWindows) return _encodeGbk(text);
return CharsetConverter.encode('GBK', text);
}
/// 估算 TSPL 内置点阵字体下一行文本的像素宽度(dots):
/// 全角(CJK) 字符宽 = 字高;半角(ASCII) 宽 = 字高/2。用于居中 / 平铺定位。
int _estTsplWidth(String s, int fontH) {
var w = 0;
for (final r in s.runes) {
w += r > 0x7F ? fontH : (fontH ~/ 2);
}
return w;
}
/// TSPL TEXT 命令辅助:将文本以 GBK 编码写入 BytesBuilder。
/// TSS24.BF2/TSS16.BF2 是 TSC 固件内置简体中文点阵字体,需 GBK 编码。
Future<void> _addTextCmd(
BytesBuilder buf, int x, int y, String font, String text) async {
if (text.isEmpty) return;
final gbk = await _encodeGbkAsync(text);
buf.add(ascii.encode('TEXT $x,$y,"$font",0,1,1,"'));
buf.add(gbk);
buf.add(ascii.encode('"\r\n'));
}
/// 桌面端把标签以 TSPL TEXT(内置中文点阵字体)+ QR BITMAP 混合方式裸发到热敏机。
/// TEXT 走 TSS24.BF2/TSS16.BF2GBK 编码,203 DPI 下比 TrueType 位图清晰数倍)。
/// QR 仍走 BITMAP(内置 QRCODE 命令扫描不可靠)。
/// 桌面端把整张标签「光栅化」成单色位图裸发到热敏机(TSPL BITMAP),令热敏输出与
/// 预览画布 100% 一致——彻底消除内置点阵字(TSS16/24)与预览 TrueType 度量不一致
/// 导致的字体重叠 / 失真。文字/抬头/页脚走光栅;条码(Code128)与二维码仍走原生
/// BARCODE / BITMAP 命令保证扫码可靠(光栅化后细条/密码易糊)。
/// 检测不到热敏机 -> 返回 false(交系统打印);检测到则强制 TSPL,失败抛异常。
/// [printerName] 非空时跳过自动检测直接使用,为空则走 _findThermalPrinter()。
Future<bool> _printFlatLabelThermal({
@@ -452,23 +401,23 @@ Future<bool> _printFlatLabelThermal({
debugPrint('[label] thermal printer = $printer');
if (printer == null) return false;
// 版面(320×160 dotsDIRECTION 1):深蓝头/米色脚在单色热敏上分别退化为
// 「黑底反白店名」「白底黑字页脚」;主体两列:左=文字+条码,右=二维码占整高。
const headerH = 26; // 头部黑带高
const bodyTop = headerH; // 26
const bodyBot = 134; // 主体下沿(页脚之上)
const w = 320, h = 160; // 40×20mm @203dpi(逻辑尺寸)
const wBytes = (w + 7) >> 3; // 40 bytes/row
// 右列二维码:占主体整高(正方形),右对齐(放大,占满主体高度)
const qrW = 104, qrH = 104;
const qrX = 320 - 8 - qrW; // 208
const qrY = bodyTop + ((bodyBot - bodyTop) - qrH) ~/ 2; // 28
const qrWBytes = (qrW + 7) >> 3; // 13 bytes/row
final codec = await ui.instantiateImageCodec(qrBytes,
targetWidth: qrW, targetHeight: qrH);
final qrImg = (await codec.getNextFrame()).image;
final qrBd = await qrImg.toByteData(format: ui.ImageByteFormat.rawRgba);
final qrRgba = qrBd!.buffer.asUint8List();
// ── 文字/抬头/页脚整幅光栅:复用预览画布(跳过条码+QR,二者改原生命令)─────────
// 画布内部 2× 超采样(640×320),此处 2×2 盒式降采样 + 亮度 128 阈值二值化,边缘更干净。
final img = await _renderLabelBitmap(
shop: shop,
name: name,
code: code,
subs: subs,
qrBytes: qrBytes,
drawBarcode: false,
drawQr: false,
);
final bd = await img.toByteData(format: ui.ImageByteFormat.rawRgba);
final rgba = bd!.buffer.asUint8List();
final iw = img.width; // 640
final buf = BytesBuilder();
buf.add(ascii.encode(
@@ -476,22 +425,43 @@ Future<bool> _printFlatLabelThermal({
'REFERENCE 0,0\r\nDENSITY 10\r\nSPEED 2\r\nCLS\r\n',
));
// ── 深蓝店名头(黑底反白):先画黑字店名,再 REVERSE 整条头部 → 白字黑底 ──
await _addTextCmd(buf, 8, 5, 'TSS16.BF2', shop);
buf.add(ascii.encode('REVERSE 0,0,320,$headerH\r\n'));
// 整幅文字位图(mode 0 覆写整版):深蓝抬头→黑底反白、米色页脚→白底黑字,
// 均由亮度阈值自然还原,无需 REVERSE;bit 0=黑点,默认 0xFF=白。
buf.add(ascii.encode('BITMAP 0,0,$wBytes,$h,0,'));
for (int y = 0; y < h; y++) {
final row = Uint8List(wBytes)..fillRange(0, wBytes, 0xFF);
for (int x = 0; x < w; x++) {
int lum = 0;
for (int dy = 0; dy < 2; dy++) {
for (int dx = 0; dx < 2; dx++) {
final i = ((y * 2 + dy) * iw + (x * 2 + dx)) * 4;
final a = rgba[i + 3];
lum += a < 128
? 255
: (0.299 * rgba[i] + 0.587 * rgba[i + 1] + 0.114 * rgba[i + 2])
.round();
}
}
if (lum ~/ 4 < 128) row[x >> 3] &= ~(0x80 >> (x & 7));
}
buf.add(row);
}
buf.add(ascii.encode('\r\n'));
// ── 米色页脚(白底黑字):店名 + 尺寸 ──
const footerY = 140;
await _addTextCmd(buf, 8, footerY, 'TSS16.BF2', shop);
const sizeStr = '40×20mm';
final sizeW = _estTsplWidth(sizeStr, 16);
await _addTextCmd(
buf, (320 - 8 - sizeW).clamp(8, 312), footerY, 'TSS16.BF2', sizeStr);
// ── 右列二维码 BITMAP ──
// ── 右列二维码(原生 BITMAP,避免光栅降采样糊码):占主体整高、右对齐 ──
const qrW = 104, qrH = 104;
const qrX = 320 - 8 - qrW; // 208
const bodyTop = 26, bodyBot = 134;
const qrY = bodyTop + ((bodyBot - bodyTop) - qrH) ~/ 2; // 28
const qrWBytes = (qrW + 7) >> 3; // 13
final qrCodec = await ui.instantiateImageCodec(qrBytes,
targetWidth: qrW, targetHeight: qrH);
final qrImg = (await qrCodec.getNextFrame()).image;
final qrBd = await qrImg.toByteData(format: ui.ImageByteFormat.rawRgba);
final qrRgba = qrBd!.buffer.asUint8List();
buf.add(ascii.encode('BITMAP $qrX,$qrY,$qrWBytes,$qrH,1,'));
for (int yy = 0; yy < qrH; yy++) {
final row = Uint8List(qrWBytes)..fillRange(0, qrWBytes, 0xFF); // 默认白
final row = Uint8List(qrWBytes)..fillRange(0, qrWBytes, 0xFF);
for (int xx = 0; xx < qrW; xx++) {
final i = (yy * qrW + xx) * 4;
final a = qrRgba[i + 3];
@@ -504,12 +474,8 @@ Future<bool> _printFlatLabelThermal({
}
buf.add(ascii.encode('\r\n'));
// 左列几何(与二维码列间距收小到 6
const leftX = 8,
leftRight = qrX - 6,
leftW = leftRight - leftX; // 8..202, 194
// ── 左列底部整幅一维条码(Code128,内容=商品编号),narrow 自适应铺满并居中 ──
// ── 左列底部整幅一维条码(原生 BARCODE,Code128,内容=商品编号),narrow 自适应铺满居中 ──
const leftX = 8, leftRight = qrX - 6, leftW = leftRight - leftX; // 8..202, 194
const kBarH = 28;
const kBarY = bodyBot - 4 - kBarH; // 102
if (code.isNotEmpty) {
@@ -521,50 +487,10 @@ Future<bool> _printFlatLabelThermal({
'BARCODE $barX,$kBarY,"128",$kBarH,0,0,$narrow,$narrow,"$code"\r\n'));
}
// ── 左列文字块:品名+编号 一行居中;度数/规格/日期 平铺一行;整体在条码上方居中 ──
const blockTop = bodyTop, blockBot = kBarY - 4; // 26..98
final nameFont = name.runes.length <= 7 ? 'TSS24.BF2' : 'TSS16.BF2';
final nameH = nameFont == 'TSS24.BF2' ? 24 : 16;
const subH = 16, kGap = 8;
final blockH = nameH + kGap + subH;
var nameY = blockTop + ((blockBot - blockTop) - blockH) ~/ 2;
if (nameY < blockTop) nameY = blockTop;
final subY = nameY + nameH + kGap;
// 品名 + 编号:整体水平居中,编号小字随后,基线对齐
final nameW = _estTsplWidth(name, nameH);
final codeW = code.isNotEmpty ? _estTsplWidth(code, 16) : 0;
final groupW = nameW + (code.isNotEmpty ? kGap + codeW : 0);
var nameX = leftX + (leftW - groupW) ~/ 2;
if (nameX < leftX) nameX = leftX;
await _addTextCmd(buf, nameX, nameY, nameFont, name);
if (code.isNotEmpty) {
await _addTextCmd(
buf, nameX + nameW + kGap, nameY + (nameH - subH), 'TSS16.BF2', code);
}
// 三字段平铺一行(space-between,两侧留内边距使外白 > 中白)
const innerPad = 10; // 对齐原型 .lbl-sub padding:0 10px
const innerLeft = leftX + innerPad, innerW = leftW - 2 * innerPad; // 18, 174
if (subs.length == 1) {
final w0 = _estTsplWidth(subs[0], subH);
await _addTextCmd(
buf, innerLeft + (innerW - w0) ~/ 2, subY, 'TSS16.BF2', subs[0]);
} else if (subs.isNotEmpty) {
final widths = subs.map((s) => _estTsplWidth(s, subH)).toList();
final sumW = widths.fold(0, (s, x) => s + x);
final gap = (innerW - sumW) / (subs.length - 1);
var cx = innerLeft.toDouble();
for (int i = 0; i < subs.length; i++) {
await _addTextCmd(buf, cx.round(), subY, 'TSS16.BF2', subs[i]);
cx += widths[i] + gap;
}
}
buf.add(ascii.encode('PRINT 1,1\r\n'));
final tspl = buf.toBytes();
debugPrint('[label] TEXT+BITMAP tspl ${tspl.length}B -> $printer');
debugPrint('[label] raster BITMAP+QR+BARCODE tspl ${tspl.length}B -> $printer');
await _sendRaw(printer, tspl);
return true;
}
@@ -728,22 +654,15 @@ Future<void> printProductLabelImpl({
),
),
// ── Footer:米色页脚(店名 + 尺寸)────────────────────────────────────
// ── Footer:米色页脚(仅尺寸;店名已在抬头,不再重复)─────────────────
pw.Container(
height: footerH,
color: _cream,
padding: const pw.EdgeInsets.symmetric(horizontal: 5),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
mainAxisAlignment: pw.MainAxisAlignment.end,
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
pw.Flexible(
child: pw.Text(shopName,
maxLines: 1,
overflow: pw.TextOverflow.clip,
style: pw.TextStyle(
font: font, fontSize: 3.5, color: _footnote)),
),
pw.Text('40×20mm',
style: pw.TextStyle(
font: font, fontSize: 3.5, color: _footnote)),
+2 -2
View File
@@ -117,7 +117,7 @@ body { width: 40mm; height: 20mm; overflow: hidden; background: #fff; }
.qr { flex-shrink: 0; height: 100%; aspect-ratio: 1; }
.qr img { width: 100%; height: 100%; object-fit: contain; display: block; }
/* ── Footer:米色页脚(店名 + 尺寸)── */
/* ── Footer:米色页脚(仅尺寸;店名已在抬头,不再重复)── */
.ftr {
background: #f4ecd8; height: 2.4mm; flex-shrink: 0;
display: flex; align-items: center; justify-content: space-between;
@@ -146,7 +146,7 @@ body { width: 40mm; height: 20mm; overflow: hidden; background: #fff; }
</div>
</div>
<div class="ftr"><span>$shopName</span><span>40×20mm</span></div>
<div class="ftr"><span></span><span>40×20mm</span></div>
</div>
<script>
@@ -185,6 +185,8 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
Map<String, _PickerItem> _codeIndex = {};
String _lastScanCode = '';
int _lastScanMs = 0;
// 扫码枪=HID键盘极快连打字符后停顿;停顿 120ms 无新输入即自动触发,免手动回车。
Timer? _scanDebounce;
static const _fields = ['qty', 'sale'];
@@ -265,6 +267,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
_remarkCtrl.dispose();
_partnerFocus.dispose();
_warehouseFocus.dispose();
_scanDebounce?.cancel();
_scanCtrl.dispose();
_scanFocus.dispose();
for (final item in _items) {
@@ -354,6 +357,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
/// 扫码枪回车触发。解析编码→本地整仓索引命中(已加载项)→未命中再走服务端
/// 按编码精确查(覆盖库存超本地加载上限 1000 的大仓)→加行/累加数量。
Future<void> _onScan(String raw) async {
_scanDebounce?.cancel();
// 立即清空并保持聚焦,承接下一次扫码
_scanCtrl.clear();
_scanFocus.requestFocus();
@@ -697,6 +701,14 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
controller: _scanCtrl,
focusNode: _scanFocus,
hintText: '扫码枪扫码…',
onChanged: (v) {
// 扫码枪整串打完会停顿,停顿 120ms 视为一次完整扫入 → 自动触发。
// 带回车后缀的枪仍走 onSubmitted_onScan 内 300ms 同码去重防重复入单。
_scanDebounce?.cancel();
if (v.trim().isEmpty) return;
_scanDebounce = Timer(
const Duration(milliseconds: 120), () => _onScan(v));
},
onSubmitted: _onScan,
suffix: Icon(LucideIcons.scanLine,
size: 16, color: context.tokens.muted),
+1 -1
View File
@@ -172,7 +172,7 @@
</div>
<div class="lbl-qr"><img alt="二维码" src="data:image/png;base64,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" /></div>
</div>
<div class="lbl-ftr"><span>岩美酒行</span><span>40×20mm</span></div>
<div class="lbl-ftr"><span></span><span>40×20mm</span></div>
</div>
<div class="lbl-note">40×20mm 热敏价签:二维码溯源 + 底部整幅一维条码(Code128,内容=商品编号,供扫码枪扫码出库)。原型为示意布局,实际由客户端热敏/PDF/Web 三路渲染。</div>
</div>