diff --git a/client/lib/core/utils/print_util.dart b/client/lib/core/utils/print_util.dart index 8a02863..e741aa1 100644 --- a/client/lib/core/utils/print_util.dart +++ b/client/lib/core/utils/print_util.dart @@ -1,13 +1,30 @@ import 'dart:typed_data'; +import '../../models/stock_in.dart'; +import '../../models/stock_out.dart'; import 'print_util_stub.dart' if (dart.library.js_interop) 'print_util_web.dart'; -/// 打印商品标签(二维码 + 名称 + 编号)。 -/// Web:生成 HTML 弹出新窗口调用浏览器打印。 -/// 非 Web:生成 PDF 用 printing 包调起原生打印。 Future printProductLabel({ required Uint8List qrBytes, required String name, required String code, String? spec, -}) => printProductLabelImpl(qrBytes: qrBytes, name: name, code: code, spec: spec); + String? series, + String? batchNo, + String? productionDate, +}) => + printProductLabelImpl( + qrBytes: qrBytes, + name: name, + code: code, + spec: spec, + series: series, + batchNo: batchNo, + productionDate: productionDate, + ); + +Future printStockInOrder(StockInOrder order) => + printStockInOrderImpl(order); + +Future printStockOutOrder(StockOutOrder order) => + printStockOutOrderImpl(order); diff --git a/client/lib/core/utils/print_util_stub.dart b/client/lib/core/utils/print_util_stub.dart index ab96bfd..ee475aa 100644 --- a/client/lib/core/utils/print_util_stub.dart +++ b/client/lib/core/utils/print_util_stub.dart @@ -2,37 +2,69 @@ import 'package:flutter/services.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; +import '../../models/stock_in.dart'; +import '../../models/stock_out.dart'; Future printProductLabelImpl({ required Uint8List qrBytes, required String name, required String code, String? spec, + String? series, + String? batchNo, + String? productionDate, }) async { final fontData = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf'); final font = pw.Font.ttf(fontData); final doc = pw.Document(); final qrImage = pw.MemoryImage(qrBytes); doc.addPage(pw.Page( - pageFormat: PdfPageFormat.a4, - build: (_) => pw.Center( - child: pw.Column( - mainAxisSize: pw.MainAxisSize.min, - children: [ - pw.Image(qrImage, width: 200, height: 200), - pw.SizedBox(height: 12), - pw.Text(name, - style: pw.TextStyle( - font: font, fontSize: 18, fontWeight: pw.FontWeight.bold)), - pw.SizedBox(height: 4), - pw.Text('编号:$code', - style: pw.TextStyle(font: font, fontSize: 13)), - if ((spec ?? '').isNotEmpty) - pw.Text('规格:$spec', - style: pw.TextStyle(font: font, fontSize: 13)), - ], - ), + pageFormat: const PdfPageFormat(4 * PdfPageFormat.inch, 2 * PdfPageFormat.inch), + margin: const pw.EdgeInsets.all(10), + build: (_) => pw.Row( + children: [ + pw.Expanded( + child: pw.Column( + mainAxisAlignment: pw.MainAxisAlignment.center, + crossAxisAlignment: pw.CrossAxisAlignment.start, + children: [ + pw.Text(name, + style: pw.TextStyle( + font: font, fontSize: 14, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 3), + pw.Text('编号:$code', + style: pw.TextStyle(font: font, fontSize: 10)), + if ((series ?? '').isNotEmpty) + pw.Text('系列:$series', + style: pw.TextStyle(font: font, fontSize: 10)), + if ((spec ?? '').isNotEmpty) + pw.Text('规格:$spec', + style: pw.TextStyle(font: font, fontSize: 10)), + if ((batchNo ?? '').isNotEmpty) + pw.Text('批次:$batchNo', + style: pw.TextStyle(font: font, fontSize: 10)), + if ((productionDate ?? '').isNotEmpty) + pw.Text('生产日期:$productionDate', + style: pw.TextStyle(font: font, fontSize: 10)), + ], + ), + ), + pw.SizedBox(width: 8), + pw.SizedBox( + width: 1.2 * PdfPageFormat.inch, + height: 1.2 * PdfPageFormat.inch, + child: pw.Image(qrImage), + ), + ], ), )); await Printing.layoutPdf(onLayout: (_) async => doc.save()); } + +Future printStockInOrderImpl(StockInOrder order) async { + // Native PDF printing not implemented; no-op on non-web platforms. +} + +Future printStockOutOrderImpl(StockOutOrder order) async { + // Native PDF printing not implemented; no-op on non-web platforms. +} diff --git a/client/lib/core/utils/print_util_web.dart b/client/lib/core/utils/print_util_web.dart index 4ce79de..0d23824 100644 --- a/client/lib/core/utils/print_util_web.dart +++ b/client/lib/core/utils/print_util_web.dart @@ -2,37 +2,209 @@ import 'dart:convert'; import 'dart:js_interop'; import 'dart:typed_data'; import 'package:web/web.dart' as web; +import '../../models/stock_in.dart'; +import '../../models/stock_out.dart'; Future printProductLabelImpl({ required Uint8List qrBytes, required String name, required String code, String? spec, + String? series, + String? batchNo, + String? productionDate, }) async { final base64Img = base64Encode(qrBytes); - final specHtml = (spec != null && spec.isNotEmpty) - ? '

