refactor(client): 出入库单打印改用「紧凑报表」版式

- 左侧竖线点缀 + 抬头左对齐内联(公司名 + 单据类型同行)
- 抬头信息改为灰底信息条;明细表收紧字号/行距、灰底表头
- 数量/单价/金额/总计改等宽数字(PDF 内置 Courier,Web monospace)
- 单据总计改为右对齐灰底条
- 仅排版调整,字段/数据/售价口径/温馨提示逻辑均不变

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-26 18:10:12 +08:00
parent a355a3a2a2
commit 1558ce1e94
2 changed files with 168 additions and 154 deletions
+60 -48
View File
@@ -709,42 +709,57 @@ pw.Widget _buildOrderDoc({
required double totalQty, required double totalQty,
required double totalAmt, required double totalAmt,
}) { }) {
final cellStyle = pw.TextStyle(font: font, fontSize: 9); // 样式三「紧凑报表」:左侧竖线点缀 + 抬头左对齐内联 + 灰底信息条 + 密集明细表
// + 数字等宽(内置 Courier,ASCII 数字适用)+ 灰底总计条。
final mono = pw.Font.courier();
final monoBold = pw.Font.courierBold();
final cellStyle = pw.TextStyle(font: font, fontSize: 8.5);
final numStyle = pw.TextStyle(font: mono, fontSize: 8.5);
final headStyle = final headStyle =
pw.TextStyle(font: bold, fontSize: 9.5, fontWeight: pw.FontWeight.bold); pw.TextStyle(font: bold, fontSize: 8.5, fontWeight: pw.FontWeight.bold);
final metaStyle = pw.TextStyle(font: font, fontSize: 10); final metaStyle = pw.TextStyle(font: font, fontSize: 9);
final boldStyle = final metaBold =
pw.TextStyle(font: bold, fontSize: 10, fontWeight: pw.FontWeight.bold); pw.TextStyle(font: bold, fontSize: 9, fontWeight: pw.FontWeight.bold);
pw.Widget metaText(String label, String value, pw.TextAlign align) => pw.Widget metaText(String label, String value, pw.TextAlign align) =>
pw.Text('$label$value', style: metaStyle, textAlign: align); pw.Text('$label$value', style: metaStyle, textAlign: align);
return pw.Column( 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, crossAxisAlignment: pw.CrossAxisAlignment.stretch,
children: [ children: [
// ── 公司名称 ────────────────────────────────────────────────────────── // ── 抬头:公司名 + 单据类型,左对齐内联 ──────────────────────────
pw.Center( pw.Row(
child: pw.Text(shopName, crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text(shopName,
style: pw.TextStyle( style: pw.TextStyle(
font: bold, fontSize: 18, fontWeight: pw.FontWeight.bold)), font: bold, fontSize: 16, fontWeight: pw.FontWeight.bold)),
), pw.SizedBox(width: 12),
pw.SizedBox(height: 4), pw.Padding(
// ── 单据类型 ────────────────────────────────────────────────────────── padding: const pw.EdgeInsets.only(bottom: 1.5),
pw.Center(
child: pw.Text(title, child: pw.Text(title,
style: pw.TextStyle( style: pw.TextStyle(
font: bold, font: font,
fontSize: 15, fontSize: 11,
fontWeight: pw.FontWeight.bold, letterSpacing: 3,
letterSpacing: 2)), color: PdfColors.grey700)),
),
],
), ),
pw.SizedBox(height: 8), pw.SizedBox(height: 8),
// ── 抬头信息(两行三列)────────────────────────────────────────────── // ── 抬头信息:灰底信息条(两行三列)────────────────────────────
pw.Container( pw.Container(
decoration: const pw.BoxDecoration( decoration: pw.BoxDecoration(
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.grey700)), color: PdfColors.grey100,
border: pw.Border.all(color: PdfColors.grey400, width: 0.5),
), ),
padding: const pw.EdgeInsets.only(bottom: 5), padding: const pw.EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: pw.Column( child: pw.Column(
children: [ children: [
pw.Row( pw.Row(
@@ -760,7 +775,7 @@ pw.Widget _buildOrderDoc({
pw.Expanded( pw.Expanded(
flex: 3, flex: 3,
child: pw.Text('$noPrefix$orderNo', child: pw.Text('$noPrefix$orderNo',
style: boldStyle, textAlign: pw.TextAlign.right)), style: metaBold, textAlign: pw.TextAlign.right)),
], ],
), ),
pw.SizedBox(height: 3), pw.SizedBox(height: 3),
@@ -782,21 +797,21 @@ pw.Widget _buildOrderDoc({
), ),
), ),
pw.SizedBox(height: 8), pw.SizedBox(height: 8),
// ── 明细表 + 单据总计 ──────────────────────────────────────────────── // ── 明细表(密集、灰底表头、数字等宽)──────────────────────────
pw.Table( pw.Table(
border: pw.TableBorder.all(color: PdfColors.grey600, width: 0.5), border: pw.TableBorder.all(color: PdfColors.grey500, width: 0.5),
columnWidths: { columnWidths: {
for (var i = 0; i < _orderColFlex.length; i++) for (var i = 0; i < _orderColFlex.length; i++)
i: pw.FlexColumnWidth(_orderColFlex[i]) i: pw.FlexColumnWidth(_orderColFlex[i])
}, },
children: [ children: [
pw.TableRow( pw.TableRow(
decoration: const pw.BoxDecoration(color: PdfColors.grey200), decoration: const pw.BoxDecoration(color: PdfColors.grey300),
children: [ children: [
for (final h in _orderHeaders) for (final h in _orderHeaders)
pw.Padding( pw.Padding(
padding: padding: const pw.EdgeInsets.symmetric(
const pw.EdgeInsets.symmetric(horizontal: 2, vertical: 3), horizontal: 2, vertical: 2.5),
child: pw.Text(h, child: pw.Text(h,
textAlign: pw.TextAlign.center, style: headStyle), textAlign: pw.TextAlign.center, style: headStyle),
), ),
@@ -808,46 +823,41 @@ pw.Widget _buildOrderDoc({
for (var i = 0; i < row.length; i++) for (var i = 0; i < row.length; i++)
pw.Padding( pw.Padding(
padding: const pw.EdgeInsets.symmetric( padding: const pw.EdgeInsets.symmetric(
horizontal: 2, vertical: 2.5), horizontal: 3, vertical: 2),
child: pw.Text(row[i], child: pw.Text(row[i],
textAlign: _colAlign(i), style: cellStyle), textAlign: _colAlign(i),
style: (i >= 6 && i <= 8) ? numStyle : cellStyle),
), ),
], ],
), ),
], ],
), ),
// ── 单据总计:独立一行,仅数量 + 金额,右对齐高亮 ────────────────── // ── 单据总计:灰底条,右对齐,仅数量 + 金额(等宽数字)──────────
pw.SizedBox(height: 6), pw.SizedBox(height: 6),
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
decoration: pw.BoxDecoration( decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.grey600, width: 0.5),
color: PdfColors.grey100, color: PdfColors.grey100,
border: pw.Border.all(color: PdfColors.grey400, width: 0.5),
), ),
padding: const pw.EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: const pw.EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: pw.Row( child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.end, mainAxisAlignment: pw.MainAxisAlignment.end,
crossAxisAlignment: pw.CrossAxisAlignment.center, crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [ children: [
pw.Text('单据总计', pw.Text('单据总计', style: metaBold),
style: pw.TextStyle( pw.SizedBox(width: 26),
font: bold, fontSize: 10.5, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(width: 30),
pw.Text('数量 ', style: metaStyle), pw.Text('数量 ', style: metaStyle),
pw.SizedBox(width: 6),
pw.Text(_qtyStr(totalQty), pw.Text(_qtyStr(totalQty),
style: pw.TextStyle( style: pw.TextStyle(font: monoBold, fontSize: 11)),
font: bold, fontSize: 11, fontWeight: pw.FontWeight.bold)), pw.SizedBox(width: 26),
pw.SizedBox(width: 30),
pw.Text('金额 ', style: metaStyle), pw.Text('金额 ', style: metaStyle),
pw.SizedBox(width: 6),
pw.Text(totalAmt.toStringAsFixed(2), pw.Text(totalAmt.toStringAsFixed(2),
style: pw.TextStyle( style: pw.TextStyle(font: monoBold, fontSize: 12)),
font: bold, fontSize: 12, fontWeight: pw.FontWeight.bold)),
], ],
), ),
), ),
// ── 温馨提示(仅出库单)──────────────────────────────────────────── // ── 温馨提示(仅出库单)──────────────────────────────────────────
if (showTips) ...[ if (showTips) ...[
pw.SizedBox(height: 8), pw.SizedBox(height: 8),
pw.Container( pw.Container(
@@ -862,13 +872,14 @@ pw.Widget _buildOrderDoc({
font: font, fontSize: 8.5, color: PdfColors.grey800)), font: font, fontSize: 8.5, color: PdfColors.grey800)),
), ),
], ],
pw.SizedBox(height: 26), pw.SizedBox(height: 24),
// ── 签字行:签收人 / 经办人 / 制单人 ──────────────────────────────── // ── 签字行:签收人 / 经办人 / 制单人 ──────────────────────────────
pw.Row( pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [ children: [
pw.Text('$signerLabel', style: metaStyle), pw.Text('$signerLabel', style: metaStyle),
pw.Text('$operatorRoleLabel${operatorName ?? ''}', style: metaStyle), pw.Text('$operatorRoleLabel${operatorName ?? ''}',
style: metaStyle),
pw.Text('制单人:${makerName ?? ''}', style: metaStyle), pw.Text('制单人:${makerName ?? ''}', style: metaStyle),
], ],
), ),
@@ -877,6 +888,7 @@ pw.Widget _buildOrderDoc({
pw.Text('备注:$remark', style: metaStyle), pw.Text('备注:$remark', style: metaStyle),
], ],
], ],
),
); );
} }
+18 -16
View File
@@ -181,27 +181,29 @@ Future<Uint8List> buildStockOutOrderPdfImpl(
StockOutOrder order, OrderPrintMeta meta) async => StockOutOrder order, OrderPrintMeta meta) async =>
throw UnsupportedError('Web 端出库单走 HTML 打印,不生成 PDF'); throw UnsupportedError('Web 端出库单走 HTML 打印,不生成 PDF');
/// 单据打印共用样式(入库/出库一致)。 /// 单据打印共用样式(样式三「紧凑报表」:左侧竖线 + 抬头左对齐内联 +
/// 灰底信息条 + 密集明细表 + 数字等宽 + 灰底总计条;入库/出库一致)。
const String _orderPrintCss = ''' const String _orderPrintCss = '''
@page { size: A4; margin: 12mm 15mm; } @page { size: A4; margin: 12mm 14mm; }
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: "PingFang SC", "Microsoft YaHei", "SimSun", sans-serif; font-size: 11px; color: #000; } body { font-family: "PingFang SC", "Microsoft YaHei", "SimSun", sans-serif; color: #111; border-left: 3px solid #111; padding-left: 14px; }
.shop { text-align: center; font-size: 20px; font-weight: bold; } .shop { display: inline; font-size: 19px; font-weight: 700; }
.title { text-align: center; font-size: 16px; font-weight: bold; letter-spacing: 4px; margin: 4px 0 8px; } .title { display: inline; margin-left: 12px; font-size: 13px; letter-spacing: 4px; color: #444; }
.head-block { border-bottom: 1px solid #555; padding-bottom: 5px; margin-bottom: 8px; } .head-block { background: #f3f3f3; border: 1px solid #ccc; padding: 7px 10px; margin: 10px 0; }
.meta-row { display: flex; font-size: 11px; } .meta-row { display: flex; font-size: 11.5px; line-height: 1.9; }
.meta-row + .meta-row { margin-top: 3px; } .meta-row + .meta-row { margin-top: 2px; }
.meta-row .c1 { flex: 5; } .meta-row .c2 { flex: 4; } .meta-row .c3 { flex: 3; text-align: right; } .meta-row .c1 { flex: 5; } .meta-row .c2 { flex: 4; } .meta-row .c3 { flex: 3; text-align: right; }
.meta-row .no { font-weight: bold; font-size: 12px; } .meta-row .no { font-weight: 700; }
table { width: 100%; border-collapse: collapse; } table { width: 100%; border-collapse: collapse; border: 1px solid #bbb; }
th, td { border: 1px solid #777; padding: 4px 4px; font-size: 10.5px; } th { background: #e5e5e5; border: 1px solid #bbb; padding: 5px 5px; font-size: 11px; font-weight: 700; text-align: center; white-space: nowrap; }
th { background: #eee; font-weight: 600; text-align: center; } td { border: 1px solid #ccc; padding: 4px 6px; font-size: 11px; }
.c { text-align: center; } .r { text-align: right; } .c { text-align: center; } .r { text-align: right; }
.subtotal-bar { display: flex; justify-content: flex-end; align-items: baseline; gap: 30px; border: 1px solid #777; background: #f3f3f3; padding: 7px 10px; margin-top: 6px; font-size: 11px; } td.r { font-family: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace; }
.subtotal-bar .lbl-total { font-weight: bold; font-size: 12px; margin-right: auto; } .subtotal-bar { display: flex; justify-content: flex-end; align-items: baseline; gap: 28px; border: 1px solid #bbb; background: #f3f3f3; padding: 7px 12px; margin-top: 8px; font-size: 12px; font-weight: 700; }
.subtotal-bar b { font-size: 13px; } .subtotal-bar .lbl-total { margin-right: auto; }
.subtotal-bar b { font-size: 14px; font-family: "SF Mono", Menlo, Consolas, monospace; }
.tips { border: 1px solid #ddd; background: #fafafa; padding: 6px 8px; margin: 8px 0; font-size: 10px; color: #555; line-height: 1.6; } .tips { border: 1px solid #ddd; background: #fafafa; padding: 6px 8px; margin: 8px 0; font-size: 10px; color: #555; line-height: 1.6; }
.sigs { display: flex; justify-content: space-between; margin-top: 26px; font-size: 11px; } .sigs { display: flex; justify-content: space-between; margin-top: 30px; font-size: 12px; }
.remark { margin-top: 10px; font-size: 11px; color: #333; } .remark { margin-top: 10px; font-size: 11px; color: #333; }
'''; ''';