feat(client): 标签打印预览弹窗(打印机选择 + 份数 + 所见即所得预览)
- 新增 LabelData 模型统一聚合单张标签字段(qrBytes/copies 等) - 抽取 _renderLabelBitmap 共享位图绘制逻辑,TSPL 裸发与预览渲染完全一致 - 新增 renderLabelPreview / listLabelPrinters / detectDefaultPrinter 公开 API - printProductLabel 新增 printerName 参数,透传到热敏裸发路径 - 新建 LabelPreviewDialog:缩略图条+大图主视图,打印机下拉+记忆(SharedPrefs) 份数调整、勾选跳过、逐张打印进度状态显示 - 库存/商品详情/入库单 打标签 均接入预览弹窗,移除旧 _LabelPrintDialog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
/// 单张标签的全部打印字段,预览和打印共用同一份数据。
|
||||
class LabelData {
|
||||
/// 商品 ID,多张懒加载 QR 时用于拉取二维码字节。
|
||||
final int? productId;
|
||||
|
||||
/// 二维码字节(PNG);单张场景调用方提前拉好;多张场景由 qrFetcher 懒加载填入。
|
||||
Uint8List? qrBytes;
|
||||
|
||||
final String name;
|
||||
final String code;
|
||||
final String? spec;
|
||||
final String? series;
|
||||
final String? batchNo;
|
||||
final String? productionDate;
|
||||
final String? remark;
|
||||
final String shopName;
|
||||
final String shopAddress;
|
||||
final String shopPhone;
|
||||
|
||||
/// 打印份数,默认 1,可在预览弹窗里调整。
|
||||
int copies;
|
||||
|
||||
LabelData({
|
||||
this.productId,
|
||||
this.qrBytes,
|
||||
required this.name,
|
||||
required this.code,
|
||||
this.spec,
|
||||
this.series,
|
||||
this.batchNo,
|
||||
this.productionDate,
|
||||
this.remark,
|
||||
this.shopName = '',
|
||||
this.shopAddress = '',
|
||||
this.shopPhone = '',
|
||||
this.copies = 1,
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import '../../models/stock_in.dart';
|
||||
import '../../models/stock_out.dart';
|
||||
import '../errors/error_reporter.dart';
|
||||
import 'label_data.dart';
|
||||
export 'label_data.dart';
|
||||
import 'print_util_stub.dart'
|
||||
if (dart.library.js_interop) 'print_util_web.dart';
|
||||
|
||||
@@ -18,6 +20,7 @@ Future<void> printProductLabel({
|
||||
String shopName = '',
|
||||
String shopAddress = '',
|
||||
String shopPhone = '',
|
||||
String? printerName,
|
||||
}) =>
|
||||
printProductLabelImpl(
|
||||
qrBytes: qrBytes,
|
||||
@@ -31,8 +34,19 @@ Future<void> printProductLabel({
|
||||
shopName: shopName,
|
||||
shopAddress: shopAddress,
|
||||
shopPhone: shopPhone,
|
||||
printerName: printerName,
|
||||
);
|
||||
|
||||
/// 渲染标签预览图(PNG 字节);Web 平台返回 null。
|
||||
Future<Uint8List?> renderLabelPreview(LabelData label) =>
|
||||
renderLabelPreviewImpl(label);
|
||||
|
||||
/// 枚举当前系统所有可用打印机名;Web 平台返回空列表。
|
||||
Future<List<String>> listLabelPrinters() => listLabelPrintersImpl();
|
||||
|
||||
/// 自动检测默认热敏打印机(名字含 deli/dl-888);找不到或 Web 返回 null。
|
||||
Future<String?> detectDefaultPrinter() => detectDefaultPrinterImpl();
|
||||
|
||||
Future<void> printStockInOrder(StockInOrder order) =>
|
||||
printStockInOrderImpl(order);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:printing/printing.dart';
|
||||
import '../../models/stock_in.dart';
|
||||
import '../../models/stock_out.dart';
|
||||
import 'label_data.dart';
|
||||
|
||||
Future<pw.Font> _loadFont() async {
|
||||
final data = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf');
|
||||
@@ -188,21 +189,15 @@ double _fitFont(String s, double maxW, double maxH, bool bold, double cap) {
|
||||
return size;
|
||||
}
|
||||
|
||||
/// 桌面端把「扁平标签」直接画成单色位图,TSPL BITMAP 裸发到热敏机。
|
||||
/// 版式(无样式):酒行名 / 酒名(长名缩小) / 度数(系列)+规格 / 日期;右侧二维码 + 扫码溯源。
|
||||
/// 检测不到热敏机 -> 返回 false(交系统打印);检测到则强制 TSPL,失败抛异常。
|
||||
Future<bool> _printFlatLabelThermal({
|
||||
/// 用 dart:ui 把一张标签绘制成 320×160 位图(热敏标签实际尺寸 40×20mm @203dpi)。
|
||||
/// 供 TSPL 裸发和预览渲染共用同一套画布逻辑,确保预览即实际输出。
|
||||
Future<ui.Image> _renderLabelBitmap({
|
||||
required String shop,
|
||||
required String name,
|
||||
required String degSpec,
|
||||
required String date,
|
||||
required Uint8List qrBytes,
|
||||
}) async {
|
||||
if (kIsWeb || !(Platform.isMacOS || Platform.isWindows)) return false;
|
||||
final printer = await _findThermalPrinter();
|
||||
debugPrint('[label] thermal printer = $printer');
|
||||
if (printer == null) return false;
|
||||
|
||||
const w = 320, h = 160; // 40×20mm @203dpi
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = ui.Canvas(
|
||||
@@ -248,7 +243,29 @@ Future<bool> _printFlatLabelThermal({
|
||||
yy += rowH;
|
||||
}
|
||||
|
||||
final img = await recorder.endRecording().toImage(w, h);
|
||||
return recorder.endRecording().toImage(w, h);
|
||||
}
|
||||
|
||||
/// 桌面端把「扁平标签」直接画成单色位图,TSPL BITMAP 裸发到热敏机。
|
||||
/// 版式(无样式):酒行名 / 酒名(长名缩小) / 度数(系列)+规格 / 日期;右侧二维码 + 扫码溯源。
|
||||
/// 检测不到热敏机 -> 返回 false(交系统打印);检测到则强制 TSPL,失败抛异常。
|
||||
/// [printerName] 非空时跳过自动检测直接使用,为空则走 _findThermalPrinter()。
|
||||
Future<bool> _printFlatLabelThermal({
|
||||
required String shop,
|
||||
required String name,
|
||||
required String degSpec,
|
||||
required String date,
|
||||
required Uint8List qrBytes,
|
||||
String? printerName,
|
||||
}) async {
|
||||
if (kIsWeb || !(Platform.isMacOS || Platform.isWindows)) return false;
|
||||
final printer = printerName ?? await _findThermalPrinter();
|
||||
debugPrint('[label] thermal printer = $printer');
|
||||
if (printer == null) return false;
|
||||
|
||||
const w = 320, h = 160;
|
||||
final img = await _renderLabelBitmap(
|
||||
shop: shop, name: name, degSpec: degSpec, date: date, qrBytes: qrBytes);
|
||||
final bd = await img.toByteData(format: ui.ImageByteFormat.rawRgba);
|
||||
final rgba = bd!.buffer.asUint8List();
|
||||
|
||||
@@ -290,6 +307,7 @@ Future<void> printProductLabelImpl({
|
||||
String shopName = '',
|
||||
String shopAddress = '',
|
||||
String shopPhone = '',
|
||||
String? printerName,
|
||||
}) async {
|
||||
try {
|
||||
final font = await _loadFont();
|
||||
@@ -316,7 +334,8 @@ Future<void> printProductLabelImpl({
|
||||
name: name,
|
||||
degSpec: degSpecVal,
|
||||
date: dateVal == '—' ? '' : dateVal, // 无生产日期(商品详情)则不显示该行
|
||||
qrBytes: qrBytes)) {
|
||||
qrBytes: qrBytes,
|
||||
printerName: printerName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -449,6 +468,60 @@ Future<void> printProductLabelImpl({
|
||||
}
|
||||
}
|
||||
|
||||
/// 渲染标签预览图(PNG 字节),与实际热敏打印使用相同画布逻辑(所见即所得)。
|
||||
/// Web 平台或 qrBytes 为空时抛异常。
|
||||
Future<Uint8List> renderLabelPreviewImpl(LabelData label) async {
|
||||
final specVal = (label.spec ?? '').isNotEmpty ? label.spec! : '';
|
||||
final seriesVal = (label.series ?? '').isNotEmpty ? label.series! : '';
|
||||
final degSpecVal = [seriesVal, specVal].where((s) => s.isNotEmpty).join(' ');
|
||||
final dateVal = (label.productionDate ?? '').isNotEmpty
|
||||
? (label.productionDate!.length > 10
|
||||
? label.productionDate!.substring(0, 10)
|
||||
: label.productionDate!)
|
||||
: '';
|
||||
final img = await _renderLabelBitmap(
|
||||
shop: label.shopName,
|
||||
name: label.name,
|
||||
degSpec: degSpecVal,
|
||||
date: dateVal,
|
||||
qrBytes: label.qrBytes!,
|
||||
);
|
||||
final bd = await img.toByteData(format: ui.ImageByteFormat.png);
|
||||
return bd!.buffer.asUint8List();
|
||||
}
|
||||
|
||||
/// 枚举当前系统上所有可用打印机名。
|
||||
/// macOS: CUPS 队列(lpstat -e);Windows: Printing.listPrinters();其他返回空列表。
|
||||
Future<List<String>> listLabelPrintersImpl() async {
|
||||
if (kIsWeb) return [];
|
||||
if (Platform.isMacOS) {
|
||||
try {
|
||||
final r = await Process.run('lpstat', ['-e']);
|
||||
if (r.exitCode == 0) {
|
||||
return (r.stdout as String)
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[label] lpstat -e 失败: $e');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (Platform.isWindows) {
|
||||
try {
|
||||
return (await Printing.listPrinters()).map((p) => p.name).toList();
|
||||
} catch (e) {
|
||||
debugPrint('[label] listPrinters 失败: $e');
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/// 自动检测默认热敏打印机(名字含 deli/dl-888);找不到返回 null。
|
||||
Future<String?> detectDefaultPrinterImpl() => _findThermalPrinter();
|
||||
|
||||
pw.Widget _buildOrderDoc({
|
||||
required pw.Font font,
|
||||
required pw.Font bold,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:web/web.dart' as web;
|
||||
import '../../models/stock_in.dart';
|
||||
import '../../models/stock_out.dart';
|
||||
import '../errors/error_reporter.dart';
|
||||
import 'label_data.dart';
|
||||
|
||||
void _openPrintWindow(String html) {
|
||||
final win = web.window.open('', '_blank');
|
||||
@@ -27,6 +28,7 @@ Future<void> printProductLabelImpl({
|
||||
String shopName = '',
|
||||
String shopAddress = '',
|
||||
String shopPhone = '',
|
||||
String? printerName,
|
||||
}) async {
|
||||
final base64Img = base64Encode(qrBytes);
|
||||
|
||||
@@ -279,6 +281,14 @@ Future<void> printStockInOrderImpl(StockInOrder order) async {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 预览 / 打印机枚举(Web 不支持热敏裸发,返回空值)─────────────────────────
|
||||
|
||||
Future<Uint8List?> renderLabelPreviewImpl(LabelData label) async => null;
|
||||
|
||||
Future<List<String>> listLabelPrintersImpl() async => [];
|
||||
|
||||
Future<String?> detectDefaultPrinterImpl() async => null;
|
||||
|
||||
// ── 出库单 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> printStockOutOrderImpl(StockOutOrder order) async {
|
||||
|
||||
@@ -16,6 +16,8 @@ import '../../widgets/multi_select_dropdown.dart' show FilterableColumnHeader;
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
import '../../core/utils/export_util.dart';
|
||||
import '../../core/utils/print_util.dart';
|
||||
import '../../core/utils/dialog_util.dart';
|
||||
import '../../widgets/label_preview_dialog.dart';
|
||||
import '../../providers/product_provider.dart';
|
||||
import '../../providers/product_option_provider.dart';
|
||||
import '../../providers/tab_state_provider.dart';
|
||||
@@ -200,21 +202,23 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
await ref.read(productRepositoryProvider).getQRCodeBytes(item.productId!);
|
||||
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
|
||||
if (!context.mounted) return;
|
||||
await safePrint(
|
||||
context,
|
||||
() => printProductLabel(
|
||||
qrBytes: qrBytes,
|
||||
name: item.productName,
|
||||
code: item.productCode,
|
||||
series: item.series.isEmpty ? null : item.series,
|
||||
spec: item.spec.isEmpty ? null : item.spec,
|
||||
batchNo: item.batchNo.isEmpty ? null : item.batchNo,
|
||||
productionDate: item.productionDate,
|
||||
remark: item.remark.isEmpty ? null : item.remark,
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
),
|
||||
final label = LabelData(
|
||||
productId: item.productId,
|
||||
qrBytes: qrBytes,
|
||||
name: item.productName,
|
||||
code: item.productCode,
|
||||
series: item.series.isEmpty ? null : item.series,
|
||||
spec: item.spec.isEmpty ? null : item.spec,
|
||||
batchNo: item.batchNo.isEmpty ? null : item.batchNo,
|
||||
productionDate: item.productionDate,
|
||||
remark: item.remark.isEmpty ? null : item.remark,
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
);
|
||||
showAppDialog(
|
||||
context: context,
|
||||
builder: (_) => LabelPreviewDialog(labels: [label]),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/config/app_config.dart';
|
||||
import '../../core/utils/print_util.dart';
|
||||
import '../../widgets/label_preview_dialog.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/product.dart';
|
||||
import '../../models/product_image.dart';
|
||||
@@ -656,8 +657,6 @@ class _QRCodeDialog extends ConsumerStatefulWidget {
|
||||
class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> {
|
||||
late Future<Uint8List> _future;
|
||||
Uint8List? _bytes;
|
||||
bool _printing = false;
|
||||
String _printStatus = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -670,25 +669,25 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _print() async {
|
||||
if (_bytes == null || _printing) return;
|
||||
setState(() { _printing = true; _printStatus = '正在打印...'; });
|
||||
void _print() {
|
||||
if (_bytes == null) return;
|
||||
final p = widget.product;
|
||||
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
|
||||
try {
|
||||
await safePrint(context, () => printProductLabel(
|
||||
qrBytes: _bytes!,
|
||||
name: p.name,
|
||||
code: p.code,
|
||||
spec: p.spec,
|
||||
series: p.series,
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
));
|
||||
} finally {
|
||||
if (mounted) setState(() { _printing = false; _printStatus = ''; });
|
||||
}
|
||||
final label = LabelData(
|
||||
productId: p.id,
|
||||
qrBytes: _bytes,
|
||||
name: p.name,
|
||||
code: p.code,
|
||||
spec: p.spec,
|
||||
series: p.series,
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
);
|
||||
showAppDialog(
|
||||
context: context,
|
||||
builder: (_) => LabelPreviewDialog(labels: [label]),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -717,15 +716,9 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> {
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: _bytes == null ? null : (_printing ? null : _print),
|
||||
icon: _printing
|
||||
? const SizedBox(
|
||||
width: 14, height: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Icon(Icons.print, size: 16),
|
||||
label: Text(_printing
|
||||
? (_printStatus.isNotEmpty ? _printStatus : '准备中...')
|
||||
: '打印'),
|
||||
onPressed: _bytes == null ? null : _print,
|
||||
icon: const Icon(Icons.print, size: 16),
|
||||
label: const Text('打印'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '../../core/utils/dialog_util.dart';
|
||||
import '../../core/errors/error_reporter.dart';
|
||||
import '../../core/utils/print_util.dart' show safePrint, printStockInOrder, printProductLabel;
|
||||
import '../../core/utils/print_util.dart' show safePrint, printStockInOrder, LabelData;
|
||||
import '../../widgets/label_preview_dialog.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -15,7 +16,6 @@ import '../../widgets/multi_select_dropdown.dart' show ColDef, ColumnToggleButto
|
||||
import '../../widgets/page_scaffold.dart';
|
||||
import '../../widgets/status_badge.dart';
|
||||
import '../../core/utils/export_util.dart';
|
||||
import '../../core/utils/print_util.dart';
|
||||
import '../../providers/inventory_provider.dart';
|
||||
import '../../providers/tab_state_provider.dart';
|
||||
import '../../providers/product_provider.dart' show productRepositoryProvider;
|
||||
@@ -365,14 +365,25 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
final order = await ref.read(stockInRepositoryProvider).get(o.id);
|
||||
if (!context.mounted) return;
|
||||
final shopInfo = ref.read(shopInfoProvider).valueOrNull;
|
||||
await showDialog(
|
||||
final labels = order.items
|
||||
.map((item) => LabelData(
|
||||
productId: item.productId,
|
||||
name: item.productName ?? '',
|
||||
code: item.productCode ?? '',
|
||||
series: item.productSeries,
|
||||
spec: item.productSpec,
|
||||
batchNo: item.batchNo,
|
||||
productionDate: item.productionDate,
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
))
|
||||
.toList();
|
||||
showAppDialog(
|
||||
context: context,
|
||||
builder: (_) => _LabelPrintDialog(
|
||||
order: order,
|
||||
productRepo: ref.read(productRepositoryProvider),
|
||||
shopName: shopInfo?.name ?? '',
|
||||
shopAddress: shopInfo?.address ?? '',
|
||||
shopPhone: shopInfo?.phone ?? '',
|
||||
builder: (_) => LabelPreviewDialog(
|
||||
labels: labels,
|
||||
qrFetcher: ref.read(productRepositoryProvider).getQRCodeBytes,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -996,149 +1007,3 @@ class _StatusFilterDropdown extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _LabelPrintDialog extends StatefulWidget {
|
||||
final StockInOrder order;
|
||||
final ProductRepository productRepo;
|
||||
final String shopName;
|
||||
final String shopAddress;
|
||||
final String shopPhone;
|
||||
const _LabelPrintDialog({
|
||||
required this.order,
|
||||
required this.productRepo,
|
||||
this.shopName = '',
|
||||
this.shopAddress = '',
|
||||
this.shopPhone = '',
|
||||
});
|
||||
|
||||
@override
|
||||
State<_LabelPrintDialog> createState() => _LabelPrintDialogState();
|
||||
}
|
||||
|
||||
class _LabelPrintDialogState extends State<_LabelPrintDialog> {
|
||||
late final List<bool> _selected;
|
||||
bool _printing = false;
|
||||
String _status = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selected = List.filled(widget.order.items.length, true);
|
||||
}
|
||||
|
||||
Future<void> _print() async {
|
||||
setState(() { _printing = true; _status = '正在打印...'; });
|
||||
int done = 0;
|
||||
for (int i = 0; i < widget.order.items.length; i++) {
|
||||
if (!_selected[i]) continue;
|
||||
final item = widget.order.items[i];
|
||||
try {
|
||||
final qrBytes = await widget.productRepo.getQRCodeBytes(item.productId);
|
||||
await printProductLabel(
|
||||
qrBytes: qrBytes,
|
||||
name: item.productName ?? '',
|
||||
code: item.productCode ?? '',
|
||||
series: item.productSeries,
|
||||
spec: item.productSpec,
|
||||
batchNo: item.batchNo,
|
||||
productionDate: item.productionDate,
|
||||
shopName: widget.shopName,
|
||||
shopAddress: widget.shopAddress,
|
||||
shopPhone: widget.shopPhone,
|
||||
);
|
||||
done++;
|
||||
if (mounted) setState(() => _status = '已打印 $done 张...');
|
||||
} catch (e, st) {
|
||||
reportError(e, st);
|
||||
if (mounted) setState(() => _status = '第${i + 1}行打印失败:$e');
|
||||
}
|
||||
}
|
||||
if (mounted) setState(() { _printing = false; _status = '完成,共打印 $done 张'; });
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = widget.order.items;
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: context.dialogWidth(520),
|
||||
constraints: const BoxConstraints(maxHeight: 520),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppTheme.primary,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('打印商品标签',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white)),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (_, i) {
|
||||
final item = items[i];
|
||||
return CheckboxListTile(
|
||||
value: _selected[i],
|
||||
onChanged: _printing
|
||||
? null
|
||||
: (v) => setState(() => _selected[i] = v ?? false),
|
||||
title: Text(
|
||||
'${item.productCode ?? ''} ${item.productName ?? ''}',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${item.productSeries ?? ''} ${item.productSpec ?? ''}',
|
||||
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
|
||||
),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_status.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(_status,
|
||||
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: (_printing || !_selected.contains(true)) ? null : _print,
|
||||
icon: const Icon(Icons.print_outlined, size: 16),
|
||||
label: Text(_printing ? '打印中...' : '打印选中'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import '../../repositories/product_repository.dart';
|
||||
import '../../core/utils/dialog_util.dart';
|
||||
import '../../core/errors/error_reporter.dart';
|
||||
import '../../core/utils/print_util.dart' show safePrint, printStockOutOrder, printProductLabel;
|
||||
import '../../core/utils/print_util.dart' show safePrint, printStockOutOrder;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -920,149 +919,3 @@ class _StatusFilterDropdown extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _LabelPrintDialog extends StatefulWidget {
|
||||
final StockOutOrder order;
|
||||
final ProductRepository productRepo;
|
||||
final String shopName;
|
||||
final String shopAddress;
|
||||
final String shopPhone;
|
||||
const _LabelPrintDialog({
|
||||
required this.order,
|
||||
required this.productRepo,
|
||||
this.shopName = '',
|
||||
this.shopAddress = '',
|
||||
this.shopPhone = '',
|
||||
});
|
||||
|
||||
@override
|
||||
State<_LabelPrintDialog> createState() => _LabelPrintDialogState();
|
||||
}
|
||||
|
||||
class _LabelPrintDialogState extends State<_LabelPrintDialog> {
|
||||
late final List<bool> _selected;
|
||||
bool _printing = false;
|
||||
String _status = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selected = List.filled(widget.order.items.length, true);
|
||||
}
|
||||
|
||||
Future<void> _print() async {
|
||||
setState(() { _printing = true; _status = '正在打印...'; });
|
||||
int done = 0;
|
||||
for (int i = 0; i < widget.order.items.length; i++) {
|
||||
if (!_selected[i]) continue;
|
||||
final item = widget.order.items[i];
|
||||
try {
|
||||
final qrBytes = await widget.productRepo.getQRCodeBytes(item.productId);
|
||||
await printProductLabel(
|
||||
qrBytes: qrBytes,
|
||||
name: item.productName ?? '',
|
||||
code: item.productCode ?? '',
|
||||
series: item.productSeries,
|
||||
spec: item.productSpec,
|
||||
shopName: widget.shopName,
|
||||
shopAddress: widget.shopAddress,
|
||||
shopPhone: widget.shopPhone,
|
||||
);
|
||||
done++;
|
||||
if (mounted) setState(() => _status = '已打印 $done 张...');
|
||||
} catch (e, st) {
|
||||
reportError(e, st);
|
||||
if (mounted) setState(() => _status = '第${i + 1}行打印失败:$e');
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() { _printing = false; _status = '完成,共打印 $done 张'; });
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = widget.order.items;
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: context.dialogWidth(520),
|
||||
constraints: const BoxConstraints(maxHeight: 520),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppTheme.primary,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('打印商品标签',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white)),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemBuilder: (_, i) {
|
||||
final item = items[i];
|
||||
return CheckboxListTile(
|
||||
value: _selected[i],
|
||||
onChanged: _printing
|
||||
? null
|
||||
: (v) => setState(() => _selected[i] = v ?? false),
|
||||
title: Text(
|
||||
'${item.productCode ?? ''} ${item.productName ?? ''}',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${item.productSeries ?? ''} ${item.productSpec ?? ''}',
|
||||
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
|
||||
),
|
||||
dense: true,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_status.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(_status,
|
||||
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('关闭'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: (_printing || !_selected.contains(true)) ? null : _print,
|
||||
icon: const Icon(Icons.print_outlined, size: 16),
|
||||
label: Text(_printing ? '打印中...' : '打印选中'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../core/errors/error_reporter.dart';
|
||||
import '../core/theme/app_theme.dart';
|
||||
import '../core/responsive/responsive.dart';
|
||||
import '../core/utils/print_util.dart';
|
||||
import '../core/utils/label_data.dart';
|
||||
|
||||
const _kPrinterKey = 'label_printer';
|
||||
|
||||
/// 标签打印预览弹窗:所见即所得预览(dart:ui 位图)+ 打印机选择 + 份数调整。
|
||||
///
|
||||
/// 单张模式(labels.length == 1):隐藏缩略图条,直接显示大图。
|
||||
/// 多张模式:左侧竖排缩略图(带勾选),右侧大图主视图。
|
||||
class LabelPreviewDialog extends StatefulWidget {
|
||||
final List<LabelData> labels;
|
||||
|
||||
/// 多张懒加载:按 productId 拉取 QR PNG 字节。单张场景可不传(qrBytes 已在 LabelData 中)。
|
||||
final Future<Uint8List> Function(int productId)? qrFetcher;
|
||||
|
||||
const LabelPreviewDialog({
|
||||
super.key,
|
||||
required this.labels,
|
||||
this.qrFetcher,
|
||||
});
|
||||
|
||||
@override
|
||||
State<LabelPreviewDialog> createState() => _LabelPreviewDialogState();
|
||||
}
|
||||
|
||||
class _LabelPreviewDialogState extends State<LabelPreviewDialog> {
|
||||
int _selectedIndex = 0;
|
||||
late List<Uint8List?> _previews;
|
||||
late List<bool> _include;
|
||||
List<String> _printers = [];
|
||||
String? _printer;
|
||||
bool _printing = false;
|
||||
String _status = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_previews = List<Uint8List?>.filled(widget.labels.length, null,
|
||||
growable: false);
|
||||
_include = List<bool>.filled(widget.labels.length, true, growable: false);
|
||||
_init();
|
||||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
// ── 枚举打印机,读取上次记忆 ──
|
||||
final printers = await listLabelPrinters();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final saved = prefs.getString(_kPrinterKey);
|
||||
String? selected;
|
||||
if (saved != null && printers.contains(saved)) {
|
||||
selected = saved;
|
||||
} else {
|
||||
selected = await detectDefaultPrinter() ??
|
||||
(printers.isNotEmpty ? printers.first : null);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_printers = printers;
|
||||
_printer = selected;
|
||||
});
|
||||
}
|
||||
|
||||
// ── 逐张渲染预览(progressive)──
|
||||
for (int i = 0; i < widget.labels.length; i++) {
|
||||
final label = widget.labels[i];
|
||||
try {
|
||||
// 懒加载 QR
|
||||
if (label.qrBytes == null &&
|
||||
label.productId != null &&
|
||||
widget.qrFetcher != null) {
|
||||
label.qrBytes = await widget.qrFetcher!(label.productId!);
|
||||
}
|
||||
if (label.qrBytes == null) continue;
|
||||
final png = await renderLabelPreview(label);
|
||||
if (mounted && png != null) {
|
||||
setState(() => _previews[i] = png);
|
||||
}
|
||||
} catch (e, st) {
|
||||
debugPrint('[preview] 渲染第${i + 1}张失败: $e');
|
||||
reportError(e, st);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int get _totalPrintCount {
|
||||
int total = 0;
|
||||
for (int i = 0; i < widget.labels.length; i++) {
|
||||
if (_include[i]) total += widget.labels[i].copies;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
Future<void> _doPrint() async {
|
||||
if (_printing) return;
|
||||
setState(() {
|
||||
_printing = true;
|
||||
_status = '正在打印...';
|
||||
});
|
||||
int done = 0;
|
||||
final total = _totalPrintCount;
|
||||
for (int i = 0; i < widget.labels.length; i++) {
|
||||
if (!_include[i]) continue;
|
||||
final label = widget.labels[i];
|
||||
for (int c = 0; c < label.copies; c++) {
|
||||
try {
|
||||
// 确保 QR 已拉取
|
||||
if (label.qrBytes == null &&
|
||||
label.productId != null &&
|
||||
widget.qrFetcher != null) {
|
||||
label.qrBytes = await widget.qrFetcher!(label.productId!);
|
||||
}
|
||||
if (label.qrBytes == null) continue;
|
||||
await printProductLabel(
|
||||
qrBytes: label.qrBytes!,
|
||||
name: label.name,
|
||||
code: label.code,
|
||||
spec: label.spec,
|
||||
series: label.series,
|
||||
batchNo: label.batchNo,
|
||||
productionDate: label.productionDate,
|
||||
remark: label.remark,
|
||||
shopName: label.shopName,
|
||||
shopAddress: label.shopAddress,
|
||||
shopPhone: label.shopPhone,
|
||||
printerName: _printer,
|
||||
);
|
||||
done++;
|
||||
if (mounted) {
|
||||
setState(() => _status = '已打印 $done/$total 张...');
|
||||
}
|
||||
} catch (e, st) {
|
||||
reportError(e, st);
|
||||
if (mounted) {
|
||||
setState(() => _status = '第${i + 1}张第${c + 1}份打印失败:$e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_printing = false;
|
||||
_status = '完成,共打印 $done 张';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── 缩略图列表项 ─────────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildThumbnail(int i) {
|
||||
final isSelected = _selectedIndex == i;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _selectedIndex = i),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isSelected ? AppTheme.primary : AppTheme.border,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: isSelected
|
||||
? AppTheme.primary.withOpacity(0.04)
|
||||
: AppTheme.surface,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(
|
||||
value: _include[i],
|
||||
onChanged: _printing
|
||||
? null
|
||||
: (v) => setState(() => _include[i] = v ?? true),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: 76,
|
||||
height: 38,
|
||||
child: _previews[i] != null
|
||||
? Image.memory(_previews[i]!, fit: BoxFit.contain)
|
||||
: const Center(
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2))),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'${i + 1}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isSelected
|
||||
? AppTheme.primary
|
||||
: AppTheme.textSecondary,
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── 大图 + 份数控件 ──────────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildMainPreview() {
|
||||
final preview = _previews[_selectedIndex];
|
||||
final label = widget.labels[_selectedIndex];
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 420, maxHeight: 230),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppTheme.border),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Colors.white,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
child: preview != null
|
||||
? Image.memory(preview, fit: BoxFit.contain)
|
||||
: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 份数调节
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('打印份数:', style: TextStyle(fontSize: 13)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline, size: 20),
|
||||
onPressed: (_printing || label.copies <= 1)
|
||||
? null
|
||||
: () => setState(() => label.copies--),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: 32,
|
||||
child: Text(
|
||||
'${label.copies}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline, size: 20),
|
||||
onPressed:
|
||||
_printing ? null : () => setState(() => label.copies++),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── build ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMulti = widget.labels.length > 1;
|
||||
final totalCount = _totalPrintCount;
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Container(
|
||||
width: context.dialogWidth(isMulti ? 640 : 440),
|
||||
constraints: const BoxConstraints(maxHeight: 560),
|
||||
child: Column(
|
||||
children: [
|
||||
// ── 顶栏 ──────────────────────────────────────────────────────────
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppTheme.primary,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('打印预览',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white)),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// ── 打印机选择行 ──────────────────────────────────────────────────
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('打印机:',
|
||||
style: TextStyle(fontSize: 13)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _printers.isEmpty
|
||||
? const Text('检测中或未检测到打印机...',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.textSecondary))
|
||||
: DropdownButtonHideUnderline(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppTheme.border),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: DropdownButton<String>(
|
||||
value: _printer,
|
||||
isExpanded: true,
|
||||
isDense: true,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.textPrimary),
|
||||
items: _printers
|
||||
.map((p) => DropdownMenuItem(
|
||||
value: p,
|
||||
child: Text(p,
|
||||
overflow:
|
||||
TextOverflow.ellipsis),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: _printing
|
||||
? null
|
||||
: (v) async {
|
||||
if (v == null) return;
|
||||
setState(() => _printer = v);
|
||||
final prefs = await SharedPreferences
|
||||
.getInstance();
|
||||
await prefs.setString(
|
||||
_kPrinterKey, v);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// ── 主体:缩略图条 + 大图 ─────────────────────────────────────────
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (isMulti)
|
||||
SizedBox(
|
||||
width: 148,
|
||||
child: ListView.builder(
|
||||
itemCount: widget.labels.length,
|
||||
itemBuilder: (_, i) => _buildThumbnail(i),
|
||||
),
|
||||
),
|
||||
if (isMulti) const SizedBox(width: 8),
|
||||
Expanded(child: _buildMainPreview()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── 底栏 ─────────────────────────────────────────────────────────
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 6, 16, 14),
|
||||
child: Row(
|
||||
children: [
|
||||
if (_status.isNotEmpty)
|
||||
Expanded(
|
||||
child: Text(
|
||||
_status,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppTheme.textSecondary),
|
||||
),
|
||||
)
|
||||
else
|
||||
const Spacer(),
|
||||
TextButton(
|
||||
onPressed:
|
||||
_printing ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton(
|
||||
onPressed: (_printing || totalCount == 0) ? null : _doPrint,
|
||||
child: Text(
|
||||
_printing ? '打印中...' : '打印 $totalCount 张'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user