Files
jiu/client/lib/screens/inventory/batch_tracking_screen.dart
T
wangjia ce1cbf404c feat: 完成7项功能任务
后端:
- fix(backend): InventoryLog 增加 Product/Warehouse 关联,Logs 接口加 Preload 修复流水记录显示"-"
- feat(backend): 新增 /inventory/batches 批次追踪接口
- feat(backend): 新增财务记录、编号规则接口(finance、number_rule handler)
- fix(backend): service/stock_test.go 修复 model.Date 类型错误

前端:
- feat(client): 入库/出库管理 Tab 调换顺序(审核在前),审核 Tab 加新建按钮,列表 Tab 无按钮
- feat(client): 入库单/出库单增加详情弹窗,显示完整单据信息和商品明细
- feat(client): 完善出库单新建表单(StockOutFormScreen),支持选客户/仓库/商品
- fix(client): StockInItem/StockOutItem.fromJson 修复读取嵌套 product 对象而非平铺字段
- feat(client): 批次追踪页面(BatchTrackingScreen)及侧边栏入口
- feat(client): 财务管理、盘点、编号规则接入真实后端 API
- fix(client): 入库/出库审核通过后 invalidate inventoryListProvider 刷新库存

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 22:03:19 +08:00

182 lines
6.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/theme/app_theme.dart';
import '../../models/inventory.dart';
import '../../providers/inventory_provider.dart';
import '../../widgets/data_table_card.dart';
class BatchTrackingScreen extends ConsumerStatefulWidget {
const BatchTrackingScreen({super.key});
@override
ConsumerState<BatchTrackingScreen> createState() =>
_BatchTrackingScreenState();
}
class _BatchTrackingScreenState extends ConsumerState<BatchTrackingScreen> {
int _page = 1;
late Future<List<BatchRecord>> _future;
@override
void initState() {
super.initState();
_fetch();
}
void _fetch() {
_future = ref
.read(inventoryRepositoryProvider)
.listBatches(page: _page, pageSize: 20)
.then((r) => r.data);
}
void _refetch() {
setState(() => _fetch());
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<BatchRecord>>(
future: _future,
builder: (context, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snap.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('加载失败:${snap.error}',
style: const TextStyle(color: AppTheme.danger)),
const SizedBox(height: 12),
ElevatedButton(
onPressed: _refetch, child: const Text('重试')),
],
),
);
}
return _buildTable(snap.data ?? []);
},
);
}
Widget _buildTable(List<BatchRecord> records) {
return DataTableCard(
totalCount: records.length,
page: _page,
onPageChanged: (p) {
setState(() {
_page = p;
_fetch();
});
},
toolbar: Row(
children: [
const Text('已审核入库批次',
style: TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
const Spacer(),
IconButton(
icon: const Icon(Icons.refresh, size: 18),
onPressed: () {
setState(() {
_page = 1;
_fetch();
});
},
tooltip: '刷新',
),
],
),
columns: const [
DataColumn(label: Text('商品')),
DataColumn(label: Text('规格')),
DataColumn(label: Text('批次号')),
DataColumn(label: Text('入库单号')),
DataColumn(label: Text('供应商')),
DataColumn(label: Text('仓库')),
DataColumn(label: Text('入库日期')),
DataColumn(label: Text('数量'), numeric: true),
DataColumn(label: Text('单价'), numeric: true),
],
rows: records.isEmpty
? [
const DataRow(cells: [
DataCell(SizedBox()),
DataCell(Text('暂无批次记录',
style: TextStyle(color: AppTheme.textSecondary))),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
])
]
: records.map((r) {
final hasBatch =
r.batchNo != null && r.batchNo!.isNotEmpty;
return DataRow(cells: [
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(r.productName ?? '-',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500)),
if (r.productCode != null)
Text(r.productCode!,
style: const TextStyle(
fontSize: 11,
color: AppTheme.textSecondary,
fontFamily: 'monospace')),
],
),
),
DataCell(Text(r.productSpec ?? '-',
style: const TextStyle(fontSize: 12))),
DataCell(hasBatch
? Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: AppTheme.primary.withOpacity(0.08),
borderRadius: BorderRadius.circular(3),
),
child: Text(r.batchNo!,
style: const TextStyle(
fontSize: 12,
color: AppTheme.primary,
fontFamily: 'monospace')),
)
: const Text('-',
style: TextStyle(
color: AppTheme.textSecondary))),
DataCell(Text(r.orderNo ?? '-',
style: const TextStyle(
fontSize: 12,
color: AppTheme.primary,
fontFamily: 'monospace'))),
DataCell(Text(r.supplierName ?? '-',
style: const TextStyle(fontSize: 12))),
DataCell(Text(r.warehouseName ?? '-',
style: const TextStyle(fontSize: 12))),
DataCell(Text(
r.orderDate?.substring(0, 10) ?? '-',
style: const TextStyle(fontSize: 12))),
DataCell(Text(
'${r.quantity.toStringAsFixed(0)} ${r.productUnit ?? ''}',
style: const TextStyle(fontWeight: FontWeight.w500))),
DataCell(Text(
'¥${r.unitPrice.toStringAsFixed(2)}',
style: const TextStyle(fontSize: 13))),
]);
}).toList(),
);
}
}