/// 登录流程的当前步骤 enum LoginStep { /// 步骤 1:输入手机号 phone, /// 步骤 2:输入验证码 otp, /// 步骤 3(可选):输入二级密码(账号已设置时触发) secondaryPasscode, } /// 登录页面状态(手动 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, this.vcodeToken = '', this.passcodeHint = '', this.recoveryEmail = '', }); /// 当前步骤(手机号输入 / 验证码输入 / 二级密码输入) final LoginStep step; /// 国家代码(默认 +65,暂不支持切换) final String countryCode; /// 已提交的手机号(步骤 2 / 3 用于显示和构建请求) final String contact; /// 是否正在请求中 final bool isLoading; /// 错误信息(null = 无错误) final String? error; /// vcode_token(OTP 验证通过后服务端下发,二级密码步骤需携带) final String vcodeToken; /// 用户设置的二级密码提示语(步骤 3 显示) final String passcodeHint; /// 脱敏找回邮箱(步骤 3 "忘记密码" 提示用) final String recoveryEmail; LoginState copyWith({ LoginStep? step, String? countryCode, String? contact, bool? isLoading, String? error, bool clearError = false, String? vcodeToken, String? passcodeHint, String? recoveryEmail, }) { return LoginState( step: step ?? this.step, countryCode: countryCode ?? this.countryCode, contact: contact ?? this.contact, isLoading: isLoading ?? this.isLoading, error: clearError ? null : (error ?? this.error), vcodeToken: vcodeToken ?? this.vcodeToken, passcodeHint: passcodeHint ?? this.passcodeHint, recoveryEmail: recoveryEmail ?? this.recoveryEmail, ); } /// 步骤 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'; } }