diff --git a/client/lib/models/stock_summary.dart b/client/lib/models/stock_summary.dart index 616bf0e..4060732 100644 --- a/client/lib/models/stock_summary.dart +++ b/client/lib/models/stock_summary.dart @@ -4,6 +4,7 @@ class StockSummary { final int monthCount; final double monthAmount; final int pendingCount; + final int draftCount; final int lastMonthCount; final double lastMonthAmount; @@ -11,6 +12,7 @@ class StockSummary { this.monthCount = 0, this.monthAmount = 0, this.pendingCount = 0, + this.draftCount = 0, this.lastMonthCount = 0, this.lastMonthAmount = 0, }); @@ -19,6 +21,7 @@ class StockSummary { monthCount: (j['month_count'] as num?)?.toInt() ?? 0, monthAmount: (j['month_amount'] as num?)?.toDouble() ?? 0, pendingCount: (j['pending_count'] as num?)?.toInt() ?? 0, + draftCount: (j['draft_count'] as num?)?.toInt() ?? 0, lastMonthCount: (j['last_month_count'] as num?)?.toInt() ?? 0, lastMonthAmount: (j['last_month_amount'] as num?)?.toDouble() ?? 0, ); diff --git a/client/lib/screens/inventory/inventory_list_screen.dart b/client/lib/screens/inventory/inventory_list_screen.dart index 7005b21..927f14f 100644 --- a/client/lib/screens/inventory/inventory_list_screen.dart +++ b/client/lib/screens/inventory/inventory_list_screen.dart @@ -50,7 +50,7 @@ class _InventoryListScreenState extends ConsumerState { static const _statusOptions = ['全部', '在售', '预警', '缺货']; // 列序照原型 inventory.html COLS:商品/规格/系列/批次/库存/成本价/单价/生产/入库/供应商/状态/操作。 - // 成本价=入库进价(unitPrice),单价=售价(salePrice)。 + // 成本价=入库进价(unitPrice),总价=库存×成本价(2026-07-03 用户拍板:原「单价/参考售价」列换掉)。 // 仓库/备注原型没有 → 默认隐藏的可选列(列设置可开)。 static const _colDefs = [ ColDef('product', '商品', required: true), @@ -59,7 +59,7 @@ class _InventoryListScreenState extends ConsumerState { ColDef('batch', '批次号'), ColDef('qty', '库存'), ColDef('cost', '成本价'), - ColDef('price', '单价'), + ColDef('price', '总价'), ColDef('prodDate', '生产日期'), ColDef('inTime', '入库时间'), ColDef('supplier', '供应商'), @@ -274,7 +274,7 @@ class _InventoryListScreenState extends ConsumerState { '批次号', '库存量', '成本价', - '单价', + '总价', '生产日期', '入库时间', '供应商', @@ -295,7 +295,9 @@ class _InventoryListScreenState extends ConsumerState { item.batchNo, '${item.quantity.toStringAsFixed(0)} ${item.unit}'.trim(), item.unitPrice != null ? item.unitPrice!.toStringAsFixed(2) : '', - item.salePrice != null ? item.salePrice!.toStringAsFixed(2) : '', + item.unitPrice != null + ? (item.unitPrice! * item.quantity).toStringAsFixed(2) + : '', item.productionDate ?? '', item.createdAt != null && item.createdAt!.length >= 10 ? item.createdAt!.substring(0, 10) @@ -362,11 +364,11 @@ class _InventoryListScreenState extends ConsumerState { fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback)), 'price' => Text( - item.salePrice != null - ? '¥${item.salePrice!.toStringAsFixed(2)}' - : '待定价', - style: item.salePrice == null - ? TextStyle(color: t.warn, fontWeight: FontWeight.w600) + item.unitPrice != null + ? _fmtCost(item.unitPrice! * item.quantity) + : '-', + style: item.unitPrice == null + ? TextStyle(color: t.faint) : const TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback)), diff --git a/client/lib/screens/public/public_product_screen.dart b/client/lib/screens/public/public_product_screen.dart index fa3482e..49c9b84 100644 --- a/client/lib/screens/public/public_product_screen.dart +++ b/client/lib/screens/public/public_product_screen.dart @@ -128,6 +128,17 @@ class _PublicProductScreenState extends State { if (_error != null || _data == null) { return Scaffold( backgroundColor: _kPaper, + // 加载失败态也要能退回 App(canPop 时显示返回,扫码直达不显示) + appBar: Navigator.of(context).canPop() + ? AppBar( + backgroundColor: _kPaper, + elevation: 0, + leading: IconButton( + icon: const Icon(LucideIcons.arrowLeft, color: _kBurgundy), + onPressed: () => Navigator.of(context).pop(), + ), + ) + : null, body: Center( child: Padding( padding: const EdgeInsets.all(32), @@ -199,10 +210,14 @@ class _PublicProductScreenState extends State { _parseQuickSpecs(spec, batch?['production_date'] as String?); final productionDate = batch?['production_date'] as String?; + // App 内「预览」推入时可返回(canPop);顾客扫码/网页直达无返回栈则不渲染, + // 页面对外保持纯净(本页为独立品牌样式,按钮用自有暖纸/酒红配色)。 + final canPop = Navigator.of(context).canPop(); return Scaffold( backgroundColor: _kPaperDeep, body: SafeArea( - child: Center( + child: Stack(children: [ + Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 520), child: CustomScrollView( @@ -286,7 +301,28 @@ class _PublicProductScreenState extends State { ], ), ), - ), + ), + if (canPop) + Positioned( + top: 12, + left: 12, + child: Material( + color: _kPaper.withValues(alpha: 0.92), + shape: const CircleBorder( + side: BorderSide(color: _kBurgundyLight)), + elevation: 2, + child: InkWell( + customBorder: const CircleBorder(), + onTap: () => Navigator.of(context).pop(), + child: const Padding( + padding: EdgeInsets.all(9), + child: + Icon(LucideIcons.arrowLeft, size: 20, color: _kBurgundy), + ), + ), + ), + ), + ]), ), ); } diff --git a/client/lib/screens/stock_in/stock_in_list_screen.dart b/client/lib/screens/stock_in/stock_in_list_screen.dart index 74d2ea2..46d221d 100644 --- a/client/lib/screens/stock_in/stock_in_list_screen.dart +++ b/client/lib/screens/stock_in/stock_in_list_screen.dart @@ -498,6 +498,14 @@ class _StockInListScreenState extends ConsumerState { : DsKpiDelta.neutral, onTap: () => _setStatus('pending'), ), + DsKpi( + title: '草稿单数 · 点击筛选', + value: '${summary?.draftCount ?? 0}', + icon: LucideIcons.filePen, + tone: DsKpiTone.warn, // 暗黄(用户拍板) + delta: (summary?.draftCount ?? 0) > 0 ? '待提交审核' : '点击筛选', + onTap: () => _setStatus('draft'), + ), ]; final mobile = context.isMobile; @@ -861,6 +869,8 @@ class _StockInListScreenState extends ConsumerState { static final _money = NumberFormat.currency(locale: 'zh_CN', symbol: '¥', decimalDigits: 0); + // 明细行内数字(去 ¥,符号只留在合计) + static final _num = NumberFormat('#,##0'); static String _fmtQty(double q) => q == q.roundToDouble() ? q.toStringAsFixed(0) : q.toString(); @@ -896,13 +906,14 @@ class _StockInListScreenState extends ConsumerState { series: it.productSeries ?? '—', spec: it.productSpec ?? '', qty: _fmtQty(it.quantity), - price: it.costPrice == 0 ? '待定价' : _money.format(it.costPrice), - amount: - it.costAmount == 0 ? '待定价' : _money.format(it.costAmount), + price: it.costPrice == 0 ? '待定价' : _num.format(it.costPrice), + amount: it.costAmount == 0 ? '待定价' : _num.format(it.costAmount), pending: it.costPrice == 0, )) .toList(), totalText: o.costTotal != null ? _money.format(o.costTotal) : '—', + priceLabel: '进价(单瓶)', + amountLabel: '总价', actionGroups: _detailActionGroups(o), ); } diff --git a/client/lib/screens/stock_out/stock_out_form_screen.dart b/client/lib/screens/stock_out/stock_out_form_screen.dart index 7c49018..2bf55cc 100644 --- a/client/lib/screens/stock_out/stock_out_form_screen.dart +++ b/client/lib/screens/stock_out/stock_out_form_screen.dart @@ -36,6 +36,7 @@ class _PickerItem { final String spec; final String unit; final double? costPrice; // 进价(成本,仅管理员可见列) + final double? salePrice; // 参考售价(入库时填写,存 product.sale_price) final double availableQty; const _PickerItem({ required this.productId, @@ -45,6 +46,7 @@ class _PickerItem { required this.spec, required this.unit, this.costPrice, + this.salePrice, required this.availableQty, }); } @@ -64,6 +66,7 @@ List<_PickerItem> _aggregatePickerItems(List rows) { spec: existing.spec, unit: existing.unit, costPrice: existing.costPrice ?? inv.unitPrice, + salePrice: existing.salePrice ?? inv.salePrice, availableQty: existing.availableQty + inv.quantity, ); } else { @@ -75,6 +78,7 @@ List<_PickerItem> _aggregatePickerItems(List rows) { spec: inv.spec, unit: inv.unit, costPrice: inv.unitPrice, + salePrice: inv.salePrice, availableQty: inv.quantity, ); } @@ -264,7 +268,7 @@ class _StockOutFormScreenState extends ConsumerState { setState(() { for (final item in selected) { if (_items.any((row) => row.productId == item.productId)) continue; - _items.add(_ItemRow( + final row = _ItemRow( productId: item.productId, productCode: item.productCode, productName: item.productName, @@ -272,7 +276,11 @@ class _StockOutFormScreenState extends ConsumerState { spec: item.spec, costPrice: item.costPrice, availableQty: item.availableQty, - )); + salePrice: item.salePrice, // 售价默认带出入库时填的参考售价 + ); + // 数量默认带出可用数量(整箱出为主,改少比补多顺手) + row.qtyCtrl.text = item.availableQty.toStringAsFixed(0); + _items.add(row); } }); } diff --git a/client/lib/screens/stock_out/stock_out_list_screen.dart b/client/lib/screens/stock_out/stock_out_list_screen.dart index 721924b..4ebe222 100644 --- a/client/lib/screens/stock_out/stock_out_list_screen.dart +++ b/client/lib/screens/stock_out/stock_out_list_screen.dart @@ -532,6 +532,17 @@ class _StockOutListScreenState extends ConsumerState { ref.read(stockOutListProvider.notifier).setStatus('pending'); }, ), + DsKpi( + title: '草稿单数 · 点击筛选', + value: '${summary?.draftCount ?? 0}', + icon: LucideIcons.filePen, + tone: DsKpiTone.warn, // 暗黄(用户拍板) + delta: (summary?.draftCount ?? 0) > 0 ? '待提交审核' : '点击筛选', + onTap: () { + setState(() => _statusFilter = 'draft'); + ref.read(stockOutListProvider.notifier).setStatus('draft'); + }, + ), ]; final mobile = context.isMobile; @@ -845,6 +856,8 @@ class _StockOutListScreenState extends ConsumerState { static final _money = NumberFormat.currency(locale: 'zh_CN', symbol: '¥', decimalDigits: 0); + // 明细行内数字(去 ¥:列多时更紧凑,符号只留在合计) + static final _num = NumberFormat('#,##0'); static String _fmtQty(double q) => q == q.roundToDouble() ? q.toStringAsFixed(0) : q.toString(); @@ -885,10 +898,10 @@ class _StockOutListScreenState extends ConsumerState { series: it.productSeries ?? '—', spec: it.productSpec ?? '', qty: _fmtQty(it.quantity), - price: pending ? '待定价' : _money.format(it.salePrice), - amount: pending ? '待定价' : _money.format(it.saleAmount), - cost: admin ? _money.format(it.costPrice) : null, - profit: admin ? (pending ? '待定价' : _money.format(profit)) : null, + price: pending ? '待定价' : _num.format(it.salePrice), + amount: pending ? '待定价' : _num.format(it.saleAmount), + cost: admin ? _num.format(it.costPrice) : null, + profit: admin ? (pending ? '待定价' : _num.format(profit)) : null, pending: pending, ); }).toList(), diff --git a/client/lib/widgets/ds/ds_kpi.dart b/client/lib/widgets/ds/ds_kpi.dart index 5e9b916..40e727e 100644 --- a/client/lib/widgets/ds/ds_kpi.dart +++ b/client/lib/widgets/ds/ds_kpi.dart @@ -5,7 +5,7 @@ import '../../core/theme/app_dims.g.dart'; import '../../core/theme/app_fonts.dart'; /// 图标块 .ic 变体(软底 + 前景色)。 -enum DsKpiTone { info, ok, blue, alert } +enum DsKpiTone { info, ok, blue, alert, warn } /// 环比方向 .d.up/.down(绿/红),neutral=纯文案(muted)。 enum DsKpiDelta { up, down, neutral } @@ -41,6 +41,8 @@ class DsKpi extends StatelessWidget { DsKpiTone.ok => (t.okSoft, t.success), DsKpiTone.blue => (t.infoSoft, t.brand400), DsKpiTone.alert => (t.accentSoft, t.accent), + // 暗黄(草稿卡):icon 与数字走 warn token + DsKpiTone.warn => (t.warnBg, t.warn), }; final deltaColor = switch (deltaTone) { DsKpiDelta.up => t.success, @@ -88,7 +90,11 @@ class DsKpi extends StatelessWidget { letterSpacing: 0.3, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, - color: tone == DsKpiTone.alert ? t.accent : t.heading)), + color: switch (tone) { + DsKpiTone.alert => t.accent, + DsKpiTone.warn => t.warn, + _ => t.heading, + })), if (delta != null) ...[ const SizedBox(height: 5), // 原型 .d 的箭头是文本字符 ▲/▼(非图标),gap4。 diff --git a/client/lib/widgets/order_detail_drawer.dart b/client/lib/widgets/order_detail_drawer.dart index 87dc7cf..975b433 100644 --- a/client/lib/widgets/order_detail_drawer.dart +++ b/client/lib/widgets/order_detail_drawer.dart @@ -287,6 +287,9 @@ class _LinesTable extends StatelessWidget { : Text(amountLabel, textAlign: TextAlign.right, style: _hStyle(t)), c6: _withProfit + ? Text('总售价', textAlign: TextAlign.right, style: _hStyle(t)) + : null, + c7: _withProfit ? Text('利润', textAlign: TextAlign.right, style: _hStyle(t)) : null, ), @@ -344,6 +347,10 @@ class _LinesTable extends StatelessWidget { : _priceCell(context, lines[i].amount, lines[i].pending, accent: true), c6: _withProfit + ? _priceCell(context, lines[i].amount, lines[i].pending, + accent: true) + : null, + c7: _withProfit ? _profitCell( context, lines[i].profit ?? '—', lines[i].pending) : null, @@ -377,31 +384,38 @@ class _LinesTable extends StatelessWidget { color: accent ? t.text : t.text)); } - // 5 列布局(对齐原型 grid 1fr 124 40 64 78);c6 非空时为出库 6 列形态 - // (数量/成本价/售价/利润,数值列等宽收窄)。 + // 5 列布局(对齐原型 grid 1fr 124 40 64 78);c6/c7 非空时为出库 7 列形态 + // (数量/成本价/售价/总售价/利润,数值列等宽收窄)。 Widget _lineRow(BuildContext context, {required Widget c1, required Widget c2, required Widget c3, required Widget c4, required Widget c5, - Widget? c6}) { - final six = c6 != null; + Widget? c6, + Widget? c7}) { + final wide = c6 != null; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: c1), const SizedBox(width: 8), - SizedBox(width: six ? 84 : 96, child: c2), + // 5 列(入库)时中间列加宽左移:商品列不再吃掉全部余宽, + // 「进价(单瓶)」表头一行放得下(2026-07-03 用户反馈)。 + SizedBox(width: wide ? 100 : 124, child: c2), const SizedBox(width: 8), - SizedBox(width: 34, child: c3), + SizedBox(width: wide ? 32 : 40, child: c3), const SizedBox(width: 8), - SizedBox(width: six ? 56 : 60, child: c4), + SizedBox(width: wide ? 62 : 88, child: c4), const SizedBox(width: 8), - SizedBox(width: six ? 56 : 72, child: c5), - if (six) ...[ + SizedBox(width: wide ? 62 : 88, child: c5), + if (c6 != null) ...[ const SizedBox(width: 8), - SizedBox(width: 56, child: c6), + SizedBox(width: 62, child: c6), + ], + if (c7 != null) ...[ + const SizedBox(width: 8), + SizedBox(width: 62, child: c7), ], ], ); @@ -421,7 +435,8 @@ class _LinesTable extends StatelessWidget { return Text(text, textAlign: TextAlign.right, style: TextStyle( - fontSize: AppDims.fsBody, + // 明细数值列缩小一号(2026-07-03 用户反馈:列多时拥挤) + fontSize: AppDims.fsSm, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontWeight: FontWeight.w600, diff --git a/client/lib/widgets/product_editor_drawer.dart b/client/lib/widgets/product_editor_drawer.dart index 91b4552..0f6ccc5 100644 --- a/client/lib/widgets/product_editor_drawer.dart +++ b/client/lib/widgets/product_editor_drawer.dart @@ -326,9 +326,9 @@ class _ProductEditorDrawerState extends ConsumerState<_ProductEditorDrawer> { )); } final cost = widget.cost ?? p.purchasePrice; - if (cost != null && cost > 0) rows.add(('成本价', b(_fmtMoney(cost)))); + if (cost != null && cost > 0) rows.add(('成本价(单瓶)', b(_fmtMoney(cost)))); if (widget.qty != null && cost != null && cost > 0) { - rows.add(('库存货值', b(_fmtMoney(widget.qty! * cost)))); + rows.add(('总成本价', b(_fmtMoney(widget.qty! * cost)))); } if ((widget.status ?? '').isNotEmpty) { rows.add(( diff --git a/client/test/golden/goldens/inventory_list_a.png b/client/test/golden/goldens/inventory_list_a.png index 6b9800e..8ebc29c 100644 Binary files a/client/test/golden/goldens/inventory_list_a.png and b/client/test/golden/goldens/inventory_list_a.png differ diff --git a/client/test/golden/goldens/inventory_list_b.png b/client/test/golden/goldens/inventory_list_b.png index 01b5d45..2f629f7 100644 Binary files a/client/test/golden/goldens/inventory_list_b.png and b/client/test/golden/goldens/inventory_list_b.png differ diff --git a/client/test/golden/goldens/inventory_list_c.png b/client/test/golden/goldens/inventory_list_c.png index 1a8ff93..bf5e4d0 100644 Binary files a/client/test/golden/goldens/inventory_list_c.png and b/client/test/golden/goldens/inventory_list_c.png differ diff --git a/client/test/golden/goldens/stock_in_list_a.png b/client/test/golden/goldens/stock_in_list_a.png index 8475d66..4d62fcc 100644 Binary files a/client/test/golden/goldens/stock_in_list_a.png and b/client/test/golden/goldens/stock_in_list_a.png differ diff --git a/client/test/golden/goldens/stock_in_list_b.png b/client/test/golden/goldens/stock_in_list_b.png index 0530850..1491bc7 100644 Binary files a/client/test/golden/goldens/stock_in_list_b.png and b/client/test/golden/goldens/stock_in_list_b.png differ diff --git a/client/test/golden/goldens/stock_in_list_c.png b/client/test/golden/goldens/stock_in_list_c.png index 2981ce4..7b5c1f5 100644 Binary files a/client/test/golden/goldens/stock_in_list_c.png and b/client/test/golden/goldens/stock_in_list_c.png differ diff --git a/client/test/golden/goldens/stock_out_list_a.png b/client/test/golden/goldens/stock_out_list_a.png index 858f8b6..deb14e1 100644 Binary files a/client/test/golden/goldens/stock_out_list_a.png and b/client/test/golden/goldens/stock_out_list_a.png differ diff --git a/client/test/golden/goldens/stock_out_list_b.png b/client/test/golden/goldens/stock_out_list_b.png index 8a3eb81..15ede41 100644 Binary files a/client/test/golden/goldens/stock_out_list_b.png and b/client/test/golden/goldens/stock_out_list_b.png differ diff --git a/client/test/golden/goldens/stock_out_list_c.png b/client/test/golden/goldens/stock_out_list_c.png index 73de09e..af37308 100644 Binary files a/client/test/golden/goldens/stock_out_list_c.png and b/client/test/golden/goldens/stock_out_list_c.png differ diff --git a/client/test/golden/proto/inventory_list_a.png b/client/test/golden/proto/inventory_list_a.png index a366e09..e4d703e 100644 Binary files a/client/test/golden/proto/inventory_list_a.png and b/client/test/golden/proto/inventory_list_a.png differ diff --git a/client/test/golden/proto/inventory_list_b.png b/client/test/golden/proto/inventory_list_b.png index 96b5753..c337df2 100644 Binary files a/client/test/golden/proto/inventory_list_b.png and b/client/test/golden/proto/inventory_list_b.png differ diff --git a/client/test/golden/proto/inventory_list_c.png b/client/test/golden/proto/inventory_list_c.png index dff4d32..f59e010 100644 Binary files a/client/test/golden/proto/inventory_list_c.png and b/client/test/golden/proto/inventory_list_c.png differ diff --git a/client/test/golden/proto/stock_in_list_a.png b/client/test/golden/proto/stock_in_list_a.png index 8751917..26c81a9 100644 Binary files a/client/test/golden/proto/stock_in_list_a.png and b/client/test/golden/proto/stock_in_list_a.png differ diff --git a/client/test/golden/proto/stock_in_list_b.png b/client/test/golden/proto/stock_in_list_b.png index cd0658b..ea7bd2b 100644 Binary files a/client/test/golden/proto/stock_in_list_b.png and b/client/test/golden/proto/stock_in_list_b.png differ diff --git a/client/test/golden/proto/stock_in_list_c.png b/client/test/golden/proto/stock_in_list_c.png index 1ad4dec..9192ee1 100644 Binary files a/client/test/golden/proto/stock_in_list_c.png and b/client/test/golden/proto/stock_in_list_c.png differ diff --git a/client/test/golden/proto/stock_out_list_a.png b/client/test/golden/proto/stock_out_list_a.png index bed6fe5..3165066 100644 Binary files a/client/test/golden/proto/stock_out_list_a.png and b/client/test/golden/proto/stock_out_list_a.png differ diff --git a/client/test/golden/proto/stock_out_list_b.png b/client/test/golden/proto/stock_out_list_b.png index dfe80b9..0271834 100644 Binary files a/client/test/golden/proto/stock_out_list_b.png and b/client/test/golden/proto/stock_out_list_b.png differ diff --git a/client/test/golden/proto/stock_out_list_c.png b/client/test/golden/proto/stock_out_list_c.png index 49f79a9..0929d5f 100644 Binary files a/client/test/golden/proto/stock_out_list_c.png and b/client/test/golden/proto/stock_out_list_c.png differ diff --git a/client/test/order_detail_drawer_columns_test.dart b/client/test/order_detail_drawer_columns_test.dart index 3052166..69c2534 100644 --- a/client/test/order_detail_drawer_columns_test.dart +++ b/client/test/order_detail_drawer_columns_test.dart @@ -19,6 +19,7 @@ void main() { spec: '500ml*6/件', qty: '2', price: '¥400.00', + amount: '¥800.00', cost: '¥355.00', profit: '¥90.00', ); @@ -47,6 +48,8 @@ void main() { ))); expect(find.text('成本价'), findsOneWidget); expect(find.text('售价'), findsOneWidget); + expect(find.text('总售价'), findsOneWidget); + expect(find.text('¥800.00'), findsNWidgets(2)); // 行总售价 + 合计金额 expect(find.text('利润'), findsOneWidget); expect(find.text('¥355.00'), findsOneWidget); expect(find.text('¥90.00'), findsNWidgets(2)); // 行利润 + 合计利润