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,372 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../widgets/page_scaffold.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/status_badge.dart';
import '../../core/theme/app_theme.dart';
class StockOutListScreen extends ConsumerStatefulWidget {
const StockOutListScreen({super.key});
@override
ConsumerState<StockOutListScreen> createState() =>
_StockOutListScreenState();
}
class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
int _page = 1;
final _searchCtrl = TextEditingController();
String _statusFilter = '全部';
String _typeFilter = '全部';
final List<Map<String, dynamic>> _mockOrders = [
{
'no': 'CK20260401001',
'department': '餐饮部',
'purpose': '宴会用酒',
'warehouse': '主仓库',
'amount': '15600.00',
'status': OrderStatus.approved,
'creator': '李四',
'date': '2026-04-01',
'type': '领用',
},
{
'no': 'CK20260401002',
'department': '客房部',
'purpose': '客房迷你吧补货',
'warehouse': '主仓库',
'amount': '8200.00',
'status': OrderStatus.approved,
'creator': '王五',
'date': '2026-04-01',
'type': '领用',
},
{
'no': 'CK20260402001',
'department': '行政部',
'purpose': '接待用酒',
'warehouse': '副仓库',
'amount': '32000.00',
'status': OrderStatus.pending,
'creator': '张三',
'date': '2026-04-02',
'type': '调拨',
},
{
'no': 'CK20260402002',
'department': '餐饮部',
'purpose': '婚宴用酒',
'warehouse': '主仓库',
'amount': '68500.00',
'status': OrderStatus.approved,
'creator': '李四',
'date': '2026-04-02',
'type': '领用',
},
{
'no': 'CK20260403001',
'department': '采购部',
'purpose': '退货',
'warehouse': '副仓库',
'amount': '12000.00',
'status': OrderStatus.pending,
'creator': '王五',
'date': '2026-04-03',
'type': '退货',
},
{
'no': 'CK20260403002',
'department': '餐饮部',
'purpose': '散客用酒',
'warehouse': '主仓库',
'amount': '5400.00',
'status': OrderStatus.draft,
'creator': '张三',
'date': '2026-04-03',
'type': '领用',
},
{
'no': 'CK20260404001',
'department': '客房部',
'purpose': 'VIP客房补充',
'warehouse': '主仓库',
'amount': '18900.00',
'status': OrderStatus.approved,
'creator': '李四',
'date': '2026-04-04',
'type': '领用',
},
{
'no': 'CK20260404002',
'department': '行政部',
'purpose': '年度总结宴',
'warehouse': '主仓库',
'amount': '45000.00',
'status': OrderStatus.pending,
'creator': '王五',
'date': '2026-04-04',
'type': '领用',
},
];
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
List<Map<String, dynamic>> get _filtered {
return _mockOrders.where((o) {
if (_typeFilter != '全部' && o['type'] != _typeFilter) return false;
if (_statusFilter != '全部') {
final map = {
'草稿': OrderStatus.draft,
'待审核': OrderStatus.pending,
'已审核': OrderStatus.approved,
'已拒绝': OrderStatus.rejected,
};
if (o['status'] != map[_statusFilter]) return false;
}
final q = _searchCtrl.text.toLowerCase();
if (q.isNotEmpty) {
final no = (o['no'] as String).toLowerCase();
final dept = (o['department'] as String).toLowerCase();
if (!no.contains(q) && !dept.contains(q)) return false;
}
return true;
}).toList();
}
@override
Widget build(BuildContext context) {
return PageScaffold(
title: '出库管理',
tabs: const [
Tab(text: '出库单'),
Tab(text: '出库查询'),
Tab(text: '出库审核'),
],
tabViews: [
_buildList(),
_buildList(),
_buildList(forceStatus: OrderStatus.pending),
],
);
}
Widget _buildList({OrderStatus? forceStatus}) {
final orders = forceStatus != null
? _mockOrders
.where((o) => o['status'] == forceStatus)
.toList()
: _filtered;
return DataTableCard(
totalCount: orders.length,
page: _page,
onPageChanged: (p) => setState(() => _page = p),
toolbar: Row(
children: [
ElevatedButton.icon(
onPressed: () => _showCreateDialog(context),
icon: const Icon(Icons.add, size: 16),
label: const Text('新建出库单'),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.file_download_outlined, size: 16),
label: const Text('导出'),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.print_outlined, size: 16),
label: const Text('打印'),
),
const Spacer(),
// Type filter
_DropdownFilter(
value: _typeFilter,
items: ['全部', '领用', '调拨', '退货'],
onChanged: (v) => setState(() => _typeFilter = v!),
hint: '出库类型',
),
const SizedBox(width: 8),
if (forceStatus == null)
_DropdownFilter(
value: _statusFilter,
items: ['全部', '草稿', '待审核', '已审核', '已拒绝'],
onChanged: (v) => setState(() => _statusFilter = v!),
hint: '状态',
),
if (forceStatus == null) const SizedBox(width: 8),
SizedBox(
width: 180,
child: TextField(
controller: _searchCtrl,
decoration: const InputDecoration(
hintText: '搜索单号/部门',
prefixIcon: Icon(Icons.search, size: 16),
hintStyle: TextStyle(fontSize: 13),
),
onChanged: (_) => setState(() => _page = 1),
),
),
],
),
columns: const [
DataColumn(label: Text('出库单号')),
DataColumn(label: Text('出库类型')),
DataColumn(label: Text('领用部门')),
DataColumn(label: Text('用途')),
DataColumn(label: Text('仓库')),
DataColumn(label: Text('金额'), numeric: true),
DataColumn(label: Text('状态')),
DataColumn(label: Text('日期')),
DataColumn(label: Text('操作')),
],
rows: orders
.map((o) => DataRow(cells: [
DataCell(Text(o['no'] as String,
style: const TextStyle(
color: AppTheme.primary,
fontFamily: 'monospace',
fontSize: 12))),
DataCell(_TypeBadge(o['type'] as String)),
DataCell(Text(o['department'] as String)),
DataCell(Text(o['purpose'] as String,
overflow: TextOverflow.ellipsis)),
DataCell(Text(o['warehouse'] as String)),
DataCell(Text('¥${o['amount']}')),
DataCell(StatusBadge(o['status'] as OrderStatus)),
DataCell(Text(o['date'] as String)),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {},
child: const Text('查看',
style: TextStyle(fontSize: 12))),
if ((o['status'] as OrderStatus) ==
OrderStatus.pending)
TextButton(
onPressed: () {},
child: const Text('审核',
style: TextStyle(
color: AppTheme.success,
fontSize: 12))),
if ((o['status'] as OrderStatus) ==
OrderStatus.draft) ...[
TextButton(
onPressed: () {},
child: const Text('编辑',
style: TextStyle(fontSize: 12))),
TextButton(
onPressed: () {},
child: const Text('删除',
style: TextStyle(
color: AppTheme.danger,
fontSize: 12))),
],
],
)),
]))
.toList(),
);
}
void _showCreateDialog(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('新建出库单'),
content: const SizedBox(
width: 400,
child: Text('出库单创建功能完整实现请参考入库单流程。'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('关闭')),
],
),
);
}
}
class _TypeBadge extends StatelessWidget {
final String type;
const _TypeBadge(this.type);
@override
Widget build(BuildContext context) {
final Color color;
switch (type) {
case '领用':
color = AppTheme.primary;
break;
case '调拨':
color = AppTheme.accent;
break;
case '退货':
color = AppTheme.danger;
break;
default:
color = AppTheme.textSecondary;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(3),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Text(type,
style: TextStyle(
color: color, fontSize: 12, fontWeight: FontWeight.w500)),
);
}
}
class _DropdownFilter extends StatelessWidget {
final String value;
final List<String> items;
final ValueChanged<String?> onChanged;
final String hint;
const _DropdownFilter({
required this.value,
required this.items,
required this.onChanged,
required this.hint,
});
@override
Widget build(BuildContext context) {
return Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
border: Border.all(color: AppTheme.border),
borderRadius: BorderRadius.circular(4),
color: AppTheme.surface,
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: value,
hint: Text(hint, style: const TextStyle(fontSize: 13)),
items: items
.map((s) => DropdownMenuItem(
value: s,
child: Text(s, style: const TextStyle(fontSize: 13))))
.toList(),
onChanged: onChanged,
style: const TextStyle(
fontSize: 13, color: AppTheme.textPrimary),
),
),
);
}
}