diff --git a/client/lib/core/utils/print_util_stub.dart b/client/lib/core/utils/print_util_stub.dart index 019021e..f98e1d6 100644 --- a/client/lib/core/utils/print_util_stub.dart +++ b/client/lib/core/utils/print_util_stub.dart @@ -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), ], - ], + ), ); } diff --git a/client/lib/core/utils/print_util_web.dart b/client/lib/core/utils/print_util_web.dart index 1bf177e..5dc1c52 100644 --- a/client/lib/core/utils/print_util_web.dart +++ b/client/lib/core/utils/print_util_web.dart @@ -181,27 +181,29 @@ Future 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; } ''';