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>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/inventory.dart';
|
||||
import '../../models/warehouse.dart';
|
||||
import '../../providers/inventory_provider.dart';
|
||||
import '../../providers/warehouse_provider.dart';
|
||||
|
||||
class InventoryCheckScreen extends ConsumerStatefulWidget {
|
||||
const InventoryCheckScreen({super.key});
|
||||
@@ -12,100 +16,124 @@ class InventoryCheckScreen extends ConsumerStatefulWidget {
|
||||
_InventoryCheckScreenState();
|
||||
}
|
||||
|
||||
class _InventoryCheckScreenState
|
||||
extends ConsumerState<InventoryCheckScreen> {
|
||||
final _checkNoCtrl =
|
||||
TextEditingController(text: 'PD20260404001');
|
||||
String _warehouse = '主仓库';
|
||||
class _InventoryCheckScreenState extends ConsumerState<InventoryCheckScreen> {
|
||||
Warehouse? _selectedWarehouse;
|
||||
String _checkType = '全盘';
|
||||
bool _submitting = false;
|
||||
bool _loadingItems = false;
|
||||
|
||||
// Mock inventory items for checking
|
||||
final List<Map<String, dynamic>> _checkItems = [
|
||||
{
|
||||
'code': 'SP001',
|
||||
'name': '茅台酒(飞天)53度500ml',
|
||||
'unit': '瓶',
|
||||
'systemQty': 286,
|
||||
'actualQtyCtrl': TextEditingController(text: '286'),
|
||||
'remark': TextEditingController(),
|
||||
},
|
||||
{
|
||||
'code': 'SP002',
|
||||
'name': '五粮液(普五)52度500ml',
|
||||
'unit': '瓶',
|
||||
'systemQty': 152,
|
||||
'actualQtyCtrl': TextEditingController(text: '150'),
|
||||
'remark': TextEditingController(text: '破损2瓶'),
|
||||
},
|
||||
{
|
||||
'code': 'SP003',
|
||||
'name': '洋河梦之蓝M6+ 45度500ml',
|
||||
'unit': '瓶',
|
||||
'systemQty': 88,
|
||||
'actualQtyCtrl': TextEditingController(text: '88'),
|
||||
'remark': TextEditingController(),
|
||||
},
|
||||
{
|
||||
'code': 'SP005',
|
||||
'name': '泸州老窖(国窖1573)52度500ml',
|
||||
'unit': '瓶',
|
||||
'systemQty': 68,
|
||||
'actualQtyCtrl': TextEditingController(text: '70'),
|
||||
'remark': TextEditingController(text: '盘盈2瓶'),
|
||||
},
|
||||
{
|
||||
'code': 'SP006',
|
||||
'name': '汾酒(青花30)53度500ml',
|
||||
'unit': '瓶',
|
||||
'systemQty': 45,
|
||||
'actualQtyCtrl': TextEditingController(text: '45'),
|
||||
'remark': TextEditingController(),
|
||||
},
|
||||
{
|
||||
'code': 'SP010',
|
||||
'name': '青岛啤酒(经典)500ml',
|
||||
'unit': '箱',
|
||||
'systemQty': 35,
|
||||
'actualQtyCtrl': TextEditingController(text: '35'),
|
||||
'remark': TextEditingController(),
|
||||
},
|
||||
];
|
||||
// Editable check items built from real inventory
|
||||
final List<_CheckItem> _checkItems = [];
|
||||
|
||||
String get _checkNo {
|
||||
final now = DateTime.now();
|
||||
final date =
|
||||
'${now.year}${now.month.toString().padLeft(2, '0')}${now.day.toString().padLeft(2, '0')}';
|
||||
return 'PD$date${_selectedWarehouse?.id.toString().padLeft(3, '0') ?? '001'}';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_checkNoCtrl.dispose();
|
||||
for (final item in _checkItems) {
|
||||
(item['actualQtyCtrl'] as TextEditingController).dispose();
|
||||
(item['remark'] as TextEditingController).dispose();
|
||||
item.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
int _getDiff(Map<String, dynamic> item) {
|
||||
final actual = int.tryParse(
|
||||
(item['actualQtyCtrl'] as TextEditingController).text) ??
|
||||
0;
|
||||
return actual - (item['systemQty'] as int);
|
||||
Future<void> _loadInventory(Warehouse wh) async {
|
||||
setState(() {
|
||||
_loadingItems = true;
|
||||
for (final item in _checkItems) {
|
||||
item.dispose();
|
||||
}
|
||||
_checkItems.clear();
|
||||
});
|
||||
try {
|
||||
final result = await ref
|
||||
.read(inventoryRepositoryProvider)
|
||||
.listInventory(warehouseId: wh.id, pageSize: 200);
|
||||
final items = result.data
|
||||
.map((inv) => _CheckItem(inventory: inv))
|
||||
.toList();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_checkItems.addAll(items);
|
||||
_loadingItems = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _loadingItems = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('加载库存失败:$e'),
|
||||
backgroundColor: AppTheme.danger),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
setState(() => _submitting = true);
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('盘点单已提交,待审核'),
|
||||
backgroundColor: AppTheme.success,
|
||||
),
|
||||
);
|
||||
context.go('/inventory');
|
||||
if (_selectedWarehouse == null) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('请先选择仓库')));
|
||||
return;
|
||||
}
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
setState(() => _submitting = true);
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
final dateStr =
|
||||
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
|
||||
final items = _checkItems.map((item) {
|
||||
final actual =
|
||||
double.tryParse(item.actualQtyCtrl.text) ?? item.inventory.quantity;
|
||||
return {
|
||||
'product_id': item.inventory.productId,
|
||||
'actual_qty': actual,
|
||||
'remark': item.remarkCtrl.text.trim(),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
await ref.read(inventoryRepositoryProvider).createCheck({
|
||||
'check_no': _checkNo,
|
||||
'warehouse_id': _selectedWarehouse!.id,
|
||||
'check_date': dateStr,
|
||||
'items': items,
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('盘点单已提交'),
|
||||
backgroundColor: AppTheme.success,
|
||||
),
|
||||
);
|
||||
context.go('/inventory');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('提交失败:$e'),
|
||||
backgroundColor: AppTheme.danger),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _submitting = false);
|
||||
}
|
||||
}
|
||||
|
||||
int _getDiff(_CheckItem item) {
|
||||
final actual =
|
||||
double.tryParse(item.actualQtyCtrl.text) ?? item.inventory.quantity;
|
||||
return (actual - item.inventory.quantity).round();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final asyncWarehouses = ref.watch(warehouseListProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.background,
|
||||
body: Column(
|
||||
@@ -127,12 +155,14 @@ class _InventoryCheckScreenState
|
||||
fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const Spacer(),
|
||||
OutlinedButton(
|
||||
onPressed: () {},
|
||||
child: const Text('保存草稿'),
|
||||
onPressed: () => context.go('/inventory'),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
onPressed: (_submitting || _checkItems.isEmpty)
|
||||
? null
|
||||
: _submit,
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
width: 14,
|
||||
@@ -142,11 +172,6 @@ class _InventoryCheckScreenState
|
||||
: const Icon(Icons.check_circle_outline, size: 16),
|
||||
label: const Text('提交盘点'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton(
|
||||
onPressed: () => context.go('/inventory'),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -175,28 +200,43 @@ class _InventoryCheckScreenState
|
||||
children: [
|
||||
_InfoField(
|
||||
label: '盘点单号',
|
||||
child: TextFormField(
|
||||
controller: _checkNoCtrl,
|
||||
readOnly: true,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace'),
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(),
|
||||
child: Text(
|
||||
_checkNo,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
_InfoField(
|
||||
label: '盘点仓库',
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _warehouse,
|
||||
items: ['主仓库', '副仓库', '全部仓库']
|
||||
.map((s) => DropdownMenuItem(
|
||||
value: s,
|
||||
child: Text(s,
|
||||
style: const TextStyle(
|
||||
fontSize: 13))))
|
||||
.toList(),
|
||||
onChanged: (v) =>
|
||||
setState(() => _warehouse = v!),
|
||||
decoration: const InputDecoration(),
|
||||
child: asyncWarehouses.when(
|
||||
loading: () =>
|
||||
const LinearProgressIndicator(),
|
||||
error: (e, _) => Text('$e',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.danger,
|
||||
fontSize: 12)),
|
||||
data: (warehouses) =>
|
||||
DropdownButtonFormField<Warehouse>(
|
||||
value: _selectedWarehouse,
|
||||
hint: const Text('请选择仓库'),
|
||||
items: warehouses
|
||||
.map((w) => DropdownMenuItem(
|
||||
value: w,
|
||||
child: Text(w.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 13))))
|
||||
.toList(),
|
||||
onChanged: (w) {
|
||||
setState(
|
||||
() => _selectedWarehouse = w);
|
||||
if (w != null) _loadInventory(w);
|
||||
},
|
||||
decoration: const InputDecoration(),
|
||||
),
|
||||
),
|
||||
),
|
||||
_InfoField(
|
||||
@@ -219,10 +259,14 @@ class _InventoryCheckScreenState
|
||||
label: '盘点日期',
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(),
|
||||
child: Text(
|
||||
'2026-04-04',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
child: Builder(builder: (ctx) {
|
||||
final now = DateTime.now();
|
||||
return Text(
|
||||
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}',
|
||||
style:
|
||||
const TextStyle(fontSize: 13),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -247,103 +291,133 @@ class _InventoryCheckScreenState
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.primaryDark)),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.accent.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
if (_checkItems.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
AppTheme.accent.withOpacity(0.1),
|
||||
borderRadius:
|
||||
BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
'差异 ${_checkItems.where((i) => _getDiff(i) != 0).length} 项',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.accent),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'差异 ${_checkItems.where((i) => _getDiff(i) != 0).length} 项',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.accent),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Table
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(36),
|
||||
1: FixedColumnWidth(80),
|
||||
2: FlexColumnWidth(3),
|
||||
3: FixedColumnWidth(50),
|
||||
4: FixedColumnWidth(80),
|
||||
5: FixedColumnWidth(120),
|
||||
6: FixedColumnWidth(80),
|
||||
7: FlexColumnWidth(2),
|
||||
},
|
||||
children: [
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF0F4FF)),
|
||||
children: [
|
||||
'序号',
|
||||
'商品编码',
|
||||
'商品名称',
|
||||
'单位',
|
||||
'账面数量',
|
||||
'实际数量',
|
||||
'差异',
|
||||
'备注',
|
||||
]
|
||||
.map((h) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 10),
|
||||
child: Text(h,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
color: AppTheme
|
||||
.primaryDark)),
|
||||
))
|
||||
.toList(),
|
||||
if (_selectedWarehouse == null)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Text('请先选择盘点仓库',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary)),
|
||||
),
|
||||
...List.generate(
|
||||
_checkItems.length,
|
||||
(i) => _buildCheckRow(i)),
|
||||
],
|
||||
),
|
||||
const Divider(height: 1),
|
||||
// Summary
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
)
|
||||
else if (_loadingItems)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else if (_checkItems.isEmpty)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Text('该仓库暂无库存记录',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary)),
|
||||
),
|
||||
)
|
||||
else
|
||||
Table(
|
||||
columnWidths: const {
|
||||
0: FixedColumnWidth(36),
|
||||
1: FixedColumnWidth(80),
|
||||
2: FlexColumnWidth(3),
|
||||
3: FixedColumnWidth(50),
|
||||
4: FixedColumnWidth(80),
|
||||
5: FixedColumnWidth(120),
|
||||
6: FixedColumnWidth(80),
|
||||
7: FlexColumnWidth(2),
|
||||
},
|
||||
children: [
|
||||
_SummaryItem(
|
||||
label: '盘点商品',
|
||||
value: '${_checkItems.length}种',
|
||||
color: AppTheme.primary,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_SummaryItem(
|
||||
label: '盘盈',
|
||||
value:
|
||||
'${_checkItems.where((i) => _getDiff(i) > 0).length}种',
|
||||
color: AppTheme.success,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_SummaryItem(
|
||||
label: '盘亏',
|
||||
value:
|
||||
'${_checkItems.where((i) => _getDiff(i) < 0).length}种',
|
||||
color: AppTheme.danger,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_SummaryItem(
|
||||
label: '相符',
|
||||
value:
|
||||
'${_checkItems.where((i) => _getDiff(i) == 0).length}种',
|
||||
color: AppTheme.textSecondary,
|
||||
TableRow(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF0F4FF)),
|
||||
children: [
|
||||
'序号',
|
||||
'商品编码',
|
||||
'商品名称',
|
||||
'单位',
|
||||
'账面数量',
|
||||
'实际数量',
|
||||
'差异',
|
||||
'备注',
|
||||
]
|
||||
.map((h) => Padding(
|
||||
padding: const EdgeInsets
|
||||
.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 10),
|
||||
child: Text(h,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight:
|
||||
FontWeight.w600,
|
||||
color: AppTheme
|
||||
.primaryDark)),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
...List.generate(
|
||||
_checkItems.length,
|
||||
(i) => _buildCheckRow(i)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_checkItems.isNotEmpty) ...[
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
_SummaryItem(
|
||||
label: '盘点商品',
|
||||
value: '${_checkItems.length}种',
|
||||
color: AppTheme.primary,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_SummaryItem(
|
||||
label: '盘盈',
|
||||
value:
|
||||
'${_checkItems.where((i) => _getDiff(i) > 0).length}种',
|
||||
color: AppTheme.success,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_SummaryItem(
|
||||
label: '盘亏',
|
||||
value:
|
||||
'${_checkItems.where((i) => _getDiff(i) < 0).length}种',
|
||||
color: AppTheme.danger,
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_SummaryItem(
|
||||
label: '相符',
|
||||
value:
|
||||
'${_checkItems.where((i) => _getDiff(i) == 0).length}种',
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -360,6 +434,7 @@ class _InventoryCheckScreenState
|
||||
TableRow _buildCheckRow(int index) {
|
||||
final item = _checkItems[index];
|
||||
final diff = _getDiff(item);
|
||||
final inv = item.inventory;
|
||||
|
||||
return TableRow(
|
||||
decoration: BoxDecoration(
|
||||
@@ -371,39 +446,44 @@ class _InventoryCheckScreenState
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text('${index + 1}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: AppTheme.textSecondary)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text(item['code'] as String,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text(inv.productCode ?? '-',
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text(item['name'] as String,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text(inv.productName ?? '-',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text(item['unit'] as String,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text(inv.productUnit ?? '-',
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text('${item['systemQty']}',
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text(inv.quantity.toStringAsFixed(0),
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
controller: item['actualQtyCtrl'] as TextEditingController,
|
||||
controller: item.actualQtyCtrl,
|
||||
decoration: const InputDecoration(),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
keyboardType: TextInputType.number,
|
||||
@@ -412,7 +492,8 @@ class _InventoryCheckScreenState
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||
child: Text(
|
||||
diff == 0 ? '0' : (diff > 0 ? '+$diff' : '$diff'),
|
||||
style: TextStyle(
|
||||
@@ -427,7 +508,7 @@ class _InventoryCheckScreenState
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: TextFormField(
|
||||
controller: item['remark'] as TextEditingController,
|
||||
controller: item.remarkCtrl,
|
||||
decoration: const InputDecoration(hintText: '备注'),
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
@@ -437,6 +518,22 @@ class _InventoryCheckScreenState
|
||||
}
|
||||
}
|
||||
|
||||
class _CheckItem {
|
||||
final Inventory inventory;
|
||||
final TextEditingController actualQtyCtrl;
|
||||
final TextEditingController remarkCtrl;
|
||||
|
||||
_CheckItem({required this.inventory})
|
||||
: actualQtyCtrl = TextEditingController(
|
||||
text: inventory.quantity.toStringAsFixed(0)),
|
||||
remarkCtrl = TextEditingController();
|
||||
|
||||
void dispose() {
|
||||
actualQtyCtrl.dispose();
|
||||
remarkCtrl.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoField extends StatelessWidget {
|
||||
final String label;
|
||||
final Widget child;
|
||||
@@ -467,9 +564,7 @@ class _SummaryItem extends StatelessWidget {
|
||||
final Color color;
|
||||
|
||||
const _SummaryItem(
|
||||
{required this.label,
|
||||
required this.value,
|
||||
required this.color});
|
||||
{required this.label, required this.value, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
Reference in New Issue
Block a user