Compare commits

...

10 Commits

Author SHA1 Message Date
wangjia 021c4f17cc chore: release client-v1.0.86
Deploy Client / build-windows (push) Successful in 1m54s
Deploy Client / build-client-web (push) Successful in 44s
Deploy Client / build-macos (push) Successful in 2m22s
Deploy Client / build-android (push) Successful in 1m35s
Deploy Client / build-ios (push) Successful in 2m47s
Deploy Client / release-deploy-client (push) Successful in 1m57s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 19:28:01 +08:00
wangjia ed7a37cc05 chore: release client-v1.0.85
Deploy Client / build-client-web (push) Successful in 50s
Deploy Client / build-macos (push) Successful in 2m16s
Deploy Client / build-android (push) Successful in 1m31s
Deploy Client / build-ios (push) Successful in 2m31s
Deploy Client / build-windows (push) Successful in 1m53s
Deploy Client / release-deploy-client (push) Successful in 1m55s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:49:00 +08:00
wangjia 47cf0ae547 chore: release client-v1.0.84
Deploy Client / build-client-web (push) Successful in 45s
Deploy Client / build-windows (push) Successful in 2m43s
Deploy Client / build-macos (push) Successful in 2m15s
Deploy Client / build-android (push) Successful in 1m20s
Deploy Client / build-ios (push) Successful in 2m52s
Deploy Client / release-deploy-client (push) Successful in 1m51s
修复出库单/入库单打印时单据明细超过一页导致 MultiPage 分页死循环卡死的回归
(_buildOrderDoc 改返回可拆分子组件列表,左侧竖线点缀改由 pageTheme 背景每页绘制)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:28:00 +08:00
wangjia 68fce1970c chore: release client-v1.0.83
Deploy Client / build-client-web (push) Successful in 46s
Deploy Client / build-windows (push) Successful in 1m54s
Deploy Client / build-macos (push) Successful in 2m22s
Deploy Client / build-android (push) Successful in 1m28s
Deploy Client / build-ios (push) Successful in 3m19s
Deploy Client / release-deploy-client (push) Successful in 1m53s
2026-06-26 18:11:40 +08:00
wangjia 2ad893fb40 docs: 出入库单打印排版方案(6 种黑白样式 HTML + PDF)
参考真实发票/送货单产出 6 种黑白排版供选型,已登记 docs 索引。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 18:10:19 +08:00
wangjia 1558ce1e94 refactor(client): 出入库单打印改用「紧凑报表」版式
- 左侧竖线点缀 + 抬头左对齐内联(公司名 + 单据类型同行)
- 抬头信息改为灰底信息条;明细表收紧字号/行距、灰底表头
- 数量/单价/金额/总计改等宽数字(PDF 内置 Courier,Web monospace)
- 单据总计改为右对齐灰底条
- 仅排版调整,字段/数据/售价口径/温馨提示逻辑均不变

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 18:10:12 +08:00
wangjia a355a3a2a2 feat(client): 出入库单打印按新版式重做 + 单价口径修正
- 版式参照单据样板:公司抬头 + 两行三列表头 + 统一明细列
  (商品编号/名称/系列/规格/批次号/生产日期/数量/单价/金额/备注)
- 单据总计独立成行(仅数量+金额,右对齐高亮)
- 出库单保留温馨提示,入库单去掉
- 签字行:制单人=当前登录用户(OrderPrintMeta),采购员/销售员=经办人;
  公司名取门店(shopInfoProvider)
- 出库单单价=售价(sale_price),金额=售价×数量;历史单 sale_price=0
  时回退 unit_price/total_price。入库单单价=进价(unit_price)
