chore: release client-v1.0.86
Deploy Client / build-windows (push) Successful in 1m54s
Deploy Client / build-client-web (push) Successful in 44s
Deploy Client / build-macos (push) Successful in 2m22s
Deploy Client / build-android (push) Successful in 1m35s
Deploy Client / build-ios (push) Successful in 2m47s
Deploy Client / release-deploy-client (push) Successful in 1m57s

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-27 19:28:01 +08:00
parent ed7a37cc05
commit 021c4f17cc
3 changed files with 298 additions and 163 deletions
+225 -163
View File
@@ -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';
@@ -729,14 +730,42 @@ List<pw.Widget> _buildOrderDoc({
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,
@@ -755,128 +784,133 @@ List<pw.Widget> _buildOrderDoc({
),
],
),
pw.SizedBox(height: 8),
// ── 抬头信息:灰底信息条(两行三列)────────────────────────────
pw.Container(
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.black, width: 0.8),
),
padding: const pw.EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: pw.Column(
children: [
pw.Row(
children: [
pw.Expanded(
flex: 5,
child: metaText(
partnerLabel, partnerName ?? '', pw.TextAlign.left)),
pw.Expanded(
flex: 4,
child: metaText(
dateLabel, _d10(orderDate), pw.TextAlign.left)),
pw.Expanded(
flex: 3,
child: pw.Text('$noPrefix$orderNo',
style: metaBold, textAlign: pw.TextAlign.right)),
],
),
pw.SizedBox(height: 3),
pw.Row(
children: [
pw.Expanded(
flex: 5,
child: metaText('联系电话', '', pw.TextAlign.left)),
pw.Expanded(
flex: 4,
child: metaText(addressLabel, '', pw.TextAlign.left)),
pw.Expanded(
flex: 3,
child: metaText(
'仓库', warehouseName ?? '', pw.TextAlign.right)),
],
),
],
),
),
pw.SizedBox(height: 8),
pw.Container(
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.black, width: 0.8),
),
pw.SizedBox(height: 8),
// ── 明细表(密集、灰底表头、数字等宽)──────────────────────────
pw.Table(
border: pw.TableBorder.all(color: PdfColors.black, width: 0.8),
columnWidths: {
for (var i = 0; i < _orderColFlex.length; i++)
i: pw.FlexColumnWidth(_orderColFlex[i])
},
padding: const pw.EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: pw.Column(
children: [
pw.TableRow(
pw.Row(
children: [
for (final h in _orderHeaders)
pw.Padding(
padding: const pw.EdgeInsets.symmetric(
horizontal: 2, vertical: 2.5),
child: pw.Text(h,
textAlign: pw.TextAlign.center, style: headStyle),
),
pw.Expanded(
flex: 5,
child: metaText(
partnerLabel, partnerName ?? '', pw.TextAlign.left)),
pw.Expanded(
flex: 4,
child:
metaText(dateLabel, _d10(orderDate), pw.TextAlign.left)),
pw.Expanded(
flex: 3,
child: pw.Text('$noPrefix$orderNo',
style: metaBold, textAlign: pw.TextAlign.right)),
],
),
for (final row in rows)
pw.TableRow(
children: [
for (var i = 0; i < row.length; i++)
pw.Padding(
padding: const pw.EdgeInsets.symmetric(
horizontal: 3, vertical: 2),
child: pw.Text(row[i],
textAlign: _colAlign(i),
style: (i >= 6 && i <= 8) ? numStyle : cellStyle),
),
],
pw.SizedBox(height: 3),
pw.Row(
children: [
pw.Expanded(
flex: 5,
child: metaText('联系电话', '', pw.TextAlign.left)),
pw.Expanded(
flex: 4,
child: metaText(addressLabel, '', pw.TextAlign.left)),
pw.Expanded(
flex: 3,
child: metaText(
'仓库', warehouseName ?? '', pw.TextAlign.right)),
],
),
],
),
),
], bottom: 8),
// ── 明细表:左外边框 3 = 竖线(可跨页拆分,尾页随行收缩)────────────
pw.Table(
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(
children: [
for (var i = 0; i < _orderHeaders.length; i++)
pw.Padding(
padding: headPad(i),
child: pw.Text(_orderHeaders[i],
textAlign: pw.TextAlign.center, style: headStyle),
),
],
),
// ── 单据总计:灰底条,右对齐,仅数量 + 金额(等宽数字)──────────
pw.SizedBox(height: 6),
for (final row in rows)
pw.TableRow(
children: [
for (var i = 0; i < row.length; i++)
pw.Padding(
padding: bodyPad(i),
child: pw.Text(row[i],
textAlign: _colAlign(i),
style: (i >= 6 && i <= 8) ? numStyle : cellStyle),
),
],
),
],
),
// ── 后段:单据总计 + 温馨提示 + 签字行 ───────────────────────────────
group([
pw.Container(
width: double.infinity,
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.black, width: 0.8),
),
padding: const pw.EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.end,
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
pw.Text('单据总计', style: metaBold),
pw.SizedBox(width: 26),
pw.Text('数量 ', style: metaStyle),
pw.Text(_qtyStr(totalQty),
style: pw.TextStyle(font: monoBold, fontSize: 11)),
pw.SizedBox(width: 26),
pw.Text('金额 ', style: metaStyle),
pw.Text(totalAmt.toStringAsFixed(2),
style: pw.TextStyle(font: monoBold, fontSize: 12)),
],
),
),
if (showTips) ...[
pw.SizedBox(height: 8),
pw.Container(
width: double.infinity,
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.black, width: 0.8),
),
padding: const pw.EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.end,
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
pw.Text('单据总计', style: metaBold),
pw.SizedBox(width: 26),
pw.Text('数量 ', style: metaStyle),
pw.Text(_qtyStr(totalQty),
style: pw.TextStyle(font: monoBold, fontSize: 11)),
pw.SizedBox(width: 26),
pw.Text('金额 ', style: metaStyle),
pw.Text(totalAmt.toStringAsFixed(2),
style: pw.TextStyle(font: monoBold, fontSize: 12)),
],
),
padding: const pw.EdgeInsets.all(5),
child: pw.Text(_kTipsText,
style: pw.TextStyle(
font: bold,
fontSize: 9,
fontWeight: pw.FontWeight.bold,
color: PdfColors.black)),
),
// ── 温馨提示(仅出库单)──────────────────────────────────────────
if (showTips) ...[
pw.SizedBox(height: 8),
pw.Container(
width: double.infinity,
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.black, width: 0.8),
),
padding: const pw.EdgeInsets.all(5),
child: pw.Text(_kTipsText,
style: pw.TextStyle(
font: bold,
fontSize: 9,
fontWeight: pw.FontWeight.bold,
color: PdfColors.black)),
),
],
pw.SizedBox(height: 24),
// ── 签字行:签收人 / 经办人 / 制单人 ──────────────────────────────
pw.Row(
],
pw.SizedBox(height: 24),
pw.Padding(
padding: const pw.EdgeInsets.only(left: 11),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text('$signerLabel', style: metaStyle),
@@ -885,42 +919,80 @@ List<pw.Widget> _buildOrderDoc({
pw.Text('制单人:${makerName ?? ''}', style: metaStyle),
],
),
if ((remark ?? '').isNotEmpty) ...[
pw.SizedBox(height: 10),
pw.Text('备注:$remark', style: metaStyle),
],
),
if ((remark ?? '').isNotEmpty) ...[
pw.SizedBox(height: 10),
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(
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)),
),
),
),
),
/// 出/入库单文档:单一 MultiPage,竖线由 `_buildOrderDoc` 的内容自身携带
/// (前/后两段原子 Container 的左边框 + 明细表 TableBorder.left),故竖线随内容
/// 高度收缩、跨页时尾页只画到最后一行;明细表可跨页拆分,无原子大组件超页,
/// 不会触发分页死循环。
pw.Document _composeOrderDoc(List<pw.Widget> Function() buildContent) {
final doc = pw.Document();
doc.addPage(pw.MultiPage(
pageFormat: PdfPageFormat.a4,
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 字节(与实际打印同一套排版,供预览光栅化 + 打印共用)。
@@ -948,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: '入 库 单',
@@ -972,9 +1041,7 @@ Future<Uint8List> buildStockInOrderPdfImpl(
rows: rows,
totalQty: totalQty,
totalAmt: totalAmt,
),
));
return doc.save();
));
}
Future<void> printStockInOrderImpl(
@@ -1023,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: '出 库 单',
@@ -1047,9 +1111,7 @@ Future<Uint8List> buildStockOutOrderPdfImpl(
rows: rows,
totalQty: totalQty,
totalAmt: totalAmt,
),
));
return doc.save();
));
}
Future<void> printStockOutOrderImpl(