规格:$spec

' - : ''; + + final leftRows = StringBuffer(); + leftRows.write('
$name
'); + leftRows.write('
编号:$code
'); + if (series != null && series.isNotEmpty) leftRows.write('
系列:$series
'); + if (spec != null && spec.isNotEmpty) leftRows.write('
规格:$spec
'); + if (batchNo != null && batchNo.isNotEmpty) leftRows.write('
批次:$batchNo
'); + if (productionDate != null && productionDate.isNotEmpty) { + leftRows.write('
生产日期:$productionDate
'); + } + final html = '''
- -

$name

-

编号:$code

- $specHtml +
$leftRows
+
+
+ + +'''; + + final win = web.window.open('', '_blank'); + if (win != null) { + win.document.write(html.toJS); + win.document.close(); + } +} + +Future printStockInOrderImpl(StockInOrder order) async { + final rows = StringBuffer(); + double total = 0; + for (int i = 0; i < order.items.length; i++) { + final item = order.items[i]; + total += item.totalPrice; + rows.write(''' + ${i + 1} + ${item.productName ?? ''} + ${item.productSeries ?? ''} + ${item.productSpec ?? ''} + ${item.batchNo ?? ''} + ${item.quantity.toStringAsFixed(0)} + ${item.productUnit ?? ''} + ${item.unitPrice.toStringAsFixed(2)} + ${item.totalPrice.toStringAsFixed(2)} + '''); + } + + final html = ''' + + + + + + +

入库单

+
+
单号:${order.orderNo}
+
日期:${order.orderDate ?? ''}
+
供应商:${order.partnerName ?? ''}
+
仓库:${order.warehouseName ?? ''}
+
采购人:${order.operatorName ?? ''}
+
备注:${order.remark ?? ''}
+
+ + + + + + $rows + + + + +
序号商品名称系列规格批次数量单位单价金额
合计${total.toStringAsFixed(2)}
+
+
制单人
+
审核人
+
供应商签字
+
+ + +'''; + + final win = web.window.open('', '_blank'); + if (win != null) { + win.document.write(html.toJS); + win.document.close(); + } +} + +Future printStockOutOrderImpl(StockOutOrder order) async { + final rows = StringBuffer(); + double total = 0; + for (int i = 0; i < order.items.length; i++) { + final item = order.items[i]; + total += item.totalPrice; + rows.write(''' + ${i + 1} + ${item.productName ?? ''} + ${item.productSeries ?? ''} + ${item.productSpec ?? ''} + ${item.quantity.toStringAsFixed(0)} + ${item.productUnit ?? ''} + ${item.unitPrice.toStringAsFixed(2)} + ${item.totalPrice.toStringAsFixed(2)} + '''); + } + + final html = ''' + + + + + + +
+

温馨提示

