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
+150 -138
View File
@@ -709,174 +709,186 @@ 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(
crossAxisAlignment: pw.CrossAxisAlignment.stretch, // 左侧竖线点缀
children: [ decoration: const pw.BoxDecoration(
// ── 公司名称 ────────────────────────────────────────────────────────── border: pw.Border(left: pw.BorderSide(color: PdfColors.black, width: 3)),
pw.Center( ),
child: pw.Text(shopName, padding: const pw.EdgeInsets.only(left: 14),
style: pw.TextStyle( child: pw.Column(
font: bold, fontSize: 18, fontWeight: pw.FontWeight.bold)), crossAxisAlignment: pw.CrossAxisAlignment.stretch,
), children: [
pw.SizedBox(height: 4), // ── 抬头:公司名 + 单据类型,左对齐内联 ──────────────────────────
// ── 单据类型 ────────────────────────────────────────────────────────── pw.Row(
pw.Center( crossAxisAlignment: pw.CrossAxisAlignment.end,
child: pw.Text(title,
style: pw.TextStyle(
font: bold,
fontSize: 15,
fontWeight: pw.FontWeight.bold,
letterSpacing: 2)),
),
pw.SizedBox(height: 8),
// ── 抬头信息(两行三列)──────────────────────────────────────────────
pw.Container(
decoration: const pw.BoxDecoration(
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.grey700)),
),
padding: const pw.EdgeInsets.only(bottom: 5),
child: pw.Column(
children: [ children: [
pw.Row( pw.Text(shopName,
children: [ style: pw.TextStyle(
pw.Expanded( font: bold, fontSize: 16, fontWeight: pw.FontWeight.bold)),
flex: 5, pw.SizedBox(width: 12),
child: metaText( pw.Padding(
partnerLabel, partnerName ?? '', pw.TextAlign.left)), padding: const pw.EdgeInsets.only(bottom: 1.5),
pw.Expanded( child: pw.Text(title,
flex: 4, style: pw.TextStyle(
child: metaText( font: font,
dateLabel, _d10(orderDate), pw.TextAlign.left)), fontSize: 11,
pw.Expanded( letterSpacing: 3,
flex: 3, color: PdfColors.grey700)),
child: pw.Text('$noPrefix$orderNo',
style: boldStyle, 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.SizedBox(height: 8), // ── 抬头信息:灰底信息条(两行三列)────────────────────────────
// ── 明细表 + 单据总计 ──────────────────────────────────────────────── pw.Container(
pw.Table( decoration: pw.BoxDecoration(
border: pw.TableBorder.all(color: PdfColors.grey600, width: 0.5), color: PdfColors.grey100,
columnWidths: { border: pw.Border.all(color: PdfColors.grey400, width: 0.5),
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.grey200),
children: [ children: [
for (final h in _orderHeaders) pw.Row(
pw.Padding( children: [
padding: pw.Expanded(
const pw.EdgeInsets.symmetric(horizontal: 2, vertical: 3), flex: 5,
child: pw.Text(h, child: metaText(
textAlign: pw.TextAlign.center, style: headStyle), 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)),
],
),
], ],
), ),
for (final row in rows) ),
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])
},
children: [
pw.TableRow( pw.TableRow(
decoration: const pw.BoxDecoration(color: PdfColors.grey300),
children: [ children: [
for (var i = 0; i < row.length; i++) for (final h in _orderHeaders)
pw.Padding( pw.Padding(
padding: const pw.EdgeInsets.symmetric( padding: const pw.EdgeInsets.symmetric(
horizontal: 2, vertical: 2.5), horizontal: 2, vertical: 2.5),
child: pw.Text(row[i], child: pw.Text(h,
textAlign: _colAlign(i), style: cellStyle), textAlign: pw.TextAlign.center, style: headStyle),
), ),
], ],
), ),
], for (final row in rows)
), pw.TableRow(
// ── 单据总计:独立一行,仅数量 + 金额,右对齐高亮 ────────────────── children: [
pw.SizedBox(height: 6), for (var i = 0; i < row.length; i++)
pw.Container( pw.Padding(
width: double.infinity, padding: const pw.EdgeInsets.symmetric(
decoration: pw.BoxDecoration( horizontal: 3, vertical: 2),
border: pw.Border.all(color: PdfColors.grey600, width: 0.5), child: pw.Text(row[i],
color: PdfColors.grey100, textAlign: _colAlign(i),
), style: (i >= 6 && i <= 8) ? numStyle : cellStyle),
padding: const pw.EdgeInsets.symmetric(horizontal: 8, vertical: 6), ),
child: pw.Row( ],
mainAxisAlignment: pw.MainAxisAlignment.end, ),
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
pw.Text('单据总计',
style: pw.TextStyle(
font: bold, fontSize: 10.5, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(width: 30),
pw.Text('数量', style: metaStyle),
pw.SizedBox(width: 6),
pw.Text(_qtyStr(totalQty),
style: pw.TextStyle(
font: bold, fontSize: 11, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(width: 30),
pw.Text('金额', style: metaStyle),
pw.SizedBox(width: 6),
pw.Text(totalAmt.toStringAsFixed(2),
style: pw.TextStyle(
font: bold, fontSize: 12, fontWeight: pw.FontWeight.bold)),
], ],
), ),
), // ── 单据总计:灰底条,右对齐,仅数量 + 金额(等宽数字)──────────
// ── 温馨提示(仅出库单)──────────────────────────────────────────── pw.SizedBox(height: 6),
if (showTips) ...[
pw.SizedBox(height: 8),
pw.Container( pw.Container(
width: double.infinity, width: double.infinity,
decoration: pw.BoxDecoration( decoration: pw.BoxDecoration(
color: PdfColors.grey100,
border: pw.Border.all(color: PdfColors.grey400, width: 0.5), border: pw.Border.all(color: PdfColors.grey400, width: 0.5),
color: PdfColors.grey50,
), ),
padding: const pw.EdgeInsets.all(5), padding: const pw.EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: pw.Text(_kTipsText, child: pw.Row(
style: pw.TextStyle( mainAxisAlignment: pw.MainAxisAlignment.end,
font: font, fontSize: 8.5, color: PdfColors.grey800)), 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)),
],
),
), ),
], // ── 温馨提示(仅出库单)──────────────────────────────────────────
pw.SizedBox(height: 26), if (showTips) ...[
// ── 签字行:签收人 / 经办人 / 制单人 ──────────────────────────────── pw.SizedBox(height: 8),
pw.Row( pw.Container(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, width: double.infinity,
children: [ decoration: pw.BoxDecoration(
pw.Text('$signerLabel', style: metaStyle), border: pw.Border.all(color: PdfColors.grey400, width: 0.5),
pw.Text('$operatorRoleLabel${operatorName ?? ''}', style: metaStyle), color: PdfColors.grey50,
pw.Text('制单人:${makerName ?? ''}', style: metaStyle), ),
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(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text('$signerLabel', style: metaStyle),
pw.Text('$operatorRoleLabel${operatorName ?? ''}',
style: metaStyle),
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.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; }
'''; ''';