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
@@ -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('打印'),
),
],
);
}