- 移除 @riverpod/@freezed 注解依赖,全部改为手写 Provider(无需 build_runner) · LoginState 改为纯 Dart,LoginViewModel/ThemeViewModel/ChatViewModel 改为 Notifier · UserNotifier 改为 FamilyAsyncNotifier<User?,int>,mini_app_provider 改为手写 Provider · 15 个 StreamProvider/StreamProvider.family 从 @riverpod 迁移至手写 - 发送消息(#25) · SendMessageRequest/SendMessageResponse DTO · SendMessageUseCase:乐观写入 DB → HTTP POST → 更新 Chat 摘要 - 接收消息 WS(#26) · WsMessageService:监听 mode2 WS 帧 → HTTP 补拉 → DB 写入 → Chat 更新 · FetchHistoryRequest/FetchHistoryResponse DTO(GET /app/api/chat/history) · FetchHistoryUseCase:拉取 → insertOrReplaceAll - DI 装配(chat_service_providers.dart) · wsMessageServiceProvider、sendMessageUseCaseProvider、fetchHistoryUseCaseProvider - 聊天列表页(#27) · ChatListViewModel(Notifier<void>)+ chat_page.dart 真实会话列表 UI · ListTile:头像首字母、最新消息摘要、未读角标、时间格式化 - 聊天详情页(#28) · ChatDetailViewModel(FamilyNotifier<ChatDetailState,int>)+ chat_detail_page.dart · 消息气泡(自己/他人分左右)、底部输入框、发送状态与错误提示 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
63 lines
1.5 KiB
Dart
63 lines
1.5 KiB
Dart
/// 登录流程的当前步骤
|
||
enum LoginStep {
|
||
/// 步骤 1:输入手机号
|
||
phone,
|
||
|
||
/// 步骤 2:输入验证码
|
||
otp,
|
||
}
|
||
|
||
/// 登录页面状态(手动 copyWith)
|
||
///
|
||
/// ViewModel 通过 `state = state.copyWith(...)` 更新状态,
|
||
/// View 通过 `ref.watch(loginViewModelProvider)` 自动响应变化。
|
||
class LoginState {
|
||
const LoginState({
|
||
this.step = LoginStep.phone,
|
||
this.countryCode = '+65',
|
||
this.contact = '',
|
||
this.isLoading = false,
|
||
this.error,
|
||
});
|
||
|
||
/// 当前步骤(手机号输入 or 验证码输入)
|
||
final LoginStep step;
|
||
|
||
/// 国家代码(默认 +65,暂不支持切换)
|
||
final String countryCode;
|
||
|
||
/// 已提交的手机号(步骤 2 用于显示和构建请求)
|
||
final String contact;
|
||
|
||
/// 是否正在请求中
|
||
final bool isLoading;
|
||
|
||
/// 错误信息(null = 无错误)
|
||
final String? error;
|
||
|
||
LoginState copyWith({
|
||
LoginStep? step,
|
||
String? countryCode,
|
||
String? contact,
|
||
bool? isLoading,
|
||
String? error,
|
||
bool clearError = false,
|
||
}) {
|
||
return LoginState(
|
||
step: step ?? this.step,
|
||
countryCode: countryCode ?? this.countryCode,
|
||
contact: contact ?? this.contact,
|
||
isLoading: isLoading ?? this.isLoading,
|
||
error: clearError ? null : (error ?? this.error),
|
||
);
|
||
}
|
||
|
||
/// 步骤 2 显示的脱敏手机号,如 "138****0000"
|
||
String get maskedContact {
|
||
if (contact.length <= 4) return contact;
|
||
final tail = contact.substring(contact.length - 4);
|
||
final stars = '*' * (contact.length - 4);
|
||
return '$stars$tail';
|
||
}
|
||
}
|