76ab19ea47
- 蓝色顶栏(#1565C0)+ 深蓝侧边栏(#0D47A1),含折叠/展开 - 底部状态栏:门店编号、登录用户、登录时间、实时时钟、版本号 - 登录页:居中卡片,支持密码显示/隐藏 - 入库单列表:筛选/搜索/日期选择/分页,三个 Tab(入库单/查询/审核) - 新建入库单:基本信息 + 商品明细动态增删,实时计算合计金额 - 出库单列表:同入库单风格 - 库存查询:4 张统计卡片 + 颜色高亮缺货/预警行,库存预警 Tab - 往来单位、财务、商品、系统设置(用户/仓库/编号规则/系统参数) - 使用 Riverpod 状态管理 + go_router 路由 + Dio HTTP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
491 lines
18 KiB
Dart
491 lines
18 KiB
Dart
import 'package:flutter/material.dart';
|
||
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';
|
||
|
||
class InventoryCheckScreen extends ConsumerStatefulWidget {
|
||
const InventoryCheckScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<InventoryCheckScreen> createState() =>
|
||
_InventoryCheckScreenState();
|
||
}
|
||
|
||
class _InventoryCheckScreenState
|
||
extends ConsumerState<InventoryCheckScreen> {
|
||
final _checkNoCtrl =
|
||
TextEditingController(text: 'PD20260404001');
|
||
String _warehouse = '主仓库';
|
||
String _checkType = '全盘';
|
||
bool _submitting = 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(),
|
||
},
|
||
];
|
||
|
||
@override
|
||
void dispose() {
|
||
_checkNoCtrl.dispose();
|
||
for (final item in _checkItems) {
|
||
(item['actualQtyCtrl'] as TextEditingController).dispose();
|
||
(item['remark'] as TextEditingController).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> _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 (mounted) setState(() => _submitting = false);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: AppTheme.background,
|
||
body: Column(
|
||
children: [
|
||
// Header
|
||
Container(
|
||
height: 52,
|
||
color: AppTheme.surface,
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
child: Row(
|
||
children: [
|
||
IconButton(
|
||
icon: const Icon(Icons.arrow_back, size: 20),
|
||
onPressed: () => context.go('/inventory'),
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text('库存盘点',
|
||
style: TextStyle(
|
||
fontSize: 16, fontWeight: FontWeight.w600)),
|
||
const Spacer(),
|
||
OutlinedButton(
|
||
onPressed: () {},
|
||
child: const Text('保存草稿'),
|
||
),
|
||
const SizedBox(width: 8),
|
||
ElevatedButton.icon(
|
||
onPressed: _submitting ? null : _submit,
|
||
icon: _submitting
|
||
? const SizedBox(
|
||
width: 14,
|
||
height: 14,
|
||
child: CircularProgressIndicator(
|
||
strokeWidth: 2, color: Colors.white))
|
||
: const Icon(Icons.check_circle_outline, size: 16),
|
||
label: const Text('提交盘点'),
|
||
),
|
||
const SizedBox(width: 8),
|
||
OutlinedButton(
|
||
onPressed: () => context.go('/inventory'),
|
||
child: const Text('取消'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const Divider(height: 1),
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
children: [
|
||
// Basic info
|
||
Card(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const Text('盘点基本信息',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: AppTheme.primaryDark)),
|
||
const SizedBox(height: 16),
|
||
Wrap(
|
||
spacing: 16,
|
||
runSpacing: 16,
|
||
children: [
|
||
_InfoField(
|
||
label: '盘点单号',
|
||
child: TextFormField(
|
||
controller: _checkNoCtrl,
|
||
readOnly: true,
|
||
style: const TextStyle(
|
||
fontFamily: 'monospace'),
|
||
decoration: const InputDecoration(),
|
||
),
|
||
),
|
||
_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(),
|
||
),
|
||
),
|
||
_InfoField(
|
||
label: '盘点类型',
|
||
child: DropdownButtonFormField<String>(
|
||
value: _checkType,
|
||
items: ['全盘', '抽盘', '循环盘点']
|
||
.map((s) => DropdownMenuItem(
|
||
value: s,
|
||
child: Text(s,
|
||
style: const TextStyle(
|
||
fontSize: 13))))
|
||
.toList(),
|
||
onChanged: (v) =>
|
||
setState(() => _checkType = v!),
|
||
decoration: const InputDecoration(),
|
||
),
|
||
),
|
||
_InfoField(
|
||
label: '盘点日期',
|
||
child: InputDecorator(
|
||
decoration: const InputDecoration(),
|
||
child: Text(
|
||
'2026-04-04',
|
||
style: const TextStyle(fontSize: 13),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
// Check items table
|
||
Card(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Text('盘点明细',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
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),
|
||
),
|
||
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(),
|
||
),
|
||
...List.generate(
|
||
_checkItems.length,
|
||
(i) => _buildCheckRow(i)),
|
||
],
|
||
),
|
||
const Divider(height: 1),
|
||
// Summary
|
||
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,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
TableRow _buildCheckRow(int index) {
|
||
final item = _checkItems[index];
|
||
final diff = _getDiff(item);
|
||
|
||
return TableRow(
|
||
decoration: BoxDecoration(
|
||
color: diff != 0
|
||
? (diff > 0
|
||
? AppTheme.success.withOpacity(0.04)
|
||
: AppTheme.danger.withOpacity(0.04))
|
||
: (index.isEven ? Colors.white : const Color(0xFFFAFAFA)),
|
||
),
|
||
children: [
|
||
Padding(
|
||
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,
|
||
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,
|
||
style: const TextStyle(fontSize: 13),
|
||
overflow: TextOverflow.ellipsis),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||
child: Text(item['unit'] as String,
|
||
style: const TextStyle(fontSize: 13)),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||
child: Text('${item['systemQty']}',
|
||
style: const TextStyle(fontSize: 13)),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.all(4),
|
||
child: TextFormField(
|
||
controller: item['actualQtyCtrl'] as TextEditingController,
|
||
decoration: const InputDecoration(),
|
||
style: const TextStyle(fontSize: 13),
|
||
keyboardType: TextInputType.number,
|
||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||
onChanged: (_) => setState(() {}),
|
||
),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||
child: Text(
|
||
diff == 0 ? '0' : (diff > 0 ? '+$diff' : '$diff'),
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
color: diff == 0
|
||
? AppTheme.textSecondary
|
||
: (diff > 0 ? AppTheme.success : AppTheme.danger),
|
||
),
|
||
),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.all(4),
|
||
child: TextFormField(
|
||
controller: item['remark'] as TextEditingController,
|
||
decoration: const InputDecoration(hintText: '备注'),
|
||
style: const TextStyle(fontSize: 13),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _InfoField extends StatelessWidget {
|
||
final String label;
|
||
final Widget child;
|
||
|
||
const _InfoField({required this.label, required this.child});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return SizedBox(
|
||
width: 220,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(label,
|
||
style: const TextStyle(
|
||
fontSize: 13, color: AppTheme.textSecondary)),
|
||
const SizedBox(height: 6),
|
||
child,
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SummaryItem extends StatelessWidget {
|
||
final String label;
|
||
final String value;
|
||
final Color color;
|
||
|
||
const _SummaryItem(
|
||
{required this.label,
|
||
required this.value,
|
||
required this.color});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
Text(label,
|
||
style: const TextStyle(
|
||
fontSize: 13, color: AppTheme.textSecondary)),
|
||
const SizedBox(width: 4),
|
||
Text(value,
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: color)),
|
||
],
|
||
);
|
||
}
|
||
}
|