docs: 迁入第一迭代过程报告并建立进展看板
- 新增 development/iterations/iteration-1/:15 份角色报告 + 进展看板(已完成/未闭环/下一步),作为双人协作的进度事实来源 - 新增 ADR-006:测试与交付容器化策略(Testcontainers / 交付 Docker 包 / 本机库仅个人联调) - Git 工作流规范补充:敏感信息只进忽略文件或 sample、测试数据不入库、测试代码限标准测试目录 - 门禁:mkdocs build --strict 通过(零警告)
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
# 12 · UI 设计还原度 QA 与登录页组装稿
|
||||
|
||||
> 作者:UI Designer
|
||||
> 日期:2026-09-04
|
||||
> 依据:04-ui-login-design-spec.md(下称「04 规范」)、08-flutter-theme-report.md、`AI宠物_iOS_UI设计稿.html`(品牌正典)、`patbond-flutter` 工作区实现代码(未提交 diff)
|
||||
> 性质:只读 QA + 组装级设计稿;需修正项由后续工单执行,本轮不改代码
|
||||
|
||||
---
|
||||
|
||||
## 1. QA 总结论
|
||||
|
||||
实现质量高,还原度好于预期。逐项对照后:
|
||||
|
||||
- **需修正:2 项**(1 项视觉回归 + 1 项主题遗漏),另有 1 项既有设计债列入规范修订。
|
||||
- **可接受偏离:3 项**(含已声明的 `minimumSize`,裁决为采纳并正式写入规范)。
|
||||
- **最严重项**:首页促销卡换用 `brandGradient` 后,白色文字对比度从旧靛蓝渐变的 ≥4.5:1 跌至 1.75–2.75:1,是本次换色**新引入**的可读性回归(见 §3 FIX-1)。
|
||||
- 色值零误差:`app_theme.dart` 全部 token 与 04 规范 §1.1 及 HTML 正典逐字节一致(用正典 HTML 提取的 19 个 hex 值交叉验证)。
|
||||
- 5 个登录组件的构造参数足以照 §5 组装稿直接拼装登录/注册/Splash,无需先改组件(一处失焦校验用 `Focus` 包裹解决,见 §5.4)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计 QA 偏差清单
|
||||
|
||||
### 2.1 主题 token(`lib/core/theme/app_theme.dart`)
|
||||
|
||||
| # | 检查项 | 规范值 | 实现值 | 判定 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| T1 | `primary` | `#FF6F4C` | `0xFFFF6F4C`,注释明确"不用于承载文字的实心填充" | 符合 |
|
||||
| T2 | `primaryStrong` / `primaryDark` | `#D6431A` / `#7A2E12` | 一致 | 符合 |
|
||||
| T3 | `accent` / `accentDark` | `#FFB648` / `#7A4B0A` | 一致 | 符合 |
|
||||
| T4 | `canvas` / `surface` / `surfaceTint` | `#FFF7ED` / `#FFFFFF` / `#FFE8D6` | 一致;`scaffoldBackgroundColor: canvas` | 符合 |
|
||||
| T5 | `ink` / `muted` / `border` | `#3E2A1F` / `#9C8977` / `#F0DCC8` | 一致 | 符合 |
|
||||
| T6 | 成功色族 | `success #7FA88A` / `successInk #3F5744` / `successSurface #E8F0E8` | 一致(`successInk`/`successSurface` 为合理的 token 化拆分) | 符合 |
|
||||
| T7 | `error` | `#D0342C` | 一致,且 `colorScheme.error`/`onError` 显式覆盖 | 符合 |
|
||||
| T8 | `brandGradient` | 135° `#FF6F4C → #FFB648` | `topLeft → bottomRight`(即 135°),色一致 | 符合 |
|
||||
| T9 | 圆角 token | 12/16/18/24/999 | `AppRadius.sm/md/lg/xl/pill` 一致 | 符合 |
|
||||
| T10 | 字号层级 | 22/w800、18/w800、15/w700、14/1.5、12/1.4 muted | `textTheme` 一致;输入框内文字走 M3 `bodyLarge` 默认 16,满足 §1.2"输入框内 16" | 符合 |
|
||||
| T11 | `filledButtonTheme` | 高 52、圆角 16、`primaryStrong` 填充、白字 15/w700、disabled = onSurface 12%/38% | 全部落实;disabled 用 `ink.withAlpha(31/97)`(=12.2%/38%,以 ink 代 onSurface 更贴暖色系) | 符合 |
|
||||
| T12 | `minimumSize` | 规范建议 `Size.fromHeight(52)` | `Size(64, 52)`(已声明偏离) | **可接受偏离,采纳为规范修订值**(裁决见 §2.4) |
|
||||
| T13 | `errorBorder` / `focusedErrorBorder` / `errorStyle` | error 1.5px / error 1.5px / 12px error 色 | 全部一致 | 符合 |
|
||||
| T14 | `helperStyle` | §3.1 注册页 helperText"12 muted" | **未定义**,将回落到 M3 默认(bodySmall + 种子生成的 onSurfaceVariant,非 `muted #9C8977`) | **需修正**(FIX-2,一行改动) |
|
||||
| T15 | `ColorScheme.fromSeed` 派生组件色 | 规范未逐一指定 | Chip/SegmentedButton 等用种子生成暖调色板 | 可接受偏离(登录纵切不涉及;哪个组件刺眼就补哪个 componentTheme,不整体重调) |
|
||||
| T16 | 品牌字体 Baloo 2 / 圆润中文标题体 | 规范允许后置 | 未引入,`BrandMark` 用系统字体 32/w700 | 可接受偏离(04 规范 §1.2 原文允许) |
|
||||
|
||||
### 2.2 五个组件(`lib/core/widgets/`)
|
||||
|
||||
| # | 组件 · 检查项 | 判定 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| C1 | `AppTextField` 错误态 | 符合 | `errorText` 由 `InputDecoration` 原生渲染(TalkBack/VoiceOver 自动关联),描边走主题 errorBorder;有测试 |
|
||||
| C2 | `AppTextField` 禁用态 | 符合 | `enabled=false` 时整体 `Opacity 0.6`,对齐 §4.1"60% 不透明度" |
|
||||
| C3 | `AppTextField` 密码切换 | 符合 | `obscurable` 默认遮蔽;切换按钮 `constraints 44×44`、带中文 tooltip、禁用时同步禁用;有测试锁定切换行为 |
|
||||
| C4 | `PrimaryButton` loading | 符合 | 20×20 白圈 strokeWidth 2.5、外层 SizedBox 锁 52 高尺寸不变、`onPressed` 置 null 锁点击、`disabledBackgroundColor: primaryStrong` 保持珊瑚填充(§4.3"loading 不是置灰"),测试逐项锁定 |
|
||||
| C5 | `PrimaryButton` 禁用 | 符合 | `onPressed: null` 走主题 disabled(ink 12%/38%),符合 §4.4 |
|
||||
| C6 | `InlineErrorBanner` | 符合 | error 底 `withAlpha(20)`(=7.84%,即 8%×255=20.4 的正确取整)、`radiusSm`、padding 12、`error_outline` 18 + 13px error 字、全宽 |
|
||||
| C7 | `BrandMark` | 符合 | logo 72(可参数化)+ brandGradient `radiusXl` 圆底 + `Icons.pets` 占位(§2.1 允许 v1 替代);字标 32/w700 primary;slogan 14 muted 可传 null——正好满足 Splash/登录共用与 §5.2 过渡前提。字标 primary 于 canvas 上约 2.6:1,属品牌字标(logo 豁免),与规范原文一致 |
|
||||
| C8 | `AuthScaffold` | 符合,附组装约束 | SafeArea + 滚动 + 水平 padding 24 + Scaffold 默认键盘避让。注意:`ConstrainedBox` 只给 **minHeight**,子 Column 高度仍无上界,**页面内不能用 `Spacer`/`Expanded` 做弹性空间**(会布局异常),垂直居中必须用 `mainAxisAlignment.center` + 固定间距——§5 组装稿已按此约束编写,不算缺陷 |
|
||||
| C9 | 组件缺口:`AppTextField` 未暴露 `focusNode`/`validator` | 可接受 | 失焦校验用外层 `Focus(onFocusChange:)` 包裹即可(见 §5.4),v1 不需改组件;若后续表单变多,建议加 `focusNode` 参数(建议级,非工单) |
|
||||
|
||||
### 2.3 需修正项汇总(供开工单)
|
||||
|
||||
| 编号 | 严重度 | 位置 | 问题 | 修正建议 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **FIX-1** | 高(视觉回归) | `lib/features/home/home_page.dart` `_PromoCard`(约 619 行起) | 促销卡底从旧靛蓝深色渐变换成 `brandGradient`(亮橙→亮琥珀)后,卡上 17px/w800 白色标题与 12px 白色副文字对比度仅 2.75:1(primary 端)至 1.75:1(accent 端),17px 加粗未达 large-text 门槛(18.7px),全部不达 AA。这也违反 04 规范 §1.1 自己的约定"brandGradient 不承载正文文字"。换色前白字在靛蓝上是达标的,属**本次迁移新引入的回归** | 促销卡渐变改为 `LinearGradient(colors: [AppColors.primaryStrong, AppColors.primary])`(文字在左侧 primaryStrong 端,白字 4.5:1 达标;右侧 primary 端只放"去使用"白底按钮与装饰)。备选:整卡 `primaryStrong` 纯色 + accent 装饰爪印。`brandGradient` 本身不动,故事环等纯装饰用法不受影响 |
|
||||
| **FIX-2** | 低(一行) | `app_theme.dart` `inputDecorationTheme` | 缺 `helperStyle`,注册页密码 helperText 颜色将是种子派生灰而非 `muted` | 补 `helperStyle: TextStyle(color: AppColors.muted, fontSize: 12)` |
|
||||
| **DEBT-1** | 低(既有设计债,不阻塞纵切) | `lib/widgets/common.dart` `TagPill` | 11px/w700 文字直接用传入色:默认 `primary`(2.75:1)与服务页 `success` sage(约 2.4:1)都不达标。08 报告第 6 条已指出 sage 一例并正确地未擅改——责任在规范本身,本报告在此修订:**TagPill 应"底用 color 8%、文字用配套深变体"**(primary→`primaryDark`、success→`successInk`、accent→`accentDark`) | 给 `TagPill` 增加可选 `inkColor` 参数(默认按上述映射),另开工单,与登录纵切解耦 |
|
||||
|
||||
### 2.4 已声明偏离的设计裁决:`minimumSize: Size(64, 52)`
|
||||
|
||||
**裁决:采纳,并以 `Size(64, 52)` 作为规范修订值**(04 规范 §6.1 建议的 `Size.fromHeight(52)` 作废)。理由:
|
||||
|
||||
1. `Size.fromHeight(52)` 展开为 `Size(double.infinity, 52)`,会给所有 `FilledButton` 无限最小宽度,Row 内既有按钮(首页"去使用"、服务页"立即预约"、对话框"确认恢复")必然布局异常——Frontend 的判断正确,`64` 恰是 Material 默认最小宽,语义上等于"只约束高度"。
|
||||
2. 登录页全宽诉求本就该由 `PrimaryButton` 的 `width: double.infinity` 承担(组件职责),而非全局主题(全局职责)。实现的分工比规范原稿更对。
|
||||
3. 副作用(既有小按钮 40→52 高)方向正确:52 高触控目标优于 40(§6.4 要求 ≥44)。若后续对话框内按钮显厚重,方案是补一个 compact 变体或对话框内改 `TextButton`,**不回退全局 52**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 五个 Tab 换色抽查
|
||||
|
||||
抽查方式:全量 grep 硬编码色值 + 逐页 diff 复核 + 未列入 diff 的三个文件(services / post_detail / main_shell)的 token 引用核对。
|
||||
|
||||
**残留硬编码:仅 2 处,均为已声明保留的功能色**——`home_page.dart:536` 雨 `#0284C7`、`:538` 雪 `#0891B2`。旧靛蓝/slate/indigo 系(`#4F46E5`、`#EEF2FF`、`#475569`、`#E2E8F0`、`Colors.indigo*` 等)全量清零。
|
||||
|
||||
逐页视觉协调性结论:
|
||||
|
||||
| 页面 | 结论 |
|
||||
| --- | --- |
|
||||
| home | 问候卡 `primaryDark` 12px/w600 于 `surfaceTint→canvas` 渐变上约 7:1,达标;装饰爪印 `primary.withAlpha(34)` 语义对;天气"多云→muted、阴天→ink"暖灰化协调(阴天用深棕做图标色略重,可接受);**促销卡见 FIX-1**;促销卡反白按钮前景改 `primaryStrong` 是正确的 AA 修正 |
|
||||
| create | 图片压暗 `ink.withAlpha(204)` + scrim 上 `Colors.white70` 副文字,协调达标 |
|
||||
| pets | 健康提醒卡 `successSurface` 底 + `successInk` 文字 + `success.withAlpha(140)` 描边,是成功色族的标准用法;进度环轨道 `border` 对 |
|
||||
| profile | 深色头卡背景随 `ink` 自动变暖深棕,卡上白字 / `accent` amber 徽语 / `Colors.white70` 统计 label / `Colors.white24` 分隔线,全部协调达标(amber `#FFB648` 于 `#3E2A1F` 上约 7:1)——这是"深色底"抽查重点,无深字深底问题 |
|
||||
| services | ⭐ 评分徽章白底 + ink 字达标;"认证服务" `TagPill(success)` 归 DEBT-1;`Colors.white.withAlpha(235)` 徽章底为合理 scrim |
|
||||
| main_shell / post_detail | 仅引用 token 字段,随主题自动切换;导航栏 `white.withAlpha(245)` 与 `surface` 一致;点赞/收藏 `primary` 图标为装饰性着色,达标豁免 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 组装总则(三页共用)
|
||||
|
||||
- **垂直弹性**:`AuthScaffold` 内禁用 `Spacer`(§2.2 C8)。登录页居中 = `Column(mainAxisAlignment: MainAxisAlignment.center)` + 首尾 `SizedBox`;注册页顶部对齐 = 默认 `MainAxisAlignment.start`。
|
||||
- **提交中整表单锁定**:`_submitting == true` 时所有 `AppTextField.enabled = false`、切换链接 `onPressed = null`、`PrimaryButton.isLoading = true`。
|
||||
- **错误三层模型**(对齐 04 规范 §4.2,全 app 后续网络页面沿用):
|
||||
|
||||
| 层 | 触发 | 呈现 | 组件 |
|
||||
| --- | --- | --- | --- |
|
||||
| 字段级 | 本地校验失败;服务端 **409/422** 可归属字段的冲突(用户名已存在 / 手机号已注册) | 对应 `AppTextField.errorText`,该字段 `onChanged` 即清除 | `AppTextField` |
|
||||
| 表单级 | **401**"用户名或密码错误"、**429**"尝试次数过多,请稍后再试"等无法归属字段的业务错误 | 主按钮上方横幅,出现时 `SemanticsService.announce(message, TextDirection.ltr)` 播报;任一字段 `onChanged` 即清除 | `InlineErrorBanner` |
|
||||
| 瞬态 | 超时、断网 | floating SnackBar"网络异常,请检查网络后重试"+ action"重试"(重放 `_submit`) | `ScaffoldMessenger` |
|
||||
|
||||
- 任何服务端异常文本/错误码不直接透出,一律映射为上表文案。
|
||||
- 提交成功后调 `TextInput.finishAutofillContext()`(触发系统保存密码),随认证状态机 300ms fade 进 `MainShellPage`。
|
||||
|
||||
## 5. 登录页组装稿(`lib/features/auth/login_page.dart`)
|
||||
|
||||
页面状态:`_accountCtrl`、`_passwordCtrl`、`_accountError`、`_passwordError`、`_formError`(String?)、`_submitting`(bool)。
|
||||
|
||||
```dart
|
||||
AuthScaffold( // 无 appBar
|
||||
child: AutofillGroup(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center, // 垂直居中,禁 Spacer
|
||||
children: [
|
||||
const SizedBox(height: 48), // §2.1 顶部最小留白
|
||||
const BrandMark(), // 默认 size 72 + 默认 slogan
|
||||
const SizedBox(height: 48),
|
||||
|
||||
Focus( // 失焦校验,见 §5.4
|
||||
onFocusChange: (has) { if (!has) _validateAccountOnBlur(); },
|
||||
child: AppTextField(
|
||||
label: '用户名 / 手机号',
|
||||
controller: _accountCtrl,
|
||||
prefixIcon: Icons.person_outline_rounded,
|
||||
errorText: _accountError,
|
||||
enabled: !_submitting,
|
||||
keyboardType: TextInputType.text,
|
||||
textInputAction: TextInputAction.next,
|
||||
autofillHints: const [AutofillHints.username],
|
||||
onChanged: (_) => _clearErrors(field: Field.account), // 清本字段 + 表单级
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Focus(
|
||||
onFocusChange: (has) { if (!has) _validatePasswordOnBlur(); },
|
||||
child: AppTextField(
|
||||
label: '密码',
|
||||
controller: _passwordCtrl,
|
||||
prefixIcon: Icons.lock_outline_rounded,
|
||||
obscurable: true,
|
||||
errorText: _passwordError,
|
||||
enabled: !_submitting,
|
||||
textInputAction: TextInputAction.done,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
onChanged: (_) => _clearErrors(field: Field.password),
|
||||
onSubmitted: (_) => _submit(), // 键盘 done 直接提交
|
||||
),
|
||||
),
|
||||
|
||||
if (_formError != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
InlineErrorBanner(message: _formError!),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
|
||||
PrimaryButton(label: '登录', isLoading: _submitting, onPressed: _submit),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
const Text('还没有账号?',
|
||||
style: TextStyle(color: AppColors.muted, fontSize: 14)),
|
||||
TextButton( // 点击区 ≥44 高由 minimumSize 保证
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryStrong,
|
||||
minimumSize: const Size(44, 44),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
|
||||
),
|
||||
onPressed: _submitting ? null : _goRegister, // push RegisterPage
|
||||
child: const Text('立即注册'),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 24), // 底部留白
|
||||
// 协议行:v1 协议页未就绪,整行不渲染(§2.2)
|
||||
// 预留区(短信验证码/第三方登录):不渲染任何占位(§2.4 / DoD)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
校验与提交:两字段仅做**非空**校验(去首尾空格),失焦(曾聚焦过才触发)+ 提交时各校验一次;不做格式强校验。`_submit()`:先总校验,有字段错即 return;置 `_submitting`,调 `POST /auth/login`;结果映射按 §4 表(登录页无 409 场景,401/429 → 横幅)。
|
||||
|
||||
## 6. 注册页组装稿(`lib/features/auth/register_page.dart`)
|
||||
|
||||
字段按 **ADR-004:仅用户名 + 手机号 + 密码 + 确认密码**;短信验证码行、第三方登录预留区一律不渲染。
|
||||
|
||||
```dart
|
||||
AuthScaffold(
|
||||
appBar: AppBar( // 透明返回栏(§3.1)
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 0,
|
||||
foregroundColor: AppColors.ink, // 返回箭头用 ink
|
||||
),
|
||||
child: AutofillGroup(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, // 顶部左对齐
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Text('创建账号', style: Theme.of(context).textTheme.headlineSmall),
|
||||
const SizedBox(height: 8),
|
||||
Text('加入 Patbond,记录毛孩子的每一天',
|
||||
style: Theme.of(context).textTheme.bodySmall),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 四个字段统一模式:Focus 包裹做失焦校验,间距 16
|
||||
Focus(onFocusChange: (has) { if (!has) _validateUsername(); },
|
||||
child: AppTextField(
|
||||
label: '用户名',
|
||||
controller: _usernameCtrl,
|
||||
prefixIcon: Icons.person_outline_rounded,
|
||||
errorText: _usernameError,
|
||||
enabled: !_submitting,
|
||||
textInputAction: TextInputAction.next,
|
||||
autofillHints: const [AutofillHints.newUsername],
|
||||
onChanged: (_) => _clearError(Field.username),
|
||||
)),
|
||||
const SizedBox(height: 16),
|
||||
Focus(onFocusChange: (has) { if (!has) _validatePhone(); },
|
||||
child: AppTextField(
|
||||
label: '手机号',
|
||||
controller: _phoneCtrl,
|
||||
prefixIcon: Icons.phone_iphone_rounded,
|
||||
errorText: _phoneError,
|
||||
enabled: !_submitting,
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: TextInputAction.next,
|
||||
autofillHints: const [AutofillHints.telephoneNumber],
|
||||
onChanged: (_) => _clearError(Field.phone),
|
||||
)),
|
||||
const SizedBox(height: 16),
|
||||
Focus(onFocusChange: (has) { if (!has) _validatePassword(); },
|
||||
child: AppTextField(
|
||||
label: '密码',
|
||||
controller: _passwordCtrl,
|
||||
prefixIcon: Icons.lock_outline_rounded,
|
||||
obscurable: true,
|
||||
errorText: _passwordError,
|
||||
helperText: '密码 8–32 位,需包含字母和数字', // 出错时被 errorText 替换
|
||||
enabled: !_submitting,
|
||||
textInputAction: TextInputAction.next,
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
onChanged: (_) => _clearError(Field.password),
|
||||
)),
|
||||
const SizedBox(height: 16),
|
||||
Focus(onFocusChange: (has) { if (!has) _validateConfirm(); },
|
||||
child: AppTextField(
|
||||
label: '确认密码',
|
||||
controller: _confirmCtrl,
|
||||
prefixIcon: Icons.lock_outline_rounded,
|
||||
obscurable: true,
|
||||
errorText: _confirmError,
|
||||
enabled: !_submitting,
|
||||
textInputAction: TextInputAction.done,
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
onChanged: (_) => _clearError(Field.confirm),
|
||||
onSubmitted: (_) => _submit(),
|
||||
)),
|
||||
|
||||
if (_formError != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
InlineErrorBanner(message: _formError!),
|
||||
],
|
||||
const SizedBox(height: 32),
|
||||
PrimaryButton(label: '注册', isLoading: _submitting, onPressed: _submit),
|
||||
const SizedBox(height: 16),
|
||||
Center(child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Text('已有账号?',
|
||||
style: TextStyle(color: AppColors.muted, fontSize: 14)),
|
||||
TextButton(/* 同登录页样式 */,
|
||||
onPressed: _submitting ? null : () => Navigator.of(context).pop(),
|
||||
child: const Text('直接登录')),
|
||||
])),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
校验规则(失焦即校验、提交再总校验,文案照 04 规范 §3.2):用户名非空 + 3–20 位字母开头字母/数字/下划线;手机号 `^1\d{10}$`;密码 8–32 位含字母和数字;确认密码与密码一致(密码字段变更时若确认已有值也重校验一致性)。
|
||||
|
||||
**409 映射(字段级)**:用户名冲突 → `_usernameError = '该用户名已被使用'`;手机号已注册 → `_phoneError = '该手机号已注册,可直接登录'`(v1 文案自带出路,"去登录"链接可后置)。401 在注册页理论上不出现,其余不可归属错误 → 横幅;网络 → SnackBar。注册成功即建立会话,`finishAutofillContext()` 后直接 fade 进首页,不回登录页。
|
||||
|
||||
## 7. Splash 组装稿(`lib/features/auth/splash_page.dart`)
|
||||
|
||||
状态机:`checking`(默认)→ `failed`(refresh 网络错误/超时且本地有 token)。成功/无 token 不换态,直接 300ms fade 路由。
|
||||
|
||||
```dart
|
||||
Scaffold(
|
||||
backgroundColor: AppColors.canvas,
|
||||
body: Center(
|
||||
child: state == SplashState.checking
|
||||
? Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const BrandMark(), // 与登录页同一构造,保证 §5.2 过渡对位
|
||||
const SizedBox(height: 32),
|
||||
// 仅当等待 >300ms 才显示(进入页面时启动 300ms 定时器置 _showSpinner)
|
||||
SizedBox.square(dimension: 20,
|
||||
child: _showSpinner
|
||||
? const CircularProgressIndicator(
|
||||
strokeWidth: 2.5, color: AppColors.primary)
|
||||
: null), // 占位保高度,避免出现时跳动
|
||||
])
|
||||
: Column(mainAxisSize: MainAxisSize.min, children: [ // 错误态(§5.3)
|
||||
const BrandMark(),
|
||||
const SizedBox(height: 24),
|
||||
const Text('网络连接失败,无法恢复登录',
|
||||
style: TextStyle(color: AppColors.ink, fontSize: 14)),
|
||||
const SizedBox(height: 16),
|
||||
OutlinedButton( // 次级按钮,高 44
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(120, 44),
|
||||
foregroundColor: AppColors.primaryStrong,
|
||||
side: const BorderSide(color: AppColors.border),
|
||||
),
|
||||
onPressed: _retryRefresh,
|
||||
child: const Text('重试')),
|
||||
const SizedBox(height: 8),
|
||||
TextButton( // 逃生通道:清除凭证进登录页
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryStrong,
|
||||
minimumSize: const Size(44, 44)),
|
||||
onPressed: _clearCredentialsAndGoLogin,
|
||||
child: const Text('改用账号登录')),
|
||||
]),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
流程约束(照 04 规范 §5.1,实现方注意):Splash 最短停留 500ms;refresh 客户端超时 5s;**网络失败不得清除本地 refresh token**(只有服务端明确 401 才清);所有页面切换用 `PageRouteBuilder` + `FadeTransition` 300ms。
|
||||
|
||||
## 8. 遗留给实现方的备忘
|
||||
|
||||
1. FIX-1、FIX-2 见 §2.3,建议与登录页开发同一工单批执行(FIX-2 直接影响注册页 helperText 观感)。
|
||||
2. `Focus(onFocusChange:)` 包裹方案依赖祖先 Focus 节点聚合子孙焦点状态,Flutter 语义保证成立;若嫌样板多,可给 `AppTextField` 加 `focusNode` 参数(建议级)。
|
||||
3. 三页均未用 `Form`/`validator`——`AppTextField` 是 errorText 受控模式,校验状态放页面 state,这与组件现状一致,勿混用两套校验。
|
||||
4. DEBT-1(TagPill)另开工单,与登录纵切解耦。
|
||||
|
||||
---
|
||||
**UI Designer** · 2026-09-04
|
||||
Reference in New Issue
Block a user