feat(client): 二维码打印功能
在商品详情二维码弹窗中新增「打印」按钮,使用 printing + pdf 生成含商品名称、编号、规格和二维码图片的 PDF,触发浏览器打印对话框。 嵌入 NotoSansSC-Regular 字体保证中文正常显示。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
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('打印'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user