import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'dart:typed_data'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:printing/printing.dart'; import '../core/auth/auth_state.dart'; import '../core/errors/error_reporter.dart'; import '../core/theme/context_tokens.dart'; import '../core/responsive/responsive.dart'; import '../core/utils/print_util.dart'; import '../models/shop.dart'; import '../models/stock_in.dart'; import '../models/stock_out.dart'; import '../providers/shop_provider.dart'; /// 单据打印预览弹窗:把待打印的 A4 PDF 光栅化成图片(所见即所得)逐页展示, /// 页内点「打印」再调系统打印。解决部分 Windows 系统打印对话框无预览的问题。 /// /// 仅桌面端使用;Web 端走浏览器自带的打印预览(见 [showStockInOrderPrint])。 class OrderPrintPreviewDialog extends StatefulWidget { /// 标题,如「入库单打印预览」。 final String title; /// 系统打印任务名(也作另存默认文件名前缀)。 final String printName; /// 惰性生成 PDF 字节。 final Future Function() pdfBuilder; const OrderPrintPreviewDialog({ super.key, required this.title, required this.printName, required this.pdfBuilder, }); @override State createState() => _OrderPrintPreviewDialogState(); } class _OrderPrintPreviewDialogState extends State { Uint8List? _pdfBytes; final List _pages = []; bool _rendering = true; bool _printing = false; String _error = ''; @override void initState() { super.initState(); _render(); } Future _render() async { try { final bytes = await widget.pdfBuilder(); if (!mounted) return; setState(() => _pdfBytes = bytes); // 逐页光栅化(progressive),DPI 取 110 兼顾清晰与速度。 await for (final raster in Printing.raster(bytes, dpi: 110)) { final png = await raster.toPng(); if (!mounted) return; setState(() => _pages.add(png)); } } catch (e, st) { reportError(e, st); if (mounted) setState(() => _error = '预览生成失败:$e'); } finally { if (mounted) setState(() => _rendering = false); } } Future _doPrint() async { final bytes = _pdfBytes; if (bytes == null || _printing) return; setState(() => _printing = true); try { await Printing.layoutPdf( name: '${widget.printName}_${_ts()}', dynamicLayout: false, onLayout: (_) async => bytes, ); if (mounted) Navigator.of(context).pop(); } catch (e, st) { reportError(e, st); if (mounted) { setState(() { _printing = false; _error = '打印失败,请检查打印机连接和驱动是否正常。\n详情:$e'; }); } } } String _ts() { final n = DateTime.now(); String p(int v) => v.toString().padLeft(2, '0'); return '${n.year}${p(n.month)}${p(n.day)}_${p(n.hour)}${p(n.minute)}${p(n.second)}'; } @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Container( width: context.dialogWidth(720), constraints: const BoxConstraints(maxHeight: 720), child: Column( children: [ // ── 顶栏 ────────────────────────────────────────────────────────── Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), decoration: BoxDecoration( color: context.tokens.primary, borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), ), child: Row( children: [ Text(widget.title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors .white)), // ds-ignore: 打印纸面模拟固定色(纸白/墨黑,不随主题) const Spacer(), IconButton( icon: const Icon(LucideIcons.x, color: Colors.white), // ds-ignore: 打印纸面模拟固定色(纸白/墨黑,不随主题) onPressed: _printing ? null : () => Navigator.of(context).pop(), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), // ── 预览区 ───────────────────────────────────────────────────────── Expanded( child: Container( width: double.infinity, color: const Color(0xFFE9ECF1), // ds-ignore: 打印纸面模拟固定色(纸白/墨黑,不随主题) child: _buildBody(), ), ), // ── 底栏 ────────────────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 14), child: Row( children: [ Expanded( child: Text( _error.isNotEmpty ? _error : (_rendering ? '正在生成预览…' : '共 ${_pages.length} 页'), style: TextStyle( fontSize: 12, color: _error.isNotEmpty ? Colors.red // ds-ignore: 打印纸面模拟固定色(纸白/墨黑,不随主题) : context.tokens.muted), ), ), TextButton( onPressed: _printing ? null : () => Navigator.of(context).pop(), child: const Text('取消'), ), const SizedBox(width: 8), FilledButton.icon( icon: const Icon(LucideIcons.printer, size: 18), onPressed: (_pdfBytes == null || _printing) ? null : _doPrint, label: Text(_printing ? '打印中…' : '打印'), ), ], ), ), ], ), ), ); } Widget _buildBody() { if (_pages.isEmpty) { if (_error.isNotEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Text(_error, textAlign: TextAlign.center, style: const TextStyle( color: Colors.red, // ds-ignore: 打印纸面模拟固定色(纸白/墨黑,不随主题) fontSize: 13)), // ds-ignore: 打印纸面模拟固定色(纸白/墨黑,不随主题) ), ); } return const Center(child: CircularProgressIndicator()); } return ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), itemCount: _pages.length, separatorBuilder: (_, __) => const SizedBox(height: 16), itemBuilder: (_, i) => Center( child: Container( decoration: BoxDecoration( color: Colors.white, // ds-ignore: 打印纸面模拟固定色(纸白/墨黑,不随主题) boxShadow: [ BoxShadow( color: Colors.black // ds-ignore: 打印纸面模拟固定色(纸白/墨黑,不随主题) .withOpacity(0.18), // ds-ignore: 打印纸面模拟固定色(纸白/墨黑,不随主题) blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Image.memory(_pages[i], fit: BoxFit.contain), ), ), ); } } /// 组装单据打印抬头信息:公司名称(门店)+ 制单人(当前登录、点击打印的人)。 Future _buildPrintMeta(WidgetRef ref) async { ShopInfo? shop; try { shop = await ref.read(shopInfoProvider.future); } catch (_) { // 门店信息拉取失败(离线等)时抬头留空,不阻塞打印。 } return OrderPrintMeta( shopName: shop?.name ?? '', shopAddress: shop?.address ?? '', shopPhone: shop?.phone ?? '', makerName: ref.read(authStateProvider).user?.realName ?? '', ); } /// 打印入库单:桌面端先弹应用内预览,Web 端走浏览器自带打印预览。 Future showStockInOrderPrint( BuildContext context, WidgetRef ref, StockInOrder order) async { final meta = await _buildPrintMeta(ref); if (!context.mounted) return; if (kIsWeb) { await safePrint(context, () => printStockInOrder(order, meta)); return; } await showDialog( context: context, barrierDismissible: false, builder: (_) => OrderPrintPreviewDialog( title: '入库单打印预览', printName: '入库单', pdfBuilder: () => buildStockInOrderPdf(order, meta), ), ); } /// 打印出库单:桌面端先弹应用内预览,Web 端走浏览器自带打印预览。 Future showStockOutOrderPrint( BuildContext context, WidgetRef ref, StockOutOrder order) async { final meta = await _buildPrintMeta(ref); if (!context.mounted) return; if (kIsWeb) { await safePrint(context, () => printStockOutOrder(order, meta)); return; } await showDialog( context: context, barrierDismissible: false, builder: (_) => OrderPrintPreviewDialog( title: '出库单打印预览', printName: '出库单', pdfBuilder: () => buildStockOutOrderPdf(order, meta), ), ); }