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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/config/app_config.dart';
|
||||||
import '../../core/theme/app_theme.dart';
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../models/product.dart';
|
import '../../models/product.dart';
|
||||||
@@ -152,7 +155,7 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
void _showQRCode() {
|
void _showQRCode() {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
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 {
|
class _QRCodeDialog extends ConsumerStatefulWidget {
|
||||||
final int productId;
|
final int productId;
|
||||||
const _QRCodeDialog({required this.productId});
|
final Product product;
|
||||||
|
const _QRCodeDialog({required this.productId, required this.product});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<_QRCodeDialog> createState() => _QRCodeDialogState();
|
ConsumerState<_QRCodeDialog> createState() => _QRCodeDialogState();
|
||||||
@@ -506,13 +510,51 @@ class _QRCodeDialog extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> {
|
class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> {
|
||||||
late Future<Uint8List> _future;
|
late Future<Uint8List> _future;
|
||||||
|
Uint8List? _bytes;
|
||||||
|
bool _printing = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_future = ref
|
_future = ref
|
||||||
.read(productRepositoryProvider)
|
.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
|
@override
|
||||||
@@ -520,8 +562,8 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('商品二维码'),
|
title: const Text('商品二维码'),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 200,
|
width: 220,
|
||||||
height: 200,
|
height: 220,
|
||||||
child: FutureBuilder<Uint8List>(
|
child: FutureBuilder<Uint8List>(
|
||||||
future: _future,
|
future: _future,
|
||||||
builder: (_, snap) {
|
builder: (_, snap) {
|
||||||
@@ -540,6 +582,15 @@ class _QRCodeDialogState extends ConsumerState<_QRCodeDialog> {
|
|||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
child: const Text('关闭'),
|
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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.1"
|
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:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -232,6 +248,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -392,6 +416,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
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:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -440,6 +472,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
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:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -464,6 +512,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
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:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -472,6 +528,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
qr:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: qr
|
||||||
|
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
record_use:
|
record_use:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ dependencies:
|
|||||||
excel: ^4.0.6
|
excel: ^4.0.6
|
||||||
path_provider: ^2.1.0
|
path_provider: ^2.1.0
|
||||||
file_picker: ^8.1.6
|
file_picker: ^8.1.6
|
||||||
|
printing: ^5.13.0
|
||||||
|
pdf: ^3.10.8
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -29,3 +31,6 @@ dev_dependencies:
|
|||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- assets/fonts/NotoSansSC-Regular.ttf
|
||||||
|
|||||||
Reference in New Issue
Block a user