- 桌面 PDF 与 Web HTML 两端同构

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:03:23 +08:00
wangjia 5187702fc5 chore: release client-v1.0.82
Deploy Client / build-client-web (push) Successful in 47s
Deploy Client / build-windows (push) Successful in 1m56s
Deploy Client / build-macos (push) Successful in 2m15s
Deploy Client / build-android (push) Successful in 1m20s
Deploy Client / build-ios (push) Successful in 2m47s
Deploy Client / release-deploy-client (push) Successful in 1m51s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 01:52:04 +08:00
wangjia 3d83f94f24 chore: release client-v1.0.81
Deploy Client / build-client-web (push) Successful in 42s
Deploy Client / build-windows (push) Successful in 1m55s
Deploy Client / build-macos (push) Successful in 2m19s
Deploy Client / build-android (push) Successful in 1m7s
Deploy Client / build-ios (push) Successful in 2m37s
Deploy Client / release-deploy-client (push) Successful in 1m53s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:40:23 +08:00
wangjia 5861b9210c chore: release client-v1.0.80
Deploy Client / build-client-web (push) Successful in 43s
Deploy Client / build-windows (push) Successful in 1m56s
Deploy Client / build-macos (push) Successful in 2m11s
Deploy Client / build-android (push) Successful in 1m29s
Deploy Client / build-ios (push) Successful in 2m47s
Deploy Client / release-deploy-client (push) Successful in 1m49s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:02:32 +08:00
15 changed files with 1320 additions and 353 deletions
+43
View File
@@ -5,6 +5,49 @@ 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/), 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). 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
### 改进
- 出库单 / 入库单打印版式全面优化:公司抬头、批次号与生产日期、数字等宽对齐、单据总计独立高亮条,整体更紧凑规整,打印更清晰。
### 修复
- 修正出库单打印的「单价 / 金额」:现按售价显示(金额 = 售价 × 数量),不再误用成本价;历史单据自动兼容。
## [1.0.82] - 2026-06-26
### 新功能
- 出库单 / 入库单打印新增「打印预览」:点击打印先弹出整页预览(所见即所得),确认无误后再调用系统打印,解决部分 Windows 电脑打印对话框无法预览的问题。
## [1.0.81] - 2026-06-24
### 改进
- 出库 / 入库单详情全屏弹窗只覆盖内容区,不再遮挡左侧菜单栏、顶栏与状态栏。
- 出库 / 入库单详情标题栏改为浅色配色(浅蓝底 + 深色文字),观感更清爽。
## [1.0.80] - 2026-06-24
### 新功能
- 出库单详情新增「售价」与「利润」列,利润 = 售价 − 成本,一眼看清每笔出库的盈亏。
### 改进
- 出库 / 入库单详情改为全屏展示,内容更大更清晰,左上角返回箭头关闭。
- 出库单详情自动带出商品的生产日期与批次号(明细未单独填写时回退商品资料),去掉冗余的「数量」列。
## [1.0.79] - 2026-06-24 ## [1.0.79] - 2026-06-24
### 改进 ### 改进
@@ -0,0 +1,20 @@
/// 单据打印的「门店抬头 + 制单人」元信息。
///
/// - 门店信息(公司名称/地址/电话)来自 `shopInfoProvider`,作为单据顶部抬头。
/// - [makerName] = 制单人 = **当前登录、点击打印的人**(非单据经办人/审核人)。
/// 入库单的「采购员」「出库单的「销售员」才是单据经办人(operator)。
class OrderPrintMeta {
final String shopName;
final String shopAddress;
final String shopPhone;
/// 制单人 = 当前登录用户(打印此单的人)。
final String makerName;
const OrderPrintMeta({
this.shopName = '',
this.shopAddress = '',
this.shopPhone = '',
this.makerName = '',
});
}
+15 -4
View File
@@ -5,6 +5,8 @@ import '../../models/stock_out.dart';
import '../errors/error_reporter.dart'; import '../errors/error_reporter.dart';
import 'label_data.dart'; import 'label_data.dart';
export 'label_data.dart'; export 'label_data.dart';
import 'order_print_meta.dart';
export 'order_print_meta.dart';
import 'print_util_stub.dart' import 'print_util_stub.dart'
if (dart.library.js_interop) 'print_util_web.dart'; if (dart.library.js_interop) 'print_util_web.dart';
@@ -47,11 +49,20 @@ Future<List<String>> listLabelPrinters() => listLabelPrintersImpl();
/// 自动检测默认热敏打印机(名字含 deli/dl-888);找不到或 Web 返回 null。 /// 自动检测默认热敏打印机(名字含 deli/dl-888);找不到或 Web 返回 null。
Future<String?> detectDefaultPrinter() => detectDefaultPrinterImpl(); Future<String?> detectDefaultPrinter() => detectDefaultPrinterImpl();
Future<void> printStockInOrder(StockInOrder order) => Future<void> printStockInOrder(StockInOrder order, OrderPrintMeta meta) =>
printStockInOrderImpl(order); printStockInOrderImpl(order, meta);
Future<void> printStockOutOrder(StockOutOrder order) => Future<void> printStockOutOrder(StockOutOrder order, OrderPrintMeta meta) =>
printStockOutOrderImpl(order); printStockOutOrderImpl(order, meta);
/// 生成入库单 PDF 字节(桌面端真实排版,供打印预览光栅化用);Web 端不支持。
Future<Uint8List> buildStockInOrderPdf(StockInOrder order, OrderPrintMeta meta) =>
buildStockInOrderPdfImpl(order, meta);
/// 生成出库单 PDF 字节(桌面端真实排版,供打印预览光栅化用);Web 端不支持。
Future<Uint8List> buildStockOutOrderPdf(
StockOutOrder order, OrderPrintMeta meta) =>
buildStockOutOrderPdfImpl(order, meta);
/// 统一打印入口:catch 任意异常,上报并弹 SnackBar 给用户。 /// 统一打印入口:catch 任意异常,上报并弹 SnackBar 给用户。
/// 用法:await safePrint(context, () => printStockInOrder(order)); /// 用法:await safePrint(context, () => printStockInOrder(order));
+376 -169
View File
@@ -16,7 +16,8 @@ import 'package:win32/win32.dart'
ClosePrinter, ClosePrinter,
DOC_INFO_1, DOC_INFO_1,
WideCharToMultiByte; WideCharToMultiByte;
import 'package:flutter/foundation.dart' show kIsWeb, debugPrint; import 'package:flutter/foundation.dart'
show kIsWeb, debugPrint, visibleForTesting;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../errors/error_reporter.dart'; import '../errors/error_reporter.dart';
import 'package:pdf/pdf.dart'; import 'package:pdf/pdf.dart';
@@ -25,6 +26,7 @@ import 'package:printing/printing.dart';
import '../../models/stock_in.dart'; import '../../models/stock_in.dart';
import '../../models/stock_out.dart'; import '../../models/stock_out.dart';
import 'label_data.dart'; import 'label_data.dart';
import 'order_print_meta.dart';
Future<pw.Font> _loadFont() async { Future<pw.Font> _loadFont() async {
final data = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf'); final data = await rootBundle.load('assets/fonts/NotoSansSC-Regular.ttf');
@@ -644,211 +646,408 @@ Future<List<String>> listLabelPrintersImpl() async {
/// 自动检测默认热敏打印机(名字含 deli/dl-888);找不到返回 null。 /// 自动检测默认热敏打印机(名字含 deli/dl-888);找不到返回 null。
Future<String?> detectDefaultPrinterImpl() => _findThermalPrinter(); Future<String?> detectDefaultPrinterImpl() => _findThermalPrinter();
pw.Widget _buildOrderDoc({ /// 截取日期前 10 位(yyyy-MM-dd),空值返回空串。
String _d10(String? s) {
if (s == null || s.isEmpty) return '';
return s.length > 10 ? s.substring(0, 10) : s;
}
/// 数量格式化:整数不带小数,否则保留 3 位。
String _qtyStr(double q) =>
q % 1 == 0 ? q.toStringAsFixed(0) : q.toStringAsFixed(3);
/// 单据明细列(入库/出库统一):商品编号→备注。
const _orderHeaders = [
'商品编号', '商品名称', '系列', '规格', '批次号',
'生产日期', '数量', '单价', '金额', '备注',
];
// 针打版式列宽:日期 / 单价 / 金额 加宽,避免大字号下数字、日期断行。
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 =
'温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,'
'如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。';
/// 各明细列对齐:系列/规格/批次号/生产日期居中,数量/单价/金额右对齐,其余左对齐。
pw.TextAlign _colAlign(int i) {
switch (i) {
case 2:
case 3:
case 4:
case 5:
return pw.TextAlign.center;
case 6:
case 7:
case 8:
return pw.TextAlign.right;
default:
return pw.TextAlign.left;
}
}
/// 构建入库单/出库单 A4 版式(参照「锐浪报表」单据:公司抬头 + 两行三列表头
/// + 明细表 + 单据总计 + 温馨提示 + 签字行)。入库/出库仅称谓不同。
List<pw.Widget> _buildOrderDoc({
required pw.Font font, required pw.Font font,
required pw.Font bold, required pw.Font bold,
required String title, required String shopName, // 公司名称(顶部抬头)
required String title, // 入 库 单 / 出 库 单
required String orderNo, required String orderNo,
required String? orderDate, required String noPrefix, // 'NO' / 'NO.'
required String? partnerLabel, required String partnerLabel, // 单位名称 / 客户名称
required String? partnerName, required String? partnerName,
required String dateLabel, // 入库日期 / 出库日期
required String? orderDate,
required String addressLabel, // 单位地址 / 送货地址
required String? warehouseName, required String? warehouseName,
required String signerLabel, // 出货人(签字)/ 收货人(签字)
required String operatorRoleLabel, // 采购员 / 销售员(= 经办人)
required String? operatorName, required String? operatorName,
required String? reviewerName, required String? makerName, // 制单人 = 打印人
required String? remark, required String? remark,
required List<String> headers, required bool showTips,
required List<List<String>> rows, required List<List<String>> rows,
required double totalQty, required double totalQty,
required double totalAmt, required double totalAmt,
String? tipsText,
}) { }) {
final headerStyle = pw.TextStyle(font: bold, fontSize: 10, fontWeight: pw.FontWeight.bold); // 针打友好版式:全纯黑、无灰底纹、纯黑实线边框、字号加大。
final cellStyle = pw.TextStyle(font: font, fontSize: 9.5); // 针式打印机打不出灰色(只能抖成稀疏散点→淡而糊),且小号汉字笔画易糊;
final metaStyle = pw.TextStyle(font: font, fontSize: 10); // 故正文加粗(重击更黑)、字号上调、所有灰色一律改纯黑。
final boldStyle = pw.TextStyle(font: bold, fontSize: 10, fontWeight: pw.FontWeight.bold); // 数字等宽(内置 CourierASCII 数字适用)。
final mono = pw.Font.courierBold();
final monoBold = pw.Font.courierBold();
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: 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: 10.5, fontWeight: pw.FontWeight.bold);
final colWidths = headers.map((h) { pw.Widget metaText(String label, String value, pw.TextAlign align) =>
if (h == '商品名称') return const pw.FlexColumnWidth(2.2); pw.Text('$label$value', style: metaStyle, textAlign: align);
if (h == '系列' || h == '规格') return const pw.FlexColumnWidth(1.2);
if (h == '商品编号') return const pw.FlexColumnWidth(1.2);
return const pw.FlexColumnWidth(0.9);
}).toList();
return pw.Column( // 竖线(左侧点缀)由内容自身携带,而非整页背景——让竖线随内容高度收缩,
crossAxisAlignment: pw.CrossAxisAlignment.start, // 跨页时尾页也只画到最后一行,不再画满整页。结构分三段:
children: [ // 前段(抬头+信息条) / 明细表 / 后段(总计+提示+签字)
pw.Center( // 前后两段是「短」原子 Container(带左边框=竖线),明细表用 TableBorder.left
child: pw.Text(title, // 当竖线(表格可跨页拆分,左边框随行收缩)。三段紧贴堆叠→竖线连续;尾段为
style: pw.TextStyle(font: bold, fontSize: 20, fontWeight: pw.FontWeight.bold)), // 最后一个原子组件,其边框到内容末尾即止→竖线贴合内容。明细表是唯一可能超
), // 一页的组件且本身可拆分,不存在「原子大组件超页」→不会触发分页死循环
pw.SizedBox(height: 8), // 1.0.84 修复的根因)。
pw.Widget group(List<pw.Widget> children,
{double top = 0, double bottom = 0}) =>
pw.Container( pw.Container(
decoration: const pw.BoxDecoration( decoration: const pw.BoxDecoration(
border: pw.Border(bottom: pw.BorderSide(color: PdfColors.grey600)), border:
pw.Border(left: pw.BorderSide(color: PdfColors.black, width: 3)),
), ),
padding: const pw.EdgeInsets.only(bottom: 5), 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( child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [ children: [
pw.Text('$partnerLabel${partnerName ?? ''}', style: metaStyle), pw.Text(shopName,
pw.Text('日期:${orderDate ?? ''}', style: metaStyle), style: pw.TextStyle(
pw.Text('NO: $orderNo', style: boldStyle), 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: bold,
fontSize: 12,
fontWeight: pw.FontWeight.bold,
letterSpacing: 3,
color: PdfColors.black)),
),
], ],
), ),
), ),
pw.SizedBox(height: 4),
pw.Row(
children: [
pw.Text('仓库:${warehouseName ?? ''}', style: metaStyle),
pw.SizedBox(width: 24),
pw.Text('经办人:${operatorName ?? ''}', style: metaStyle),
],
),
pw.SizedBox(height: 8), pw.SizedBox(height: 8),
pw.Table( pw.Container(
border: pw.TableBorder.all(color: PdfColors.grey500), decoration: pw.BoxDecoration(
columnWidths: { for (var i = 0; i < colWidths.length; i++) i: colWidths[i] }, border: pw.Border.all(color: PdfColors.black, width: 0.8),
children: [ ),
pw.TableRow( padding: const pw.EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: const pw.BoxDecoration(color: PdfColors.grey200), child: pw.Column(
children: headers.map((h) => pw.Padding( children: [
padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 3), pw.Row(
child: pw.Text(h, textAlign: pw.TextAlign.center, style: headerStyle), children: [
)).toList(), pw.Expanded(
), flex: 5,
...rows.map((row) => pw.TableRow( child: metaText(
children: row.map((cell) => pw.Padding( partnerLabel, partnerName ?? '', pw.TextAlign.left)),
padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 2), pw.Expanded(
child: pw.Text(cell, style: cellStyle), flex: 4,
)).toList(), child:
)), metaText(dateLabel, _d10(orderDate), pw.TextAlign.left)),
pw.TableRow( pw.Expanded(
decoration: const pw.BoxDecoration(color: PdfColors.grey100), flex: 3,
children: List.generate(headers.length, (i) { child: pw.Text('$noPrefix$orderNo',
if (i == headers.length - 3) { style: metaBold, textAlign: pw.TextAlign.right)),
return pw.Padding( ],
padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 2), ),
child: pw.Text('单据总计', textAlign: pw.TextAlign.right, style: boldStyle), pw.SizedBox(height: 3),
); pw.Row(
} children: [
if (i == headers.length - 2) { pw.Expanded(
return pw.Padding( flex: 5,
padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 2), child: metaText('联系电话', '', pw.TextAlign.left)),
child: pw.Text( pw.Expanded(
totalQty % 1 == 0 ? totalQty.toStringAsFixed(0) : totalQty.toStringAsFixed(3), flex: 4,
textAlign: pw.TextAlign.right, style: boldStyle, child: metaText(addressLabel, '', pw.TextAlign.left)),
), pw.Expanded(
); flex: 3,
} child: metaText(
if (i == headers.length - 1) { '仓库', warehouseName ?? '', pw.TextAlign.right)),
return pw.Padding( ],
padding: const pw.EdgeInsets.symmetric(horizontal: 3, vertical: 2), ),
child: pw.Text(totalAmt.toStringAsFixed(2), ],
textAlign: pw.TextAlign.right, style: boldStyle), ),
);
}
return pw.SizedBox();
}),
),
],
), ),
if ((remark ?? '').isNotEmpty) ...[ ], bottom: 8),
pw.SizedBox(height: 6), // ── 明细表:左外边框 3 = 竖线(可跨页拆分,尾页随行收缩)────────────
pw.Text('备注:$remark', style: metaStyle), 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),
),
],
),
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),
),
],
),
], ],
if (tipsText != null) ...[ ),
pw.SizedBox(height: 6), // ── 后段:单据总计 + 温馨提示 + 签字行 ───────────────────────────────
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( pw.Container(
width: double.infinity,
decoration: pw.BoxDecoration( decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.grey400), border: pw.Border.all(color: PdfColors.black, width: 0.8),
color: PdfColors.grey50,
), ),
padding: const pw.EdgeInsets.all(5), padding: const pw.EdgeInsets.all(5),
child: pw.Text(tipsText, child: pw.Text(_kTipsText,
style: pw.TextStyle(font: font, fontSize: 9, color: PdfColors.grey700)), style: pw.TextStyle(
font: bold,
fontSize: 9,
fontWeight: pw.FontWeight.bold,
color: PdfColors.black)),
), ),
], ],
pw.SizedBox(height: 36), pw.SizedBox(height: 24),
pw.Row( pw.Padding(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, padding: const pw.EdgeInsets.only(left: 11),
children: [ child: pw.Row(
_sig(font, bold, '收/出货人(签字)'), mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
_sig(font, bold, '经办人:${operatorName ?? ''}'), children: [
_sig(font, bold, '制单人:${reviewerName ?? operatorName ?? ''}'), pw.Text('$signerLabel', style: metaStyle),
], pw.Text('$operatorRoleLabel${operatorName ?? ''}',
), style: metaStyle),
], pw.Text('制单人:${makerName ?? ''}', style: metaStyle),
); ],
}
pw.Widget _sig(pw.Font font, pw.Font bold, String label) {
return pw.Column(
children: [
pw.Container(width: 120, height: 28),
pw.Container(
width: 120,
decoration: const pw.BoxDecoration(
border: pw.Border(top: pw.BorderSide(color: PdfColors.grey700)),
), ),
padding: const pw.EdgeInsets.only(top: 3),
child: pw.Text(label,
textAlign: pw.TextAlign.center,
style: pw.TextStyle(font: font, fontSize: 10)),
), ),
], if ((remark ?? '').isNotEmpty) ...[
); pw.SizedBox(height: 10),
pw.Padding(
padding: const pw.EdgeInsets.only(left: 11),
child: pw.Text('备注:$remark', style: metaStyle),
),
],
], top: 8),
];
} }
Future<void> printStockInOrderImpl(StockInOrder order) async { /// 出/入库单文档:单一 MultiPage,竖线由 `_buildOrderDoc` 的内容自身携带
try { /// (前/后两段原子 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 {
final font = await _loadFont(); final font = await _loadFont();
final bold = await _loadBoldFont(); final bold = await _loadBoldFont();
double totalQty = 0, totalAmt = 0; double totalQty = 0, totalAmt = 0;
final rows = <List<String>>[]; final rows = <List<String>>[];
for (int i = 0; i < order.items.length; i++) { for (final it in order.items) {
final it = order.items[i];
totalQty += it.quantity; totalQty += it.quantity;
totalAmt += it.totalPrice; totalAmt += it.totalPrice;
rows.add([ rows.add([
'${i + 1}',
it.productCode ?? '', it.productCode ?? '',
it.productName ?? '', it.productName ?? '',
it.productSeries ?? '', it.productSeries ?? '',
it.productSpec ?? '', it.productSpec ?? '',
it.quantity % 1 == 0 ? it.quantity.toStringAsFixed(0) : it.quantity.toStringAsFixed(3), it.batchNo ?? '',
it.productUnit ?? '', _d10(it.productionDate),
_qtyStr(it.quantity),
it.unitPrice.toStringAsFixed(2), it.unitPrice.toStringAsFixed(2),
it.totalPrice.toStringAsFixed(2), it.totalPrice.toStringAsFixed(2),
it.productionDate ?? '', '',
it.batchNo ?? '',
]); ]);
} }
final doc = pw.Document(); return _buildOrderPdf(() => _buildOrderDoc(
doc.addPage(pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.symmetric(horizontal: 15 * PdfPageFormat.mm, vertical: 12 * PdfPageFormat.mm),
build: (_) => [
_buildOrderDoc(
font: font, bold: bold, font: font, bold: bold,
shopName: meta.shopName,
title: '入 库 单', title: '入 库 单',
orderNo: order.orderNo, orderNo: order.orderNo,
orderDate: order.orderDate, noPrefix: 'NO',
partnerLabel: '供应商', partnerLabel: '单位名称',
partnerName: order.partnerName, partnerName: order.partnerName,
dateLabel: '入库日期',
orderDate: order.orderDate,
addressLabel: '单位地址',
warehouseName: order.warehouseName, warehouseName: order.warehouseName,
signerLabel: '出货人(签字)',
operatorRoleLabel: '采购员',
operatorName: order.operatorName, operatorName: order.operatorName,
reviewerName: order.reviewerName, makerName: meta.makerName,
remark: order.remark, remark: order.remark,
headers: ['序号', '商品编号', '商品名称', '系列', '规格', '数量', '单位', '单价', '金额', '生产日期', '批次'], showTips: false,
rows: rows, rows: rows,
totalQty: totalQty, totalQty: totalQty,
totalAmt: totalAmt, totalAmt: totalAmt,
), ));
], }
));
final bytes = await doc.save(); Future<void> printStockInOrderImpl(
StockInOrder order, OrderPrintMeta meta) async {
try {
final bytes = await buildStockInOrderPdfImpl(order, meta);
if (!kIsWeb && Platform.isMacOS) { if (!kIsWeb && Platform.isMacOS) {
await Future<void>.delayed(Duration.zero); await Future<void>.delayed(Duration.zero);
} }
@@ -862,55 +1061,63 @@ Future<void> printStockInOrderImpl(StockInOrder order) async {
} }
} }
Future<void> printStockOutOrderImpl(StockOutOrder order) async { /// 生成出库单 A4 PDF 字节(与实际打印同一套排版,供预览光栅化 + 打印共用)。
Future<Uint8List> buildStockOutOrderPdfImpl(
StockOutOrder order, OrderPrintMeta meta) async {
final font = await _loadFont(); final font = await _loadFont();
final bold = await _loadBoldFont(); final bold = await _loadBoldFont();
double totalQty = 0, totalAmt = 0; double totalQty = 0, totalAmt = 0;
final rows = <List<String>>[]; final rows = <List<String>>[];
for (int i = 0; i < order.items.length; i++) { for (final it in order.items) {
final it = order.items[i]; // 出库单「单价」= 售价:App 单据存 sale_price;历史导入单 sale_price=0、
// 实际售价存在 unit_price/total_price,故 sale_price 为空时回退到 unit_price。
final unit = it.salePrice > 0 ? it.salePrice : it.unitPrice;
final amt = it.salePrice > 0 ? it.salePrice * it.quantity : it.totalPrice;
totalQty += it.quantity; totalQty += it.quantity;
totalAmt += it.totalPrice; totalAmt += amt;
rows.add([ rows.add([
'${i + 1}',
it.productCode ?? '', it.productCode ?? '',
it.productName ?? '', it.productName ?? '',
it.productSeries ?? '', it.productSeries ?? '',
it.productSpec ?? '', it.productSpec ?? '',
it.productUnit ?? '', it.batchNo ?? '',
it.quantity % 1 == 0 ? it.quantity.toStringAsFixed(0) : it.quantity.toStringAsFixed(3), _d10(it.productionDate),
it.unitPrice.toStringAsFixed(2), _qtyStr(it.quantity),
it.totalPrice.toStringAsFixed(2), unit.toStringAsFixed(2),
amt.toStringAsFixed(2),
'',
]); ]);
} }
final doc = pw.Document(); return _buildOrderPdf(() => _buildOrderDoc(
doc.addPage(pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.symmetric(horizontal: 15 * PdfPageFormat.mm, vertical: 12 * PdfPageFormat.mm),
build: (_) => [
_buildOrderDoc(
font: font, bold: bold, font: font, bold: bold,
shopName: meta.shopName,
title: '出 库 单', title: '出 库 单',
orderNo: order.orderNo, orderNo: order.orderNo,
orderDate: order.orderDate, noPrefix: 'NO.',
partnerLabel: '客户', partnerLabel: '客户名称',
partnerName: order.partnerName, partnerName: order.partnerName,
dateLabel: '出库日期',
orderDate: order.orderDate,
addressLabel: '送货地址',
warehouseName: order.warehouseName, warehouseName: order.warehouseName,
signerLabel: '收货人(签字)',
operatorRoleLabel: '销售员',
operatorName: order.operatorName, operatorName: order.operatorName,
reviewerName: order.reviewerName, makerName: meta.makerName,
remark: order.remark, remark: order.remark,
headers: ['序号', '商品编号', '商品名称', '系列', '规格', '单位', '数量', '单价', '金额'], showTips: true,
rows: rows, rows: rows,
totalQty: totalQty, totalQty: totalQty,
totalAmt: totalAmt, totalAmt: totalAmt,
tipsText: '温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。', ));
), }
],
)); Future<void> printStockOutOrderImpl(
StockOutOrder order, OrderPrintMeta meta) async {
try { try {
final bytes = await doc.save(); final bytes = await buildStockOutOrderPdfImpl(order, meta);
if (!kIsWeb && Platform.isMacOS) { if (!kIsWeb && Platform.isMacOS) {
await Future<void>.delayed(Duration.zero); await Future<void>.delayed(Duration.zero);
} }
+130 -132
View File
@@ -6,6 +6,7 @@ import '../../models/stock_in.dart';
import '../../models/stock_out.dart'; import '../../models/stock_out.dart';
import '../errors/error_reporter.dart'; import '../errors/error_reporter.dart';
import 'label_data.dart'; import 'label_data.dart';
import 'order_print_meta.dart';
void _openPrintWindow(String html) { void _openPrintWindow(String html) {
final win = web.window.open('', '_blank'); final win = web.window.open('', '_blank');
@@ -170,28 +171,85 @@ body { width: 38mm; height: 20mm; overflow: hidden; background: #fff; }
} }
} }
// ── 单据 PDF 字节(Web 走 HTML 打印路径,不生成 PDF;预览弹窗仅桌面端调用)──────
Future<Uint8List> buildStockInOrderPdfImpl(
StockInOrder order, OrderPrintMeta meta) async =>
throw UnsupportedError('Web 端入库单走 HTML 打印,不生成 PDF');
Future<Uint8List> buildStockOutOrderPdfImpl(
StockOutOrder order, OrderPrintMeta meta) async =>
throw UnsupportedError('Web 端出库单走 HTML 打印,不生成 PDF');
/// 单据打印共用样式(样式三「紧凑报表」:左侧竖线 + 抬头左对齐内联 +
/// 灰底信息条 + 密集明细表 + 数字等宽 + 灰底总计条;入库/出库一致)。
const String _orderPrintCss = '''
@page { size: A4; margin: 12mm 14mm; }
* { box-sizing: border-box; margin: 0; padding: 0; }
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: 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; }
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: 30px; font-size: 12px; }
.remark { margin-top: 10px; font-size: 11px; color: #333; }
''';
const String _orderTipsHtml =
'<div class="tips">温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,'
'如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。</div>';
/// 截取日期前 10 位(yyyy-MM-dd)。
String _d10(String? s) =>
(s == null || s.isEmpty) ? '' : (s.length > 10 ? s.substring(0, 10) : s);
/// 单据明细表头(入库/出库一致)。
const String _orderTableHead = '''<thead><tr>
<th style="width:12%">商品编号</th>
<th style="width:16%">商品名称</th>
<th style="width:9%">系列</th>
<th style="width:10%">规格</th>
<th style="width:14%">批次号</th>
<th style="width:11%">生产日期</th>
<th style="width:5%">数量</th>
<th style="width:8%">单价</th>
<th style="width:9%">金额</th>
<th style="width:6%">备注</th>
</tr></thead>''';
// ── 入库单 ────────────────────────────────────────────────────────────────── // ── 入库单 ──────────────────────────────────────────────────────────────────
Future<void> printStockInOrderImpl(StockInOrder order) async { Future<void> printStockInOrderImpl(
StockInOrder order, OrderPrintMeta meta) async {
final rows = StringBuffer(); final rows = StringBuffer();
double totalQty = 0; double totalQty = 0;
double totalAmt = 0; double totalAmt = 0;
for (int i = 0; i < order.items.length; i++) { for (final item in order.items) {
final item = order.items[i];
totalQty += item.quantity; totalQty += item.quantity;
totalAmt += item.totalPrice; totalAmt += item.totalPrice;
rows.write('''<tr> rows.write('''<tr>
<td class="c">${i + 1}</td>
<td>${item.productCode ?? ''}</td> <td>${item.productCode ?? ''}</td>
<td>${item.productName ?? ''}</td> <td>${item.productName ?? ''}</td>
<td class="c">${item.productSeries ?? ''}</td> <td class="c">${item.productSeries ?? ''}</td>
<td class="c">${item.productSpec ?? ''}</td> <td class="c">${item.productSpec ?? ''}</td>
<td class="c">${item.batchNo ?? ''}</td>
<td class="c">${_d10(item.productionDate)}</td>
<td class="r">${item.quantity % 1 == 0 ? item.quantity.toStringAsFixed(0) : item.quantity.toStringAsFixed(3)}</td> <td class="r">${item.quantity % 1 == 0 ? item.quantity.toStringAsFixed(0) : item.quantity.toStringAsFixed(3)}</td>
<td class="c">${item.productUnit ?? ''}</td>
<td class="r">${item.unitPrice.toStringAsFixed(2)}</td> <td class="r">${item.unitPrice.toStringAsFixed(2)}</td>
<td class="r">${item.totalPrice.toStringAsFixed(2)}</td> <td class="r">${item.totalPrice.toStringAsFixed(2)}</td>
<td class="c">${item.productionDate ?? ''}</td> <td></td>
<td class="c">${item.batchNo ?? ''}</td>
</tr>'''); </tr>''');
} }
@@ -203,72 +261,38 @@ Future<void> printStockInOrderImpl(StockInOrder order) async {
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>$_orderPrintCss</style>
@page { size: A4; margin: 12mm 15mm; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: "PingFang SC", "Microsoft YaHei", "SimSun", sans-serif; font-size: 11px; color: #000; }
.hdr { text-align: center; margin-bottom: 10px; }
.hdr .title { font-size: 22px; font-weight: bold; margin-bottom: 4px; }
.meta { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 6px; border-bottom: 1px solid #666; padding-bottom: 5px; }
.meta .left, .meta .center, .meta .right { flex: 1; }
.meta .center { text-align: center; }
.meta .right { text-align: right; font-weight: bold; font-size: 12px; }
.meta2 { display: flex; gap: 24px; margin-bottom: 8px; font-size: 11px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 4px; }
th, td { border: 1px solid #999; padding: 4px 5px; }
th { background: #f0f0f0; font-weight: 600; text-align: center; font-size: 11px; }
td { font-size: 10.5px; }
.c { text-align: center; }
.r { text-align: right; }
.subtotal { font-weight: bold; background: #f7f7f7; }
.remark { margin: 6px 0; font-size: 11px; color: #333; }
.sigs { display: flex; justify-content: space-between; margin-top: 28px; }
.sig { text-align: center; min-width: 120px; }
.sig-line { border-top: 1px solid #555; padding-top: 4px; margin-top: 28px; font-size: 11px; }
.footer-note { text-align: right; font-size: 9px; color: #aaa; margin-top: 6px; }
</style>
</head> </head>
<body> <body>
<div class="hdr"><div class="title">入 库 单</div></div> <div class="shop">${meta.shopName}</div>
<div class="meta"> <div class="title">入 库 单</div>
<div class="left">供应商:${order.partnerName ?? ''}</div> <div class="head-block">
<div class="center">入库日期:${order.orderDate ?? ''}</div> <div class="meta-row">
<div class="right">NO: ${order.orderNo}</div> <div class="c1">单位名称:${order.partnerName ?? ''}</div>
</div> <div class="c2">入库日期:${_d10(order.orderDate)}</div>
<div class="meta2"> <div class="c3 no">NO${order.orderNo}</div>
<span>仓库:${order.warehouseName ?? ''}</span> </div>
<span>采购人:${order.operatorName ?? ''}</span> <div class="meta-row">
<div class="c1">联系电话:</div>
<div class="c2">单位地址:</div>
<div class="c3">仓库:${order.warehouseName ?? ''}</div>
</div>
</div> </div>
<table> <table>
<thead><tr> $_orderTableHead
<th style="width:3%">序号</th>
<th style="width:9%">商品编号</th>
<th style="width:18%">商品名称</th>
<th style="width:10%">系列</th>
<th style="width:9%">规格</th>
<th style="width:5%">数量</th>
<th style="width:5%">单位</th>
<th style="width:8%">单价</th>
<th style="width:8%">金额</th>
<th style="width:10%">生产日期</th>
<th style="width:10%">批次号</th>
</tr></thead>
<tbody>$rows</tbody> <tbody>$rows</tbody>
<tfoot><tr class="subtotal">
<td colspan="5" class="r">单据总计</td>
<td class="r">${totalQty % 1 == 0 ? totalQty.toStringAsFixed(0) : totalQty.toStringAsFixed(3)}</td>
<td></td><td></td>
<td class="r">${totalAmt.toStringAsFixed(2)}</td>
<td colspan="2"></td>
</tr></tfoot>
</table> </table>
$remarkLine <div class="subtotal-bar">
<div class="sigs"> <span class="lbl-total">单据总计</span>
<div class="sig"><div class="sig-line">出货人(签字)</div></div> <span>数量 <b>${totalQty % 1 == 0 ? totalQty.toStringAsFixed(0) : totalQty.toStringAsFixed(3)}</b></span>
<div class="sig"><div class="sig-line">采购员:${order.operatorName ?? ''}</div></div> <span>金额 <b>${totalAmt.toStringAsFixed(2)}</b></span>
<div class="sig"><div class="sig-line">制单人:${order.reviewerName ?? order.operatorName ?? ''}</div></div>
</div> </div>
<div class="footer-note">酒库管理系统</div> <div class="sigs">
<span>出货人(签字):</span>
<span>采购员:${order.operatorName ?? ''}</span>
<span>制单人:${meta.makerName}</span>
</div>
$remarkLine
<script>window.onload = function(){ window.print(); };</script> <script>window.onload = function(){ window.print(); };</script>
</body> </body>
</html>'''; </html>''';
@@ -291,24 +315,30 @@ Future<String?> detectDefaultPrinterImpl() async => null;
// ── 出库单 ────────────────────────────────────────────────────────────────── // ── 出库单 ──────────────────────────────────────────────────────────────────
Future<void> printStockOutOrderImpl(StockOutOrder order) async { Future<void> printStockOutOrderImpl(
StockOutOrder order, OrderPrintMeta meta) async {
final rows = StringBuffer(); final rows = StringBuffer();
double totalQty = 0; double totalQty = 0;
double totalAmt = 0; double totalAmt = 0;
for (int i = 0; i < order.items.length; i++) { for (final item in order.items) {
final item = order.items[i]; // 出库单「单价」= 售价:App 单据存 sale_price;历史导入单 sale_price=0、
// 实际售价存在 unit_price/total_price,故 sale_price 为空时回退到 unit_price。
final unit = item.salePrice > 0 ? item.salePrice : item.unitPrice;
final amt =
item.salePrice > 0 ? item.salePrice * item.quantity : item.totalPrice;
totalQty += item.quantity; totalQty += item.quantity;
totalAmt += item.totalPrice; totalAmt += amt;
rows.write('''<tr> rows.write('''<tr>
<td class="c">${i + 1}</td>
<td>${item.productCode ?? ''}</td> <td>${item.productCode ?? ''}</td>
<td>${item.productName ?? ''}</td> <td>${item.productName ?? ''}</td>
<td class="c">${item.productSeries ?? ''}</td> <td class="c">${item.productSeries ?? ''}</td>
<td class="c">${item.productSpec ?? ''}</td> <td class="c">${item.productSpec ?? ''}</td>
<td class="c">${item.productUnit ?? ''}</td> <td class="c">${item.batchNo ?? ''}</td>
<td class="c">${_d10(item.productionDate)}</td>
<td class="r">${item.quantity % 1 == 0 ? item.quantity.toStringAsFixed(0) : item.quantity.toStringAsFixed(3)}</td> <td class="r">${item.quantity % 1 == 0 ? item.quantity.toStringAsFixed(0) : item.quantity.toStringAsFixed(3)}</td>
<td class="r">${item.unitPrice.toStringAsFixed(2)}</td> <td class="r">${unit.toStringAsFixed(2)}</td>
<td class="r">${item.totalPrice.toStringAsFixed(2)}</td> <td class="r">${amt.toStringAsFixed(2)}</td>
<td></td>
</tr>'''); </tr>''');
} }
@@ -320,71 +350,39 @@ Future<void> printStockOutOrderImpl(StockOutOrder order) async {
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>$_orderPrintCss</style>
@page { size: A4; margin: 12mm 15mm; }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: "PingFang SC", "Microsoft YaHei", "SimSun", sans-serif; font-size: 11px; color: #000; }
.hdr { text-align: center; margin-bottom: 10px; }
.hdr .title { font-size: 22px; font-weight: bold; margin-bottom: 4px; }
.meta { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 6px; border-bottom: 1px solid #666; padding-bottom: 5px; }
.meta .left, .meta .center, .meta .right { flex: 1; }
.meta .center { text-align: center; }
.meta .right { text-align: right; font-weight: bold; font-size: 12px; }
.meta2 { display: flex; gap: 24px; margin-bottom: 8px; font-size: 11px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 4px; }
th, td { border: 1px solid #999; padding: 4px 5px; }
th { background: #f0f0f0; font-weight: 600; text-align: center; font-size: 11px; }
td { font-size: 10.5px; }
.c { text-align: center; }
.r { text-align: right; }
.subtotal { font-weight: bold; background: #f7f7f7; }
.remark { margin: 6px 0; font-size: 11px; color: #333; }
.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: 28px; }
.sig { text-align: center; min-width: 120px; }
.sig-line { border-top: 1px solid #555; padding-top: 4px; margin-top: 28px; font-size: 11px; }
.footer-note { text-align: right; font-size: 9px; color: #aaa; margin-top: 6px; }
</style>
</head> </head>
<body> <body>
<div class="hdr"><div class="title">出 库 单</div></div> <div class="shop">${meta.shopName}</div>
<div class="meta"> <div class="title">出 库 单</div>
<div class="left">客户:${order.partnerName ?? ''}</div> <div class="head-block">
<div class="center">出库日期:${order.orderDate ?? ''}</div> <div class="meta-row">
<div class="right">NO.${order.orderNo}</div> <div class="c1">客户名称:${order.partnerName ?? ''}</div>
</div> <div class="c2">出库日期:${_d10(order.orderDate)}</div>
<div class="meta2"> <div class="c3 no">NO.${order.orderNo}</div>
<span>仓库:${order.warehouseName ?? ''}</span> </div>
<span>经办人:${order.operatorName ?? ''}</span> <div class="meta-row">
<div class="c1">联系电话:</div>
<div class="c2">送货地址:</div>
<div class="c3">仓库:${order.warehouseName ?? ''}</div>
</div>
</div> </div>
<table> <table>
<thead><tr> $_orderTableHead
<th style="width:3%">序号</th>
<th style="width:9%">商品编号</th>
<th style="width:20%">商品名称</th>
<th style="width:11%">系列</th>
<th style="width:10%">规格</th>
<th style="width:5%">单位</th>
<th style="width:5%">数量</th>
<th style="width:9%">单价</th>
<th style="width:9%">金额</th>
</tr></thead>
<tbody>$rows</tbody> <tbody>$rows</tbody>
<tfoot><tr class="subtotal">
<td colspan="6" class="r">单据总计</td>
<td class="r">${totalQty % 1 == 0 ? totalQty.toStringAsFixed(0) : totalQty.toStringAsFixed(3)}</td>
<td></td>
<td class="r">${totalAmt.toStringAsFixed(2)}</td>
</tr></tfoot>
</table> </table>
$remarkLine <div class="subtotal-bar">
<div class="tips">温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。</div> <span class="lbl-total">单据总计</span>
<div class="sigs"> <span>数量 <b>${totalQty % 1 == 0 ? totalQty.toStringAsFixed(0) : totalQty.toStringAsFixed(3)}</b></span>
<div class="sig"><div class="sig-line">收货人(签字)</div></div> <span>金额 <b>${totalAmt.toStringAsFixed(2)}</b></span>
<div class="sig"><div class="sig-line">销售员:${order.operatorName ?? ''}</div></div>
<div class="sig"><div class="sig-line">制单人:${order.reviewerName ?? order.operatorName ?? ''}</div></div>
</div> </div>
<div class="footer-note">酒库管理系统</div> $_orderTipsHtml
<div class="sigs">
<span>收货人(签字):</span>
<span>销售员:${order.operatorName ?? ''}</span>
<span>制单人:${meta.makerName}</span>
</div>
$remarkLine
<script>window.onload = function(){ window.print(); };</script> <script>window.onload = function(){ window.print(); };</script>
</body> </body>
</html>'''; </html>''';
+3 -2
View File
@@ -61,8 +61,9 @@ class StockOutItem {
productSeries: lineOrProduct('series', 'series'), productSeries: lineOrProduct('series', 'series'),
productSpec: lineOrProduct('spec', 'spec'), productSpec: lineOrProduct('spec', 'spec'),
productUnit: (json['product'] as Map<String, dynamic>?)?['unit'] as String?, productUnit: (json['product'] as Map<String, dynamic>?)?['unit'] as String?,
batchNo: json['batch_no'] as String?, // 批次号/生产日期:优先明细行快照,回退 product 关联(App 新建单据快照列常为空)
productionDate: json['production_date'] as String?, batchNo: lineOrProduct('batch_no', 'batch_no'),
productionDate: lineOrProduct('production_date', 'production_date'),
); );
} }
} }
@@ -5,8 +5,8 @@ import '../../core/responsive/responsive.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../core/auth/auth_state.dart'; import '../../core/auth/auth_state.dart';
import '../../core/utils/print_util.dart';
import '../../core/utils/date_util.dart'; import '../../core/utils/date_util.dart';
import '../../widgets/order_print_preview_dialog.dart';
import '../../models/stock_in.dart'; import '../../models/stock_in.dart';
import '../../core/config/app_constants.dart'; import '../../core/config/app_constants.dart';
import '../../providers/partner_provider.dart'; import '../../providers/partner_provider.dart';
@@ -227,7 +227,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
Future<void> _printOrder() async { Future<void> _printOrder() async {
if (_loadedOrder == null) return; if (_loadedOrder == null) return;
await safePrint(context, () => printStockInOrder(_loadedOrder!)); await showStockInOrderPrint(context, ref, _loadedOrder!);
} }
Future<void> _submit(bool asDraft) async { Future<void> _submit(bool asDraft) async {
@@ -1,8 +1,9 @@
import '../../core/utils/dialog_util.dart'; import '../../core/utils/dialog_util.dart';
import '../../core/errors/error_reporter.dart'; import '../../core/errors/error_reporter.dart';
import '../../core/exceptions.dart'; import '../../core/exceptions.dart';
import '../../core/utils/print_util.dart' show safePrint, printStockInOrder, LabelData; import '../../core/utils/print_util.dart' show LabelData;
import '../../widgets/label_preview_dialog.dart'; import '../../widgets/label_preview_dialog.dart';
import '../../widgets/order_print_preview_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -529,7 +530,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
onPrint: () async { onPrint: () async {
final order = await ref.read(stockInRepositoryProvider).get(o.id); final order = await ref.read(stockInRepositoryProvider).get(o.id);
if (context.mounted) { if (context.mounted) {
await safePrint(context, () => printStockInOrder(order)); await showStockInOrderPrint(context, ref, order);
} }
}, },
afterPrint: [ afterPrint: [
@@ -607,6 +608,8 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
Future<void> _showDetail(BuildContext context, int orderId) async { Future<void> _showDetail(BuildContext context, int orderId) async {
showAppDialog( showAppDialog(
context: context, context: context,
// 用分支 Navigator 承载,全屏弹窗只覆盖内容区,不盖住左侧栏/顶栏/状态栏
useRootNavigator: false,
builder: (ctx) => _StockInDetailDialog( builder: (ctx) => _StockInDetailDialog(
orderId: orderId, orderId: orderId,
repository: ref.read(stockInRepositoryProvider), repository: ref.read(stockInRepositoryProvider),
@@ -1044,40 +1047,36 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Dialog( return Dialog.fullscreen(
child: Container( child: SafeArea(
width: context.dialogWidth(720),
constraints: const BoxConstraints(maxHeight: 600),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: AppTheme.primary, color: AppTheme.brand50,
borderRadius: BorderRadius.only( border: Border(bottom: BorderSide(color: AppTheme.border)),
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
), ),
child: Row( child: Row(
children: [ children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: AppTheme.primaryDark),
tooltip: '返回',
onPressed: () => Navigator.of(context).pop(),
),
const Text('入库单详情', const Text('入库单详情',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.white)), color: AppTheme.primaryDark)),
const Spacer(), const Spacer(),
if (_loadedOrder != null) if (_loadedOrder != null)
IconButton( IconButton(
icon: const Icon(Icons.print_outlined, color: Colors.white), icon: const Icon(Icons.print_outlined, color: AppTheme.primaryDark),
tooltip: '打印', tooltip: '打印',
onPressed: () => safePrint(context, () => printStockInOrder(_loadedOrder!)), onPressed: () => showStockInOrderPrint(context, ref, _loadedOrder!),
), ),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
], ],
), ),
), ),
@@ -7,8 +7,8 @@ import 'package:go_router/go_router.dart';
import '../../core/responsive/responsive.dart'; import '../../core/responsive/responsive.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../core/auth/auth_state.dart'; import '../../core/auth/auth_state.dart';
import '../../core/utils/print_util.dart';
import '../../core/utils/date_util.dart'; import '../../core/utils/date_util.dart';
import '../../widgets/order_print_preview_dialog.dart';
import '../../models/inventory.dart'; import '../../models/inventory.dart';
import '../../models/stock_out.dart'; import '../../models/stock_out.dart';
import '../../widgets/date_picker_field.dart'; import '../../widgets/date_picker_field.dart';
@@ -258,7 +258,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
Future<void> _printOrder() async { Future<void> _printOrder() async {
if (_loadedOrder == null) return; if (_loadedOrder == null) return;
await safePrint(context, () => printStockOutOrder(_loadedOrder!)); await showStockOutOrderPrint(context, ref, _loadedOrder!);
} }
Future<void> _submit(bool asDraft) async { Future<void> _submit(bool asDraft) async {
@@ -1,6 +1,6 @@
import '../../core/utils/dialog_util.dart'; import '../../core/utils/dialog_util.dart';
import '../../core/errors/error_reporter.dart'; import '../../core/errors/error_reporter.dart';
import '../../core/utils/print_util.dart' show safePrint, printStockOutOrder; import '../../widgets/order_print_preview_dialog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -17,7 +17,6 @@ import '../../widgets/page_scaffold.dart';
import '../../widgets/status_badge.dart'; import '../../widgets/status_badge.dart';
import '../../widgets/search_chip.dart'; import '../../widgets/search_chip.dart';
import '../../core/utils/export_util.dart'; import '../../core/utils/export_util.dart';
import '../../core/utils/print_util.dart';
import '../../providers/inventory_provider.dart'; import '../../providers/inventory_provider.dart';
import '../../providers/tab_state_provider.dart'; import '../../providers/tab_state_provider.dart';
import '../../providers/product_provider.dart'; import '../../providers/product_provider.dart';
@@ -559,7 +558,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
onPrint: () async { onPrint: () async {
final order = await ref.read(stockOutRepositoryProvider).get(o.id); final order = await ref.read(stockOutRepositoryProvider).get(o.id);
if (context.mounted) { if (context.mounted) {
await safePrint(context, () => printStockOutOrder(order)); await showStockOutOrderPrint(context, ref, order);
} }
}, },
onSettle: () => _confirmSettle(context, o.id, 'stock_out'), onSettle: () => _confirmSettle(context, o.id, 'stock_out'),
@@ -607,6 +606,8 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
Future<void> _showDetail(BuildContext context, int orderId) async { Future<void> _showDetail(BuildContext context, int orderId) async {
showAppDialog( showAppDialog(
context: context, context: context,
// 用分支 Navigator 承载,全屏弹窗只覆盖内容区,不盖住左侧栏/顶栏/状态栏
useRootNavigator: false,
builder: (ctx) => _StockOutDetailDialog( builder: (ctx) => _StockOutDetailDialog(
orderId: orderId, orderId: orderId,
repository: ref.read(stockOutRepositoryProvider), repository: ref.read(stockOutRepositoryProvider),
@@ -957,40 +958,36 @@ class _StockOutDetailDialogState extends ConsumerState<_StockOutDetailDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Dialog( return Dialog.fullscreen(
child: Container( child: SafeArea(
width: context.dialogWidth(720),
constraints: const BoxConstraints(maxHeight: 600),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: AppTheme.primary, color: AppTheme.brand50,
borderRadius: BorderRadius.only( border: Border(bottom: BorderSide(color: AppTheme.border)),
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
), ),
child: Row( child: Row(
children: [ children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: AppTheme.primaryDark),
tooltip: '返回',
onPressed: () => Navigator.of(context).pop(),
),
const Text('出库单详情', const Text('出库单详情',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.white)), color: AppTheme.primaryDark)),
const Spacer(), const Spacer(),
if (_loadedOrder != null) if (_loadedOrder != null)
IconButton( IconButton(
icon: const Icon(Icons.print_outlined, color: Colors.white), icon: const Icon(Icons.print_outlined, color: AppTheme.primaryDark),
tooltip: '打印', tooltip: '打印',
onPressed: () => safePrint(context, () => printStockOutOrder(_loadedOrder!)), onPressed: () => showStockOutOrderPrint(context, ref, _loadedOrder!),
), ),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
], ],
), ),
), ),
@@ -1073,7 +1070,7 @@ class _StockOutDetailDialogState extends ConsumerState<_StockOutDetailDialog> {
children: [ children: [
TableRow( TableRow(
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)), decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
children: ['序号', '商品编码', '名称', '系列', '规格', '生产日期', '批次号', '数量', '', '金额'] children: ['序号', '商品编码', '名称', '系列', '规格', '生产日期', '批次号', '成本价', '', '利润']
.map((h) => Padding( .map((h) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Text(h, child: Text(h,
@@ -1102,9 +1099,9 @@ class _StockOutDetailDialogState extends ConsumerState<_StockOutDetailDialog> {
: (item.productionDate ?? '-')), : (item.productionDate ?? '-')),
_TableCell( _TableCell(
(item.batchNo?.isNotEmpty ?? false) ? item.batchNo! : '-'), (item.batchNo?.isNotEmpty ?? false) ? item.batchNo! : '-'),
_TableCell(item.quantity.toStringAsFixed(3)),
_TableCell('¥${item.unitPrice.toStringAsFixed(2)}'), _TableCell('¥${item.unitPrice.toStringAsFixed(2)}'),
_TableCell('¥${item.totalPrice.toStringAsFixed(2)}'), _TableCell('¥${item.salePrice.toStringAsFixed(2)}'),
_TableCell('¥${(item.salePrice - item.unitPrice).toStringAsFixed(2)}'),
], ],
); );
}), }),
@@ -0,0 +1,282 @@
import 'dart:typed_data';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:printing/printing.dart';
import '../core/auth/auth_state.dart';
import '../core/errors/error_reporter.dart';
import '../core/theme/app_theme.dart';
import '../core/responsive/responsive.dart';
import '../core/utils/print_util.dart';
import '../models/shop.dart';
import '../models/stock_in.dart';
import '../models/stock_out.dart';
import '../providers/shop_provider.dart';
/// 单据打印预览弹窗:把待打印的 A4 PDF 光栅化成图片(所见即所得)逐页展示,
/// 页内点「打印」再调系统打印。解决部分 Windows 系统打印对话框无预览的问题。
///
/// 仅桌面端使用;Web 端走浏览器自带的打印预览(见 [showStockInOrderPrint])。
class OrderPrintPreviewDialog extends StatefulWidget {
/// 标题,如「入库单打印预览」。
final String title;
/// 系统打印任务名(也作另存默认文件名前缀)。
final String printName;
/// 惰性生成 PDF 字节。
final Future<Uint8List> Function() pdfBuilder;
const OrderPrintPreviewDialog({
super.key,
required this.title,
required this.printName,
required this.pdfBuilder,
});
@override
State<OrderPrintPreviewDialog> createState() =>
_OrderPrintPreviewDialogState();
}
class _OrderPrintPreviewDialogState extends State<OrderPrintPreviewDialog> {
Uint8List? _pdfBytes;
final List<Uint8List> _pages = [];
bool _rendering = true;
bool _printing = false;
String _error = '';
@override
void initState() {
super.initState();
_render();
}
Future<void> _render() async {
try {
final bytes = await widget.pdfBuilder();
if (!mounted) return;
setState(() => _pdfBytes = bytes);
// 逐页光栅化(progressive),DPI 取 110 兼顾清晰与速度。
await for (final raster in Printing.raster(bytes, dpi: 110)) {
final png = await raster.toPng();
if (!mounted) return;
setState(() => _pages.add(png));
}
} catch (e, st) {
reportError(e, st);
if (mounted) setState(() => _error = '预览生成失败:$e');
} finally {
if (mounted) setState(() => _rendering = false);
}
}
Future<void> _doPrint() async {
final bytes = _pdfBytes;
if (bytes == null || _printing) return;
setState(() => _printing = true);
try {
await Printing.layoutPdf(
name: '${widget.printName}_${_ts()}',
dynamicLayout: false,
onLayout: (_) async => bytes,
);
if (mounted) Navigator.of(context).pop();
} catch (e, st) {
reportError(e, st);
if (mounted) {
setState(() {
_printing = false;
_error = '打印失败,请检查打印机连接和驱动是否正常。\n详情:$e';
});
}
}
}
String _ts() {
final n = DateTime.now();
String p(int v) => v.toString().padLeft(2, '0');
return '${n.year}${p(n.month)}${p(n.day)}_${p(n.hour)}${p(n.minute)}${p(n.second)}';
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
width: context.dialogWidth(720),
constraints: const BoxConstraints(maxHeight: 720),
child: Column(
children: [
// ── 顶栏 ──────────────────────────────────────────────────────────
Container(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
decoration: const BoxDecoration(
color: AppTheme.primary,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
Text(widget.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white)),
const Spacer(),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed:
_printing ? null : () => Navigator.of(context).pop(),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
),
// ── 预览区 ─────────────────────────────────────────────────────────
Expanded(
child: Container(
width: double.infinity,
color: const Color(0xFFE9ECF1),
child: _buildBody(),
),
),
// ── 底栏 ──────────────────────────────────────────────────────────
Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 14),
child: Row(
children: [
Expanded(
child: Text(
_error.isNotEmpty
? _error
: (_rendering
? '正在生成预览…'
: '${_pages.length}'),
style: TextStyle(
fontSize: 12,
color: _error.isNotEmpty
? Colors.red
: AppTheme.textSecondary),
),
),
TextButton(
onPressed:
_printing ? null : () => Navigator.of(context).pop(),
child: const Text('取消'),
),
const SizedBox(width: 8),
FilledButton.icon(
icon: const Icon(Icons.print, size: 18),
onPressed: (_pdfBytes == null || _printing)
? null
: _doPrint,
label: Text(_printing ? '打印中…' : '打印'),
),
],
),
),
],
),
),
);
}
Widget _buildBody() {
if (_pages.isEmpty) {
if (_error.isNotEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(_error,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red, fontSize: 13)),
),
);
}
return const Center(child: CircularProgressIndicator());
}
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
itemCount: _pages.length,
separatorBuilder: (_, __) => const SizedBox(height: 16),
itemBuilder: (_, i) => Center(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.18),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Image.memory(_pages[i], fit: BoxFit.contain),
),
),
);
}
}
/// 组装单据打印抬头信息:公司名称(门店)+ 制单人(当前登录、点击打印的人)。
Future<OrderPrintMeta> _buildPrintMeta(WidgetRef ref) async {
ShopInfo? shop;
try {
shop = await ref.read(shopInfoProvider.future);
} catch (_) {
// 门店信息拉取失败(离线等)时抬头留空,不阻塞打印。
}
return OrderPrintMeta(
shopName: shop?.name ?? '',
shopAddress: shop?.address ?? '',
shopPhone: shop?.phone ?? '',
makerName: ref.read(authStateProvider).user?.realName ?? '',
);
}
/// 打印入库单:桌面端先弹应用内预览,Web 端走浏览器自带打印预览。
Future<void> showStockInOrderPrint(
BuildContext context, WidgetRef ref, StockInOrder order) async {
final meta = await _buildPrintMeta(ref);
if (!context.mounted) return;
if (kIsWeb) {
await safePrint(context, () => printStockInOrder(order, meta));
return;
}
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => OrderPrintPreviewDialog(
title: '入库单打印预览',
printName: '入库单',
pdfBuilder: () => buildStockInOrderPdf(order, meta),
),
);
}
/// 打印出库单:桌面端先弹应用内预览,Web 端走浏览器自带打印预览。
Future<void> showStockOutOrderPrint(
BuildContext context, WidgetRef ref, StockOutOrder order) async {
final meta = await _buildPrintMeta(ref);
if (!context.mounted) return;
if (kIsWeb) {
await safePrint(context, () => printStockOutOrder(order, meta));
return;
}
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => OrderPrintPreviewDialog(
title: '出库单打印预览',
printName: '出库单',
pdfBuilder: () => buildStockOutOrderPdf(order, meta),
),
);
}
@@ -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)));
}
+281
View File
@@ -0,0 +1,281 @@
<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>出入库单排版方案 · 6 种黑白样式</title>
<style>
* { box-sizing:border-box; margin:0; padding:0; }
body { background:#e9ebee; font-family:"PingFang SC","Microsoft YaHei",sans-serif; color:#111; padding-bottom:60px; }
.page-head { position:sticky; top:0; z-index:10; background:#fff; border-bottom:1px solid #ccc; padding:14px 24px; display:flex; align-items:center; gap:24px; flex-wrap:wrap; }
.page-head h1 { font-size:17px; font-weight:700; }
.page-head nav a { color:#111; text-decoration:none; font-size:13px; margin-right:14px; border-bottom:1px dashed #999; padding-bottom:1px; }
.style-block { max-width:900px; margin:34px auto; }
.style-block > h2 { font-size:16px; margin:0 12px 4px; }
.style-block > .desc { font-size:12.5px; color:#555; margin:0 12px 16px; line-height:1.6; }
.sheet { background:#fff; box-shadow:0 2px 12px rgba(0,0,0,.16); padding:46px 40px; margin-bottom:26px; }
.doc table { width:100%; border-collapse:collapse; }
.doc th { text-align:center; white-space:nowrap; }
.doc td.num { text-align:right; }
.doc td.ctr { text-align:center; }
.doc .meta-row { display:flex; }
.doc .total { display:flex; }
.doc .sign { display:flex; justify-content:space-between; margin-top:40px; font-size:13px; }
.doc .tips { font-size:11px; color:#555; line-height:1.6; border:1px solid #ddd; background:#fafafa; padding:7px 10px; margin-top:12px; }
/* 一 极简线框 */
.s1 .doc { color:#1a1a1a; }
.s1 .hd { display:flex; align-items:baseline; gap:14px; border-bottom:2px solid #111; padding-bottom:12px; margin-bottom:16px; }
.s1 .co { font-size:23px; font-weight:700; letter-spacing:1px; }
.s1 .ty { font-size:14px; color:#666; letter-spacing:6px; }
.s1 .meta { margin-bottom:16px; }
.s1 .meta-row { justify-content:flex-start; gap:52px; font-size:12.5px; color:#333; line-height:2.1; }
.s1 .m-no { font-weight:700; color:#111; }
.s1 table { border:none; }
.s1 th { border:none; border-bottom:2px solid #111; padding:8px 6px; font-size:12px; font-weight:700; letter-spacing:.5px; }
.s1 td { border:none; border-bottom:1px solid #e2e2e2; padding:9px 6px; font-size:12px; }
.s1 .total { border-top:2px solid #111; margin-top:-1px; justify-content:flex-end; gap:44px; padding:12px 4px; font-size:13px; }
.s1 .t-label { margin-right:auto; color:#666; font-weight:700; }
.s1 .total b { font-size:15px; font-weight:700; }
.s1 .sign { margin-top:48px; }
.s1 .tips { border:none; border-left:3px solid #111; background:none; padding:2px 0 2px 10px; }
/* 二 现代分区 */
.s2 .hd { text-align:center; margin-bottom:14px; }
.s2 .co { font-size:24px; font-weight:800; }
.s2 .ty { display:inline-block; margin-top:8px; font-size:14px; font-weight:700; letter-spacing:6px; border:1.5px solid #111; border-radius:2px; padding:3px 18px; }
.s2 .meta { display:grid; grid-template-columns:repeat(3,1fr); border:1px solid #bbb; margin-bottom:14px; }
.s2 .meta-row { display:contents; }
.s2 .m { border:.5px solid #ccc; padding:8px 10px; font-size:12.5px; }
.s2 table { border:1px solid #ccc; }
.s2 th { background:#ececec; border:1px solid #ccc; padding:8px 6px; font-size:12px; font-weight:700; }
.s2 td { border:1px solid #ddd; padding:7px 8px; font-size:12px; }
.s2 tbody tr:nth-child(even) { background:#f7f7f7; }
.s2 .total { margin-top:12px; justify-content:flex-end; align-items:center; gap:36px; background:#efefef; border:1px solid #bbb; border-radius:2px; padding:12px 18px; }
.s2 .t-label { margin-right:auto; font-weight:800; font-size:14px; }
.s2 .total b { font-size:17px; font-weight:800; }
/* 三 紧凑报表 */
.s3 .doc { border-left:4px solid #111; padding-left:18px; }
.s3 .hd { display:flex; align-items:baseline; gap:12px; margin-bottom:10px; }
.s3 .co { font-size:19px; font-weight:700; }
.s3 .ty { font-size:13px; letter-spacing:4px; color:#444; }
.s3 .meta { background:#f3f3f3; border:1px solid #ccc; padding:7px 10px; margin-bottom:10px; }
.s3 .meta-row { justify-content:space-between; font-size:11.5px; line-height:1.9; }
.s3 .m-no { font-weight:700; }
.s3 table { border:1px solid #bbb; }
.s3 th { background:#e5e5e5; border:1px solid #bbb; padding:5px 5px; font-size:11px; font-weight:700; }
.s3 td { border:1px solid #ccc; padding:4px 6px; font-size:11px; }
.s3 td.num { font-family:"SF Mono","Roboto Mono",Menlo,Consolas,monospace; }
.s3 .total { justify-content:flex-end; gap:28px; margin-top:8px; border:1px solid #bbb; background:#f3f3f3; padding:7px 12px; font-size:12px; font-weight:700; }
.s3 .t-label { margin-right:auto; }
.s3 .total b { font-size:14px; font-family:"SF Mono",Menlo,Consolas,monospace; }
.s3 .sign { margin-top:30px; font-size:12px; }
/* 四 西式发票 */
.s4 .topbar { display:flex; justify-content:space-between; align-items:flex-start; border-bottom:3px solid #111; padding-bottom:14px; margin-bottom:16px; }
.s4 .co { font-size:30px; font-weight:800; letter-spacing:1px; }
.s4 .docmeta { text-align:right; }
.s4 .doctype { font-size:18px; font-weight:700; letter-spacing:6px; margin-bottom:8px; }
.s4 .mbox { font-size:12px; line-height:1.8; color:#333; }
.s4 .party { margin-bottom:14px; }
.s4 .plabel { font-size:10.5px; letter-spacing:3px; color:#888; text-transform:uppercase; margin-bottom:5px; }
.s4 .pgrid { display:flex; gap:40px; font-size:13px; }
.s4 .seclabel { font-size:11px; letter-spacing:3px; color:#111; font-weight:700; border-bottom:1px solid #111; padding-bottom:5px; text-transform:uppercase; }
.s4 th { border-bottom:1.5px solid #111; padding:8px 6px; font-size:11.5px; font-weight:700; }
.s4 td { border-bottom:1px solid #e0e0e0; padding:8px 6px; font-size:12px; }
.s4 .grand { display:flex; flex-direction:column; align-items:flex-end; gap:4px; margin-top:14px; }
.s4 .grand > div { display:flex; gap:18px; min-width:230px; justify-content:space-between; font-size:13px; }
.s4 .grand .g1 { justify-content:flex-end; color:#888; letter-spacing:2px; }
.s4 .grand .g3 { border-top:2px solid #111; padding-top:6px; margin-top:2px; font-weight:800; }
.s4 .grand b { font-size:15px; } .s4 .grand .g3 b { font-size:19px; }
.s4 .sign { border-top:1px solid #ccc; padding-top:14px; }
/* 五 瑞士网格 */
.s5 .kicker { font-size:12px; letter-spacing:4px; text-transform:uppercase; color:#888; }
.s5 .headline { font-size:46px; font-weight:800; letter-spacing:4px; line-height:1; margin:4px 0 16px; }
.s5 .metaline { display:flex; gap:42px; flex-wrap:wrap; border-top:1px solid #111; border-bottom:1px solid #111; padding:10px 0; font-size:12.5px; margin-bottom:22px; }
.s5 .metaline i { display:block; font-style:normal; font-size:10px; letter-spacing:2px; text-transform:uppercase; color:#999; margin-bottom:3px; }
.s5 .sec { font-size:10.5px; letter-spacing:3px; text-transform:uppercase; color:#666; margin-bottom:6px; }
.s5 th { border-top:2px solid #111; border-bottom:2px solid #111; padding:8px 6px; font-size:11px; letter-spacing:.5px; font-weight:700; }
.s5 td { border-bottom:1px solid #e4e4e4; padding:9px 6px; font-size:12px; }
.s5 td.num { font-variant-numeric:tabular-nums; font-feature-settings:"tnum"; }
.s5 .total8 { display:flex; justify-content:flex-end; align-items:baseline; gap:14px; border-bottom:3px double #111; padding:12px 2px; margin-top:-1px; }
.s5 .total8 > span { margin-right:auto; font-size:11px; letter-spacing:3px; text-transform:uppercase; color:#666; }
.s5 .total8 em { font-style:normal; font-size:11px; color:#999; letter-spacing:2px; }
.s5 .total8 b { font-size:18px; font-weight:800; }
.s5 .sign { margin-top:46px; }
/* 六 填空表单 */
.s6 .frame { border:1px solid #111; outline:3px solid #111; outline-offset:3px; padding:18px 18px 16px; }
.s6 .co9 { text-align:center; font-size:14px; letter-spacing:2px; color:#333; }
.s6 .title9 { text-align:center; font-size:22px; font-weight:800; letter-spacing:10px; margin:2px 0 14px; }
.s6 .meta9 { display:grid; grid-template-columns:repeat(3,1fr); gap:6px 24px; margin-bottom:12px; }
.s6 .f { display:flex; align-items:baseline; font-size:12.5px; }
.s6 .f label { color:#333; white-space:nowrap; } .s6 .f label::after { content:""; }
.s6 .f .fill { flex:1; border-bottom:1px solid #111; min-height:18px; padding:0 4px; font-weight:600; }
.s6 table, .s6 th, .s6 td { border:1px solid #111; }
.s6 th { background:#eee; padding:6px; font-size:11.5px; font-weight:700; }
.s6 td { padding:6px 7px; font-size:11.5px; }
.s6 .total9 { display:flex; border:1px solid #111; border-top:none; padding:8px 12px; font-size:13px; font-weight:700; }
.s6 .total9 span:first-child { margin-right:auto; }
.s6 .total9 span+span { margin-left:36px; }
.s6 .sign9 { display:flex; margin-top:16px; gap:10px; }
.s6 .sign9 div { flex:1; border:1px solid #111; padding:14px 10px 8px; font-size:12.5px; }
@media print {
@page { size:A4; margin:10mm; }
body { background:#fff; padding:0; }
.page-head { display:none; }
.style-block { max-width:none; margin:0; break-before:page; }
.style-block:first-of-type { break-before:auto; }
.style-block > h2 { font-size:13px; margin:0 0 2px; }
.style-block > .desc { font-size:10px; color:#555; margin:0 0 8px; }
.sheet { box-shadow:none; border:1px solid #e3e3e3; padding:18px 20px; margin-bottom:10px; break-inside:avoid; }
}
</style></head><body>
<header class="page-head"><h1>出入库单排版方案 · 6 种黑白样式</h1><nav><a href="#s1">极简线框</a><a href="#s2">现代分区</a><a href="#s3">紧凑报表</a><a href="#s4">西式发票</a><a href="#s5">瑞士网格</a><a href="#s6">填空表单</a></nav></header>
<section id="s1" class="style-block">
<h2>样式一 · 极简线框</h2><p class="desc">无竖线、仅细横线分隔,大留白、左对齐抬头 —— 清爽现代,激光打印省墨。</p>
<div class="sheet s1"><div class="doc">
<div class="hd"><div class="co">鼎晟酒行</div><div class="ty">入 库 单</div></div>
<div class="meta">
<div class="meta-row r1"><span class="m m-partner">单位名称:丁小峰</span><span class="m m-date">入库日期:2026-06-23</span><span class="m m-no">NORK20260623000013</span></div>
<div class="meta-row r2"><span class="m m-phone">联系电话:</span><span class="m m-addr">单位地址:</span><span class="m m-wh">仓库:鼎晟酒行</span></div>
</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1328</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719005920</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1329</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719007000</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr></tbody></table>
<div class="total"><span class="t-label">单据总计</span><span class="t-qty">数量 <b>24</b></span><span class="t-amt">金额 <b>44160.00</b></span></div>
<div class="sign"><span>出货人(签字):</span><span>采购员:汤宇飞</span><span>制单人:汤宇飞</span></div></div></div>
<div class="sheet s1"><div class="doc">
<div class="hd"><div class="co">鼎晟酒行</div><div class="ty">出 库 单</div></div>
<div class="meta">
<div class="meta-row r1"><span class="m m-partner">客户名称:刘总</span><span class="m m-date">出库日期:2026-06-23</span><span class="m m-no">NO.CK20260623000020</span></div>
<div class="meta-row r2"><span class="m m-phone">联系电话:</span><span class="m m-addr">送货地址:</span><span class="m m-wh">仓库:鼎晟酒行</span></div>
</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr></tbody></table>
<div class="total"><span class="t-label">单据总计</span><span class="t-qty">数量 <b>2</b></span><span class="t-amt">金额 <b>6160.00</b></span></div>
<div class="tips">温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。</div><div class="sign"><span>收货人(签字):</span><span>销售员:汤宇飞</span><span>制单人:汤宇飞</span></div></div></div>
</section>
<section id="s2" class="style-block">
<h2>样式二 · 现代分区</h2><p class="desc">抬头字段做成带框表单格、明细斑马纹、总计高亮条 —— 信息分区清晰。</p>
<div class="sheet s2"><div class="doc">
<div class="hd"><div class="co">鼎晟酒行</div><div class="ty">入 库 单</div></div>
<div class="meta">
<div class="meta-row r1"><span class="m m-partner">单位名称:丁小峰</span><span class="m m-date">入库日期:2026-06-23</span><span class="m m-no">NORK20260623000013</span></div>
<div class="meta-row r2"><span class="m m-phone">联系电话:</span><span class="m m-addr">单位地址:</span><span class="m m-wh">仓库:鼎晟酒行</span></div>
</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1328</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719005920</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1329</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719007000</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr></tbody></table>
<div class="total"><span class="t-label">单据总计</span><span class="t-qty">数量 <b>24</b></span><span class="t-amt">金额 <b>44160.00</b></span></div>
<div class="sign"><span>出货人(签字):</span><span>采购员:汤宇飞</span><span>制单人:汤宇飞</span></div></div></div>
<div class="sheet s2"><div class="doc">
<div class="hd"><div class="co">鼎晟酒行</div><div class="ty">出 库 单</div></div>
<div class="meta">
<div class="meta-row r1"><span class="m m-partner">客户名称:刘总</span><span class="m m-date">出库日期:2026-06-23</span><span class="m m-no">NO.CK20260623000020</span></div>
<div class="meta-row r2"><span class="m m-phone">联系电话:</span><span class="m m-addr">送货地址:</span><span class="m m-wh">仓库:鼎晟酒行</span></div>
</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr></tbody></table>
<div class="total"><span class="t-label">单据总计</span><span class="t-qty">数量 <b>2</b></span><span class="t-amt">金额 <b>6160.00</b></span></div>
<div class="tips">温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。</div><div class="sign"><span>收货人(签字):</span><span>销售员:汤宇飞</span><span>制单人:汤宇飞</span></div></div></div>
</section>
<section id="s3" class="style-block">
<h2>样式三 · 紧凑报表</h2><p class="desc">小字号、数字等宽对齐、左侧竖线点缀 —— 单页能塞更多行,密集报表风。</p>
<div class="sheet s3"><div class="doc">
<div class="hd"><div class="co">鼎晟酒行</div><div class="ty">入 库 单</div></div>
<div class="meta">
<div class="meta-row r1"><span class="m m-partner">单位名称:丁小峰</span><span class="m m-date">入库日期:2026-06-23</span><span class="m m-no">NORK20260623000013</span></div>
<div class="meta-row r2"><span class="m m-phone">联系电话:</span><span class="m m-addr">单位地址:</span><span class="m m-wh">仓库:鼎晟酒行</span></div>
</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1328</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719005920</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1329</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719007000</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr></tbody></table>
<div class="total"><span class="t-label">单据总计</span><span class="t-qty">数量 <b>24</b></span><span class="t-amt">金额 <b>44160.00</b></span></div>
<div class="sign"><span>出货人(签字):</span><span>采购员:汤宇飞</span><span>制单人:汤宇飞</span></div></div></div>
<div class="sheet s3"><div class="doc">
<div class="hd"><div class="co">鼎晟酒行</div><div class="ty">出 库 单</div></div>
<div class="meta">
<div class="meta-row r1"><span class="m m-partner">客户名称:刘总</span><span class="m m-date">出库日期:2026-06-23</span><span class="m m-no">NO.CK20260623000020</span></div>
<div class="meta-row r2"><span class="m m-phone">联系电话:</span><span class="m m-addr">送货地址:</span><span class="m m-wh">仓库:鼎晟酒行</span></div>
</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr></tbody></table>
<div class="total"><span class="t-label">单据总计</span><span class="t-qty">数量 <b>2</b></span><span class="t-amt">金额 <b>6160.00</b></span></div>
<div class="tips">温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。</div><div class="sign"><span>收货人(签字):</span><span>销售员:汤宇飞</span><span>制单人:汤宇飞</span></div></div></div>
</section>
<section id="s4" class="style-block">
<h2>样式四 · 西式发票</h2><p class="desc">左上大字号商号 + 右上单号块、底部「单据总计」金额栈、字母大写小标题 —— 借鉴欧美 INVOICE。</p>
<div class="sheet s4"><div class="doc">
<div class="topbar"><div class="co">鼎晟酒行</div>
<div class="docmeta"><div class="doctype">入 库 单</div>
<div class="mbox"><div>NORK20260623000013</div><div>入库日期:2026-06-23</div><div>仓库:鼎晟酒行</div></div></div></div>
<div class="party"><div class="plabel">往来信息</div>
<div class="pgrid"><span>单位名称:丁小峰</span><span>联系电话:</span><span>单位地址:</span></div></div>
<div class="seclabel">商品明细 / ITEMS</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1328</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719005920</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1329</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719007000</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr></tbody></table>
<div class="grand"><div class="g1">单据总计</div>
<div class="g2"><span>数量</span><b>24</b></div>
<div class="g3"><span>金额</span><b>44160.00</b></div></div>
<div class="sign"><span>出货人(签字):</span><span>采购员:汤宇飞</span><span>制单人:汤宇飞</span></div></div></div>
<div class="sheet s4"><div class="doc">
<div class="topbar"><div class="co">鼎晟酒行</div>
<div class="docmeta"><div class="doctype">出 库 单</div>
<div class="mbox"><div>NO.CK20260623000020</div><div>出库日期:2026-06-23</div><div>仓库:鼎晟酒行</div></div></div></div>
<div class="party"><div class="plabel">往来信息</div>
<div class="pgrid"><span>客户名称:刘总</span><span>联系电话:</span><span>送货地址:</span></div></div>
<div class="seclabel">商品明细 / ITEMS</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr></tbody></table>
<div class="grand"><div class="g1">单据总计</div>
<div class="g2"><span>数量</span><b>2</b></div>
<div class="g3"><span>金额</span><b>6160.00</b></div></div>
<div class="tips">温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。</div><div class="sign"><span>收货人(签字):</span><span>销售员:汤宇飞</span><span>制单人:汤宇飞</span></div></div></div>
</section>
<section id="s5" class="style-block">
<h2>样式五 · 瑞士网格</h2><p class="desc">超大标题 + 细基线网格、小标签等宽数字、表格仅上下粗线 —— 杂志/瑞士排版。</p>
<div class="sheet s5"><div class="doc">
<div class="kicker">鼎晟酒行</div>
<h1 class="headline">入 库 单</h1>
<div class="metaline"><span><i>单号</i>RK20260623000013</span><span><i>入库日期</i>2026-06-23</span><span><i>单位名称</i>丁小峰</span><span><i>仓库</i>鼎晟酒行</span></div>
<div class="sec">商品明细</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1328</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719005920</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1329</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719007000</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr></tbody></table>
<div class="total8"><span>单据总计</span><em>数量</em><b>24</b><em>金额</em><b>44160.00</b></div>
<div class="sign"><span>出货人(签字):</span><span>采购员:汤宇飞</span><span>制单人:汤宇飞</span></div></div></div>
<div class="sheet s5"><div class="doc">
<div class="kicker">鼎晟酒行</div>
<h1 class="headline">出 库 单</h1>
<div class="metaline"><span><i>单号</i>CK20260623000020</span><span><i>出库日期</i>2026-06-23</span><span><i>客户名称</i>刘总</span><span><i>仓库</i>鼎晟酒行</span></div>
<div class="sec">商品明细</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr></tbody></table>
<div class="total8"><span>单据总计</span><em>数量</em><b>2</b><em>金额</em><b>6160.00</b></div>
<div class="tips">温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。</div><div class="sign"><span>收货人(签字):</span><span>销售员:汤宇飞</span><span>制单人:汤宇飞</span></div></div></div>
</section>
<section id="s6" class="style-block">
<h2>样式六 · 填空表单</h2><p class="desc">双线外框、字段下划线填空、底部多签字格 —— 仿手工三联单/预印单据,正式可手填。</p>
<div class="sheet s6"><div class="doc form9"><div class="frame">
<div class="co9">鼎晟酒行</div>
<div class="title9">入 库 单</div>
<div class="meta9">
<div class="f"><label>单位名称</label><span class="fill">丁小峰</span></div>
<div class="f"><label>入库日期</label><span class="fill">2026-06-23</span></div>
<div class="f"><label>单据编号</label><span class="fill">RK20260623000013</span></div>
<div class="f"><label>联系电话</label><span class="fill"></span></div>
<div class="f"><label>单位地址</label><span class="fill"></span></div>
<div class="f"><label>仓库</label><span class="fill">鼎晟酒行</span></div>
</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1328</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719005920</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1329</td><td class="">茅台1935原箱2026</td><td class="ctr">普通/53度</td><td class="ctr">500ml*6/件</td><td class="ctr">8719007000</td><td class="ctr">2026-02-28</td><td class="num">6</td><td class="num">600.00</td><td class="num">3600.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">6</td><td class="num">3080.00</td><td class="num">18480.00</td><td class=""></td></tr></tbody></table>
<div class="total9"><span>单据总计</span><span>数量 24</span><span>金额 44160.00</span></div>
<div class="sign9"><div>出货人(签字):</div><div>采购员:汤宇飞</div><div>制单人:汤宇飞</div></div>
</div></div></div>
<div class="sheet s6"><div class="doc form9"><div class="frame">
<div class="co9">鼎晟酒行</div>
<div class="title9">出 库 单</div>
<div class="meta9">
<div class="f"><label>客户名称</label><span class="fill">刘总</span></div>
<div class="f"><label>出库日期</label><span class="fill">2026-06-23</span></div>
<div class="f"><label>单据编号</label><span class="fill">CK20260623000020</span></div>
<div class="f"><label>联系电话</label><span class="fill"></span></div>
<div class="f"><label>送货地址</label><span class="fill"></span></div>
<div class="f"><label>仓库</label><span class="fill">鼎晟酒行</span></div>
</div>
<table><thead><tr><th>商品编号</th><th>商品名称</th><th>系列</th><th>规格</th><th>批次号</th><th>生产日期</th><th>数量</th><th>单价</th><th>金额</th><th>备注</th></tr></thead><tbody><tr><td class="">P1331</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-126/009192582386</td><td class="ctr">2026-03-13</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr><tr><td class="">P1330</td><td class="">茅台公斤2026原箱</td><td class="ctr">普通/53度</td><td class="ctr">1.00L*6/件</td><td class="ctr">2025-184/248896472708</td><td class="ctr">2026-06-01</td><td class="num">1</td><td class="num">3080.00</td><td class="num">3080.00</td><td class=""></td></tr></tbody></table>
<div class="total9"><span>单据总计</span><span>数量 2</span><span>金额 6160.00</span></div>
<div class="tips">温馨提示:签收时,请务必核对好酒品数量、年份和日期、批次及物流码,如有问题及时反馈,酒品无质量问题一经售出概不退换,谢谢合作。</div>
<div class="sign9"><div>收货人(签字):</div><div>销售员:汤宇飞</div><div>制单人:汤宇飞</div></div>
</div></div></div>
</section>
</body></html>
Binary file not shown.
+60
View File
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文档索引 — 酒库管理系统</title>
<style>
:root{
--primary:#2563AC; --primary-dark:#154072; --accent:#8B2331;
--ink:#232934; --muted:#6E7888; --border:#DCE2EB; --paper:#F5F7FA; --head:#F0F4FF;
}
*{box-sizing:border-box;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;}
body{margin:0;background:var(--paper);color:var(--ink);padding:28px;line-height:1.6;}
h1{font-size:20px;margin:0 0 4px;}
.sub{color:var(--muted);font-size:13px;margin-bottom:20px;}
h2{font-size:15px;margin:24px 0 8px;color:var(--primary-dark);border-bottom:2px solid var(--head);padding-bottom:6px;}
ul{margin:6px 0;padding-left:20px;}
li{margin:5px 0;font-size:13.5px;}
a{color:var(--primary);text-decoration:none;}
a:hover{text-decoration:underline;}
.hint{color:var(--muted);font-size:12px;}
.tag{display:inline-block;font-size:10px;padding:1px 7px;border-radius:9px;background:#EEF2F8;color:var(--muted);margin-left:6px;}
.tag.html{background:#E5EEF9;color:var(--primary);}
</style>
</head>
<body>
<h1>酒库管理系统 · 文档索引</h1>
<div class="sub">项目所有文档的单一入口。新增文档须同时登记到此处。</div>
<h2>设计方案 / 原型</h2>
<ul>
<li><a href="design/order-return-prototype.html">退单原型(已审核单据)</a><span class="tag html">HTML</span></li>
<li><a href="design/order-print-layouts.html">出入库单打印排版方案(6 种黑白样式精选)</a><span class="tag html">HTML</span> · <a href="design/order-print-layouts.pdf">PDF</a></li>
</ul>
<h2>实现计划</h2>
<ul>
<li class="hint">(暂无)</li>
</ul>
<h2>排障 / 运维 Runbook</h2>
<ul>
<li><a href="runbooks/ec2-to-ali-migration.html">EC2 → 阿里云 迁移计划</a><span class="tag html">HTML</span> <span class="hint">— jiu 服务/数据/DB/文件/域名/CI 整体迁移</span></li>
</ul>
<h2>知识库 / 调研 / 测试</h2>
<ul>
<li><a href="testing/license-test-flow.html">授权兑换券测试流程</a><span class="tag html">HTML</span></li>
</ul>
<h2>历史 Markdown 文档(逐步迁移)</h2>
<ul>
<li><a href="deployment-nas-gitea.md">部署:NAS Gitea/Forgejo</a><span class="tag">MD</span></li>
<li><a href="dev-setup.md">开发环境搭建</a><span class="tag">MD</span></li>
<li><a href="android-signing.md">Android 签名</a> · <a href="ios-signing.md">iOS 签名</a> · <a href="macos-signing.md">macOS 签名</a><span class="tag">MD</span></li>
<li><a href="user-manual.md">用户手册</a><span class="tag">MD</span></li>
<li class="hint">另有 api/ architecture/ requirements/ review/ security/ runbooks/ context/ 等目录下的过程文档</li>
</ul>
</body>
</html>