Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 021c4f17cc | |||
| ed7a37cc05 | |||
| 47cf0ae547 |
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.86] - 2026-06-27
|
||||
|
||||
### 修复
|
||||
- 修复出库单 / 入库单打印时左侧竖线总是画满整页、与单据内容长度不一致的问题:现竖线随内容自动收缩,单据较短或跨页最后一页时只画到最后一行,不再多打一长截。
|
||||
|
||||
## [1.0.85] - 2026-06-27
|
||||
|
||||
### 改进
|
||||
- 出库单 / 入库单打印改为针式打印机友好版式:全部纯黑、去除灰色底纹、表格改纯黑实线边框、字体加粗加大,针打输出更清晰、不再发淡发糊;同时优化列宽,避免单价 / 金额 / 日期等数字被换行截断。
|
||||
|
||||
## [1.0.84] - 2026-06-26
|
||||
|
||||
### 修复
|
||||
- 修复出库单 / 入库单打印时,单据明细超过一页会导致程序卡死无响应的问题(新版打印版式引入的回归)。
|
||||
|
||||
## [1.0.83] - 2026-06-26
|
||||
|
||||
### 改进
|
||||
|
||||
@@ -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 =
|
||||
@@ -686,7 +688,7 @@ pw.TextAlign _colAlign(int i) {
|
||||
|
||||
/// 构建入库单/出库单 A4 版式(参照「锐浪报表」单据:公司抬头 + 两行三列表头
|
||||
/// + 明细表 + 单据总计 + 温馨提示 + 签字行)。入库/出库仅称谓不同。
|
||||
pw.Widget _buildOrderDoc({
|
||||
List<pw.Widget> _buildOrderDoc({
|
||||
required pw.Font font,
|
||||
required pw.Font bold,
|
||||
required String shopName, // 公司名称(顶部抬头)
|
||||
@@ -709,32 +711,61 @@ 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);
|
||||
|
||||
return pw.Container(
|
||||
// 左侧竖线点缀
|
||||
decoration: const pw.BoxDecoration(
|
||||
border: pw.Border(left: pw.BorderSide(color: PdfColors.black, width: 3)),
|
||||
),
|
||||
padding: const pw.EdgeInsets.only(left: 14),
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// ── 抬头:公司名 + 单据类型,左对齐内联 ──────────────────────────
|
||||
pw.Row(
|
||||
// 竖线(左侧点缀)由内容自身携带,而非整页背景——让竖线随内容高度收缩,
|
||||
// 跨页时尾页也只画到最后一行,不再画满整页。结构分三段:
|
||||
// 前段(抬头+信息条) / 明细表 / 后段(总计+提示+签字)
|
||||
// 前后两段是「短」原子 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 [
|
||||
// ── 前段:抬头 + 信息条 ──────────────────────────────────────────────
|
||||
group([
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.only(left: 11),
|
||||
child: pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||
children: [
|
||||
pw.Text(shopName,
|
||||
@@ -745,136 +776,141 @@ 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),
|
||||
),
|
||||
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.grey500, width: 0.5),
|
||||
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(
|
||||
decoration: const pw.BoxDecoration(color: PdfColors.grey300),
|
||||
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(
|
||||
color: PdfColors.grey100,
|
||||
border: pw.Border.all(color: PdfColors.grey400, width: 0.5),
|
||||
),
|
||||
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)),
|
||||
],
|
||||
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)),
|
||||
),
|
||||
// ── 温馨提示(仅出库单)──────────────────────────────────────────
|
||||
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,
|
||||
),
|
||||
padding: const pw.EdgeInsets.all(5),
|
||||
child: pw.Text(_kTipsText,
|
||||
style: pw.TextStyle(
|
||||
font: font, fontSize: 8.5, color: PdfColors.grey800)),
|
||||
),
|
||||
],
|
||||
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),
|
||||
@@ -883,15 +919,82 @@ 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,竖线由 `_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 字节(与实际打印同一套排版,供预览光栅化 + 打印共用)。
|
||||
Future<Uint8List> buildStockInOrderPdfImpl(
|
||||
StockInOrder order, OrderPrintMeta meta) async {
|
||||
@@ -917,12 +1020,7 @@ Future<Uint8List> buildStockInOrderPdfImpl(
|
||||
]);
|
||||
}
|
||||
|
||||
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: (_) => [
|
||||
_buildOrderDoc(
|
||||
return _buildOrderPdf(() => _buildOrderDoc(
|
||||
font: font, bold: bold,
|
||||
shopName: meta.shopName,
|
||||
title: '入 库 单',
|
||||
@@ -943,10 +1041,7 @@ Future<Uint8List> buildStockInOrderPdfImpl(
|
||||
rows: rows,
|
||||
totalQty: totalQty,
|
||||
totalAmt: totalAmt,
|
||||
),
|
||||
],
|
||||
));
|
||||
return doc.save();
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> printStockInOrderImpl(
|
||||
@@ -995,12 +1090,7 @@ Future<Uint8List> buildStockOutOrderPdfImpl(
|
||||
]);
|
||||
}
|
||||
|
||||
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: (_) => [
|
||||
_buildOrderDoc(
|
||||
return _buildOrderPdf(() => _buildOrderDoc(
|
||||
font: font, bold: bold,
|
||||
shopName: meta.shopName,
|
||||
title: '出 库 单',
|
||||
@@ -1021,10 +1111,7 @@ Future<Uint8List> buildStockOutOrderPdfImpl(
|
||||
rows: rows,
|
||||
totalQty: totalQty,
|
||||
totalAmt: totalAmt,
|
||||
),
|
||||
],
|
||||
));
|
||||
return doc.save();
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> printStockOutOrderImpl(
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:jiu_client/core/utils/print_util_stub.dart';
|
||||
|
||||
/// 出/入库单打印分页回归测试。
|
||||
///
|
||||
/// 背景:1.0.83 的打印改版用「非可拆分 Container 包裹整块内容」,当明细超过一页
|
||||
/// 时 MultiPage 无法拆分、分页算法不前进 → 无限加页、内存暴涨 → 打印卡死。
|
||||
/// 1.0.86 改为三段结构(前/后两段短原子 Container + 可拆分明细表,竖线由内容
|
||||
/// 自身携带),明细表可跨页拆分,无「原子大组件超页」,正常分页且竖线尾页贴合
|
||||
/// 内容。本测试锁定该行为,防止再退回卡死实现。
|
||||
void main() {
|
||||
late pw.Font font;
|
||||
|
||||
setUpAll(() {
|
||||
// 直接从仓库内字体文件加载(无需 Flutter asset bundle / binding)。
|
||||
final bytes =
|
||||
File('assets/fonts/NotoSansSC-Regular.ttf').readAsBytesSync();
|
||||
font = pw.Font.ttf(bytes.buffer.asByteData());
|
||||
});
|
||||
|
||||
List<List<String>> makeRows(int n) => [
|
||||
for (var i = 0; i < n; i++)
|
||||
[
|
||||
'ZXZ0112$i',
|
||||
'茅台金贵宾2011',
|
||||
'普通/53度',
|
||||
'500ml/单品',
|
||||
'2010-32/07852',
|
||||
'2026-01-19',
|
||||
'1',
|
||||
'1000.00',
|
||||
'1000.00',
|
||||
'',
|
||||
],
|
||||
];
|
||||
|
||||
Future<int> pageCount(int rows) => debugOrderDocPageCount(
|
||||
() => debugBuildOrderDoc(
|
||||
font: font,
|
||||
bold: font,
|
||||
rows: makeRows(rows),
|
||||
totalQty: rows.toDouble(),
|
||||
totalAmt: rows * 1000.0,
|
||||
),
|
||||
);
|
||||
|
||||
test('单页:少量明细生成 1 页', () async {
|
||||
expect(await pageCount(1), 1);
|
||||
expect(await pageCount(3), 1);
|
||||
});
|
||||
|
||||
test('跨页:明细超过一页能正常分页且不卡死(回归 1.0.84 打印卡死)', () async {
|
||||
// 60 行远超一页:旧实现会在此无限加页→卡死。新实现应正常分页到多页。
|
||||
// 用 timeout 兜底——若退回卡死实现,此处会超时失败而非整个测试进程挂死。
|
||||
final pages = await pageCount(60);
|
||||
expect(pages, greaterThan(1));
|
||||
}, timeout: const Timeout(Duration(seconds: 30)));
|
||||
|
||||
test('明细行数越多页数单调不减(分页稳定推进,不会原地打转)', () async {
|
||||
final p20 = await pageCount(20);
|
||||
final p120 = await pageCount(120);
|
||||
expect(p120, greaterThanOrEqualTo(p20));
|
||||
expect(p120, greaterThan(1));
|
||||
}, timeout: const Timeout(Duration(seconds: 45)));
|
||||
}
|
||||
Reference in New Issue
Block a user