feat(client): 二维码打印功能

在商品详情二维码弹窗中新增「打印」按钮,使用 printing + pdf
生成含商品名称、编号、规格和二维码图片的 PDF,触发浏览器打印对话框。
嵌入 NotoSansSC-Regular 字体保证中文正常显示。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-30 19:21:50 +08:00
parent cf646226a8
commit 1747c58246
4 changed files with 125 additions and 5 deletions
Binary file not shown.
@@ -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<ProductDetailScreen> {
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<Uint8List> _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<void> _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<Uint8List>(
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('打印'),
),
],
);
}
+64
View File
@@ -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:
+5
View File
@@ -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