diff --git a/backend/internal/handler/inventory.go b/backend/internal/handler/inventory.go index daabd6f..a56bbf9 100644 --- a/backend/internal/handler/inventory.go +++ b/backend/internal/handler/inventory.go @@ -160,6 +160,37 @@ func (h *InventoryHandler) List(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": rows, "total": total, "page": page, "page_size": pageSize}) } +// inventorySummary 全店库存 KPI 汇总(不随分页,供库存屏 KPI 卡) +type inventorySummary struct { + SkuCount int64 `json:"sku_count"` // SKU 总数(库存行数) + StockValue float64 `json:"stock_value"` // 库存货值 Σ(qty×进价) + InStockQty float64 `json:"in_stock_qty"` // 在库总数量 + ShortageCount int64 `json:"shortage_count"` // 缺货数(qty<=0) + WarningCount int64 `json:"warning_count"` // 预警数(0 0 AND p.min_stock IS NOT NULL AND inv.quantity < p.min_stock THEN 1 ELSE 0 END), 0) AS warning_count + FROM inventories inv + LEFT JOIN stock_in_items sii ON sii.id = inv.stock_in_item_id + LEFT JOIN products p ON p.id = inv.product_id AND p.deleted_at IS NULL + WHERE inv.shop_id = ? AND inv.deleted_at IS NULL` + var s inventorySummary + if err := h.db.Raw(sql, shopID).Scan(&s).Error; err != nil { + util.RespondError(c, http.StatusInternalServerError, "QUERY_ERROR", "查询失败") + return + } + c.JSON(http.StatusOK, s) +} + // Logs GET /api/v1/inventory/logs func (h *InventoryHandler) Logs(c *gin.Context) { shopID := middleware.GetShopID(c) diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 53ff755..f200c1c 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -183,6 +183,7 @@ func Setup(r *gin.Engine, db *gorm.DB) { inventory := api.Group("/inventory") { inventory.GET("", inventoryH.List) + inventory.GET("/summary", inventoryH.Summary) inventory.GET("/logs", inventoryH.Logs) inventory.PUT("/:id/remark", inventoryH.UpdateRemark) inventory.POST("/checks", inventoryH.CreateCheck) diff --git a/client/lib/models/inventory.dart b/client/lib/models/inventory.dart index b1ce039..26e2e53 100644 --- a/client/lib/models/inventory.dart +++ b/client/lib/models/inventory.dart @@ -72,6 +72,29 @@ class Inventory { ); } +/// 全店库存 KPI 汇总(GET /inventory/summary,不随分页)。 +class InventorySummary { + final int skuCount; + final double stockValue; + final double inStockQty; + final int shortageCount; + final int warningCount; + const InventorySummary({ + this.skuCount = 0, + this.stockValue = 0, + this.inStockQty = 0, + this.shortageCount = 0, + this.warningCount = 0, + }); + factory InventorySummary.fromJson(Map j) => InventorySummary( + skuCount: (j['sku_count'] as num?)?.toInt() ?? 0, + stockValue: (j['stock_value'] as num?)?.toDouble() ?? 0, + inStockQty: (j['in_stock_qty'] as num?)?.toDouble() ?? 0, + shortageCount: (j['shortage_count'] as num?)?.toInt() ?? 0, + warningCount: (j['warning_count'] as num?)?.toInt() ?? 0, + ); +} + class InventoryLog { final int warehouseId; final String? warehouseName; diff --git a/client/lib/providers/inventory_provider.dart b/client/lib/providers/inventory_provider.dart index 62225b6..ebeabe8 100644 --- a/client/lib/providers/inventory_provider.dart +++ b/client/lib/providers/inventory_provider.dart @@ -16,6 +16,14 @@ final inventoryListProvider = InventoryListNotifier.new, ); +/// 全店库存 KPI 汇总(不随分页;换店/网络恢复自动刷新)。 +final inventorySummaryProvider = + FutureProvider.autoDispose((ref) { + ref.watch(authStateProvider.select((s) => s.user?.shopId)); + ref.watch(networkRecoveryCountProvider); + return ref.read(inventoryRepositoryProvider).getSummary(); +}); + class InventoryListNotifier extends AsyncNotifier> { int _page = 1; int _pageSize = AppConstants.defaultPageSize; diff --git a/client/lib/repositories/inventory_repository.dart b/client/lib/repositories/inventory_repository.dart index c2de4e0..858697e 100644 --- a/client/lib/repositories/inventory_repository.dart +++ b/client/lib/repositories/inventory_repository.dart @@ -41,6 +41,18 @@ class InventoryRepository { } } + Future getSummary() async { + try { + final resp = await _client.get('/inventory/summary'); + return InventorySummary.fromJson(resp.data as Map); + } on DioException catch (e) { + throw AppException( + e.response?.data?['error'] as String? ?? '获取库存汇总失败', + statusCode: e.response?.statusCode, + ); + } + } + Future> createCheck(Map data) async { try { final resp = await _client.post('/inventory/checks', data: data); diff --git a/client/lib/screens/inventory/inventory_list_screen.dart b/client/lib/screens/inventory/inventory_list_screen.dart index 952e6af..a9bced4 100644 --- a/client/lib/screens/inventory/inventory_list_screen.dart +++ b/client/lib/screens/inventory/inventory_list_screen.dart @@ -42,18 +42,23 @@ class _InventoryListScreenState extends ConsumerState { Set _filterWarehouse = {}; Set _filterSpec = {}; Set _filterSeries = {}; + // 库存状态前端筛选(派生自 qty vs 安全库存):全部/在售/预警/缺货 + String _statusFilter = '全部'; Set? _hiddenCols; // null = 尚未载入本地存档 bool _mobileTableView = false; // 移动端:false=卡片列表,true=表格(横向滚动) static const _screenId = 'inventory_list'; - // 列序照原型重排;商品 = name+code 合并两行。 - // 库存量/成本价列按项目铁律不展示(装箱数脏数据 + 进价敏感);仓库/备注作可隐藏列。 + // 列序照原型 inventory.html:商品/规格/系列/批次/库存/成本价/单价/生产/入库/供应商/状态/操作。 + // 成本价=入库进价(unitPrice),单价=售价(salePrice);库存列照原型显示(脏数据另清)。 + // 仓库/备注原型没有 → 默认隐藏的可选列(列设置可开)。 static const _colDefs = [ ColDef('product', '商品', required: true), ColDef('spec', '规格'), ColDef('series', '系列'), ColDef('batch', '批次号'), + ColDef('qty', '库存'), + ColDef('cost', '成本价'), ColDef('price', '单价'), ColDef('prodDate', '生产日期'), ColDef('inTime', '入库时间'), @@ -64,10 +69,15 @@ class _InventoryListScreenState extends ConsumerState { ColDef('actions', '操作', required: true), ]; + // 仓库/备注默认隐藏(原型无此两列);用户在列设置里可开。 + static const _defaultHidden = {'warehouse', 'remark'}; + @override void initState() { super.initState(); + // 默认隐藏仓库/备注(原型无此两列);本地有存档则覆盖。 + _hiddenCols = Set.of(_defaultHidden); ColumnPrefs.load(_screenId).then((saved) { if (saved != null && mounted) setState(() => _hiddenCols = saved); }); @@ -285,6 +295,40 @@ class _InventoryListScreenState extends ConsumerState { } /// 商品列:name(可点进详情)+ code 两行(还原原型 .pname/.pcode)。 + void _exportInventory(List items) { + exportExcel( + filename: '库存查询', + headers: const [ + '商品编码', '商品名称', '规格', '系列', '批次号', '库存量', + '成本价', '单价', '生产日期', '入库时间', '供应商', '安全库存', '状态' + ], + rows: items.map((item) { + final status = item.quantity == 0 + ? '缺货' + : (item.minStock != null && item.quantity < item.minStock!) + ? '库存不足' + : '正常'; + return [ + item.productCode, + item.productName, + item.spec, + item.series, + item.batchNo, + '${item.quantity.toStringAsFixed(0)} ${item.unit}'.trim(), + item.unitPrice != null ? item.unitPrice!.toStringAsFixed(2) : '', + item.salePrice != null ? item.salePrice!.toStringAsFixed(2) : '', + item.productionDate ?? '', + item.createdAt != null && item.createdAt!.length >= 10 + ? item.createdAt!.substring(0, 10) + : '', + item.supplierName, + item.minStock ?? '', + status, + ]; + }).toList(), + ); + } + Widget _productCell(Inventory item, BuildContext context) { final linked = item.productId != null; final body = Column( @@ -327,11 +371,23 @@ class _InventoryListScreenState extends ConsumerState { 'batch' => DataCell(Text(item.batchNo.isEmpty ? '-' : item.batchNo, style: const TextStyle(fontFamily: 'monospace', fontSize: 12))), 'warehouse' => DataCell(Text(item.warehouseName.isEmpty ? '-' : item.warehouseName)), + 'qty' => DataCell(Text( + item.quantity.toStringAsFixed(0), + style: TextStyle( + fontWeight: FontWeight.w600, + fontFamily: 'monospace', + // 低库存染色(qty ≤ 安全库存):原型 .qty.low → accent + color: (item.minStock != null && item.quantity <= item.minStock!) + ? context.tokens.accent + : context.tokens.text))), + 'cost' => DataCell(Text( + item.unitPrice != null ? '¥${item.unitPrice!.toStringAsFixed(2)}' : '-', + style: const TextStyle(fontFamily: 'monospace'))), 'price' => DataCell(Text( - item.unitPrice != null ? '¥${item.unitPrice!.toStringAsFixed(2)}' : '待定价', - style: item.unitPrice == null + item.salePrice != null ? '¥${item.salePrice!.toStringAsFixed(2)}' : '待定价', + style: item.salePrice == null ? TextStyle(color: context.tokens.warn) - : null)), + : const TextStyle(fontFamily: 'monospace'))), 'prodDate' => DataCell(Text(item.productionDate ?? '-')), 'inTime' => DataCell(Text( item.createdAt != null && item.createdAt!.length >= 10 @@ -353,10 +409,12 @@ class _InventoryListScreenState extends ConsumerState { ))), 'status' => DataCell(_InventoryStatusBadge(item)), 'actions' => DataCell(item.productId != null - ? TextButton( - onPressed: () => _printLabel(context, item), - child: Text('打标签', - style: TextStyle(fontSize: 12, color: context.tokens.primary)), + ? IconButton( + icon: const Icon(Icons.visibility_outlined, size: 18), + tooltip: '查看详情', + onPressed: () => context.push('/products/${item.productId}'), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), ) : const SizedBox()), _ => const DataCell(SizedBox()), @@ -458,24 +516,12 @@ class _InventoryListScreenState extends ConsumerState { @override Widget build(BuildContext context) { - return PageScaffold( - title: '库存管理', - initialTab: ref.read(inventoryTabProvider), - onTabChanged: (i) => ref.read(inventoryTabProvider.notifier).state = i, - tabs: const [ - Tab(text: '库存查询'), - Tab(text: '库存预警'), - Tab(text: '流水记录'), - ], - tabViews: [ - _buildInventoryTab(), - _buildWarningTab(), - _buildLogTab(), - ], - ); + // 照原型 inventory.html:单视图(无 tab)。库存预警并入 KPI 缺货卡点击筛选, + // 流水记录移到商品详情页。 + return _buildInventoryView(); } - Widget _buildInventoryTab() { + Widget _buildInventoryView() { final asyncInventory = ref.watch(inventoryListProvider); return asyncInventory.when( @@ -500,9 +546,6 @@ class _InventoryListScreenState extends ConsumerState { ), data: (result) { final items = result.data; - final warningCount = - items.where((i) => i.minStock != null && i.quantity < i.minStock!).length; - final emptyCount = items.where((i) => i.quantity == 0).length; // 仓库选项从当前页派生(客户端筛选),系列/规格从 product-options API 加载(服务端筛选) final warehouseOptions = items @@ -519,9 +562,17 @@ class _InventoryListScreenState extends ConsumerState { ?.map((s) => s.name) .toList() ?? []; - final filteredItems = _filterWarehouse.isEmpty - ? items - : items.where((i) => _filterWarehouse.contains(i.warehouseName)).toList(); + String statusOf(Inventory i) => i.quantity == 0 + ? '缺货' + : (i.minStock != null && i.quantity < i.minStock!) + ? '预警' + : '在售'; + final filteredItems = items + .where((i) => + (_filterWarehouse.isEmpty || + _filterWarehouse.contains(i.warehouseName)) && + (_statusFilter == '全部' || statusOf(i) == _statusFilter)) + .toList(); final hidden = _hiddenCols ?? {}; final visibleCols = @@ -529,34 +580,70 @@ class _InventoryListScreenState extends ConsumerState { return Column( children: [ - // 副标(还原原型 .head .sub):商品总数 + 实时提示 + // 头部(原型 .head):标题 + SKU 数 + 列设置/导出 Container( width: double.infinity, color: context.tokens.bg, padding: const EdgeInsets.fromLTRB( - AppDims.sp3, AppDims.sp3, AppDims.sp3, 0), - child: Text( - '共 ${NumberFormat.decimalPattern().format(result.total)} 个商品 · 数据实时', - style: - TextStyle(fontSize: AppDims.fsSm, color: context.tokens.muted), + AppDims.sp4, AppDims.sp4, AppDims.sp4, AppDims.sp2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text('库存管理', + style: TextStyle( + fontSize: AppDims.fsH1, + fontWeight: FontWeight.w700, + color: context.tokens.heading)), + const SizedBox(width: AppDims.sp3), + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Text( + '共 ${NumberFormat.decimalPattern().format(result.total)} 个 SKU · 数据实时', + style: TextStyle( + fontSize: AppDims.fsSm, + color: context.tokens.muted)), + ), + const Spacer(), + if (!context.isMobile) ...[ + ColumnToggleButton( + columns: _colDefs, + hidden: hidden, + onChanged: (v) { + setState(() => _hiddenCols = v); + ColumnPrefs.save(_screenId, v); + }, + ), + const SizedBox(width: AppDims.sp2), + OutlinedButton.icon( + onPressed: () => _exportInventory(filteredItems), + icon: const Icon(Icons.download, size: 16), + label: const Text('导出'), + ), + ], + ], ), ), // KPI 卡(还原原型 .kpi):宽屏 4 等分,窄屏横向滚动 Builder(builder: (context) { - final inStockQty = - items.fold(0, (s, i) => s + i.quantity); + // KPI 走全店汇总接口(不随分页)。 + final summary = ref.watch(inventorySummaryProvider).valueOrNull; + String yuanWan(double v) => v >= 10000 + ? '¥${(v / 10000).toStringAsFixed(v >= 1000000 ? 0 : 1)}万' + : '¥${v.toStringAsFixed(0)}'; final cards = [ KpiCard( - title: '商品总数', - value: '${result.total}', + title: 'SKU 总数', + value: NumberFormat.decimalPattern() + .format(summary?.skuCount ?? result.total), icon: Icons.inventory_2, tone: KpiTone.info, - delta: '种 · 点击清筛选', + delta: '点击清筛选', onTap: () { setState(() { _filterWarehouse = {}; _filterSpec = {}; _filterSeries = {}; + _statusFilter = '全部'; _searchCtrl.clear(); }); final n = ref.read(inventoryListProvider.notifier); @@ -565,25 +652,30 @@ class _InventoryListScreenState extends ConsumerState { n.setSeries([]); }, ), + KpiCard( + title: '库存货值', + value: summary != null ? yuanWan(summary.stockValue) : '—', + icon: Icons.savings_outlined, + tone: KpiTone.ok, + delta: '按进价', + ), KpiCard( title: '在库数量', - value: inStockQty.toStringAsFixed(0), + value: NumberFormat.decimalPattern() + .format((summary?.inStockQty ?? 0).round()), icon: Icons.warehouse_outlined, tone: KpiTone.blue, delta: '件', ), KpiCard( - title: '库存预警', - value: '$warningCount', - icon: Icons.warning_amber, - tone: KpiTone.warn, - delta: warningCount > 0 ? '需补货' : '正常', - ), - KpiCard( - title: '缺货商品', - value: '$emptyCount', - icon: Icons.remove_shopping_cart, + title: '缺货预警', + value: '${summary?.shortageCount ?? 0}', + icon: Icons.error_outline, tone: KpiTone.alert, + delta: (summary?.warningCount ?? 0) > 0 + ? '需补货 ${summary!.warningCount} 项' + : '点击筛选', + onTap: () => setState(() => _statusFilter = '缺货'), ), ]; final mobile = context.isMobile; @@ -622,32 +714,7 @@ class _InventoryListScreenState extends ConsumerState { ref.read(inventoryListProvider.notifier).setPageSize(s), toolbar: Builder(builder: (ctx) { final isMobile = ctx.isMobile; - void doExport() => exportExcel( - filename: '库存查询', - headers: ['商品编码', '商品名称', '规格', '系列', '批次号', '仓库', '库存量', '单价', '生产日期', '入库时间', '供应商', '安全库存', '状态'], - rows: filteredItems.map((item) { - final status = item.quantity == 0 - ? '缺货' - : (item.minStock != null && item.quantity < item.minStock!) - ? '库存不足' - : '正常'; - return [ - item.productCode.isEmpty ? '' : item.productCode, - item.productName.isEmpty ? '' : item.productName, - item.spec.isEmpty ? '' : item.spec, - item.series.isEmpty ? '' : item.series, - item.batchNo.isEmpty ? '' : item.batchNo, - item.warehouseName.isEmpty ? '' : item.warehouseName, - '${item.quantity.toStringAsFixed(0)} ${item.unit}'.trim(), - item.unitPrice != null ? item.unitPrice!.toStringAsFixed(2) : '', - item.productionDate ?? '', - item.createdAt != null && item.createdAt!.length >= 10 ? item.createdAt!.substring(0, 10) : '', - item.supplierName.isEmpty ? '' : item.supplierName, - item.minStock ?? '', - status, - ]; - }).toList(), - ); + void doExport() => _exportInventory(filteredItems); final searchField = TextField( controller: _searchCtrl, @@ -729,43 +796,63 @@ class _InventoryListScreenState extends ConsumerState { ); } - // 桌面:搜索置于最前(原型 searchbox 240),按钮成组靠左 + // 桌面(原型 .toolbar):搜索 + 状态筛选 + 重置(导出/列设置在头部) return Row( children: [ SizedBox(width: 240, child: searchField), const SizedBox(width: AppDims.sp3), - if (canCheck) ...[ - WriteGuard( - child: OutlinedButton.icon( - onPressed: () => context.go('/inventory/check'), - icon: const Icon(Icons.fact_check, size: 16), - label: const Text('发起盘点'), + PopupMenuButton( + initialValue: _statusFilter, + onSelected: (v) => setState(() => _statusFilter = v), + itemBuilder: (_) => const ['全部', '在售', '预警', '缺货'] + .map((s) => + PopupMenuItem(value: s, child: Text(s))) + .toList(), + child: Container( + height: 38, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: ctx.tokens.border), + borderRadius: BorderRadius.circular(AppDims.rMd), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('状态', + style: TextStyle( + fontSize: 13, color: ctx.tokens.text)), + if (_statusFilter != '全部') ...[ + const SizedBox(width: 6), + Text(_statusFilter, + style: TextStyle( + fontSize: 13, + color: ctx.tokens.primary, + fontWeight: FontWeight.w600)), + ], + const SizedBox(width: 4), + Icon(Icons.keyboard_arrow_down, + size: 16, color: ctx.tokens.muted), + ], ), ), - const SizedBox(width: AppDims.sp2), - ], - OutlinedButton.icon( - onPressed: doExport, - icon: const Icon(Icons.download, size: 16), - label: const Text('导出'), - ), - const SizedBox(width: AppDims.sp2), - ColumnToggleButton( - columns: _colDefs, - hidden: hidden, - onChanged: (v) { - setState(() => _hiddenCols = v); - ColumnPrefs.save(_screenId, v); - }, - ), - const SizedBox(width: AppDims.sp2), - OutlinedButton.icon( - onPressed: () => - ref.read(inventoryListProvider.notifier).reload(), - icon: const Icon(Icons.refresh, size: 16), - label: const Text('刷新'), ), const Spacer(), + TextButton( + onPressed: () { + setState(() { + _filterWarehouse = {}; + _filterSpec = {}; + _filterSeries = {}; + _statusFilter = '全部'; + _searchCtrl.clear(); + }); + final n = ref.read(inventoryListProvider.notifier); + n.setKeyword(''); + n.setSpec([]); + n.setSeries([]); + }, + child: const Text('重置'), + ), ], ); }), @@ -802,7 +889,7 @@ class _InventoryListScreenState extends ConsumerState { }; return DataColumn( label: label, - numeric: c.key == 'price', + numeric: const {'qty', 'cost', 'price'}.contains(c.key), ); }).toList(), rows: items.isEmpty @@ -1208,12 +1295,12 @@ class _ImportProgressDialog extends StatelessWidget { const SizedBox(height: 12), LinearProgressIndicator( value: state.uploadPercent / 100, - backgroundColor: Colors.grey[200], + backgroundColor: context.tokens.borderSubtle, valueColor: AlwaysStoppedAnimation(context.tokens.primary), ), const SizedBox(height: 6), Text('${state.uploadPercent}%', - style: TextStyle(fontSize: 12, color: Colors.grey[600])), + style: TextStyle(fontSize: 12, color: context.tokens.muted)), ], ); @@ -1238,7 +1325,7 @@ class _ImportProgressDialog extends StatelessWidget { ]), const SizedBox(height: 8), LinearProgressIndicator( - backgroundColor: Colors.grey[200], + backgroundColor: context.tokens.borderSubtle, valueColor: AlwaysStoppedAnimation(context.tokens.primary), ), ], @@ -1266,7 +1353,7 @@ class _ImportProgressDialog extends StatelessWidget { Text('共 ${state.total} 行', style: const TextStyle(fontSize: 13)), Text('成功插入:${state.imported} 行', style: const TextStyle(fontSize: 13)), Text('重复更新:${state.updated} 行', - style: TextStyle(fontSize: 13, color: Colors.grey[600])), + style: TextStyle(fontSize: 13, color: context.tokens.muted)), if (state.errors.isNotEmpty) Text('失败:${state.errors.length} 行', style: TextStyle(fontSize: 13, color: context.tokens.danger)), diff --git a/client/test/golden/goldens/device_management_a.png b/client/test/golden/goldens/device_management_a.png index f74057e..dda875f 100644 Binary files a/client/test/golden/goldens/device_management_a.png and b/client/test/golden/goldens/device_management_a.png differ diff --git a/client/test/golden/goldens/device_management_b.png b/client/test/golden/goldens/device_management_b.png index c58c67b..45571f0 100644 Binary files a/client/test/golden/goldens/device_management_b.png and b/client/test/golden/goldens/device_management_b.png differ diff --git a/client/test/golden/goldens/device_management_c.png b/client/test/golden/goldens/device_management_c.png index 0a94ab1..6d51c59 100644 Binary files a/client/test/golden/goldens/device_management_c.png and b/client/test/golden/goldens/device_management_c.png differ diff --git a/client/test/golden/goldens/finance_a.png b/client/test/golden/goldens/finance_a.png index 2cd2cf8..5d66b28 100644 Binary files a/client/test/golden/goldens/finance_a.png and b/client/test/golden/goldens/finance_a.png differ diff --git a/client/test/golden/goldens/finance_b.png b/client/test/golden/goldens/finance_b.png index dfb5643..d970be3 100644 Binary files a/client/test/golden/goldens/finance_b.png and b/client/test/golden/goldens/finance_b.png differ diff --git a/client/test/golden/goldens/finance_c.png b/client/test/golden/goldens/finance_c.png index df45007..5512354 100644 Binary files a/client/test/golden/goldens/finance_c.png and b/client/test/golden/goldens/finance_c.png differ diff --git a/client/test/golden/goldens/inventory_list_a.png b/client/test/golden/goldens/inventory_list_a.png index ba4e53f..a871823 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 74c1f6d..c8928f3 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 3c3491c..96c8bc6 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/inventory_list_mobile_a.png b/client/test/golden/goldens/inventory_list_mobile_a.png index bf6f3a1..dbfa797 100644 Binary files a/client/test/golden/goldens/inventory_list_mobile_a.png and b/client/test/golden/goldens/inventory_list_mobile_a.png differ diff --git a/client/test/golden/goldens/inventory_list_mobile_b.png b/client/test/golden/goldens/inventory_list_mobile_b.png index 39490b9..6979b41 100644 Binary files a/client/test/golden/goldens/inventory_list_mobile_b.png and b/client/test/golden/goldens/inventory_list_mobile_b.png differ diff --git a/client/test/golden/goldens/inventory_list_mobile_c.png b/client/test/golden/goldens/inventory_list_mobile_c.png index 15e8219..7d5b2d0 100644 Binary files a/client/test/golden/goldens/inventory_list_mobile_c.png and b/client/test/golden/goldens/inventory_list_mobile_c.png differ diff --git a/client/test/golden/goldens/partners_a.png b/client/test/golden/goldens/partners_a.png index 8a326ae..acffe7a 100644 Binary files a/client/test/golden/goldens/partners_a.png and b/client/test/golden/goldens/partners_a.png differ diff --git a/client/test/golden/goldens/partners_b.png b/client/test/golden/goldens/partners_b.png index ba8e28b..ac8a526 100644 Binary files a/client/test/golden/goldens/partners_b.png and b/client/test/golden/goldens/partners_b.png differ diff --git a/client/test/golden/goldens/partners_c.png b/client/test/golden/goldens/partners_c.png index 97b9f23..afadbc5 100644 Binary files a/client/test/golden/goldens/partners_c.png and b/client/test/golden/goldens/partners_c.png differ diff --git a/client/test/golden/goldens/product_detail_a.png b/client/test/golden/goldens/product_detail_a.png index 016908c..fe11594 100644 Binary files a/client/test/golden/goldens/product_detail_a.png and b/client/test/golden/goldens/product_detail_a.png differ diff --git a/client/test/golden/goldens/product_detail_b.png b/client/test/golden/goldens/product_detail_b.png index ec5615a..81f3842 100644 Binary files a/client/test/golden/goldens/product_detail_b.png and b/client/test/golden/goldens/product_detail_b.png differ diff --git a/client/test/golden/goldens/product_detail_c.png b/client/test/golden/goldens/product_detail_c.png index 64ccee3..a4b33b8 100644 Binary files a/client/test/golden/goldens/product_detail_c.png and b/client/test/golden/goldens/product_detail_c.png differ diff --git a/client/test/golden/goldens/products_archive_a.png b/client/test/golden/goldens/products_archive_a.png index a583698..7264a0a 100644 Binary files a/client/test/golden/goldens/products_archive_a.png and b/client/test/golden/goldens/products_archive_a.png differ diff --git a/client/test/golden/goldens/products_archive_b.png b/client/test/golden/goldens/products_archive_b.png index f6700f1..5aa4046 100644 Binary files a/client/test/golden/goldens/products_archive_b.png and b/client/test/golden/goldens/products_archive_b.png differ diff --git a/client/test/golden/goldens/products_archive_c.png b/client/test/golden/goldens/products_archive_c.png index 24e2df0..665b712 100644 Binary files a/client/test/golden/goldens/products_archive_c.png and b/client/test/golden/goldens/products_archive_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 c2ecd3d..acde251 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 7c583e5..62c09f2 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 16fe1b4..1e3bada 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_in_list_mobile_a.png b/client/test/golden/goldens/stock_in_list_mobile_a.png index 7e352ae..71f9ca4 100644 Binary files a/client/test/golden/goldens/stock_in_list_mobile_a.png and b/client/test/golden/goldens/stock_in_list_mobile_a.png differ diff --git a/client/test/golden/goldens/stock_in_list_mobile_b.png b/client/test/golden/goldens/stock_in_list_mobile_b.png index ba6d786..751fa0c 100644 Binary files a/client/test/golden/goldens/stock_in_list_mobile_b.png and b/client/test/golden/goldens/stock_in_list_mobile_b.png differ diff --git a/client/test/golden/goldens/stock_in_list_mobile_c.png b/client/test/golden/goldens/stock_in_list_mobile_c.png index 60999fb..42879f7 100644 Binary files a/client/test/golden/goldens/stock_in_list_mobile_c.png and b/client/test/golden/goldens/stock_in_list_mobile_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 88f64f2..52cbd7a 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 0fdab20..ccd095d 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 ade7de2..3041662 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/goldens/stock_out_list_mobile_a.png b/client/test/golden/goldens/stock_out_list_mobile_a.png index d1098ce..b27e35c 100644 Binary files a/client/test/golden/goldens/stock_out_list_mobile_a.png and b/client/test/golden/goldens/stock_out_list_mobile_a.png differ diff --git a/client/test/golden/goldens/stock_out_list_mobile_b.png b/client/test/golden/goldens/stock_out_list_mobile_b.png index 6886cff..8448506 100644 Binary files a/client/test/golden/goldens/stock_out_list_mobile_b.png and b/client/test/golden/goldens/stock_out_list_mobile_b.png differ diff --git a/client/test/golden/goldens/stock_out_list_mobile_c.png b/client/test/golden/goldens/stock_out_list_mobile_c.png index 015d052..7fb0223 100644 Binary files a/client/test/golden/goldens/stock_out_list_mobile_c.png and b/client/test/golden/goldens/stock_out_list_mobile_c.png differ diff --git a/client/test/golden/inventory_list_golden_test.dart b/client/test/golden/inventory_list_golden_test.dart index 907e8d5..de63302 100644 --- a/client/test/golden/inventory_list_golden_test.dart +++ b/client/test/golden/inventory_list_golden_test.dart @@ -76,6 +76,13 @@ List _overrides() => [ productSeriesListProvider.overrideWith(() => _FakeSeriesNotifier()), productSpecListProvider.overrideWith(() => _FakeSpecNotifier()), isReadonlyProvider.overrideWithValue(false), + inventorySummaryProvider.overrideWith((ref) => const InventorySummary( + skuCount: 1284, + stockValue: 2640000, + inStockQty: 18920, + shortageCount: 17, + warningCount: 6, + )), ]; void main() { diff --git a/client/test/golden/proto/app_shell_a.png b/client/test/golden/proto/app_shell_a.png index 8b2d952..8ff8316 100644 Binary files a/client/test/golden/proto/app_shell_a.png and b/client/test/golden/proto/app_shell_a.png differ diff --git a/client/test/support/golden_harness.dart b/client/test/support/golden_harness.dart index 2a44101..79c4efc 100644 --- a/client/test/support/golden_harness.dart +++ b/client/test/support/golden_harness.dart @@ -12,6 +12,10 @@ Future ensureGoldenFonts() async { final loader = FontLoader('NotoSansSC') ..addFont(rootBundle.load('assets/fonts/NotoSansSC.ttf')); await loader.load(); + // 把内置 'monospace' family 也指向 Noto(否则等宽数字/批次号在 golden 渲染成黑块)。 + final mono = FontLoader('monospace') + ..addFont(rootBundle.load('assets/fonts/NotoSansSC.ttf')); + await mono.load(); // 真实 Material 图标字体(否则 golden 里图标渲染成豆腐块 □)。 for (final key in const [ 'fonts/MaterialIcons-Regular.otf',