diff --git a/client/lib/repositories/product_repository.dart b/client/lib/repositories/product_repository.dart index 8cde304..6d248f6 100644 --- a/client/lib/repositories/product_repository.dart +++ b/client/lib/repositories/product_repository.dart @@ -87,6 +87,27 @@ class ProductRepository { } } + /// 进价历史点(date/price),按日期倒序。失败抛 AppException。 + Future> getPriceHistory(int id) async { + try { + final resp = await _client.get('/products/$id/price-history'); + final data = (resp.data as Map)['data'] as List? ?? []; + return data.map((e) { + final m = e as Map; + final d = (m['date'] as String? ?? ''); + return ( + date: d.length >= 10 ? d.substring(0, 10) : d, + price: (m['price'] as num).toDouble(), + ); + }).toList(); + } on DioException catch (e) { + throw AppException( + e.response?.data?['error'] as String? ?? '获取价格历史失败', + statusCode: e.response?.statusCode, + ); + } + } + Future uploadImage(int productId, String? filePath, {Uint8List? bytes, String? fileName}) async { try { diff --git a/client/lib/screens/products/product_detail_screen.dart b/client/lib/screens/products/product_detail_screen.dart index b032924..3b2d0e9 100644 --- a/client/lib/screens/products/product_detail_screen.dart +++ b/client/lib/screens/products/product_detail_screen.dart @@ -38,9 +38,10 @@ class _ProductDetailScreenState extends ConsumerState { bool _savingDesc = false; bool _descChanged = false; - // 富数据(库存聚合 + 流水),随详情异步加载,失败不阻塞主数据。 + // 富数据(库存聚合 + 流水 + 价格历史),随详情异步加载,失败不阻塞主数据。 List _invRows = []; List _logs = []; + List<({String date, double price})> _prices = []; late TextEditingController _descCtrl; @@ -82,10 +83,14 @@ class _ProductDetailScreenState extends ConsumerState { await repo.listInventory(productId: widget.productId, pageSize: 1000); final logs = await repo.listLogs(productId: widget.productId, pageSize: 100); + final prices = await ref + .read(productRepositoryProvider) + .getPriceHistory(widget.productId); if (mounted) { setState(() { _invRows = inv.data; _logs = logs.data; + _prices = prices; }); } } catch (_) { @@ -311,6 +316,10 @@ class _ProductDetailScreenState extends ConsumerState { const SizedBox(height: 20), _buildWarehouseDist(p), ], + if (_prices.isNotEmpty) ...[ + const SizedBox(height: 20), + _buildPriceHistory(), + ], if (_logs.isNotEmpty) ...[ const SizedBox(height: 20), _buildRecentLogs(), @@ -452,6 +461,52 @@ class _ProductDetailScreenState extends ConsumerState { ); } + Widget _buildPriceHistory() { + return _statCard( + title: '价格历史', + hint: Text('进价变更', + style: TextStyle(fontSize: AppDims.fsXs, color: context.tokens.muted)), + body: Column( + children: _prices.asMap().entries.map((e) { + final i = e.key; + final p = e.value; + // _prices 倒序(新→旧):i+1 是更早一笔,据此算涨跌。 + final prev = i + 1 < _prices.length ? _prices[i + 1].price : null; + final up = prev != null && p.price > prev; + final down = prev != null && p.price < prev; + final color = up + ? context.tokens.success + : down + ? context.tokens.danger + : context.tokens.heading; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 9), + child: Row( + children: [ + SizedBox( + width: 96, + child: Text(p.date, + style: TextStyle( + fontFamily: 'monospace', + fontSize: AppDims.fsSm, + color: context.tokens.muted)), + ), + if (up || down) + Icon(up ? Icons.arrow_drop_up : Icons.arrow_drop_down, + size: 18, color: color), + Text('¥${p.price.toStringAsFixed(2)}', + style: TextStyle( + fontFamily: 'monospace', + fontWeight: FontWeight.w700, + color: color)), + ], + ), + ); + }).toList(), + ), + ); + } + Widget _buildRecentLogs() { return _statCard( title: '近期流水', diff --git a/client/test/golden/goldens/product_detail_a.png b/client/test/golden/goldens/product_detail_a.png index 8131548..ff51de9 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 0ede3bf..d879481 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 2877454..13c6d7f 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/product_detail_golden_test.dart b/client/test/golden/product_detail_golden_test.dart index 0596ec7..ab257a7 100644 --- a/client/test/golden/product_detail_golden_test.dart +++ b/client/test/golden/product_detail_golden_test.dart @@ -25,6 +25,14 @@ class _FakeProductRepo implements ProductRepository { series: '飞天', spec: '500ml×6', brand: '茅台', purchasePrice: 2680, categoryName: '酱香'); @override + Future> getPriceHistory(int id) async => + const [ + (date: '2026-06-12', price: 2680), + (date: '2026-04-20', price: 2620), + (date: '2026-02-08', price: 2700), + (date: '2025-11-15', price: 2580), + ]; + @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); }