feat:取消系统自带软键盘设计,改为系统自用键盘组件-初步

feat:将打印分为图片打印和命令式打印-初步
This commit is contained in:
dzc committed 2026-09-20 18:24:01 +08:00
1 parent 94ee7415b1
commit e68f41375b
38 files changed
+1471 -408

No files matched your search

+1
View File
@@ -147,6 +147,7 @@ class AppMqTool {
androidOrderId: msg.androidOrderCode,
historyId: msg.historyId!,
pic: msg.pic!,
printCmd: msg.printCmd,
printType: msg.printType ?? '1',
printNo: msg.printNo ?? 1,
retryVersion: msg.retryVersion ?? 0,
@@ -91,6 +91,15 @@ abstract class AppSettingTool {
static const String _productMealPeriodsKey = "productMealPeriods";
static const String _serialCardConfigKey = "serialCardConfig";
/// 打印输出方式在 sysJson 中的键,与 [SysSetting.printerOutputMode] 字段名一致。
static const String _printerOutputModeKey = "printerOutputMode";
/// 打印输出方式取值:下载后端图片转 ESC/POS 栅格指令(默认,兼容所有后端版本)。
static const String printerOutputModeImage = 'IMAGE';
/// 打印输出方式取值:直接写入后端随打印任务下发的 Base64 指令流,出纸更快。
static const String printerOutputModeCommand = 'COMMAND';
/// v8 之前短暂保存定时任务配置的旧 sysJson 键,仅供一次性迁移读取。
static const String _scheduledTaskConfigsKey = "scheduledTaskConfigs";
@@ -947,6 +956,15 @@ abstract class AppSettingTool {
return setting;
}
/// 切换本机打印输出方式:IMAGE=图片栅格打印,COMMAND=指令流直写。
///
/// 仅写 sysJson 局部键,不触碰后台下发的打印机绑定字段;指令模式下
/// 无指令数据的旧任务会自动回退图片打印,因此可随时来回切换。
static Future<void> setPrinterOutputMode(String mode) =>
_updateSysJson((sysJson) {
sysJson[_printerOutputModeKey] = mode;
});
/// 绑定设备信息
static Future<bool> bindSys(int serviceId) async {
var androidId = await AppSysTool.getAndroidId();
+111 -15
View File
@@ -178,6 +178,47 @@ class PrintManage extends BaseFacilityManage {
}
}
/// 将后端拼装的 ESC/POS 指令流写入 USB;前后补走纸,并按本机配置追加切刀。
///
/// 指令内容由服务端按凭证版式拼装,本方法只负责包装边界与写入;
/// 返回 true 只表示指令、走纸和可选切刀命令均被 USB 接口接受,不代表从硬件层确认出纸。
Future<bool> printCommand(List<int> commandBytes) async {
if (ObjectUtil.isEmpty(manager)) {
AppDeviceStatusTool.updatePrinterWorking(false);
return false;
}
try {
if (commandBytes.isEmpty) {
lastError = '打印指令内容为空';
return false;
}
final setting = await AppSettingTool.getSysSetting();
final generator = EscGenerator(
paperSizeWidthMM,
maxPerLine,
profile!,
);
final bytes = <int>[
...generator.feed(1),
...commandBytes,
...generator.feed(1),
if (setting.printerAutoCut) ...generator.cut(),
];
AppDeviceStatusTool.updatePrinterWorking(true);
await manager!.writeBytes(bytes);
lastError = null;
AppDeviceStatusTool.updatePrinter(true);
return true;
} catch (e) {
lastError = e.toString();
AppDeviceStatusTool.updatePrinter(false);
logger.e("|$_tag|指令打印失败error|$e|");
return false;
} finally {
AppDeviceStatusTool.updatePrinterWorking(false);
}
}
/// 将测试文字和灰度校准块渲染成 576px PNG,避免依赖打印机中文代码页。
Future<imglib.Image> _buildTestPageImage(
PrinterUsbDeviceInfo device,
@@ -351,21 +392,76 @@ class PrintManage extends BaseFacilityManage {
DateTime.now(),
format: DateFormats.full,
);
final imageMode =
setting.printerImageMode == 'ESC_STAR' ? 'ESC_STAR' : 'GS_V0';
final testPage = await _buildTestPageImage(
device,
printedAt,
imageMode,
);
final bytes = <int>[
...generator.feed(1),
...(imageMode == 'ESC_STAR'
? generator.image(testPage)
: generator.imageRaster(testPage)),
...generator.feed(2),
if (setting.printerAutoCut) ...generator.cut(),
];
// 指令模式下测试打印同样走指令链路,验证的才是本机实际使用的打印通道;
// 文本行控制在 48 列(80mm)以内,避免依赖包内 row 折行的既有缺陷。
final List<int> bytes;
if (setting.printerOutputMode ==
AppSettingTool.printerOutputModeCommand) {
bytes = <int>[
...generator.feed(1),
...generator.text(
'HQ 打印机测试',
styles: const PosStyles(
align: PosAlign.center,
bold: true,
height: PosTextSize.size2,
width: PosTextSize.size2,
),
containsChinese: true,
linesAfter: 1,
),
...generator.text(
'指令链路测试',
styles: const PosStyles(align: PosAlign.center),
containsChinese: true,
linesAfter: 1,
),
...generator.hr(),
...generator.text('打印时间:$printedAt', containsChinese: true),
...generator.text(
'打印机:${device.displayName}',
containsChinese: true,
),
...generator.text(
'VID:${device.vendorId}',
containsChinese: true,
),
...generator.text(
'PID:${device.productId}',
containsChinese: true,
),
...generator.text('纸宽:80mm / 48列', containsChinese: true),
...generator.text('打印方式:指令打印', containsChinese: true),
...generator.hr(),
...generator.text(
'TEST PRINT COMPLETED',
styles: const PosStyles(align: PosAlign.center, bold: true),
),
...generator.text(
'如能看到本页文字,指令打印链路正常',
styles: const PosStyles(align: PosAlign.center),
containsChinese: true,
),
...generator.feed(2),
if (setting.printerAutoCut) ...generator.cut(),
];
} else {
final imageMode =
setting.printerImageMode == 'ESC_STAR' ? 'ESC_STAR' : 'GS_V0';
final testPage = await _buildTestPageImage(
device,
printedAt,
imageMode,
);
bytes = <int>[
...generator.feed(1),
...(imageMode == 'ESC_STAR'
? generator.image(testPage)
: generator.imageRaster(testPage)),
...generator.feed(2),
if (setting.printerAutoCut) ...generator.cut(),
];
}
testManager = USBPrinterManager(
USBPrinter(
name: device.productName,
+43 -2
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:common_utils/common_utils.dart';
import 'package:get/get.dart';
@@ -34,12 +35,14 @@ abstract class SyncPrintTool {
/// 将 MQTT 或 HTTP 待办按服务端任务 ID 转换为唯一的本地任务。
///
/// [orderId] 是服务端订单 ID,[androidOrderId] 允许线上预约订单为空;[historyId]
/// 是本地唯一去重键,[retryVersion] 只有高于本地失败版本时才允许再次进入打印队列。
/// 是本地唯一去重键,[retryVersion] 只有高于本地失败版本时才允许再次进入打印队列;
/// [printCmd] 是后端拼装的 Base64 指令流,指令打印模式直写 USB,可空。
static Future<void> addTask({
required String orderId,
String? androidOrderId,
required String historyId,
required String pic,
String? printCmd,
String printType = '1',
int printNo = 1,
int retryVersion = 0,
@@ -49,6 +52,7 @@ abstract class SyncPrintTool {
androidOrderId: androidOrderId,
historyId: historyId,
pic: pic,
printCmd: printCmd,
printType: printType,
printNo: printNo,
retryVersion: retryVersion,
@@ -67,6 +71,7 @@ abstract class SyncPrintTool {
String? androidOrderId,
required String historyId,
required String pic,
required String? printCmd,
required String printType,
required int printNo,
required int retryVersion,
@@ -90,6 +95,7 @@ abstract class SyncPrintTool {
: orderId
..serviceOrderId = orderId
..invoiceUrl = pic
..printCmd = printCmd?.trim().isNotEmpty == true ? printCmd : null
..printType = printType
..printNo = printNo
..retryVersion = retryVersion
@@ -108,6 +114,7 @@ abstract class SyncPrintTool {
serviceOrderId: orderId,
servicePrintId: historyId,
invoiceUrl: pic,
printCmd: printCmd?.trim().isNotEmpty == true ? printCmd : null,
printType: printType,
printNo: printNo,
retryVersion: retryVersion,
@@ -150,6 +157,7 @@ abstract class SyncPrintTool {
androidOrderId: task.androidOrderCode,
historyId: task.historyId!,
pic: task.pic!,
printCmd: task.printCmd,
printType: task.printType ?? '1',
printNo: task.printNo ?? 1,
retryVersion: task.retryVersion ?? 0,
@@ -207,6 +215,8 @@ abstract class SyncPrintTool {
static Future<void> _print(PrintHistoryEntity task) async {
final app = Get.find<AppControl>();
final dao = app.dataBase.getPrintHistoryDao;
// 指令解码在尝试循环外完成一次:Base64 损坏属于任务数据问题,重试不会好转,直接回退图片。
final commandBytes = await _resolveCommandBytes(task);
for (var index = 0; index < attemptDelays.length; index++) {
final delay = attemptDelays[index];
if (delay > Duration.zero) {
@@ -220,7 +230,11 @@ abstract class SyncPrintTool {
if (!opened) {
throw StateError(app.printManage.lastError ?? 'USB 打印机连接失败');
}
final accepted = await app.printManage.printImg(task.invoiceUrl);
// 指令字节可用时直写指令流;图片模式、无指令或指令损坏时走图片栅格打印。
// 指令写入失败只按原节奏重试指令,不回退图片,避免同一任务两种内容先后出纸。
final accepted = commandBytes != null
? await app.printManage.printCommand(commandBytes)
: await app.printManage.printImg(task.invoiceUrl);
if (!accepted) {
throw StateError(app.printManage.lastError ?? 'USB 未完整接收打印数据');
}
@@ -250,6 +264,33 @@ abstract class SyncPrintTool {
await _report(task);
}
/// 指令打印模式下解码任务携带的 Base64 打印指令流。
///
/// 图片模式、任务无指令、内容为空或 Base64 损坏时返回 null,调用方回退图片栅格打印;
/// 解码失败不进入 USB 重试节奏,因为重试同一份损坏数据不会改变结果。
static Future<List<int>?> _resolveCommandBytes(
PrintHistoryEntity task,
) async {
final setting = await AppSettingTool.getSysSetting();
if (setting.printerOutputMode != AppSettingTool.printerOutputModeCommand) {
return null;
}
final raw = task.printCmd?.trim() ?? '';
if (raw.isEmpty) return null;
try {
final decoded = base64.decode(raw.replaceAll(RegExp(r'\s'), ''));
if (decoded.isEmpty) {
logger.w('|$_tag|打印指令内容为空,回退图片打印|task:${task.servicePrintId}|');
return null;
}
return decoded;
} catch (error) {
logger.w(
'|$_tag|打印指令解码失败,回退图片打印|task:${task.servicePrintId}|error:$error|');
return null;
}
}
/// 向服务端提交当前重试版本的最终结果;失败时保留 resultPending 等待网络恢复补报。
static Future<void> _report(PrintHistoryEntity task) async {
if (!AppNetworkTool.isAutoNetwork()) {