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:
wangjia
2026-04-04 12:51:41 +08:00
parent d6397083c0
commit 76ab19ea47
22 changed files with 6386 additions and 0 deletions
@@ -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': '泸州老窖(国窖157352度500ml',
'unit': '',
'systemQty': 68,
'actualQtyCtrl': TextEditingController(text: '70'),
'remark': TextEditingController(text: '盘盈2瓶'),
},
{
'code': 'SP006',
'name': '汾酒(青花3053度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)),
],
);
}
}