feat:取消系统自带软键盘设计,改为系统自用键盘组件-初步
feat:将打印分为图片打印和命令式打印-初步
This commit is contained in:
1 parent
94ee7415b1
commit
e68f41375b
38 files changed
+1471
-408
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in new issue
Block a user