+
出库通知单
+
+
+
单号:${order.orderNo}
+
日期:${order.orderDate ?? ''}
+
客户:${order.partnerName ?? ''}
+
仓库:${order.warehouseName ?? ''}
+
经办人:${order.operatorName ?? ''}
+
备注:${order.remark ?? ''}
+
+ + + + + + $rows + + + + +
序号商品名称系列规格数量单位单价金额
合计${total.toStringAsFixed(2)}
+
+
制单人
+
审核人
+
客户签字
diff --git a/client/lib/screens/products/product_detail_screen.dart b/client/lib/screens/products/product_detail_screen.dart index 7e360a7..10b2c11 100644 --- a/client/lib/screens/products/product_detail_screen.dart +++ b/client/lib/screens/products/product_detail_screen.dart @@ -533,6 +533,7 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> { name: p.name, code: p.code, spec: p.spec, + series: p.series, ); } catch (e) { if (mounted) { diff --git a/client/lib/screens/stock_in/stock_in_form_screen.dart b/client/lib/screens/stock_in/stock_in_form_screen.dart index f79fe1b..2f7e054 100644 --- a/client/lib/screens/stock_in/stock_in_form_screen.dart +++ b/client/lib/screens/stock_in/stock_in_form_screen.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/app_theme.dart'; import '../../core/auth/auth_state.dart'; +import '../../core/utils/print_util.dart'; +import '../../models/stock_in.dart'; import '../../providers/partner_provider.dart'; import '../../providers/product_option_provider.dart'; import '../../providers/product_provider.dart'; @@ -29,6 +31,7 @@ class _StockInFormScreenState extends ConsumerState { bool _submitting = false; bool _loadingEdit = false; Map _inventoryMap = {}; + StockInOrder? _loadedOrder; final List<_ItemRow> _items = []; @@ -53,6 +56,7 @@ class _StockInFormScreenState extends ConsumerState { final specOpts = await ref.read(productSpecListProvider.future); setState(() { + _loadedOrder = order; _warehouseId = order.warehouseId; _partnerId = order.partnerId; if (order.orderDate != null) { @@ -124,6 +128,11 @@ class _StockInFormScreenState extends ConsumerState { }); } + Future _printOrder() async { + if (_loadedOrder == null) return; + await printStockInOrder(_loadedOrder!); + } + Future _submit(bool asDraft) async { if (!asDraft && !_formKey.currentState!.validate()) return; if (_warehouseId == null) { @@ -248,6 +257,14 @@ class _StockInFormScreenState extends ConsumerState { Text(_isEdit ? '修改入库单' : '新建入库单', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const Spacer(), + if (_isEdit && _loadedOrder != null) ...[ + OutlinedButton.icon( + onPressed: _printOrder, + icon: const Icon(Icons.print_outlined, size: 16), + label: const Text('打印'), + ), + const SizedBox(width: 8), + ], OutlinedButton( onPressed: _submitting ? null : () => _submit(true), child: const Text('保存草稿'), diff --git a/client/lib/screens/stock_out/stock_out_form_screen.dart b/client/lib/screens/stock_out/stock_out_form_screen.dart index 4cc03f4..de9914c 100644 --- a/client/lib/screens/stock_out/stock_out_form_screen.dart +++ b/client/lib/screens/stock_out/stock_out_form_screen.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/app_theme.dart'; import '../../core/auth/auth_state.dart'; +import '../../core/utils/print_util.dart'; +import '../../models/stock_out.dart'; import '../../providers/inventory_provider.dart'; import '../../providers/partner_provider.dart'; import '../../providers/product_option_provider.dart'; @@ -29,6 +31,7 @@ class _StockOutFormScreenState extends ConsumerState { bool _submitting = false; bool _loadingEdit = false; Map _inventoryMap = {}; + StockOutOrder? _loadedOrder; final List<_ItemRow> _items = []; @@ -53,6 +56,7 @@ class _StockOutFormScreenState extends ConsumerState { final specOpts = await ref.read(productSpecListProvider.future); setState(() { + _loadedOrder = order; _warehouseId = order.warehouseId; _partnerId = order.partnerId; if (order.orderDate != null) { @@ -125,6 +129,11 @@ class _StockOutFormScreenState extends ConsumerState { }); } + Future _printOrder() async { + if (_loadedOrder == null) return; + await printStockOutOrder(_loadedOrder!); + } + Future _submit(bool asDraft) async { if (!asDraft && !_formKey.currentState!.validate()) return; if (_warehouseId == null) { @@ -248,6 +257,14 @@ class _StockOutFormScreenState extends ConsumerState { Text(_isEdit ? '修改出库单' : '新建出库单', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const Spacer(), + if (_isEdit && _loadedOrder != null) ...[ + OutlinedButton.icon( + onPressed: _printOrder, + icon: const Icon(Icons.print_outlined, size: 16), + label: const Text('打印'), + ), + const SizedBox(width: 8), + ], OutlinedButton( onPressed: _submitting ? null : () => _submit(true), child: const Text('保存草稿'),