从账号管理 三个入口共用的一个页面(TAPD #1003149),靠 CredentialMode 枚举(changePassword / registerEmail / deleteAccount)区分三种场景——本质上都是"改一个敏感字段,必须先过一次邮箱验证码"这同一个模式。改密码/注册邮箱走"表单 → 验证码"两步;注销账号没有表单,进页即直接向账号邮箱发验证码,跳过表单步骤。本页不是独立路由,与 烹饪日志详情 / 帮助详情 一样,靠 Navigator.push 一个未命名的 MaterialPageRoute 打开。
lib/features/auth/presentation/account_credential_page.dart(384 行)authControllerProvider(startCredentialUpdate / resendCredentialCode / confirmCredentialUpdate / startAccountDeletion / confirmAccountDeletion)wipeAllLocalData(ref)(lib/core/providers/device_providers.dart,用于账号删除相关的全量本地清理——比登出时的 wipeLocalDeviceData 更彻底,连 App 偏好设置也一并清空;代码注释还明确说明,它也会用于“另一台手机发现该账号已被删除”时的对端清理路径。)PasswordPolicy.validate、EmailCodeThrottle.remainingAuthScaffold / AuthField / PrimaryAuthButton / PolicyFooterMaterialPageRoute 打开,mode 是唯一构造参数;其中 changePassword / registerEmail 入口是直接 push,但 deleteAccount 入口会先弹出确认对话框,用户确认后才 push(AccountCredentialPage(mode: CredentialMode.deleteAccount))。按 mode 与 _step(form / code)分支:
initState 会先把 _step 切到 code、记录 _lastSentAt = DateTime.now() 并启动倒计时,然后在首帧回调里调用 _startDelete() 发码;只有发码成功后才会把发送目标写入 _sentTo,失败则直接 pop 返回上一页deleteAccount 的 code 步标题会切到专门的删除验证标题;changePassword 和 registerEmail 的 code 步共用同一个通用验证码标题。补充:所有进入 code 步的 "Verify" 提交(包括改密码、注册邮箱、注销账号)都会先在本地检查验证码长度是否正好为 6 位;如果不足 6 位,会直接弹出 authCodeIncompleteHint 的 SnackBar 并返回,不会调用 confirmCredentialUpdate 或 confirmAccountDeletion。
_submitForm()
→ registerEmail 模式:邮箱非空+含'@'校验
→ PasswordPolicy.validate(password) 不通过 → 显示对应错误
→ 两次密码不一致 → 显示 authPwMismatch
→ authControllerProvider.notifier.startCredentialUpdate(newEmail, newPassword)
→ 成功 → step = code,启动倒计时
resendCredentialCode(),与其它两处验证码页同一套节流阶梯_verify()
→ authControllerProvider.notifier.confirmCredentialUpdate(code)
→ 成功 → Navigator.pop(context) // 回到账号管理页
→ SnackBar 提示"密码已修改" 或 "邮箱已注册"(按 mode 区分文案)
AuthState.user 刷新(邮箱/用户名可能变化),session 保持已登录,不需要重新登录initState 首帧回调_startDelete()
→ authControllerProvider.notifier.startAccountDeletion()
→ 失败 → 直接 pop 回账号管理页(listener 已经弹出错误提示)
→ 成功 → 记下发送目标邮箱,进入 code 步
补充:删除成功后调用的 wipeAllLocalData(ref) 不只是清 SharedPreferences。它一开始会先执行内部 _teardownLocalDevices(ref):停止 deviceBindingRealtimeProvider 的跨手机设备同步、调用 deviceConnectionManagerProvider.stopAll() 关闭本机所有设备管理,再把当前已知/已显示的设备逐个 markForgotten、移出 BLE 重连队列并异步 disconnectDevice;随后清掉各设备的本地 Wi‑Fi 残留并 known.clear(),最后 boosters.clearAllLocal() 让当前运行中的设备卡片立刻消失。因此,账号删除会立即拆掉本机的 BLE/云连接态与设备列表,而不是只等下次启动后才反映。
补充:账号删除成功后的 wipeAllLocalData(ref) 还会先显式执行 cookLogProvider.notifier.clearAllLocal(),把本地烹饪日志列表及其同步账本一并清空;这不是只靠 prefs.clear() 间接生效,而是为了让当前这次运行中的 UI 也立刻不再显示已删除账号的历史 cook log。
_verify()(_isDelete 分支)
→ authControllerProvider.notifier.confirmAccountDeletion(code)
→ 成功:
→ wipeAllLocalData(ref) // TAPD #1003353:连 App 偏好设置也清空,回到接近首装状态
→ Navigator.popUntil(isFirst) // 弹出整个账号管理路由栈,不只是回到上一页
→ SnackBar 提示账号已注销
wipeAllLocalData(ref) 在 prefs.clear() 之后,还会立即重新初始化区域与温度单位,并 invalidate(appLanguageProvider),所以当前这次运行中的 UI 也会立刻回到接近首装的默认区域/单位/语言状态,而不只是下次启动后才生效。watch authControllerProvider 读 state.busy / state.debugCode / state.user(改密码步的当前邮箱副标题);ref.listen 统一弹错误 SnackBar。_step 是页面的本地可变状态;mode 是 AccountCredentialPage 的必填构造字段(另有可选 key 参数)。重发倒计时靠本地 Timer.periodic。
无。
SharedPreferences(语言、单位、通知设置、引导标记等)。两条路径分别调用 wipeLocalDeviceData 与 wipeAllLocalData 两个不同深度的清理函数,命名上容易混淆,实现上是刻意区分的。