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