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,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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user