@@ -16,7 +16,8 @@ import 'package:win32/win32.dart'
ClosePrinter ,
DOC_INFO_1 ,
WideCharToMultiByte ;
import ' package:flutter/foundation.dart ' show kIsWeb , debugPrint ;
import ' package:flutter/foundation.dart '
show kIsWeb , debugPrint , visibleForTesting ;
import ' package:flutter/services.dart ' ;
import ' ../errors/error_reporter.dart ' ;
import ' package:pdf/pdf.dart ' ;
@@ -660,7 +661,8 @@ const _orderHeaders = [
' 商品编号 ' , ' 商品名称 ' , ' 系列 ' , ' 规格 ' , ' 批次号 ' ,
' 生产日期 ' , ' 数量 ' , ' 单价 ' , ' 金额 ' , ' 备注 ' ,
] ;
const _orderColFlex = < double > [ 1.5 , 2.0 , 1.1 , 1.3 , 1.6 , 1.3 , 0.7 , 1.1 , 1.2 , 0.9 ] ;
// 针打版式列宽:日期 / 单价 / 金额 加宽,避免大字号下数字、日期断行。
const _orderColFlex = < double > [ 1.4 , 1.8 , 1.0 , 1.2 , 1.6 , 1.45 , 0.7 , 1.45 , 1.45 , 0.7 ] ;
/// 温馨提示文案(入库/出库统一)。
const _kTipsText =
@@ -709,29 +711,61 @@ List<pw.Widget> _buildOrderDoc({
required double totalQty ,
required double totalAmt ,
} ) {
// 样式三「紧凑报表」:左侧竖线点缀 + 抬头左对齐内联 + 灰底信息条 + 密集明细表
// + 数字等宽(内置 Courier,ASCII 数字适用)+ 灰底总计条。
final mono = pw . Font . courier ( ) ;
// 针打友好版式:全纯黑、无灰底纹、纯黑实线边框、字号加大。
// 针式打印机打不出灰色(只能抖成稀疏散点→淡而糊),且小号汉字笔画易糊;
// 故正文加粗(重击更黑)、字号上调、所有灰色一律改纯黑。
// 数字等宽(内置 Courier, ASCII 数字适用)。
final mono = pw . Font . courierBold ( ) ;
final monoBold = pw . Font . courierBold ( ) ;
final cellStyle = pw . TextStyle ( font: font , fontSize: 8.5 ) ;
final numStyle = pw . TextStyle ( font: mono , fontSize: 8.5 ) ;
final cellStyle = pw . TextStyle (
font: bold , fontSize: 9 , fontWeight: pw . FontWeight . bold ) ;
final numStyle = pw . TextStyle ( font: mono , fontSize: 9 ) ;
final headStyle =
pw . TextStyle ( font: bold , fontSize: 8.5 , fontWeight: pw . FontWeight . bold ) ;
final metaStyle = pw . TextStyle ( font: font , fontSize: 9 ) ;
pw . TextStyle ( font: bold , fontSize: 9.5 , fontWeight: pw . FontWeight . bold ) ;
final metaStyle = pw . TextStyle (
font: bold , fontSize: 10 , fontWeight: pw . FontWeight . bold ) ;
final metaBold =
pw . TextStyle ( font: bold , fontSize: 9 , fontWeight: pw . FontWeight . bold ) ;
pw . TextStyle ( font: bold , fontSize: 10.5 , fontWeight: pw . FontWeight . bold ) ;
pw . Widget metaText ( String label , String value , pw . TextAlign align ) = >
pw . Text ( ' $ label : $ value ' , style: metaStyle , textAlign: align ) ;
// 返回明细块的「子组件列表」而非单个 Container:作为 MultiPage 的顶层
// children,明细表可跨页拆分。左侧竖线点缀改由 pageTheme 背景每页绘制
// (见 _orderPageTheme)——原先用非可拆分的 pw.Container 包裹整块内容,
// 当内容超过一页时 MultiPage 无法拆分,分页算法不前进→无限加页、内存暴涨
// →打印卡死(根因)。
// 竖线(左侧点缀)由内容自身携带,而非整页背景——让竖线随内容高度收缩,
// 跨页时尾页也只画到最后一行,不再画满整页。结构分三段:
// 前段(抬头+信息条) / 明细表 / 后段(总计+提示+签字)
// 前后两段是「短」原子 Container(带左边框=竖线),明细表用 TableBorder.left
// 当竖线(表格可跨页拆分,左边框随行收缩)。三段紧贴堆叠→竖线连续;尾段为
// 最后一个原子组件,其边框到内容末尾即止→竖线贴合内容。明细表是唯一可能超
// 一页的组件且本身可拆分,不存在「原子大组件超页」→不会触发分页死循环
// ( 1.0.84 修复的根因)。
pw . Widget group ( List < pw . Widget > children ,
{ double top = 0 , double bottom = 0 } ) = >
pw . Container (
decoration: const pw . BoxDecoration (
border:
pw . Border ( left: pw . BorderSide ( color: PdfColors . black , width: 3 ) ) ,
) ,
padding: pw . EdgeInsets . only ( top: top , bottom: bottom ) ,
child: pw . Column (
crossAxisAlignment: pw . CrossAxisAlignment . stretch ,
mainAxisSize: pw . MainAxisSize . min ,
children: children ,
) ,
) ;
// 明细表首列额外左缩进,使表格内容与各区块内容左缩进对齐。
pw . EdgeInsets headPad ( int i ) = > i = = 0
? const pw . EdgeInsets . only ( left: 11 , right: 2 , top: 2.5 , bottom: 2.5 )
: const pw . EdgeInsets . symmetric ( horizontal: 2 , vertical: 2.5 ) ;
pw . EdgeInsets bodyPad ( int i ) = > i = = 0
? const pw . EdgeInsets . only ( left: 11 , right: 3 , top: 2 , bottom: 2 )
: const pw . EdgeInsets . symmetric ( horizontal: 3 , vertical: 2 ) ;
return [
// ── 抬头:公司名 + 单据类型,左对齐内联 ──────────────────────────
pw . Row (
// ── 前段:抬头 + 信息条 ──────────────────── ──────────────────────────
group ( [
pw . Padding (
padding: const pw . EdgeInsets . only ( left: 11 ) ,
child: pw . Row (
crossAxisAlignment: pw . CrossAxisAlignment . end ,
children: [
pw . Text ( shopName ,
@@ -742,19 +776,19 @@ List<pw.Widget> _buildOrderDoc({
padding: const pw . EdgeInsets . only ( bottom: 1.5 ) ,
child: pw . Text ( title ,
style: pw . TextStyle (
font: font ,
fontSize: 11 ,
font: bold ,
fontSize: 12 ,
fontWeight: pw . FontWeight . bold ,
letterSpacing: 3 ,
color: PdfColors . grey700 ) ) ,
color: PdfColors . black ) ) ,
) ,
] ,
) ,
) ,
pw . SizedBox ( height: 8 ) ,
// ── 抬头信息:灰底信息条(两行三列)────────────────────────────
pw . Container (
decoration: pw . BoxDecoration (
color: PdfColors . grey100 ,
border: pw . Border . all ( color: PdfColors . grey400 , width: 0.5 ) ,
border: pw . Border . all ( color: PdfColors . black , width: 0.8 ) ,
) ,
padding: const pw . EdgeInsets . symmetric ( horizontal: 8 , vertical: 6 ) ,
child: pw . Column (
@@ -767,8 +801,8 @@ List<pw.Widget> _buildOrderDoc({
partnerLabel , partnerName ? ? ' ' , pw . TextAlign . left ) ) ,
pw . Expanded (
flex: 4 ,
child: metaText (
dateLabel , _d10 ( orderDate ) , pw . TextAlign . left ) ) ,
child:
metaText ( dateLabel , _d10 ( orderDate ) , pw . TextAlign . left ) ) ,
pw . Expanded (
flex: 3 ,
child: pw . Text ( ' $ noPrefix $ orderNo ' ,
@@ -793,23 +827,28 @@ List<pw.Widget> _buildOrderDoc({
] ,
) ,
) ,
pw . SizedBox ( height : 8 ) ,
// ── 明细表(密集、灰底表头、数字等宽)────────────── ────────────
] , bottom : 8 ) ,
// ── 明细表:左外边框 3 = 竖线(可跨页拆分,尾页随行收缩) ────────────
pw . Table (
border: pw . TableBorder . all ( color: PdfColors . grey500 , width: 0.5 ) ,
border: const pw . TableBorder (
left: pw . BorderSide ( color: PdfColors . black , width: 3 ) ,
top: pw . BorderSide ( color: PdfColors . black , width: 0.8 ) ,
right: pw . BorderSide ( color: PdfColors . black , width: 0.8 ) ,
bottom: pw . BorderSide ( color: PdfColors . black , width: 0.8 ) ,
horizontalInside: pw . BorderSide ( color: PdfColors . black , width: 0.8 ) ,
verticalInside: pw . BorderSide ( color: PdfColors . black , width: 0.8 ) ,
) ,
columnWidths: {
for ( var i = 0 ; i < _orderColFlex . length ; i + + )
i: pw . FlexColumnWidth ( _orderColFlex [ i ] )
} ,
children: [
pw . TableRow (
decoration: const pw . BoxDecoration ( color: PdfColors . grey300 ) ,
children: [
for ( final h in _orderHeaders )
for ( var i = 0 ; i < _orderHeaders . length ; i + + )
pw . Padding (
padding: const pw . EdgeInsets . symmetric (
horizontal: 2 , vertical: 2.5 ) ,
child: pw . Text ( h ,
padding: headPad ( i ) ,
child: pw . Text ( _orderHeaders [ i ] ,
textAlign: pw . TextAlign . center , style: headStyle ) ,
) ,
] ,
@@ -819,8 +858,7 @@ List<pw.Widget> _buildOrderDoc({
children: [
for ( var i = 0 ; i < row . length ; i + + )
pw . Padding (
padding: const pw . EdgeInsets . symmetric (
horizontal: 3 , vertical: 2 ) ,
padding: bodyPad ( i ) ,
child: pw . Text ( row [ i ] ,
textAlign: _colAlign ( i ) ,
style: ( i > = 6 & & i < = 8 ) ? numStyle : cellStyle ) ,
@@ -829,13 +867,12 @@ List<pw.Widget> _buildOrderDoc({
) ,
] ,
) ,
// ── 单据总计:灰底条,右对齐,仅数量 + 金额(等宽数字) ──────────
pw . SizedBox ( height: 6 ) ,
// ── 后段: 单据总计 + 温馨提示 + 签字行 ───────────────────── ──────────
group ( [
pw . Container (
width: double . infinity ,
decoration: pw . BoxDecoration (
color: PdfColors . grey100 ,
border: pw . Border . all ( color: PdfColors . grey400 , width: 0.5 ) ,
border: pw . Border . all ( color: PdfColors . black , width: 0.8 ) ,
) ,
padding: const pw . EdgeInsets . symmetric ( horizontal: 10 , vertical: 5 ) ,
child: pw . Row (
@@ -854,24 +891,26 @@ List<pw.Widget> _buildOrderDoc({
] ,
) ,
) ,
// ── 温馨提示(仅出库单)──────────────────────────────────────────
if ( showTips ) . . . [
pw . SizedBox ( height: 8 ) ,
pw . Container (
width: double . infinity ,
decoration: pw . BoxDecoration (
border: pw . Border . all ( color: PdfColors . grey400 , width: 0.5 ) ,
color: PdfColors . grey50 ,
border: pw . Border . all ( color: PdfColors . black , width: 0.8 ) ,
) ,
padding: const pw . EdgeInsets . all ( 5 ) ,
child: pw . Text ( _kTipsText ,
style: pw . TextStyle (
font: font , fontSize: 8.5 , color: PdfColors . grey800 ) ) ,
font: bold ,
fontSize: 9 ,
fontWeight: pw . FontWeight . bold ,
color: PdfColors . black ) ) ,
) ,
] ,
pw . SizedBox ( height: 24 ) ,
// ── 签字行:签收人 / 经办人 / 制单人 ──────────────────────────────
pw . Row (
pw . Padding (
padding: const pw . EdgeInsets . only ( left: 11 ) ,
child: pw . Row (
mainAxisAlignment: pw . MainAxisAlignment . spaceBetween ,
children: [
pw . Text ( ' $ signerLabel : ' , style: metaStyle ) ,
@@ -880,42 +919,80 @@ List<pw.Widget> _buildOrderDoc({
pw . Text ( ' 制单人: ${ makerName ? ? ' ' } ' , style: metaStyle ) ,
] ,
) ,
) ,
if ( ( remark ? ? ' ' ) . isNotEmpty ) . . . [
pw . SizedBox ( height: 10 ) ,
pw . Text ( ' 备注: $ remark ' , style: metaStyle ) ,
pw . Padding (
padding: const pw . EdgeInsets . only ( left: 11 ) ,
child: pw . Text ( ' 备注: $ remark ' , style: metaStyle ) ,
) ,
] ,
] , top: 8 ) ,
] ;
}
/// 出/入库单 MultiPage 页面主题:内容区左缩进 + 全页高左侧竖线点缀(每页都画)。
/// 用 pageTheme 背景绘制竖线,让明细内容(含表格)可跨页拆分,避免非可拆分
/// Container 在内容超一页时触发 MultiPage 分页死循环。
pw . PageTheme _orderPageTheme ( ) = > pw . PageTheme (
/// 出/入库单文档:单一 MultiPage,竖线由 `_buildOrderDoc` 的内容自身携带
/// (前/后两段原子 Container 的左边框 + 明细表 TableBorder.left),故竖线随内容
/// 高度收缩、跨页时尾页只画到最后一行;明细表可跨页拆分,无原子大组件超页,
/// 不会触发分页死循环。
pw . Document _composeOrderDoc ( List < pw . Widget > Function ( ) buildContent ) {
final doc = pw . Document ( ) ;
doc . addPage ( pw . MultiPage (
pageFormat: PdfPageFormat . a4 ,
// 左边距 = 原始外边距 15mm + (竖线宽 3 + 内缩 14),与旧版视觉缩进一致。
margin: const pw . EdgeInsets . fromLTRB (
15 * PdfPageFormat . mm + 17 ,
12 * PdfPageFormat . mm ,
15 * PdfPageFormat . mm ,
12 * PdfPageFormat . mm ,
) ,
buildBackground: ( ctx ) = > pw . FullPage (
ignoreMargins: true ,
child: pw . Padding (
padding: const pw . EdgeInsets . fromLTRB (
15 * PdfPageFormat . mm ,
12 * PdfPageFormat . mm ,
0 ,
12 * PdfPageFormat . mm ,
) ,
child: pw . Container (
decoration: const pw . BoxDecoration (
border: pw . Border (
left: pw . BorderSide ( color: PdfColors . black , width: 3 ) ) ,
) ,
) ,
) ,
margin: const pw . EdgeInsets . symmetric (
horizontal: 15 * PdfPageFormat . mm ,
vertical: 12 * PdfPageFormat . mm ,
) ,
build: ( _ ) = > buildContent ( ) ,
) ) ;
return doc ;
}
Future < Uint8List > _buildOrderPdf (
List < pw . Widget > Function ( ) buildContent ) async = >
_composeOrderDoc ( buildContent ) . save ( ) ;
/// 测试用:构建文档并返回页数(用于回归「明细超一页不卡死且能正常分页」)。
@ visibleForTesting
Future < int > debugOrderDocPageCount (
List < pw . Widget > Function ( ) buildContent ) async {
final doc = _composeOrderDoc ( buildContent ) ;
await doc . save ( ) ;
return doc . document . pdfPageList . pages . length ;
}
/// 测试用:暴露 `_buildOrderDoc`,便于在不依赖 model 的情况下构造内容。
@ visibleForTesting
List < pw . Widget > debugBuildOrderDoc ( {
required pw . Font font ,
required pw . Font bold ,
required List < List < String > > rows ,
required double totalQty ,
required double totalAmt ,
bool showTips = true ,
} ) = >
_buildOrderDoc (
font: font ,
bold: bold ,
shopName: ' 测试酒行 ' ,
title: ' 出 库 单 ' ,
orderNo: ' CK202503010002 ' ,
noPrefix: ' NO. ' ,
partnerLabel: ' 客户名称 ' ,
partnerName: ' 张三 ' ,
dateLabel: ' 出库日期 ' ,
orderDate: ' 2025-03-01 ' ,
addressLabel: ' 送货地址 ' ,
warehouseName: ' 主仓 ' ,
signerLabel: ' 收货人(签字) ' ,
operatorRoleLabel: ' 销售员 ' ,
operatorName: ' 李四 ' ,
makerName: ' 王五 ' ,
remark: null ,
showTips: showTips ,
rows: rows ,
totalQty: totalQty ,
totalAmt: totalAmt ,
) ;
/// 生成入库单 A4 PDF 字节(与实际打印同一套排版,供预览光栅化 + 打印共用)。
@@ -943,10 +1020,7 @@ Future<Uint8List> buildStockInOrderPdfImpl(
] ) ;
}
final doc = pw . Document ( ) ;
doc . addPage ( pw . MultiPage (
pageTheme: _orderPageTheme ( ) ,
build: ( _ ) = > _buildOrderDoc (
return _buildOrderPdf ( ( ) = > _buildOrderDoc (
font: font , bold: bold ,
shopName: meta . shopName ,
title: ' 入 库 单 ' ,
@@ -967,9 +1041,7 @@ Future<Uint8List> buildStockInOrderPdfImpl(
rows: rows ,
totalQty: totalQty ,
totalAmt: totalAmt ,
) ,
) ) ;
return doc . save ( ) ;
}
Future < void > printStockInOrderImpl (
@@ -1018,10 +1090,7 @@ Future<Uint8List> buildStockOutOrderPdfImpl(
] ) ;
}
final doc = pw . Document ( ) ;
doc . addPage ( pw . MultiPage (
pageTheme: _orderPageTheme ( ) ,
build: ( _ ) = > _buildOrderDoc (
return _buildOrderPdf ( ( ) = > _buildOrderDoc (
font: font , bold: bold ,
shopName: meta . shopName ,
title: ' 出 库 单 ' ,
@@ -1042,9 +1111,7 @@ Future<Uint8List> buildStockOutOrderPdfImpl(
rows: rows ,
totalQty: totalQty ,
totalAmt: totalAmt ,
) ,
) ) ;
return doc . save ( ) ;
}
Future < void > printStockOutOrderImpl (