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,
|
Future<ProductImage> uploadImage(int productId, String? filePath,
|
||||||
{Uint8List? bytes, String? fileName}) async {
|
{Uint8List? bytes, String? fileName}) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -38,9 +38,10 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
bool _savingDesc = false;
|
bool _savingDesc = false;
|
||||||
bool _descChanged = false;
|
bool _descChanged = false;
|
||||||
|
|
||||||
// 富数据(库存聚合 + 流水),随详情异步加载,失败不阻塞主数据。
|
// 富数据(库存聚合 + 流水 + 价格历史),随详情异步加载,失败不阻塞主数据。
|
||||||
List<Inventory> _invRows = [];
|
List<Inventory> _invRows = [];
|
||||||
List<InventoryLog> _logs = [];
|
List<InventoryLog> _logs = [];
|
||||||
|
List<({String date, double price})> _prices = [];
|
||||||
|
|
||||||
late TextEditingController _descCtrl;
|
late TextEditingController _descCtrl;
|
||||||
|
|
||||||
@@ -82,10 +83,14 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
await repo.listInventory(productId: widget.productId, pageSize: 1000);
|
await repo.listInventory(productId: widget.productId, pageSize: 1000);
|
||||||
final logs =
|
final logs =
|
||||||
await repo.listLogs(productId: widget.productId, pageSize: 100);
|
await repo.listLogs(productId: widget.productId, pageSize: 100);
|
||||||
|
final prices = await ref
|
||||||
|
.read(productRepositoryProvider)
|
||||||
|
.getPriceHistory(widget.productId);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_invRows = inv.data;
|
_invRows = inv.data;
|
||||||
_logs = logs.data;
|
_logs = logs.data;
|
||||||
|
_prices = prices;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -311,6 +316,10 @@ class _ProductDetailScreenState extends ConsumerState<ProductDetailScreen> {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildWarehouseDist(p),
|
_buildWarehouseDist(p),
|
||||||
],
|
],
|
||||||
|
if (_prices.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildPriceHistory(),
|
||||||
|
],
|
||||||
if (_logs.isNotEmpty) ...[
|
if (_logs.isNotEmpty) ...[
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
_buildRecentLogs(),
|
_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() {
|
Widget _buildRecentLogs() {
|
||||||
return _statCard(
|
return _statCard(
|
||||||
title: '近期流水',
|
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: '茅台',
|
series: '飞天', spec: '500ml×6', brand: '茅台',
|
||||||
purchasePrice: 2680, categoryName: '酱香');
|
purchasePrice: 2680, categoryName: '酱香');
|
||||||
@override
|
@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) =>
|
dynamic noSuchMethod(Invocation invocation) =>
|
||||||
super.noSuchMethod(invocation);
|
super.noSuchMethod(invocation);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user