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 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 =
pw.TextStyle(font: bold, fontSize: 9.5, fontWeight: pw.FontWeight.bold);
final metaStyle = pw.TextStyle(font: font, fontSize: 10);
final boldStyle =
pw.TextStyle(font: bold, fontSize: 10, fontWeight: pw.FontWeight.bold);
pw.TextStyle(font: bold, fontSize: 8.5, fontWeight: pw.FontWeight.bold);
final metaStyle = pw.TextStyle(font: font, fontSize: 9);
final metaBold =
pw.TextStyle(font: bold, fontSize: 9, fontWeight: pw.FontWeight.bold);
pw.Widget metaText(String label, String value, pw.TextAlign align) =>
pw.Text('$label$value', style: metaStyle, textAlign: align);
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
children: [
// ── 公司名称 ──────────────────────────────────────────────────────────
pw.Center(
child: pw.Text(shopName,
style: pw.TextStyle(
font: bold, fontSize: 18, fontWeight: pw.FontWeight.bold)),
),
pw.SizedBox(height: 4),
// ── 单据类型 ──────────────────────────────────────────────────────────
pw.Center(
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(
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(
crossAxisAlignment: pw.CrossAxisAlignment.end,
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: 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.Text(shopName,
style: pw.TextStyle(
font: bold, fontSize: 16, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(width: 12),
pw.Padding(
padding: const pw.EdgeInsets.only(bottom: 1.5),
child: pw.Text(title,
style: pw.TextStyle(
font: font,
fontSize: 11,
letterSpacing: 3,
color: PdfColors.grey700)),
),
],
),
),
pw.SizedBox(height: 8),
// ── 明细表 + 单据总计 ────────────────────────────────────────────────
pw.Table(
border: pw.TableBorder.all(color: PdfColors.grey600, width: 0.5),
columnWidths: {
for (var i = 0; i < _orderColFlex.length; i++)
i: pw.FlexColumnWidth(_orderColFlex[i])
},
children: [
pw.TableRow(
decoration: const pw.BoxDecoration(color: PdfColors.grey200),
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: [
for (final h in _orderHeaders)
pw.Padding(
padding:
const pw.EdgeInsets.symmetric(horizontal: 2, vertical: 3),
child: pw.Text(h,
textAlign: pw.TextAlign.center, style: headStyle),
),
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)),
],
),
],
),
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(
decoration: const pw.BoxDecoration(color: PdfColors.grey300),
children: [
for (var i = 0; i < row.length; i++)
for (final h in _orderHeaders)
pw.Padding(
padding: const pw.EdgeInsets.symmetric(
horizontal: 2, vertical: 2.5),
child: pw.Text(row[i],
textAlign: _colAlign(i), style: cellStyle),
child: pw.Text(h,
textAlign: pw.TextAlign.center, style: headStyle),
),
],
),
],
),
// ── 单据总计:独立一行,仅数量 + 金额,右对齐高亮 ──────────────────
pw.SizedBox(height: 6),
pw.Container(
width: double.infinity,
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.grey600, width: 0.5),
color: PdfColors.grey100,
),
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)),
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),
),
],
),
],
),
),
// ── 温馨提示(仅出库单)────────────────────────────────────────────
if (showTips) ...[
pw.SizedBox(height: 8),
// ── 单据总计:灰底条,右对齐,仅数量 + 金额(等宽数字)──────────
pw.SizedBox(height: 6),
pw.Container(
width: double.infinity,
decoration: pw.BoxDecoration(
color: PdfColors.grey100,
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)),
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)),
],
),
),
],
pw.SizedBox(height: 26),
// ── 签字行:签收人 / 经办人 / 制单人 ────────────────────────────────
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text('$signerLabel', style: metaStyle),
pw.Text('$operatorRoleLabel${operatorName ?? ''}', style: metaStyle),
pw.Text('制单人:${makerName ?? ''}', style: metaStyle),
// ── 温馨提示(仅出库单)──────────────────────────────────────────
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(
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 =>
throw UnsupportedError('Web 端出库单走 HTML 打印,不生成 PDF');
/// 单据打印共用样式(入库/出库一致)。
/// 单据打印共用样式(样式三「紧凑报表」:左侧竖线 + 抬头左对齐内联 +
/// 灰底信息条 + 密集明细表 + 数字等宽 + 灰底总计条;入库/出库一致)。
const String _orderPrintCss = '''
@page { size: A4; margin: 12mm 15mm; }
@page { size: A4; margin: 12mm 14mm; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: "PingFang SC", "Microsoft YaHei", "SimSun", sans-serif; font-size: 11px; color: #000; }
.shop { text-align: center; font-size: 20px; font-weight: bold; }
.title { text-align: center; font-size: 16px; font-weight: bold; letter-spacing: 4px; margin: 4px 0 8px; }
.head-block { border-bottom: 1px solid #555; padding-bottom: 5px; margin-bottom: 8px; }
.meta-row { display: flex; font-size: 11px; }
.meta-row + .meta-row { margin-top: 3px; }
body { font-family: "PingFang SC", "Microsoft YaHei", "SimSun", sans-serif; color: #111; border-left: 3px solid #111; padding-left: 14px; }
.shop { display: inline; font-size: 19px; font-weight: 700; }
.title { display: inline; margin-left: 12px; font-size: 13px; letter-spacing: 4px; color: #444; }
.head-block { background: #f3f3f3; border: 1px solid #ccc; padding: 7px 10px; margin: 10px 0; }
.meta-row { display: flex; font-size: 11.5px; line-height: 1.9; }
.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 .no { font-weight: bold; font-size: 12px; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #777; padding: 4px 4px; font-size: 10.5px; }
th { background: #eee; font-weight: 600; text-align: center; }
.meta-row .no { font-weight: 700; }
table { width: 100%; border-collapse: collapse; border: 1px solid #bbb; }
th { background: #e5e5e5; border: 1px solid #bbb; padding: 5px 5px; font-size: 11px; font-weight: 700; text-align: center; white-space: nowrap; }
td { border: 1px solid #ccc; padding: 4px 6px; font-size: 11px; }
.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; }
.subtotal-bar .lbl-total { font-weight: bold; font-size: 12px; margin-right: auto; }
.subtotal-bar b { font-size: 13px; }
td.r { font-family: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace; }
.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 .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; }
.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; }
''';