feat(mine): 我的 Tab 全量实现 (#5~#13)

从 im-client-ios-swift-demo 搬运 Settings 逻辑,对齐 Gitea issue #5–#13

## 基础设施
- AuthNotifier 新增 currentUid 字段,login() 接受 uid 参数 (#5)
- LoginViewModel 登录成功后传入 user.uid
- ApiPaths 补充 account/block/store 系列路径
- Tab 重命名"设置"→"我的",icon 改为 person_outline (#5)
- AppRouteName 新增5条子路由 (edit-profile/blocklist/language/network-diagnostics/about)
- app_router + auth_guard 同步注册新路由

## Settings Feature
- SettingsViewModel 重写为 NotifierProvider(去除 @riverpod 依赖)
  - build() 自动触发 loadProfile()
  - logout() 完整流程:API → WS 断开 → DB 关闭 → AuthNotifier
  - 6 个 navigateTo* 方法
- SettingsPage 完整 UI:资料卡 / 偏好设置 / 工具 / 关于 / 退出登录按钮 (#5 #7)
- FetchProfileUseCase: GET /app/api/user/profile (#5)
- LogoutUseCase: logout + disconnect + closeDatabase (#7)
- UpdateProfileUseCase + UpdateProfileRequest: POST /app/api/user/update-profile (#6)
- EditProfilePage + EditProfileViewModel: 昵称/bio 编辑 (#6)
- LanguagePage: 语言选择 UI 框架,l10n_sdk 待接入 (#9)
- BlocklistPage: 黑名单框架,API 待实现 (#10)
- NetworkDiagnosticsPage + ViewModel: 四步诊断(连通/TCP/DNS/HTTPS)(#12)
- AboutPage: 版本号 + 服务条款/隐私政策入口 (#13)
- settings_providers.dart: 扩展 DI 装配

## 文档
- Doc/mine_tab_architecture.md: 架构说明、数据流、路由、待完成事项

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
pp-bot
2026-03-23 17:20:51 +09:00
parent 33c31b87ac
commit aeeda6f059
22 changed files with 1621 additions and 37 deletions

View File

@@ -0,0 +1,33 @@
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/data/remote/get_profile_request.dart';
/// 获取当前用户资料用例
///
/// 调用 GET /app/api/user/profile服务端通过 JWT token 识别当前用户。
///
/// ## 数据流位置
///
/// ```
/// SettingsViewModel.loadProfile()
/// → ★ FetchProfileUseCase.execute() ★ ← 你在这里
/// → NetworksSdkApi.executeRequest(GetProfileRequest())
/// → 服务端 GET /app/api/user/profile
/// ← ProfileResponse → User
/// ```
class FetchProfileUseCase {
final NetworksSdkApi _client;
const FetchProfileUseCase({required NetworksSdkApi client}) : _client = client;
/// 获取当前登录用户的资料
///
/// 抛出 [ApiError] 或 [Exception] 时由调用方处理。
Future<ProfileResponse> execute() async {
final response = await _client.executeRequest(GetProfileRequest());
if (response == null) {
throw Exception('获取资料失败:服务端返回空数据');
}
return response;
}
}

View File

@@ -0,0 +1,54 @@
import 'package:storage_sdk/storage_sdk.dart';
import 'package:im_app/core/services/socket_manager.dart';
import 'package:im_app/domain/repositories/auth_repository.dart';
/// 退出登录用例
///
/// 封装完整登出流程:
/// 1. 调用服务端 /app/api/auth/logout清除 token
/// 2. 断开 WebSocket 连接
/// 3. 关闭本地数据库StorageSdk
///
/// AuthNotifier.logout() 由 SettingsViewModel 在 UseCase 完成后调用,
/// 触发 go_router 重定向至登录页。
///
/// ## 数据流位置
///
/// ```
/// SettingsViewModel.logout()
/// → ★ LogoutUseCase.execute() ★ ← 你在这里
/// → AuthRepository.logout() → POST /app/api/auth/logout
/// → SocketManager.disconnect()
/// → StorageSdkLifecycle.closeDatabase()
/// → AuthNotifier.logout() → 路由跳转 /login
/// ```
class LogoutUseCase {
final AuthRepository _authRepository;
final SocketManager _socketManager;
final StorageSdkApi _storageApi;
StorageSdkLifecycle get _storageLifecycle => _storageApi as StorageSdkLifecycle;
const LogoutUseCase({
required AuthRepository authRepository,
required SocketManager socketManager,
required StorageSdkApi storageApi,
}) : _authRepository = authRepository,
_socketManager = socketManager,
_storageApi = storageApi;
/// 执行完整登出流程
///
/// 抛出异常时,调用方仍应调用 AuthNotifier.logout() 确保本地状态清除。
Future<void> execute() async {
// 1. 服务端登出(清除 token
await _authRepository.logout();
// 2. 断开 WebSocket
await _socketManager.disconnect();
// 3. 关闭本地数据库
await _storageLifecycle.closeDatabase();
}
}

View File

@@ -0,0 +1,33 @@
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/data/remote/update_profile_request.dart';
/// 更新用户资料用例
///
/// ## 数据流位置
///
/// ```
/// EditProfileViewModel.save()
/// → ★ UpdateProfileUseCase.execute() ★ ← 你在这里
/// → NetworksSdkApi.executeRequest(UpdateProfileRequest)
/// → 服务端 POST /app/api/user/update-profile
/// ```
class UpdateProfileUseCase {
final NetworksSdkApi _client;
const UpdateProfileUseCase({required NetworksSdkApi client}) : _client = client;
Future<void> execute({
required String nickname,
String? bio,
String? profilePicUrl,
}) async {
await _client.executeRequest(
UpdateProfileRequest(
nickname: nickname,
bio: bio,
profilePic: profilePicUrl,
),
);
}
}