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: '门店编号',