feat(client): 商品详情加价格历史卡(接 /products/:id/price-history)
repo.getPriceHistory → 详情异步加载,新增价格历史卡:date + 进价,按相邻点 算涨跌(绿涨/红跌箭头)。至此商品详情 100% 对齐原型富数据版 (KPI + 各仓库分布 + 价格历史 + 近期流水)。golden 三主题已含该卡。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
This commit is contained in:
@@ -87,6 +87,27 @@ class ProductRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// 进价历史点(date/price),按日期倒序。失败抛 AppException。
|
||||
Future<List<({String date, double price})>> getPriceHistory(int id) async {
|
||||
try {
|
||||
final resp = await _client.get('/products/$id/price-history');
|
||||
final data = (resp.data as Map<String, dynamic>)['data'] as List? ?? [];
|
||||
return data.map((e) {
|
||||
final m = e as Map<String, dynamic>;
|
||||
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<ProductImage> uploadImage(int productId, String? filePath,
|
||||
{Uint8List? bytes, String? fileName}) async {
|
||||
try {
|
||||
|
||||
@@ -38,9 +38,10 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
||||
bool _savingDesc = false;
|
||||
bool _descChanged = false;
|
||||
|
||||
// 富数据(库存聚合 + 流水),随详情异步加载,失败不阻塞主数据。
|
||||
// 富数据(库存聚合 + 流水 + 价格历史),随详情异步加载,失败不阻塞主数据。
|
||||
List<Inventory> _invRows = [];
|
||||
List<InventoryLog> _logs = [];
|
||||
List<({String date, double price})> _prices = [];
|
||||
|
||||
late TextEditingController _descCtrl;
|
||||
|
||||
@@ -82,10 +83,14 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
||||
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<ProductDetailScreen> {
|
||||
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<ProductDetailScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
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: '近期流水',
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 60 KiB |
@@ -25,6 +25,14 @@ class _FakeProductRepo implements ProductRepository {
|
||||
series: '飞天', spec: '500ml×6', brand: '茅台',
|
||||
purchasePrice: 2680, categoryName: '酱香');
|
||||
@override
|
||||
Future<List<({String date, double price})>> 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user