feat(client): 初版 Flutter UI 框架,仿照参考截图实现
- 蓝色顶栏(#1565C0)+ 深蓝侧边栏(#0D47A1),含折叠/展开 - 底部状态栏:门店编号、登录用户、登录时间、实时时钟、版本号 - 登录页:居中卡片,支持密码显示/隐藏 - 入库单列表:筛选/搜索/日期选择/分页,三个 Tab(入库单/查询/审核) - 新建入库单:基本信息 + 商品明细动态增删,实时计算合计金额 - 出库单列表:同入库单风格 - 库存查询:4 张统计卡片 + 颜色高亮缺货/预警行,库存预警 Tab - 往来单位、财务、商品、系统设置(用户/仓库/编号规则/系统参数) - 使用 Riverpod 状态管理 + go_router 路由 + Dio HTTP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
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)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user