/// 登录流程的当前步骤 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'; } }