import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../core/errors/error_reporter.dart'; import '../core/theme/app_theme.dart'; import '../core/responsive/responsive.dart'; import '../core/utils/print_util.dart'; import '../core/utils/label_data.dart'; const _kPrinterKey = 'label_printer'; /// 标签打印预览弹窗:所见即所得预览(dart:ui 位图)+ 打印机选择 + 份数调整。 /// /// 单张模式(labels.length == 1):隐藏缩略图条,直接显示大图。 /// 多张模式:左侧竖排缩略图(带勾选),右侧大图主视图。 class LabelPreviewDialog extends StatefulWidget { final List labels; /// 多张懒加载:按 productId 拉取 QR PNG 字节。单张场景可不传(qrBytes 已在 LabelData 中)。 final Future Function(int productId)? qrFetcher; const LabelPreviewDialog({ super.key, required this.labels, this.qrFetcher, }); @override State createState() => _LabelPreviewDialogState(); } class _LabelPreviewDialogState extends State { int _selectedIndex = 0; late List _previews; late List _include; List _printers = []; String? _printer; bool _printing = false; bool _printersChecked = false; String _status = ''; @override void initState() { super.initState(); _previews = List.filled(widget.labels.length, null, growable: false); _include = List.filled(widget.labels.length, true, growable: false); _init(); } Future _init() async { // ── 枚举打印机,读取上次记忆 ── final printers = await listLabelPrinters(); final prefs = await SharedPreferences.getInstance(); final saved = prefs.getString(_kPrinterKey); String? selected; if (saved != null && printers.contains(saved)) { selected = saved; } else { selected = await detectDefaultPrinter() ?? (printers.isNotEmpty ? printers.first : null); } if (mounted) { setState(() { _printers = printers; _printer = selected; _printersChecked = true; }); } // ── 逐张渲染预览(progressive)── for (int i = 0; i < widget.labels.length; i++) { final label = widget.labels[i]; try { // 懒加载 QR if (label.qrBytes == null && label.productId != null && widget.qrFetcher != null) { label.qrBytes = await widget.qrFetcher!(label.productId!); } if (label.qrBytes == null) continue; final png = await renderLabelPreview(label); if (mounted && png != null) { setState(() => _previews[i] = png); } } catch (e, st) { debugPrint('[preview] 渲染第${i + 1}张失败: $e'); reportError(e, st); } } } int get _totalPrintCount { int total = 0; for (int i = 0; i < widget.labels.length; i++) { if (_include[i]) total += widget.labels[i].copies; } return total; } Future _doPrint() async { if (_printing) return; setState(() { _printing = true; _status = '正在打印...'; }); int done = 0; final total = _totalPrintCount; for (int i = 0; i < widget.labels.length; i++) { if (!_include[i]) continue; final label = widget.labels[i]; for (int c = 0; c < label.copies; c++) { try { // 确保 QR 已拉取 if (label.qrBytes == null && label.productId != null && widget.qrFetcher != null) { label.qrBytes = await widget.qrFetcher!(label.productId!); } if (label.qrBytes == null) continue; await printProductLabel( qrBytes: label.qrBytes!, name: label.name, code: label.code, spec: label.spec, series: label.series, batchNo: label.batchNo, productionDate: label.productionDate, remark: label.remark, shopName: label.shopName, shopAddress: label.shopAddress, shopPhone: label.shopPhone, printerName: _printer, ); done++; if (mounted) { setState(() => _status = '已打印 $done/$total 张...'); } } catch (e, st) { reportError(e, st); if (mounted) { setState(() => _status = '第${i + 1}张第${c + 1}份打印失败:$e'); } } } } if (mounted) { setState(() { _printing = false; _status = '完成,共打印 $done 张'; }); Navigator.of(context).pop(); } } // ── 缩略图列表项 ───────────────────────────────────────────────────────────── Widget _buildThumbnail(int i) { final isSelected = _selectedIndex == i; return GestureDetector( onTap: () => setState(() => _selectedIndex = i), child: Container( margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6), decoration: BoxDecoration( border: Border.all( color: isSelected ? AppTheme.primary : AppTheme.border, width: isSelected ? 2 : 1, ), borderRadius: BorderRadius.circular(6), color: isSelected ? AppTheme.primary.withOpacity(0.04) : AppTheme.surface, ), child: Row( children: [ SizedBox( width: 20, height: 20, child: Checkbox( value: _include[i], onChanged: _printing ? null : (v) => setState(() => _include[i] = v ?? true), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, ), ), const SizedBox(width: 4), SizedBox( width: 76, height: 38, child: _previews[i] != null ? Image.memory(_previews[i]!, fit: BoxFit.contain) : const Center( child: SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))), ), const SizedBox(width: 6), Text( '${i + 1}', style: TextStyle( fontSize: 12, color: isSelected ? AppTheme.primary : AppTheme.textSecondary, fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), ], ), ), ); } // ── 大图 + 份数控件 ────────────────────────────────────────────────────────── Widget _buildMainPreview() { final preview = _previews[_selectedIndex]; final label = widget.labels[_selectedIndex]; return Column( children: [ Expanded( child: Center( child: Container( constraints: const BoxConstraints(maxWidth: 420, maxHeight: 230), decoration: BoxDecoration( border: Border.all(color: AppTheme.border), borderRadius: BorderRadius.circular(4), color: Colors.white, ), child: ClipRRect( borderRadius: BorderRadius.circular(3), child: preview != null ? Image.memory(preview, fit: BoxFit.contain) : const Center(child: CircularProgressIndicator()), ), ), ), ), const SizedBox(height: 8), // 份数调节 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('打印份数:', style: TextStyle(fontSize: 13)), IconButton( icon: const Icon(Icons.remove_circle_outline, size: 20), onPressed: (_printing || label.copies <= 1) ? null : () => setState(() => label.copies--), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), const SizedBox(width: 4), SizedBox( width: 32, child: Text( '${label.copies}', textAlign: TextAlign.center, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600), ), ), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.add_circle_outline, size: 20), onPressed: _printing ? null : () => setState(() => label.copies++), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ], ); } // ── build ──────────────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { final isMulti = widget.labels.length > 1; final totalCount = _totalPrintCount; return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Container( width: context.dialogWidth(isMulti ? 640 : 440), constraints: const BoxConstraints(maxHeight: 560), child: Column( children: [ // ── 顶栏 ────────────────────────────────────────────────────────── Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), decoration: const BoxDecoration( color: AppTheme.primary, borderRadius: BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), ), child: Row( children: [ const Text('打印预览', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white)), const Spacer(), IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.of(context).pop(), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), // ── 打印机选择行 ────────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: Row( children: [ const Text('打印机:', style: TextStyle(fontSize: 13)), const SizedBox(width: 8), Expanded( child: _printers.isEmpty ? Text( _printersChecked ? '无打印机可用' : '正在检测打印机...', style: const TextStyle( fontSize: 13, color: AppTheme.textSecondary)) : DropdownButtonHideUnderline( child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4), decoration: BoxDecoration( border: Border.all(color: AppTheme.border), borderRadius: BorderRadius.circular(6), ), child: DropdownButton( value: _printer, isExpanded: true, isDense: true, style: const TextStyle( fontSize: 13, color: AppTheme.textPrimary), items: _printers .map((p) => DropdownMenuItem( value: p, child: Text(p, overflow: TextOverflow.ellipsis), )) .toList(), onChanged: _printing ? null : (v) async { if (v == null) return; setState(() => _printer = v); final prefs = await SharedPreferences .getInstance(); await prefs.setString( _kPrinterKey, v); }, ), ), ), ), ], ), ), const SizedBox(height: 8), // ── 主体:缩略图条 + 大图 ───────────────────────────────────────── Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (isMulti) SizedBox( width: 148, child: ListView.builder( itemCount: widget.labels.length, itemBuilder: (_, i) => _buildThumbnail(i), ), ), if (isMulti) const SizedBox(width: 8), Expanded(child: _buildMainPreview()), ], ), ), ), // ── 底栏 ───────────────────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(16, 6, 16, 14), child: Row( children: [ if (_status.isNotEmpty) Expanded( child: Text( _status, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary), ), ) else const Spacer(), TextButton( onPressed: _printing ? null : () => Navigator.of(context).pop(), child: const Text('取消'), ), const SizedBox(width: 8), FilledButton( onPressed: (_printing || totalCount == 0 || _printer == null) ? null : _doPrint, child: Text( _printing ? '打印中...' : '打印 $totalCount 张'), ), ], ), ), ], ), ), ); } }