feat(client): 全模块 API 对接 + 修复登陆跳转 + 菜单 UI 优化

API 对接:
- 入库/出库/库存/财务/往来单位/基础数据全部对接后端 REST API
- 新增 repositories、providers、models 层,统一分层架构
- auth 从 flutter_secure_storage 迁移到 shared_preferences

登陆跳转修复:
- 将 _RouterNotifier 提取为独立 Riverpod provider,appRouterProvider
  使用 ref.read 避免依赖链导致 router 重建后跳回 /login
- redirect 函数新增 initialized 守卫,防止 auth 未恢复时误重定向
- 添加调试日志(Router/Auth/ApiClient)定位 401 触发的 logout 链路

退出菜单 UI:
- 去掉 ListTile,改用 Row + 自定义 padding,文字左对齐
- MouseRegion + AnimatedContainer 实现 hover 高亮(普通项蓝底/退出红底)
- 菜单圆角 6px,elevation 8,分割线高度 1px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-07 22:20:28 +08:00
parent 31ea370cea
commit d717fb3735
70 changed files with 6919 additions and 2580 deletions
+12 -8
View File
@@ -15,7 +15,7 @@ class LoginScreen extends ConsumerStatefulWidget {
class _LoginScreenState extends ConsumerState<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _hotelCodeCtrl = TextEditingController();
final _shopCodeCtrl = TextEditingController();
final _usernameCtrl = TextEditingController();
final _passwordCtrl = TextEditingController();
final _hotelCodeFocus = FocusNode();
@@ -42,7 +42,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
_loadHistory();
_hotelCodeFocus.addListener(() {
if (_hotelCodeFocus.hasFocus && _hotelCodeHistory.isNotEmpty) {
_openDropdown(_hotelLayerLink, _hotelCodeHistory, _hotelCodeCtrl,
_openDropdown(_hotelLayerLink, _hotelCodeHistory, _shopCodeCtrl,
isHotel: true);
} else if (!_hotelCodeFocus.hasFocus) {
_closeHotel();
@@ -68,7 +68,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
});
// If a field is already focused, open its dropdown now that history loaded
if (_hotelCodeFocus.hasFocus && hotels.isNotEmpty) {
_openDropdown(_hotelLayerLink, hotels, _hotelCodeCtrl, isHotel: true);
_openDropdown(_hotelLayerLink, hotels, _shopCodeCtrl, isHotel: true);
}
if (_usernameFocus.hasFocus && users.isNotEmpty) {
_openDropdown(_usernameLayerLink, users, _usernameCtrl, isHotel: false);
@@ -174,7 +174,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
void dispose() {
_hotelEntry?.remove();
_usernameEntry?.remove();
_hotelCodeCtrl.dispose();
_shopCodeCtrl.dispose();
_usernameCtrl.dispose();
_passwordCtrl.dispose();
_hotelCodeFocus.dispose();
@@ -189,17 +189,21 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
_errorMessage = null;
});
try {
debugPrint('[Login] calling AuthRepository.login...');
final user = await AuthRepository.login(
hotelCode: _hotelCodeCtrl.text.trim(),
shopCode: _shopCodeCtrl.text.trim(),
username: _usernameCtrl.text.trim(),
password: _passwordCtrl.text,
);
debugPrint('[Login] API success, username=${user.username}');
await LoginHistoryStorage.record(
_hotelCodeCtrl.text.trim(),
_shopCodeCtrl.text.trim(),
_usernameCtrl.text.trim(),
);
await ref.read(authStateProvider.notifier).login(user);
debugPrint('[Login] notifier.login done, mounted=$mounted, going to /stock-in');
if (mounted) context.go('/stock-in');
debugPrint('[Login] context.go called');
} on AuthException catch (e) {
setState(() => _errorMessage = e.message);
} catch (e) {
@@ -283,11 +287,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen> {
),
const SizedBox(height: 32),
// Hotel code field
// Shop code field
CompositedTransformTarget(
link: _hotelLayerLink,
child: TextFormField(
controller: _hotelCodeCtrl,
controller: _shopCodeCtrl,
focusNode: _hotelCodeFocus,
decoration: InputDecoration(
labelText: '门店编号',
File diff suppressed because it is too large Load Diff
+401 -444
View File
@@ -1,9 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../widgets/page_scaffold.dart';
import '../../widgets/data_table_card.dart';
import '../../core/models/page_result.dart';
import '../../core/theme/app_theme.dart';
import '../../widgets/form_dialog.dart';
import '../../models/partner.dart';
import '../../providers/partner_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/page_scaffold.dart';
class PartnersScreen extends ConsumerStatefulWidget {
const PartnersScreen({super.key});
@@ -13,147 +16,17 @@ class PartnersScreen extends ConsumerStatefulWidget {
}
class _PartnersScreenState extends ConsumerState<PartnersScreen> {
int _suppliersPage = 1;
int _customersPage = 1;
final _searchCtrl = TextEditingController();
final List<Map<String, dynamic>> _suppliers = [
{
'code': 'GYS001',
'name': '贵州茅台酒股份有限公司',
'type': '白酒厂商',
'contact': '王建国',
'phone': '0851-22886688',
'address': '贵州省仁怀市茅台镇',
'balance': '-86000.00',
'status': '合作中',
'creditDays': 30,
},
{
'code': 'GYS002',
'name': '四川五粮液股份有限公司',
'type': '白酒厂商',
'contact': '李明华',
'phone': '0831-12345678',
'address': '四川省宜宾市翠屏区',
'balance': '-42500.00',
'status': '合作中',
'creditDays': 30,
},
{
'code': 'GYS003',
'name': '江苏洋河酒厂股份有限公司',
'type': '白酒厂商',
'contact': '张秀英',
'phone': '0527-83999999',
'address': '江苏省宿迁市宿城区',
'balance': '0.00',
'status': '合作中',
'creditDays': 45,
},
{
'code': 'GYS004',
'name': '剑南春(集团)有限责任公司',
'type': '白酒厂商',
'contact': '陈志强',
'phone': '0838-88888888',
'address': '四川省德阳市绵竹市',
'balance': '-28600.00',
'status': '合作中',
'creditDays': 30,
},
{
'code': 'GYS005',
'name': '泸州老窖股份有限公司',
'type': '白酒厂商',
'contact': '赵丽红',
'phone': '0830-12233456',
'address': '四川省泸州市江阳区',
'balance': '-15200.00',
'status': '合作中',
'creditDays': 30,
},
{
'code': 'GYS006',
'name': '法国拉菲集团中国总代理',
'type': '葡萄酒进口商',
'contact': 'Pierre Liu',
'phone': '021-61234567',
'address': '上海市黄浦区外滩18号',
'balance': '-124800.00',
'status': '合作中',
'creditDays': 60,
},
{
'code': 'GYS007',
'name': '人头马轩尼诗(中国)有限公司',
'type': '洋酒进口商',
'contact': '刘经理',
'phone': '021-54321678',
'address': '上海市浦东新区',
'balance': '-30240.00',
'status': '合作中',
'creditDays': 60,
},
{
'code': 'GYS008',
'name': '山西汾酒集团有限责任公司',
'type': '白酒厂商',
'contact': '周建明',
'phone': '0357-33666888',
'address': '山西省吕梁市汾阳市',
'balance': '0.00',
'status': '暂停合作',
'creditDays': 30,
},
];
final List<Map<String, dynamic>> _customers = [
{
'code': 'KH001',
'name': '北京国贸大酒店',
'type': '五星级酒店',
'contact': '采购部',
'phone': '010-65051234',
'address': '北京市朝阳区建国门外大街1号',
'balance': '45000.00',
'status': '合作中',
},
{
'code': 'KH002',
'name': '上海外滩华尔道夫酒店',
'type': '五星级酒店',
'contact': '采购经理',
'phone': '021-63228888',
'address': '上海市黄浦区中山东一路2号',
'balance': '0.00',
'status': '合作中',
},
{
'code': 'KH003',
'name': '广州白云国际会议中心',
'type': '会议中心',
'contact': '餐饮总监',
'phone': '020-86001234',
'address': '广州市白云区云城东路1号',
'balance': '28600.00',
'status': '合作中',
},
{
'code': 'KH004',
'name': '深圳湾万象城购物中心',
'type': '商超零售',
'contact': '王采购',
'phone': '0755-82345678',
'address': '深圳市南山区望海路购物公园',
'balance': '12000.00',
'status': '合作中',
},
];
final _supplierSearchCtrl = TextEditingController();
final _customerSearchCtrl = TextEditingController();
Timer? _supplierDebounce;
Timer? _customerDebounce;
@override
void dispose() {
_searchCtrl.dispose();
_supplierSearchCtrl.dispose();
_customerSearchCtrl.dispose();
_supplierDebounce?.cancel();
_customerDebounce?.cancel();
super.dispose();
}
@@ -166,352 +39,436 @@ class _PartnersScreenState extends ConsumerState<PartnersScreen> {
Tab(text: '客户'),
],
tabViews: [
_buildSupplierList(),
_buildCustomerList(),
_buildSupplierTab(),
_buildCustomerTab(),
],
);
}
Widget _buildSupplierList() {
final q = _searchCtrl.text.toLowerCase();
final filtered = _suppliers.where((s) {
if (q.isEmpty) return true;
return (s['name'] as String).toLowerCase().contains(q) ||
(s['code'] as String).toLowerCase().contains(q);
}).toList();
Widget _buildSupplierTab() {
final asyncPartners = ref.watch(supplierListProvider);
return asyncPartners.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('加载失败:$e',
style: const TextStyle(color: AppTheme.danger)),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () =>
ref.read(supplierListProvider.notifier).reload(),
child: const Text('重试'),
),
],
),
),
data: (result) => _buildPartnerList(
result,
isSupplier: true,
searchCtrl: _supplierSearchCtrl,
onSearchChanged: (v) {
_supplierDebounce?.cancel();
_supplierDebounce = Timer(const Duration(milliseconds: 300), () {
ref.read(supplierListProvider.notifier).setKeyword(v);
});
},
onPageChanged: (p) =>
ref.read(supplierListProvider.notifier).setPage(p),
onAdd: () => _showPartnerDialog(context, isSupplier: true),
onEdit: (p) =>
_showPartnerDialog(context, isSupplier: true, partner: p),
onDelete: (p) => _confirmDelete(context, p, isSupplier: true),
),
);
}
Widget _buildCustomerTab() {
final asyncPartners = ref.watch(customerListProvider);
return asyncPartners.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('加载失败:$e',
style: const TextStyle(color: AppTheme.danger)),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () =>
ref.read(customerListProvider.notifier).reload(),
child: const Text('重试'),
),
],
),
),
data: (result) => _buildPartnerList(
result,
isSupplier: false,
searchCtrl: _customerSearchCtrl,
onSearchChanged: (v) {
_customerDebounce?.cancel();
_customerDebounce = Timer(const Duration(milliseconds: 300), () {
ref.read(customerListProvider.notifier).setKeyword(v);
});
},
onPageChanged: (p) =>
ref.read(customerListProvider.notifier).setPage(p),
onAdd: () => _showPartnerDialog(context, isSupplier: false),
onEdit: (p) =>
_showPartnerDialog(context, isSupplier: false, partner: p),
onDelete: (p) => _confirmDelete(context, p, isSupplier: false),
),
);
}
Widget _buildPartnerList(
PageResult<Partner> result, {
required bool isSupplier,
required TextEditingController searchCtrl,
required void Function(String) onSearchChanged,
required void Function(int) onPageChanged,
required VoidCallback onAdd,
required void Function(Partner) onEdit,
required void Function(Partner) onDelete,
}) {
final partners = result.data;
return DataTableCard(
totalCount: filtered.length,
page: _suppliersPage,
onPageChanged: (p) => setState(() => _suppliersPage = p),
totalCount: result.total,
page: result.page,
onPageChanged: onPageChanged,
toolbar: Row(
children: [
ElevatedButton.icon(
onPressed: () => _showAddPartnerDialog(context, isSupplier: true),
onPressed: onAdd,
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('导出'),
label: Text(isSupplier ? '新建' : '新建'),
),
const Spacer(),
SizedBox(
width: 200,
child: TextField(
controller: _searchCtrl,
decoration: const InputDecoration(
hintText: '搜索供应商名称/编码',
prefixIcon: Icon(Icons.search, size: 16),
hintStyle: TextStyle(fontSize: 13),
controller: searchCtrl,
decoration: InputDecoration(
hintText: isSupplier ? '搜索供应商名称/编码' : '搜索客户名称/编码',
prefixIcon: const Icon(Icons.search, size: 16),
hintStyle: const TextStyle(fontSize: 13),
),
onChanged: (_) => setState(() {}),
onChanged: onSearchChanged,
),
),
],
),
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('应付余额'), numeric: true),
DataColumn(label: Text('状态')),
DataColumn(label: Text('操作')),
columns: [
const DataColumn(label: Text('编码')),
const DataColumn(label: Text('名称')),
const DataColumn(label: Text('联系人')),
const DataColumn(label: Text('联系电话')),
const DataColumn(label: Text('地址')),
const DataColumn(label: Text('操作')),
],
rows: filtered
.map((s) => DataRow(cells: [
DataCell(Text(s['code'] as String,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: AppTheme.textSecondary))),
DataCell(SizedBox(
width: 180,
child: Text(s['name'] as String,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500)),
)),
DataCell(Text(s['type'] as String)),
DataCell(Text(s['contact'] as String)),
DataCell(Text(s['phone'] as String)),
DataCell(Text('${s['creditDays']}')),
DataCell(Text(
'¥${s['balance']}',
style: TextStyle(
color: (s['balance'] as String).startsWith('-')
? AppTheme.danger
: AppTheme.textPrimary,
fontWeight: FontWeight.w500,
),
)),
DataCell(_StatusChip(s['status'] as String)),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {},
child: const Text('查看',
style: TextStyle(fontSize: 12))),
TextButton(
onPressed: () {},
child: const Text('编辑',
style: TextStyle(fontSize: 12))),
TextButton(
onPressed: () {},
child: const Text('对账',
style: TextStyle(fontSize: 12))),
],
)),
]))
.toList(),
rows: partners.isEmpty
? [
DataRow(cells: [
const DataCell(SizedBox()),
DataCell(Text(isSupplier ? '暂无供应商' : '暂无客户',
style:
const TextStyle(color: AppTheme.textSecondary))),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
])
]
: partners
.map((p) => DataRow(
cells: [
DataCell(Text(p.code ?? '-',
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: AppTheme.textSecondary))),
DataCell(SizedBox(
width: 180,
child: Text(p.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w500)),
)),
DataCell(Text(p.contact ?? '-')),
DataCell(Text(p.phone ?? '-')),
DataCell(SizedBox(
width: 160,
child: Text(p.address ?? '-',
overflow: TextOverflow.ellipsis),
)),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
key: Key('btn_edit_${p.id}'),
onPressed: () => onEdit(p),
child: const Text('编辑',
style: TextStyle(fontSize: 12)),
),
TextButton(
key: Key('btn_delete_${p.id}'),
onPressed: () => onDelete(p),
child: const Text('删除',
style: TextStyle(
fontSize: 12,
color: AppTheme.danger)),
),
],
)),
],
))
.toList(),
);
}
Widget _buildCustomerList() {
return DataTableCard(
totalCount: _customers.length,
page: _customersPage,
onPageChanged: (p) => setState(() => _customersPage = p),
toolbar: Row(
children: [
ElevatedButton.icon(
onPressed: () =>
_showAddPartnerDialog(context, isSupplier: false),
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 Spacer(),
SizedBox(
width: 200,
child: TextField(
decoration: const InputDecoration(
hintText: '搜索客户名称/编码',
prefixIcon: Icon(Icons.search, size: 16),
hintStyle: TextStyle(fontSize: 13),
),
),
),
],
),
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('操作')),
],
rows: _customers
.map((c) => DataRow(cells: [
DataCell(Text(c['code'] as String,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: AppTheme.textSecondary))),
DataCell(SizedBox(
width: 180,
child: Text(c['name'] as String,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w500)),
)),
DataCell(Text(c['type'] as String)),
DataCell(Text(c['contact'] as String)),
DataCell(Text(c['phone'] as String)),
DataCell(Text(
'¥${c['balance']}',
style: const TextStyle(fontWeight: FontWeight.w500),
)),
DataCell(_StatusChip(c['status'] as String)),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {},
child: const Text('查看',
style: TextStyle(fontSize: 12))),
TextButton(
onPressed: () {},
child: const Text('编辑',
style: TextStyle(fontSize: 12))),
],
)),
]))
.toList(),
);
}
void _showAddPartnerDialog(BuildContext context, {required bool isSupplier}) {
final nameCtrl = TextEditingController();
final contactCtrl = TextEditingController();
final phoneCtrl = TextEditingController();
final addressCtrl = TextEditingController();
void _showPartnerDialog(BuildContext context,
{required bool isSupplier, Partner? partner}) {
showDialog(
context: context,
builder: (ctx) => FormDialog(
title: isSupplier ? '新增供应商' : '新增客户',
width: 520,
onConfirm: () {
Navigator.of(ctx).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isSupplier ? '供应商添加成功' : '客户添加成功'),
backgroundColor: AppTheme.success,
),
);
builder: (ctx) => _PartnerFormDialog(
isSupplier: isSupplier,
partner: partner,
onSaved: () {
if (isSupplier) {
ref.read(supplierListProvider.notifier).reload();
} else {
ref.read(customerListProvider.notifier).reload();
}
},
content: Column(
children: [
_DialogField(
label: isSupplier ? '供应商名称' : '客户名称',
required: true,
child: TextFormField(
controller: nameCtrl,
decoration: InputDecoration(
hintText: isSupplier ? '请输入供应商全称' : '请输入客户名称',
),
),
);
}
Future<void> _confirmDelete(BuildContext context, Partner partner,
{required bool isSupplier}) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('确认删除'),
content: Text('确认删除「${partner.name}」?此操作不可恢复。'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.danger,
foregroundColor: Colors.white),
child: const Text('删除'),
),
],
),
);
if (confirmed == true && mounted) {
try {
if (isSupplier) {
await ref
.read(supplierListProvider.notifier)
.deletePartner(partner.id);
} else {
await ref
.read(customerListProvider.notifier)
.deletePartner(partner.id);
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('删除成功'),
backgroundColor: AppTheme.success));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('删除失败:$e'),
backgroundColor: AppTheme.danger));
}
}
}
}
}
class _PartnerFormDialog extends ConsumerStatefulWidget {
final bool isSupplier;
final Partner? partner;
final VoidCallback onSaved;
const _PartnerFormDialog({
required this.isSupplier,
this.partner,
required this.onSaved,
});
@override
ConsumerState<_PartnerFormDialog> createState() =>
_PartnerFormDialogState();
}
class _PartnerFormDialogState extends ConsumerState<_PartnerFormDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameCtrl;
late final TextEditingController _codeCtrl;
late final TextEditingController _contactCtrl;
late final TextEditingController _phoneCtrl;
late final TextEditingController _addressCtrl;
late final TextEditingController _remarkCtrl;
bool _saving = false;
@override
void initState() {
super.initState();
final p = widget.partner;
_nameCtrl = TextEditingController(text: p?.name ?? '');
_codeCtrl = TextEditingController(text: p?.code ?? '');
_contactCtrl = TextEditingController(text: p?.contact ?? '');
_phoneCtrl = TextEditingController(text: p?.phone ?? '');
_addressCtrl = TextEditingController(text: p?.address ?? '');
_remarkCtrl = TextEditingController(text: p?.remark ?? '');
}
@override
void dispose() {
_nameCtrl.dispose();
_codeCtrl.dispose();
_contactCtrl.dispose();
_phoneCtrl.dispose();
_addressCtrl.dispose();
_remarkCtrl.dispose();
super.dispose();
}
Future<void> _save() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _saving = true);
final data = {
'name': _nameCtrl.text.trim(),
'type': widget.isSupplier ? 'supplier' : 'customer',
if (_codeCtrl.text.trim().isNotEmpty) 'code': _codeCtrl.text.trim(),
if (_contactCtrl.text.trim().isNotEmpty)
'contact': _contactCtrl.text.trim(),
if (_phoneCtrl.text.trim().isNotEmpty) 'phone': _phoneCtrl.text.trim(),
if (_addressCtrl.text.trim().isNotEmpty)
'address': _addressCtrl.text.trim(),
if (_remarkCtrl.text.trim().isNotEmpty) 'remark': _remarkCtrl.text.trim(),
};
try {
final notifier = widget.isSupplier
? ref.read(supplierListProvider.notifier)
: ref.read(customerListProvider.notifier);
if (widget.partner != null) {
await notifier.updatePartner(widget.partner!.id, data);
} else {
await notifier.createPartner(data);
}
if (mounted) {
Navigator.of(context).pop();
widget.onSaved();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(widget.partner != null ? '更新成功' : '创建成功'),
backgroundColor: AppTheme.success,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('保存失败:$e'),
backgroundColor: AppTheme.danger),
);
}
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
final isEdit = widget.partner != null;
final typeName = widget.isSupplier ? '供应商' : '客户';
return Dialog(
child: Container(
width: 520,
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(isEdit ? '编辑$typeName' : '新建$typeName',
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 20),
TextFormField(
controller: _nameCtrl,
decoration: InputDecoration(labelText: '$typeName名称'),
validator: (v) =>
(v == null || v.isEmpty) ? '不能为空' : null,
),
),
const SizedBox(height: 12),
Row(
children: [
const SizedBox(height: 12),
Row(children: [
Expanded(
child: _DialogField(
label: '联系人',
child: TextFormField(
controller: contactCtrl,
decoration: const InputDecoration(hintText: '联系人姓名'),
),
child: TextFormField(
controller: _codeCtrl,
decoration: const InputDecoration(labelText: '编码'),
),
),
const SizedBox(width: 12),
Expanded(
child: _DialogField(
label: '联系电话',
child: TextFormField(
controller: phoneCtrl,
decoration: const InputDecoration(hintText: '手机/座机'),
),
child: TextFormField(
controller: _contactCtrl,
decoration: const InputDecoration(labelText: '联系人'),
),
),
],
),
const SizedBox(height: 12),
_DialogField(
label: '地址',
child: TextFormField(
controller: addressCtrl,
decoration: const InputDecoration(hintText: '详细地址'),
),
),
if (isSupplier) ...[
]),
const SizedBox(height: 12),
TextFormField(
controller: _phoneCtrl,
decoration: const InputDecoration(labelText: '联系电话'),
),
const SizedBox(height: 12),
TextFormField(
controller: _addressCtrl,
decoration: const InputDecoration(labelText: '地址'),
),
const SizedBox(height: 12),
TextFormField(
controller: _remarkCtrl,
decoration: const InputDecoration(labelText: '备注'),
maxLines: 2,
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
child: _DialogField(
label: '账期(天)',
child: TextFormField(
initialValue: '30',
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: '结款账期'),
),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
const SizedBox(width: 12),
Expanded(
child: _DialogField(
label: '供应商类型',
child: DropdownButtonFormField<String>(
value: '白酒厂商',
items: ['白酒厂商', '葡萄酒进口商', '洋酒进口商', '啤酒厂商', '其他']
.map((s) => DropdownMenuItem(
value: s,
child: Text(s,
style: const TextStyle(fontSize: 13))))
.toList(),
onChanged: (_) {},
decoration: const InputDecoration(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _saving ? null : _save,
child: _saving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Text('保存'),
),
],
),
],
],
),
),
);
}
}
class _DialogField extends StatelessWidget {
final String label;
final Widget child;
final bool required;
const _DialogField({
required this.label,
required this.child,
this.required = false,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (required)
const Text('* ',
style: TextStyle(color: AppTheme.danger, fontSize: 13)),
Text(label,
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
],
),
const SizedBox(height: 6),
child,
],
);
}
}
class _StatusChip extends StatelessWidget {
final String status;
const _StatusChip(this.status);
@override
Widget build(BuildContext context) {
final bool isActive = status == '合作中';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: isActive
? const Color(0xFFE8F5E9)
: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(3),
),
child: Text(
status,
style: TextStyle(
color: isActive ? AppTheme.success : AppTheme.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
);
File diff suppressed because it is too large Load Diff
+254 -79
View File
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/theme/app_theme.dart';
import '../../models/warehouse.dart';
import '../../providers/warehouse_provider.dart';
class SettingsScreen extends ConsumerStatefulWidget {
const SettingsScreen({super.key});
@@ -144,12 +146,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
}
Widget _buildWarehousesTab() {
final warehouses = [
{'code': 'WH001', 'name': '主仓库', 'location': '一楼东侧', 'manager': '张三', 'capacity': 1000, 'used': 680, 'status': true},
{'code': 'WH002', 'name': '副仓库', 'location': '二楼西侧', 'manager': '李四', 'capacity': 500, 'used': 210, 'status': true},
{'code': 'WH003', 'name': '保税仓库', 'location': '地下一层', 'manager': '王五', 'capacity': 300, 'used': 0, 'status': false},
];
final asyncWarehouses = ref.watch(warehouseListProvider);
return Column(
children: [
Container(
@@ -159,87 +156,144 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
child: Row(
children: [
ElevatedButton.icon(
onPressed: () {},
onPressed: () => _showWarehouseDialog(context),
icon: const Icon(Icons.add, size: 16),
label: const Text('增仓库'),
label: const Text(''),
),
],
),
),
const Divider(height: 1),
Expanded(
child: SingleChildScrollView(
child: DataTable(
headingRowColor: WidgetStateProperty.all(const Color(0xFFF0F4FF)),
columns: const [
DataColumn(label: Text('仓库编号')),
DataColumn(label: Text('仓库名称')),
DataColumn(label: Text('位置')),
DataColumn(label: Text('负责人')),
DataColumn(label: Text('使用情况')),
DataColumn(label: Text('状态')),
DataColumn(label: Text('操作')),
],
rows: warehouses
.map((w) => DataRow(cells: [
DataCell(Text(w['code'] as String,
style: const TextStyle(
fontFamily: 'monospace', fontSize: 12))),
DataCell(Text(w['name'] as String,
style: const TextStyle(fontWeight: FontWeight.w500))),
DataCell(Text(w['location'] as String)),
DataCell(Text(w['manager'] as String)),
DataCell(SizedBox(
width: 140,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${w['used']}/${w['capacity']}',
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary),
),
const SizedBox(height: 2),
LinearProgressIndicator(
value: (w['capacity'] as int) > 0
? (w['used'] as int) / (w['capacity'] as int)
: 0,
backgroundColor: AppTheme.border,
valueColor: AlwaysStoppedAnimation<Color>(
(w['used'] as int) / (w['capacity'] as int) > 0.8
? AppTheme.danger
: AppTheme.primary,
),
),
],
),
)),
DataCell(Switch(
value: w['status'] as bool,
onChanged: (_) {},
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
)),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {},
child: const Text('编辑',
style: TextStyle(fontSize: 12))),
],
)),
]))
.toList(),
child: asyncWarehouses.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('加载失败:$e',
style: const TextStyle(color: AppTheme.danger)),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () =>
ref.read(warehouseListProvider.notifier).reload(),
child: const Text('重试'),
),
],
),
),
data: (warehouses) {
if (warehouses.isEmpty) {
return const Center(
child: Text('暂无仓库',
style:
TextStyle(color: AppTheme.textSecondary)));
}
return SingleChildScrollView(
child: DataTable(
headingRowColor:
WidgetStateProperty.all(const Color(0xFFF0F4FF)),
columns: const [
DataColumn(label: Text('仓库名称')),
DataColumn(label: Text('位置')),
DataColumn(label: Text('默认仓库')),
DataColumn(label: Text('操作')),
],
rows: warehouses
.map((w) => DataRow(cells: [
DataCell(Text(w.name,
style: const TextStyle(
fontWeight: FontWeight.w500))),
DataCell(Text(w.location ?? '-')),
DataCell(w.isDefault
? const Icon(Icons.check_circle,
color: AppTheme.success, size: 18)
: const SizedBox()),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
key: Key('btn_edit_${w.id}'),
onPressed: () =>
_showWarehouseDialog(context,
warehouse: w),
child: const Text('编辑',
style: TextStyle(fontSize: 12)),
),
TextButton(
key: Key('btn_delete_${w.id}'),
onPressed: () =>
_confirmDeleteWarehouse(context, w),
child: const Text('删除',
style: TextStyle(
fontSize: 12,
color: AppTheme.danger)),
),
],
)),
]))
.toList(),
),
);
},
),
),
],
);
}
Future<void> _confirmDeleteWarehouse(
BuildContext context, Warehouse w) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('确认删除'),
content: Text('确认删除仓库「${w.name}」?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消')),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.danger,
foregroundColor: Colors.white),
child: const Text('删除'),
),
],
),
);
if (confirmed == true && mounted) {
try {
await ref
.read(warehouseListProvider.notifier)
.deleteWarehouse(w.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('删除成功'),
backgroundColor: AppTheme.success));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('删除失败:$e'),
backgroundColor: AppTheme.danger));
}
}
}
}
void _showWarehouseDialog(BuildContext context, {Warehouse? warehouse}) {
showDialog(
context: context,
builder: (ctx) => _WarehouseFormDialog(
warehouse: warehouse,
onSaved: () =>
ref.read(warehouseListProvider.notifier).reload(),
),
);
}
Widget _buildNumberRulesTab() {
final rules = [
{'type': '入库单', 'prefix': 'RK', 'format': 'RK{年}{月}{日}{序号4}', 'example': 'RK20260404001', 'currentNo': 4},
@@ -337,12 +391,6 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
_ParamRow(label: '入库单需要审核', value: '', isSwitch: true),
_ParamRow(label: '出库单需要审核', value: '', isSwitch: true),
_ParamRow(label: '允许超量出库', value: '', isSwitch: false),
const SizedBox(height: 16),
const Text('库存预警设置',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.primaryDark)),
const Divider(height: 24),
_ParamRow(label: '启用库存预警', value: '', isSwitch: true),
_ParamRow(label: '预警提醒方式', value: '系统内通知'),
],
),
),
@@ -415,6 +463,133 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
}
}
class _WarehouseFormDialog extends ConsumerStatefulWidget {
final Warehouse? warehouse;
final VoidCallback onSaved;
const _WarehouseFormDialog({this.warehouse, required this.onSaved});
@override
ConsumerState<_WarehouseFormDialog> createState() =>
_WarehouseFormDialogState();
}
class _WarehouseFormDialogState extends ConsumerState<_WarehouseFormDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameCtrl;
late final TextEditingController _locationCtrl;
late bool _isDefault;
bool _saving = false;
@override
void initState() {
super.initState();
_nameCtrl = TextEditingController(text: widget.warehouse?.name ?? '');
_locationCtrl =
TextEditingController(text: widget.warehouse?.location ?? '');
_isDefault = widget.warehouse?.isDefault ?? false;
}
@override
void dispose() {
_nameCtrl.dispose();
_locationCtrl.dispose();
super.dispose();
}
Future<void> _save() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _saving = true);
final data = {
'name': _nameCtrl.text.trim(),
if (_locationCtrl.text.trim().isNotEmpty)
'location': _locationCtrl.text.trim(),
'is_default': _isDefault,
};
try {
final notifier = ref.read(warehouseListProvider.notifier);
if (widget.warehouse != null) {
await notifier.updateWarehouse(widget.warehouse!.id, data);
} else {
await notifier.createWarehouse(data);
}
if (mounted) {
Navigator.of(context).pop();
widget.onSaved();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
widget.warehouse != null ? '仓库更新成功' : '仓库创建成功'),
backgroundColor: AppTheme.success,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('保存失败:$e'),
backgroundColor: AppTheme.danger),
);
}
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.warehouse != null ? '编辑仓库' : '新建仓库'),
content: SizedBox(
width: 400,
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameCtrl,
decoration: const InputDecoration(labelText: '仓库名称'),
validator: (v) =>
(v == null || v.isEmpty) ? '不能为空' : null,
),
const SizedBox(height: 12),
TextFormField(
controller: _locationCtrl,
decoration: const InputDecoration(labelText: '位置'),
),
const SizedBox(height: 12),
CheckboxListTile(
title: const Text('设为默认仓库'),
value: _isDefault,
onChanged: (v) => setState(() => _isDefault = v ?? false),
contentPadding: EdgeInsets.zero,
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: _saving ? null : _save,
child: _saving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: const Text('保存'),
),
],
);
}
}
class _RoleBadge extends StatelessWidget {
final String role;
const _RoleBadge(this.role);
+83 -17
View File
@@ -72,7 +72,7 @@ class _AppShellState extends ConsumerState<AppShell> {
const Icon(Icons.business,
color: Colors.white70, size: 14),
const SizedBox(width: 4),
Text(user.hotelNo,
Text(user.shopNo,
style: const TextStyle(
color: Colors.white70, fontSize: 13)),
const SizedBox(width: 20),
@@ -86,6 +86,11 @@ class _AppShellState extends ConsumerState<AppShell> {
PopupMenuButton<String>(
icon: const Icon(Icons.keyboard_arrow_down,
color: Colors.white70),
offset: const Offset(0, 36),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6)),
color: Colors.white,
elevation: 8,
onSelected: (v) {
if (v == 'logout') {
ref.read(authStateProvider.notifier).logout();
@@ -93,21 +98,24 @@ class _AppShellState extends ConsumerState<AppShell> {
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'profile',
child: ListTile(
leading: Icon(Icons.manage_accounts, size: 18),
title: Text('个人设置'),
dense: true,
)),
const PopupMenuDivider(),
const PopupMenuItem(
value: 'logout',
child: ListTile(
leading: Icon(Icons.logout, size: 18),
title: Text('退出登录'),
dense: true,
)),
const PopupMenuItem<String>(
value: 'profile',
padding: EdgeInsets.zero,
child: _HoverMenuItem(
icon: Icons.manage_accounts_outlined,
label: '个人设置',
),
),
const PopupMenuDivider(height: 1),
const PopupMenuItem<String>(
value: 'logout',
padding: EdgeInsets.zero,
child: _HoverMenuItem(
icon: Icons.logout_outlined,
label: '退出登录',
danger: true,
),
),
],
),
const SizedBox(width: 8),
@@ -161,7 +169,7 @@ class _AppShellState extends ConsumerState<AppShell> {
if (user != null) ...[
_StatusItem(
icon: Icons.store,
text: '门店编号:${user.hotelNo}'),
text: '门店编号:${user.shopNo}'),
const _StatusDivider(),
_StatusItem(
icon: Icons.person,
@@ -334,3 +342,61 @@ class _ClockWidgetState extends State<_ClockWidget> {
return _StatusItem(icon: Icons.access_time, text: '当前时间:$_time');
}
}
class _HoverMenuItem extends StatefulWidget {
final IconData icon;
final String label;
final bool danger;
const _HoverMenuItem({
required this.icon,
required this.label,
this.danger = false,
});
@override
State<_HoverMenuItem> createState() => _HoverMenuItemState();
}
class _HoverMenuItemState extends State<_HoverMenuItem> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final color = widget.danger
? const Color(0xFFE53935)
: const Color(0xFF333333);
final hoverBg = widget.danger
? const Color(0xFFFFF0F0)
: const Color(0xFFF0F4FF);
return MouseRegion(
onEnter: (_) => setState(() => _hovered = true),
onExit: (_) => setState(() => _hovered = false),
cursor: SystemMouseCursors.click,
child: AnimatedContainer(
duration: const Duration(milliseconds: 120),
width: 152,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: _hovered ? hoverBg : Colors.transparent,
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
Icon(widget.icon, size: 16, color: color),
const SizedBox(width: 10),
Text(
widget.label,
style: TextStyle(
fontSize: 14,
color: color,
fontWeight: FontWeight.w400,
),
),
],
),
),
);
}
}
@@ -3,6 +3,11 @@ 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';
import '../../models/product.dart';
import '../../providers/partner_provider.dart';
import '../../providers/product_provider.dart';
import '../../providers/stock_in_provider.dart';
import '../../providers/warehouse_provider.dart';
class StockInFormScreen extends ConsumerStatefulWidget {
const StockInFormScreen({super.key});
@@ -13,37 +18,25 @@ class StockInFormScreen extends ConsumerStatefulWidget {
class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
final _formKey = GlobalKey<FormState>();
final _orderNoCtrl = TextEditingController(text: 'RK20260404004');
final _remarkCtrl = TextEditingController();
String _supplier = '贵州茅台酒股份有限公司';
String _warehouse = '主仓库';
int? _warehouseId;
int? _partnerId;
DateTime _orderDate = DateTime.now();
bool _submitting = false;
final List<Map<String, dynamic>> _items = [
{
'name': '茅台酒(飞天)53度500ml',
'spec': '500ml/瓶',
'unit': '',
'qty': TextEditingController(text: '10'),
'price': TextEditingController(text: '2600.00'),
},
{
'name': '五粮液(普五)52度500ml',
'spec': '500ml/瓶',
'unit': '',
'qty': TextEditingController(text: '20'),
'price': TextEditingController(text: '1050.00'),
},
];
final List<_ItemRow> _items = [];
@override
void initState() {
super.initState();
_items.add(_ItemRow());
}
@override
void dispose() {
_orderNoCtrl.dispose();
_remarkCtrl.dispose();
for (final item in _items) {
(item['qty'] as TextEditingController).dispose();
(item['price'] as TextEditingController).dispose();
item.dispose();
}
super.dispose();
}
@@ -51,60 +44,94 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
double get _totalAmount {
double total = 0;
for (final item in _items) {
final qty = double.tryParse(
(item['qty'] as TextEditingController).text) ??
0;
final price = double.tryParse(
(item['price'] as TextEditingController).text) ??
0;
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = double.tryParse(item.priceCtrl.text) ?? 0;
total += qty * price;
}
return total;
}
void _addItem() {
setState(() {
_items.add({
'name': '',
'spec': '',
'unit': '',
'qty': TextEditingController(),
'price': TextEditingController(),
});
});
setState(() => _items.add(_ItemRow()));
}
void _removeItem(int index) {
setState(() {
final item = _items.removeAt(index);
(item['qty'] as TextEditingController).dispose();
(item['price'] as TextEditingController).dispose();
_items[index].dispose();
_items.removeAt(index);
});
}
Future<void> _submit(bool asDraft) async {
if (!asDraft && !_formKey.currentState!.validate()) return;
setState(() => _submitting = true);
await Future.delayed(const Duration(milliseconds: 600));
if (mounted) {
if (_warehouseId == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(asDraft ? '已保存为草稿' : '入库单已提交审核'),
backgroundColor: AppTheme.success,
),
const SnackBar(content: Text('请选择入库仓库'), backgroundColor: AppTheme.danger),
);
context.go('/stock-in');
return;
}
if (_items.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请添加商品明细'), backgroundColor: AppTheme.danger),
);
return;
}
setState(() => _submitting = true);
final itemsData = _items.map((item) {
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = double.tryParse(item.priceCtrl.text) ?? 0;
return {
'product_id': item.productId ?? 0,
'quantity': qty,
'unit_price': price,
'total_price': qty * price,
};
}).toList();
final data = {
'warehouse_id': _warehouseId,
if (_partnerId != null) 'partner_id': _partnerId,
'order_date':
'${_orderDate.year}-${_orderDate.month.toString().padLeft(2, '0')}-${_orderDate.day.toString().padLeft(2, '0')}',
if (_remarkCtrl.text.trim().isNotEmpty) 'remark': _remarkCtrl.text.trim(),
'items': itemsData,
'status': asDraft ? 'draft' : 'pending',
};
try {
await ref.read(stockInListProvider.notifier).createOrder(data);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(asDraft ? '已保存为草稿' : '入库单已提交审核'),
backgroundColor: AppTheme.success,
),
);
context.go('/stock-in');
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('操作失败:$e'), backgroundColor: AppTheme.danger),
);
}
} finally {
if (mounted) setState(() => _submitting = false);
}
if (mounted) setState(() => _submitting = false);
}
@override
Widget build(BuildContext context) {
final asyncWarehouses = ref.watch(warehouseListProvider);
final asyncSuppliers = ref.watch(supplierListProvider);
return Scaffold(
backgroundColor: AppTheme.background,
body: Column(
children: [
// Page header
Container(
height: 52,
color: AppTheme.surface,
@@ -155,7 +182,6 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Basic info card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
@@ -172,71 +198,70 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
spacing: 16,
runSpacing: 16,
children: [
_FormField(
label: '入库单号',
child: TextFormField(
controller: _orderNoCtrl,
readOnly: true,
style: const TextStyle(
fontFamily: 'monospace'),
decoration: const InputDecoration(
suffixIcon: Icon(
Icons.autorenew,
size: 16),
),
),
),
_FormField(
label: '供应商',
required: true,
child: DropdownButtonFormField<String>(
value: _supplier,
items: [
'贵州茅台酒股份有限公司',
'四川五粮液股份有限公司',
'江苏洋河酒厂股份有限公司',
'剑南春(集团)有限责任公司',
'泸州老窖股份有限公司',
'山西汾酒股份有限公司',
]
.map((s) => DropdownMenuItem(
value: s,
child: Text(s,
style: const TextStyle(
fontSize: 13))))
.toList(),
onChanged: (v) =>
setState(() => _supplier = v!),
validator: (v) => v == null || v.isEmpty
? '请选择供应商'
: null,
decoration: const InputDecoration(),
),
),
// Warehouse dropdown
_FormField(
label: '入库仓库',
required: true,
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(),
child: asyncWarehouses.when(
loading: () =>
const LinearProgressIndicator(),
error: (_, __) => const Text('加载失败'),
data: (warehouses) =>
DropdownButtonFormField<int>(
value: _warehouseId,
hint: const Text('请选择仓库',
style:
TextStyle(fontSize: 13)),
items: warehouses
.map((w) => DropdownMenuItem(
value: w.id,
child: Text(w.name,
style: const TextStyle(
fontSize: 13))))
.toList(),
onChanged: (v) => setState(
() => _warehouseId = v),
validator: (v) =>
v == null ? '不能为空' : null,
decoration: const InputDecoration(),
),
),
),
// Partner dropdown
_FormField(
label: '供应商',
child: asyncSuppliers.when(
loading: () =>
const LinearProgressIndicator(),
error: (_, __) => const Text('加载失败'),
data: (result) =>
DropdownButtonFormField<int>(
value: _partnerId,
hint: const Text('请选择供应商',
style:
TextStyle(fontSize: 13)),
items: result.data
.map((p) => DropdownMenuItem(
value: p.id,
child: Text(p.name,
style: const TextStyle(
fontSize: 13))))
.toList(),
onChanged: (v) => setState(
() => _partnerId = v),
decoration: const InputDecoration(),
),
),
),
// Date picker
_FormField(
label: '入库日期',
required: true,
child: InkWell(
onTap: _pickDate,
child: InputDecorator(
decoration: const InputDecoration(),
decoration:
const InputDecoration(),
child: Row(
children: [
Expanded(
@@ -249,7 +274,8 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
const Icon(
Icons.calendar_today,
size: 16,
color: AppTheme.textSecondary),
color:
AppTheme.textSecondary),
],
),
),
@@ -274,7 +300,6 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
),
),
const SizedBox(height: 12),
// Items card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
@@ -299,17 +324,14 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
],
),
const SizedBox(height: 12),
// Items table
Table(
columnWidths: const {
0: FixedColumnWidth(36),
1: FlexColumnWidth(3),
2: FlexColumnWidth(2),
3: FixedColumnWidth(60),
2: FlexColumnWidth(1.5),
3: FlexColumnWidth(1.5),
4: FlexColumnWidth(1.5),
5: FlexColumnWidth(1.5),
6: FlexColumnWidth(1.5),
7: FixedColumnWidth(60),
5: FixedColumnWidth(60),
},
children: [
TableRow(
@@ -317,23 +339,24 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
color: Color(0xFFF0F4FF)),
children: [
'序号',
'商品名称',
'规格',
'单位',
'商品',
'数量',
'单价',
'金额',
'操作',
]
.map((h) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 10),
padding:
const EdgeInsets.symmetric(
horizontal: 8,
vertical: 10),
child: Text(h,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color:
AppTheme.primaryDark)),
fontWeight:
FontWeight.w600,
color: AppTheme
.primaryDark)),
))
.toList(),
),
@@ -343,7 +366,6 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
],
),
const Divider(height: 1),
// Total
Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
@@ -379,10 +401,9 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
TableRow _buildItemRow(int index) {
final item = _items[index];
final qtyCtrl = item['qty'] as TextEditingController;
final priceCtrl = item['price'] as TextEditingController;
final qty = double.tryParse(qtyCtrl.text) ?? 0;
final price = double.tryParse(priceCtrl.text) ?? 0;
final asyncProducts = ref.watch(productListProvider);
final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = double.tryParse(item.priceCtrl.text) ?? 0;
final amount = qty * price;
return TableRow(
@@ -390,90 +411,89 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
color: index.isEven ? Colors.white : const Color(0xFFFAFAFA),
),
children: [
// Index
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Text('${index + 1}',
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
),
// Name
Padding(
padding: const EdgeInsets.all(4),
child: asyncProducts.when(
loading: () => const LinearProgressIndicator(),
error: (_, __) => const Text('加载失败'),
data: (result) => DropdownButtonFormField<int>(
value: item.productId,
hint: const Text('选择商品',
style: TextStyle(fontSize: 13)),
items: result.data
.map((p) => DropdownMenuItem(
value: p.id,
child: Text('${p.name}${p.spec ?? p.unit}',
style: const TextStyle(fontSize: 13))))
.toList(),
onChanged: (v) {
setState(() {
item.productId = v;
// Auto-fill price if available
if (v != null) {
final found = result.data.firstWhere(
(p) => p.id == v,
orElse: () => Product(
id: 0, code: '', name: '', unit: ''));
if (found.purchasePrice != null) {
item.priceCtrl.text =
found.purchasePrice!.toStringAsFixed(2);
}
}
});
},
validator: (v) => v == null ? '不能为空' : null,
decoration: const InputDecoration(),
),
),
),
Padding(
padding: const EdgeInsets.all(4),
child: TextFormField(
initialValue: item['name'] as String,
decoration:
const InputDecoration(hintText: '商品名称'),
style: const TextStyle(fontSize: 13),
onChanged: (v) => item['name'] = v,
validator: (v) =>
(v == null || v.isEmpty) ? '必填' : null,
),
),
// Spec
Padding(
padding: const EdgeInsets.all(4),
child: TextFormField(
initialValue: item['spec'] as String,
decoration: const InputDecoration(hintText: '规格'),
style: const TextStyle(fontSize: 13),
onChanged: (v) => item['spec'] = v,
),
),
// Unit
Padding(
padding: const EdgeInsets.all(4),
child: DropdownButtonFormField<String>(
value: item['unit'] as String,
items: ['', '', '', '', '']
.map((u) => DropdownMenuItem(
value: u,
child: Text(u, style: const TextStyle(fontSize: 13))))
.toList(),
onChanged: (v) => setState(() => item['unit'] = v!),
decoration: const InputDecoration(),
),
),
// Qty
Padding(
padding: const EdgeInsets.all(4),
child: TextFormField(
controller: qtyCtrl,
controller: item.qtyCtrl,
decoration: const InputDecoration(hintText: '0'),
style: const TextStyle(fontSize: 13),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))
FilteringTextInputFormatter.allow(
RegExp(r'^\d+\.?\d{0,2}'))
],
onChanged: (_) => setState(() {}),
validator: (v) {
if (v == null || v.isEmpty) return '必填';
if (v == null || v.isEmpty) return '不能为空';
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
return null;
},
),
),
// Price
Padding(
padding: const EdgeInsets.all(4),
child: TextFormField(
controller: priceCtrl,
controller: item.priceCtrl,
decoration: const InputDecoration(
hintText: '0.00', prefixText: '¥'),
style: const TextStyle(fontSize: 13),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))
FilteringTextInputFormatter.allow(
RegExp(r'^\d+\.?\d{0,2}'))
],
onChanged: (_) => setState(() {}),
validator: (v) {
if (v == null || v.isEmpty) return '必填';
if (v == null || v.isEmpty) return '不能为空';
if ((double.tryParse(v) ?? 0) <= 0) return '>0';
return null;
},
),
),
// Amount
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: Text(
@@ -482,13 +502,13 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
fontSize: 13, fontWeight: FontWeight.w500),
),
),
// Actions
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
child: IconButton(
icon: const Icon(Icons.delete_outline,
size: 18, color: AppTheme.danger),
onPressed: _items.length > 1 ? () => _removeItem(index) : null,
onPressed:
_items.length > 1 ? () => _removeItem(index) : null,
tooltip: '删除',
padding: EdgeInsets.zero,
constraints:
@@ -510,6 +530,17 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
}
}
class _ItemRow {
int? productId;
final TextEditingController qtyCtrl = TextEditingController();
final TextEditingController priceCtrl = TextEditingController();
void dispose() {
qtyCtrl.dispose();
priceCtrl.dispose();
}
}
class _FormField extends StatelessWidget {
final String label;
final Widget child;
@@ -534,7 +565,8 @@ class _FormField extends StatelessWidget {
children: [
if (required)
const Text('*',
style: TextStyle(color: AppTheme.danger, fontSize: 13)),
style:
TextStyle(color: AppTheme.danger, fontSize: 13)),
Text(label,
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
@@ -1,144 +1,43 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../widgets/page_scaffold.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/status_badge.dart';
import '../../core/theme/app_theme.dart';
import '../../models/stock_in.dart';
import '../../providers/stock_in_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/page_scaffold.dart';
import '../../widgets/status_badge.dart';
class StockInListScreen extends ConsumerStatefulWidget {
const StockInListScreen({super.key});
@override
ConsumerState<StockInListScreen> createState() =>
_StockInListScreenState();
ConsumerState<StockInListScreen> createState() => _StockInListScreenState();
}
class _StockInListScreenState extends ConsumerState<StockInListScreen> {
int _page = 1;
final _searchCtrl = TextEditingController();
String _statusFilter = '全部';
String _statusFilter = '';
DateTimeRange? _dateRange;
final List<Map<String, dynamic>> _mockOrders = [
{
'no': 'RK20260401001',
'supplier': '贵州茅台酒股份有限公司',
'warehouse': '主仓库',
'amount': '85000.00',
'status': OrderStatus.approved,
'creator': '张三',
'date': '2026-04-01',
},
{
'no': 'RK20260401002',
'supplier': '四川五粮液股份有限公司',
'warehouse': '副仓库',
'amount': '42500.00',
'status': OrderStatus.pending,
'creator': '李四',
'date': '2026-04-01',
},
{
'no': 'RK20260402001',
'supplier': '江苏洋河酒厂股份有限公司',
'warehouse': '主仓库',
'amount': '36800.00',
'status': OrderStatus.approved,
'creator': '王五',
'date': '2026-04-02',
},
{
'no': 'RK20260402002',
'supplier': '剑南春(集团)有限责任公司',
'warehouse': '主仓库',
'amount': '28600.00',
'status': OrderStatus.draft,
'creator': '张三',
'date': '2026-04-02',
},
{
'no': 'RK20260403001',
'supplier': '泸州老窖股份有限公司',
'warehouse': '副仓库',
'amount': '55200.00',
'status': OrderStatus.approved,
'creator': '李四',
'date': '2026-04-03',
},
{
'no': 'RK20260403002',
'supplier': '古井贡酒股份有限公司',
'warehouse': '主仓库',
'amount': '19800.00',
'status': OrderStatus.rejected,
'creator': '王五',
'date': '2026-04-03',
},
{
'no': 'RK20260403003',
'supplier': '贵州茅台酒股份有限公司',
'warehouse': '主仓库',
'amount': '126000.00',
'status': OrderStatus.pending,
'creator': '张三',
'date': '2026-04-03',
},
{
'no': 'RK20260404001',
'supplier': '山西汾酒股份有限公司',
'warehouse': '主仓库',
'amount': '33600.00',
'status': OrderStatus.approved,
'creator': '李四',
'date': '2026-04-04',
},
{
'no': 'RK20260404002',
'supplier': '郎酒股份有限公司',
'warehouse': '副仓库',
'amount': '47300.00',
'status': OrderStatus.draft,
'creator': '王五',
'date': '2026-04-04',
},
{
'no': 'RK20260404003',
'supplier': '四川五粮液股份有限公司',
'warehouse': '主仓库',
'amount': '68500.00',
'status': OrderStatus.pending,
'creator': '张三',
'date': '2026-04-04',
},
];
String? get _startDate => _dateRange != null
? '${_dateRange!.start.year}-${_dateRange!.start.month.toString().padLeft(2, '0')}-${_dateRange!.start.day.toString().padLeft(2, '0')}'
: null;
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
String? get _endDate => _dateRange != null
? '${_dateRange!.end.year}-${_dateRange!.end.month.toString().padLeft(2, '0')}-${_dateRange!.end.day.toString().padLeft(2, '0')}'
: null;
List<Map<String, dynamic>> _filteredOrders({OrderStatus? forceStatus}) {
return _mockOrders.where((o) {
if (forceStatus != null && o['status'] != forceStatus) return false;
if (_statusFilter != '全部' && forceStatus == null) {
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 supplier = (o['supplier'] as String).toLowerCase();
if (!no.contains(q) && !supplier.contains(q)) return false;
}
return true;
}).toList();
OrderStatus _apiStatusToEnum(String status) {
switch (status) {
case 'pending':
return OrderStatus.pending;
case 'approved':
return OrderStatus.approved;
case 'rejected':
return OrderStatus.rejected;
default:
return OrderStatus.draft;
}
}
@override
@@ -147,23 +46,62 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
title: '入库管理',
tabs: const [
Tab(text: '入库单'),
Tab(text: '入库查询'),
Tab(text: '入库审核'),
],
tabViews: [
_buildOrderList(),
_buildQueryView(),
_buildOrderList(forceStatus: OrderStatus.pending),
_buildListTab(filterStatus: null),
_buildListTab(filterStatus: 'pending'),
],
);
}
Widget _buildOrderList({OrderStatus? forceStatus}) {
final orders = _filteredOrders(forceStatus: forceStatus);
Widget _buildListTab({String? filterStatus}) {
final asyncOrders = ref.watch(stockInListProvider);
return asyncOrders.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('加载失败:$e',
style: const TextStyle(color: AppTheme.danger)),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () =>
ref.read(stockInListProvider.notifier).reload(),
child: const Text('重试'),
),
],
),
),
data: (result) {
// Client-side filter for review tab
final orders = filterStatus != null
? result.data
.where((o) => o.status == filterStatus)
.toList()
: result.data;
return _buildOrderTable(
orders: orders,
totalCount: filterStatus != null ? orders.length : result.total,
page: result.page,
showStatusFilter: filterStatus == null,
);
},
);
}
Widget _buildOrderTable({
required List<StockInOrder> orders,
required int totalCount,
required int page,
required bool showStatusFilter,
}) {
return DataTableCard(
totalCount: orders.length,
page: _page,
onPageChanged: (p) => setState(() => _page = p),
totalCount: totalCount,
page: page,
onPageChanged: (p) =>
ref.read(stockInListProvider.notifier).setPage(p),
toolbar: Row(
children: [
ElevatedButton.icon(
@@ -171,58 +109,41 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
icon: const Icon(Icons.add, size: 16),
label: const Text('新建入库单'),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: () {},
icon: const Icon(Icons.file_upload_outlined, 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(),
if (forceStatus == null) ...[
if (showStatusFilter) ...[
_StatusFilterDropdown(
value: _statusFilter,
onChanged: (v) => setState(() {
_statusFilter = v!;
_page = 1;
}),
onChanged: (v) {
setState(() => _statusFilter = v ?? '');
ref
.read(stockInListProvider.notifier)
.setStatus(v ?? '');
},
),
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),
),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: _pickDateRange,
icon: const Icon(Icons.date_range, size: 16),
label: Text(
_dateRange == null
? '选择日期'
: '${_dateRange!.start.toString().substring(0, 10)} ~ ${_dateRange!.end.toString().substring(0, 10)}',
: '$_startDate ~ $_endDate',
style: const TextStyle(fontSize: 13),
),
),
if (_dateRange != null) ...[
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.clear, size: 16),
onPressed: () {
setState(() => _dateRange = null);
ref
.read(stockInListProvider.notifier)
.setDateRange(null, null);
},
),
],
],
),
columns: const [
@@ -231,119 +152,75 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
DataColumn(label: Text('仓库')),
DataColumn(label: Text('金额'), numeric: true),
DataColumn(label: Text('状态')),
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(Text(o['supplier'] as String)),
DataCell(Text(o['warehouse'] as String)),
DataCell(Text('¥${o['amount']}')),
DataCell(StatusBadge(o['status'] as OrderStatus)),
DataCell(Text(o['creator'] as String)),
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))),
if ((o['status'] as OrderStatus) ==
OrderStatus.draft)
TextButton(
onPressed: () {},
child: const Text('删除',
style: TextStyle(
color: AppTheme.danger,
fontSize: 12))),
],
)),
]))
.toList(),
);
}
Widget _buildQueryView() {
return Column(
children: [
Container(
color: AppTheme.surface,
padding: const EdgeInsets.all(12),
child: Row(
children: [
const Text('查询条件:', style: TextStyle(fontSize: 13)),
const SizedBox(width: 8),
SizedBox(
width: 200,
child: TextField(
controller: _searchCtrl,
decoration: const InputDecoration(
hintText: '单号/供应商',
prefixIcon: Icon(Icons.search, size: 16),
hintStyle: TextStyle(fontSize: 13),
),
onChanged: (_) => setState(() => _page = 1),
),
),
const SizedBox(width: 8),
_StatusFilterDropdown(
value: _statusFilter,
onChanged: (v) => setState(() {
_statusFilter = v!;
_page = 1;
}),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: _pickDateRange,
icon: const Icon(Icons.date_range, size: 16),
label: Text(
_dateRange == null ? '选择日期范围' : '${_dateRange!.start.toString().substring(0, 10)} ~ ${_dateRange!.end.toString().substring(0, 10)}',
style: const TextStyle(fontSize: 13),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () => setState(() {}),
icon: const Icon(Icons.search, size: 16),
label: const Text('查询'),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: () => setState(() {
_searchCtrl.clear();
_statusFilter = '全部';
_dateRange = null;
}),
child: const Text('重置'),
),
],
),
),
const Divider(height: 1),
Expanded(child: _buildOrderList()),
],
rows: orders.isEmpty
? [
const DataRow(cells: [
DataCell(SizedBox()),
DataCell(Text('暂无入库单',
style: TextStyle(color: AppTheme.textSecondary))),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
])
]
: orders
.map((o) => DataRow(
cells: [
DataCell(Text(o.orderNo,
style: const TextStyle(
color: AppTheme.primary,
fontFamily: 'monospace',
fontSize: 12))),
DataCell(Text(o.partnerName ?? '-')),
DataCell(Text(o.warehouseName ?? '-')),
DataCell(Text(o.totalAmount != null
? '¥${o.totalAmount!.toStringAsFixed(2)}'
: '-')),
DataCell(StatusBadge(
_apiStatusToEnum(o.status))),
DataCell(Text(o.orderDate?.substring(0, 10) ?? '-')),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
if (o.status == 'draft')
TextButton(
onPressed: () =>
_confirmSubmit(context, o),
child: const Text('提交',
style: TextStyle(
fontSize: 12,
color: AppTheme.primary)),
),
if (o.status == 'pending') ...[
TextButton(
key: Key('btn_approve_${o.id}'),
onPressed: () =>
_confirmApprove(context, o),
child: const Text('通过',
style: TextStyle(
fontSize: 12,
color: AppTheme.success)),
),
TextButton(
key: Key('btn_reject_${o.id}'),
onPressed: () =>
_confirmReject(context, o),
child: const Text('拒绝',
style: TextStyle(
fontSize: 12,
color: AppTheme.danger)),
),
],
],
)),
],
))
.toList(),
);
}
@@ -354,7 +231,126 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
lastDate: DateTime(2030),
initialDateRange: _dateRange,
);
if (range != null) setState(() => _dateRange = range);
if (range != null) {
setState(() => _dateRange = range);
ref
.read(stockInListProvider.notifier)
.setDateRange(_startDate, _endDate);
}
}
Future<void> _confirmSubmit(BuildContext context, StockInOrder o) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('提交审核'),
content: Text('确认提交入库单「${o.orderNo}」?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消')),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('提交')),
],
),
);
if (confirmed == true && mounted) {
try {
await ref
.read(stockInListProvider.notifier)
.submitOrder(o.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('已提交审核'),
backgroundColor: AppTheme.success));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('提交失败:$e'),
backgroundColor: AppTheme.danger));
}
}
}
}
Future<void> _confirmApprove(BuildContext context, StockInOrder o) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('审核确认'),
content: Text('确认审核通过入库单「${o.orderNo}」?审核后将入库并增加库存。'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消')),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.success,
foregroundColor: Colors.white),
child: const Text('通过'),
),
],
),
);
if (confirmed == true && mounted) {
try {
await ref
.read(stockInListProvider.notifier)
.approveOrder(o.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('审核通过'), backgroundColor: AppTheme.success));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('审核失败:$e'),
backgroundColor: AppTheme.danger));
}
}
}
}
Future<void> _confirmReject(BuildContext context, StockInOrder o) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('拒绝确认'),
content: Text('确认拒绝入库单「${o.orderNo}」?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消')),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.danger,
foregroundColor: Colors.white),
child: const Text('拒绝'),
),
],
),
);
if (confirmed == true && mounted) {
try {
await ref
.read(stockInListProvider.notifier)
.rejectOrder(o.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('已拒绝'), backgroundColor: AppTheme.accent));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('操作失败:$e'),
backgroundColor: AppTheme.danger));
}
}
}
}
}
@@ -379,15 +375,16 @@ class _StatusFilterDropdown extends StatelessWidget {
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: value,
items: ['全部', '草稿', '待审核', '已审核', '已拒绝']
.map((s) => DropdownMenuItem(
value: s,
child: Text(s, style: const TextStyle(fontSize: 13))))
.toList(),
value: value.isEmpty ? '' : value,
items: [
const DropdownMenuItem(value: '', child: Text('全部状态', style: TextStyle(fontSize: 13))),
const DropdownMenuItem(value: 'draft', child: Text('草稿', style: TextStyle(fontSize: 13))),
const DropdownMenuItem(value: 'pending', child: Text('待审核', style: TextStyle(fontSize: 13))),
const DropdownMenuItem(value: 'approved', child: Text('已审核', style: TextStyle(fontSize: 13))),
const DropdownMenuItem(value: 'rejected', child: Text('已拒绝', style: TextStyle(fontSize: 13))),
],
onChanged: onChanged,
style: const TextStyle(
fontSize: 13, color: AppTheme.textPrimary),
style: const TextStyle(fontSize: 13, color: AppTheme.textPrimary),
),
),
);
@@ -1,9 +1,11 @@
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';
import '../../models/stock_out.dart';
import '../../providers/stock_out_provider.dart';
import '../../widgets/data_table_card.dart';
import '../../widgets/page_scaffold.dart';
import '../../widgets/status_badge.dart';
class StockOutListScreen extends ConsumerStatefulWidget {
const StockOutListScreen({super.key});
@@ -14,128 +16,28 @@ class StockOutListScreen extends ConsumerStatefulWidget {
}
class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
int _page = 1;
final _searchCtrl = TextEditingController();
String _statusFilter = '全部';
String _typeFilter = '全部';
String _statusFilter = '';
DateTimeRange? _dateRange;
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': '领用',
},
];
String? get _startDate => _dateRange != null
? '${_dateRange!.start.year}-${_dateRange!.start.month.toString().padLeft(2, '0')}-${_dateRange!.start.day.toString().padLeft(2, '0')}'
: null;
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
String? get _endDate => _dateRange != null
? '${_dateRange!.end.year}-${_dateRange!.end.month.toString().padLeft(2, '0')}-${_dateRange!.end.day.toString().padLeft(2, '0')}'
: null;
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();
OrderStatus _apiStatusToEnum(String status) {
switch (status) {
case 'pending':
return OrderStatus.pending;
case 'approved':
return OrderStatus.approved;
case 'rejected':
return OrderStatus.rejected;
default:
return OrderStatus.draft;
}
}
@override
@@ -144,28 +46,61 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
title: '出库管理',
tabs: const [
Tab(text: '出库单'),
Tab(text: '出库查询'),
Tab(text: '出库审核'),
],
tabViews: [
_buildList(),
_buildList(),
_buildList(forceStatus: OrderStatus.pending),
_buildListTab(filterStatus: null),
_buildListTab(filterStatus: 'pending'),
],
);
}
Widget _buildList({OrderStatus? forceStatus}) {
final orders = forceStatus != null
? _mockOrders
.where((o) => o['status'] == forceStatus)
.toList()
: _filtered;
Widget _buildListTab({String? filterStatus}) {
final asyncOrders = ref.watch(stockOutListProvider);
return asyncOrders.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('加载失败:$e',
style: const TextStyle(color: AppTheme.danger)),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () =>
ref.read(stockOutListProvider.notifier).reload(),
child: const Text('重试'),
),
],
),
),
data: (result) {
final orders = filterStatus != null
? result.data
.where((o) => o.status == filterStatus)
.toList()
: result.data;
return _buildOrderTable(
orders: orders,
totalCount: filterStatus != null ? orders.length : result.total,
page: result.page,
showStatusFilter: filterStatus == null,
);
},
);
}
Widget _buildOrderTable({
required List<StockOutOrder> orders,
required int totalCount,
required int page,
required bool showStatusFilter,
}) {
return DataTableCard(
totalCount: orders.length,
page: _page,
onPageChanged: (p) => setState(() => _page = p),
totalCount: totalCount,
page: page,
onPageChanged: (p) =>
ref.read(stockOutListProvider.notifier).setPage(p),
toolbar: Row(
children: [
ElevatedButton.icon(
@@ -173,110 +108,136 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
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(
if (showStatusFilter) ...[
_StatusFilterDropdown(
value: _statusFilter,
items: ['全部', '草稿', '待审核', '已审核', '已拒绝'],
onChanged: (v) => setState(() => _statusFilter = v!),
hint: '状态',
onChanged: (v) {
setState(() => _statusFilter = v ?? '');
ref
.read(stockOutListProvider.notifier)
.setStatus(v ?? '');
},
),
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),
const SizedBox(width: 8),
],
OutlinedButton.icon(
onPressed: _pickDateRange,
icon: const Icon(Icons.date_range, size: 16),
label: Text(
_dateRange == null
? '选择日期'
: '$_startDate ~ $_endDate',
style: const TextStyle(fontSize: 13),
),
),
if (_dateRange != null) ...[
const SizedBox(width: 4),
IconButton(
icon: const Icon(Icons.clear, size: 16),
onPressed: () {
setState(() => _dateRange = null);
ref
.read(stockOutListProvider.notifier)
.setDateRange(null, null);
},
),
],
],
),
columns: const [
DataColumn(label: Text('出库单号')),
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))),
rows: orders.isEmpty
? [
const DataRow(cells: [
DataCell(SizedBox()),
DataCell(Text('暂无出库单',
style: TextStyle(color: AppTheme.textSecondary))),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
])
]
: orders
.map((o) => DataRow(
cells: [
DataCell(Text(o.orderNo,
style: const TextStyle(
color: AppTheme.primary,
fontFamily: 'monospace',
fontSize: 12))),
DataCell(Text(o.partnerName ?? '-')),
DataCell(Text(o.warehouseName ?? '-')),
DataCell(Text(o.totalAmount != null
? '¥${o.totalAmount!.toStringAsFixed(2)}'
: '-')),
DataCell(StatusBadge(
_apiStatusToEnum(o.status))),
DataCell(Text(o.orderDate?.substring(0, 10) ?? '-')),
DataCell(Row(
mainAxisSize: MainAxisSize.min,
children: [
if (o.status == 'draft')
TextButton(
onPressed: () =>
_confirmSubmit(context, o),
child: const Text('提交',
style: TextStyle(
fontSize: 12,
color: AppTheme.primary)),
),
if (o.status == 'pending') ...[
TextButton(
key: Key('btn_approve_${o.id}'),
onPressed: () =>
_confirmApprove(context, o),
child: const Text('通过',
style: TextStyle(
fontSize: 12,
color: AppTheme.success)),
),
TextButton(
key: Key('btn_reject_${o.id}'),
onPressed: () =>
_confirmReject(context, o),
child: const Text('拒绝',
style: TextStyle(
fontSize: 12,
color: AppTheme.danger)),
),
],
],
)),
],
],
)),
]))
.toList(),
))
.toList(),
);
}
Future<void> _pickDateRange() async {
final range = await showDateRangePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
initialDateRange: _dateRange,
);
if (range != null) {
setState(() => _dateRange = range);
ref
.read(stockOutListProvider.notifier)
.setDateRange(_startDate, _endDate);
}
}
void _showCreateDialog(BuildContext context) {
showDialog(
context: context,
@@ -284,63 +245,144 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
title: const Text('新建出库单'),
content: const SizedBox(
width: 400,
child: Text('出库单创建功能完整实现请参考入库单流程。'),
child: Text('出库单完整创建功能请参考入库单流程。'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('关闭')),
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)),
Future<void> _confirmSubmit(
BuildContext context, StockOutOrder o) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('提交审核'),
content: Text('确认提交出库单「${o.orderNo}」?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消')),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('提交')),
],
),
child: Text(type,
style: TextStyle(
color: color, fontSize: 12, fontWeight: FontWeight.w500)),
);
if (confirmed == true && mounted) {
try {
await ref
.read(stockOutListProvider.notifier)
.submitOrder(o.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('已提交审核'),
backgroundColor: AppTheme.success));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('提交失败:$e'),
backgroundColor: AppTheme.danger));
}
}
}
}
Future<void> _confirmApprove(
BuildContext context, StockOutOrder o) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('审核确认'),
content: Text('确认审核通过出库单「${o.orderNo}」?审核后将出库并扣减库存。'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消')),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.success,
foregroundColor: Colors.white),
child: const Text('通过'),
),
],
),
);
if (confirmed == true && mounted) {
try {
await ref
.read(stockOutListProvider.notifier)
.approveOrder(o.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('审核通过'), backgroundColor: AppTheme.success));
}
} catch (e) {
// Show inventory-shortage error clearly
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('审核失败:$e'),
backgroundColor: AppTheme.danger,
duration: const Duration(seconds: 5)));
}
}
}
}
Future<void> _confirmReject(
BuildContext context, StockOutOrder o) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('拒绝确认'),
content: Text('确认拒绝出库单「${o.orderNo}」?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('取消')),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.danger,
foregroundColor: Colors.white),
child: const Text('拒绝'),
),
],
),
);
if (confirmed == true && mounted) {
try {
await ref
.read(stockOutListProvider.notifier)
.rejectOrder(o.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('已拒绝'), backgroundColor: AppTheme.accent));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('操作失败:$e'),
backgroundColor: AppTheme.danger));
}
}
}
}
}
class _DropdownFilter extends StatelessWidget {
class _StatusFilterDropdown extends StatelessWidget {
final String value;
final List<String> items;
final ValueChanged<String?> onChanged;
final String hint;
const _DropdownFilter({
const _StatusFilterDropdown({
required this.value,
required this.items,
required this.onChanged,
required this.hint,
});
@override
@@ -355,16 +397,16 @@ class _DropdownFilter extends StatelessWidget {
),
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(),
value: value.isEmpty ? '' : value,
items: const [
DropdownMenuItem(value: '', child: Text('全部状态', style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'draft', child: Text('草稿', style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'pending', child: Text('待审核', style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'approved', child: Text('已审核', style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'rejected', child: Text('已拒绝', style: TextStyle(fontSize: 13))),
],
onChanged: onChanged,
style: const TextStyle(
fontSize: 13, color: AppTheme.textPrimary),
style: const TextStyle(fontSize: 13, color: AppTheme.textPrimary),
),
),
);