diff --git a/client/assets/fonts/NotoSansSC-Regular.ttf b/client/assets/fonts/NotoSansSC-Regular.ttf new file mode 100644 index 0000000..fc0fda9 Binary files /dev/null and b/client/assets/fonts/NotoSansSC-Regular.ttf differ diff --git a/client/lib/screens/products/product_detail_screen.dart b/client/lib/screens/products/product_detail_screen.dart index c49b6bf..18c1eac 100644 --- a/client/lib/screens/products/product_detail_screen.dart +++ b/client/lib/screens/products/product_detail_screen.dart @@ -4,6 +4,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:pdf/pdf.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:printing/printing.dart'; import '../../core/config/app_config.dart'; import '../../core/theme/app_theme.dart'; import '../../models/product.dart'; @@ -152,7 +155,7 @@ class _ProductDetailScreenState extends ConsumerState { void _showQRCode() { showDialog( context: context, - builder: (_) => _QRCodeDialog(productId: widget.productId), + builder: (_) => _QRCodeDialog(productId: widget.productId, product: _product!), ); } @@ -498,7 +501,8 @@ class _UploadButton extends StatelessWidget { class _QRCodeDialog extends ConsumerStatefulWidget { final int productId; - const _QRCodeDialog({required this.productId}); + final Product product; + const _QRCodeDialog({required this.productId, required this.product}); @override ConsumerState<_QRCodeDialog> createState() => _QRCodeDialogState(); @@ -506,13 +510,51 @@ class _QRCodeDialog extends ConsumerStatefulWidget { class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> { late Future _future; + Uint8List? _bytes; + bool _printing = false; @override void initState() { super.initState(); _future = ref .read(productRepositoryProvider) - .getQRCodeBytes(widget.productId); + .getQRCodeBytes(widget.productId) + ..then((b) => _bytes = b); + } + + Future _print() async { + if (_bytes == null || _printing) return; + setState(() => _printing = true); + try { + final fontData = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf'); + final font = pw.Font.ttf(fontData); + final p = widget.product; + final doc = pw.Document(); + final qrImage = pw.MemoryImage(_bytes!); + 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(p.name, + style: pw.TextStyle(font: font, fontSize: 18, fontWeight: pw.FontWeight.bold)), + pw.SizedBox(height: 4), + pw.Text('编号:${p.code}', + style: pw.TextStyle(font: font, fontSize: 13)), + if ((p.spec ?? '').isNotEmpty) + pw.Text('规格:${p.spec}', + style: pw.TextStyle(font: font, fontSize: 13)), + ], + ), + ), + )); + await Printing.layoutPdf(onLayout: (_) async => doc.save()); + } finally { + if (mounted) setState(() => _printing = false); + } } @override @@ -520,8 +562,8 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> { return AlertDialog( title: const Text('商品二维码'), content: SizedBox( - width: 200, - height: 200, + width: 220, + height: 220, child: FutureBuilder( future: _future, builder: (_, snap) { @@ -540,6 +582,15 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> { onPressed: () => Navigator.of(context).pop(), 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: const Text('打印'), + ), ], ); } diff --git a/client/pubspec.lock b/client/pubspec.lock index f84144b..386114f 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -25,6 +25,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.1" + barcode: + dependency: transitive + description: + name: barcode + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + url: "https://pub.dev" + source: hosted + version: "2.2.9" + bidi: + dependency: transitive + description: + name: bidi + sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" + url: "https://pub.dev" + source: hosted + version: "2.0.13" boolean_selector: dependency: transitive description: @@ -232,6 +248,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + url: "https://pub.dev" + source: hosted + version: "4.3.0" intl: dependency: "direct main" description: @@ -392,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -440,6 +472,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b + url: "https://pub.dev" + source: hosted + version: "3.12.0" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 + url: "https://pub.dev" + source: hosted + version: "1.0.4" petitparser: dependency: transitive description: @@ -464,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + printing: + dependency: "direct main" + description: + name: printing + sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692" + url: "https://pub.dev" + source: hosted + version: "5.14.3" pub_semver: dependency: transitive description: @@ -472,6 +528,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" record_use: dependency: transitive description: diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 6f10bd6..01e5fa8 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -20,6 +20,8 @@ dependencies: excel: ^4.0.6 path_provider: ^2.1.0 file_picker: ^8.1.6 + printing: ^5.13.0 + pdf: ^3.10.8 dev_dependencies: flutter_test: @@ -29,3 +31,6 @@ dev_dependencies: flutter: uses-material-design: true + + assets: + - assets/fonts/NotoSansSC-Regular.ttf