Files
patbond-doc/docs/development/iterations/iteration-1/04-ui-login-design-spec.md
T
lixi 209021e7d2 docs: 迁入第一迭代过程报告并建立进展看板
- 新增 development/iterations/iteration-1/:15 份角色报告 + 进展看板(已完成/未闭环/下一步),作为双人协作的进度事实来源
- 新增 ADR-006:测试与交付容器化策略(Testcontainers / 交付 Docker 包 / 本机库仅个人联调)
- Git 工作流规范补充:敏感信息只进忽略文件或 sample、测试数据不入库、测试代码限标准测试目录
- 门禁:mkdocs build --strict 通过(零警告)
2026-09-04 10:45:05 +08:00

347 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Patbond 第一迭代 · 登录/注册 UI 设计规范
> 作者:UI Designer
> 日期:2026-09-03
> 迭代:Iteration 1「真实登录纵切」(development-plan.md §7 M1、§8
> 素材来源:`AI宠物_iOS_UI设计稿.html`(视觉语言)、`patbond-flutter/lib/`(已实现主题与组件)、`patbond-doc/docs/development/development-plan.md`(§6.2 接口、§7 M1、§9/§10 质量门禁)
---
## 0. 关键前提:两套视觉语言的分歧与决策
现有素材存在一个必须先决策的分歧:
| 来源 | 主色 | 底色 | 字体 | 定位 |
| --- | --- | --- | --- | --- |
| HTML 设计稿(正式设计交付物) | 珊瑚橙 `#FF6F4C` 暖色系 | 奶油色 `#FFF7ED` | Baloo 2 标题 + Inter 正文 | 品牌方向 |
| Flutter `app_theme.dart`(当前实现) | 靛蓝 `#4F46E5` | 冷灰 `#F8FAFC` | 系统字体 + CJK fallback | 脚手架占位 |
**决策:以 HTML 设计稿的暖色系为品牌正典(canonical)。** 理由:设计稿是明确的品牌交付物,宠物社区产品的暖色调是刻意的情感设计;而 Flutter 的靛蓝主题是典型的模板默认色。登录页是用户接触产品的第一屏,应当承载品牌。
**落地策略(降低迁移风险):** 本规范全部使用语义 token 命名(`primary``surface``ink`…),与 `AppColors` 现有字段一一对应。主题集中在 `app_theme.dart` 单文件,重映射色值即可全局切换。若团队决定第一迭代不动主题,本规范的布局、组件、状态定义在靛蓝主题下同样成立,仅色值不同——两种情况都不需要改登录页代码。
---
## 1. 现有设计系统提炼
### 1.1 色板(语义 token → 暖色正典值 / 现有 Flutter 值)
| Token | 暖色正典(设计稿) | 现有 Flutter | 用途 |
| --- | --- | --- | --- |
| `primary` | `#FF6F4C` 珊瑚橙 | `#4F46E5` | 品牌色、图标、装饰、渐变起点 |
| `primaryStrong` | `#D6431A`(新增,加深珊瑚) | — | 实心按钮填充、可点击文字链接。白字对比度约 4.5:1,满足 WCAG AA`#FF6F4C` 白字仅 2.75:1,不得用于承载文字的实心填充 |
| `primaryDark` | `#7A2E12` coral-dark | — | 浅色底上的强调文字、按钮反白替代 |
| `accent` | `#FFB648` amber | — | 渐变终点、徽章、会员/促销 CTA |
| `accentDark` | `#7A4B0A` amber-dark | — | amber 底上的文字 |
| `canvas` | `#FFF7ED` bg-cream | `#F8FAFC` | 页面背景 |
| `surface` | `#FFFFFF` | `#FFFFFF` | 卡片、输入框背景 |
| `surfaceTint` | `#FFE8D6` peach | `#E0E7FF` | 图标底、占位块、选中指示 |
| `ink` | `#3E2A1F` | `#0F172A` | 主文字 |
| `muted` | `#9C8977` | `#64748B` | 次级文字、占位符 |
| `border` | `#F0DCC8` | `#E2E8F0` | 描边、分隔线 |
| `success` | `#7FA88A` sage(文字用 `#3F5744`,底用 `#E8F0E8` | `#10B981` | 成功提示 |
| `warning` | `#F59E0B` | `#F59E0B` | 警告 |
| `error` | `#D0342C`(新增;设计稿与主题均缺失) | — | 校验错误文字、错误描边、失败提示。白底上约 5.4:1,AA 达标 |
| `brandGradient` | `linear-gradient(135deg, #FF6F4C, #FFB648)` | — | 品牌 hero 区、头像环、装饰性大面积(不承载正文文字) |
### 1.2 字号层级
设计稿是 320px 画框内的缩样(9–19px),不能按像素照搬;Flutter `textTheme` 已是放大到真机的合理映射,作为基准沿用:
| 层级 | 字号/字重 | 对应 | 登录页用途 |
| --- | --- | --- | --- |
| Display(品牌字标) | 32 / w700,标题字体(Baloo 2 或圆润中文标题体,可后置) | 设计稿 `.logo` 放大 | 登录页 "Patbond" 字标 |
| `headlineSmall` | 22 / w800 | 已有 | 页面标题("欢迎回来"/"创建账号" |
| `titleLarge` | 18 / w800 | 已有 | 分区标题 |
| `titleMedium` | 15 / w700 | 已有 | 按钮文字、表单 label |
| `bodyMedium` | 14 / 1.5 行高 | 已有 | 正文、输入内容(输入框内建议 16 防 iOS 缩放) |
| `bodySmall` | 12 / 1.4 行高,muted 色 | 已有 | 辅助说明、协议文案 |
| Caption | 11 / w700 | TagPill 已有 | 徽章、错误行(错误行用 12) |
### 1.3 圆角
设计稿圆角层级:芯片胶囊 999 > hero 20 > 卡片 1618 > 输入框/小卡 14 > 按钮 1012。Flutter 现值:Card 24、Input 18、SnackBar 16。取两者交集定标准:
| Token | 值 | 用途 |
| --- | --- | --- |
| `radiusSm` | 12 | 小按钮、内嵌 CTA |
| `radiusMd` | 16 | 主按钮、SnackBar、菜单组 |
| `radiusLg` | 18 | 输入框(沿用 `inputDecorationTheme` 现值)、feed 卡 |
| `radiusXl` | 24 | 大卡片(沿用 `cardTheme` 现值) |
| `radiusPill` | 999 | 芯片、徽章 |
### 1.4 间距
基数 4。常用刻度:4 / 8 / 12 / 16 / 24 / 32 / 48。页面水平留白 16(现有页面 `EdgeInsets.fromLTRB(16, 16, 16, 30)` 一致),卡片内边距 1824`SectionCard` 默认 18)。
### 1.5 既有组件风格基线
- **输入框**`inputDecorationTheme` 已定义,直接复用):白底 filled,圆角 18,内边距 H16/V14,默认描边 `border` 1px,聚焦描边 `primary` 1.5px。
- **按钮**:现有页面用 Material 3 `FilledButton` / `OutlinedButton` / `TextButton`,未做全局主题化——本迭代补齐(见 §6)。
- **卡片**:白底、1px 极浅描边、零 elevation(阴影极克制,与设计稿一致)。
- **已有可复用组件**`lib/widgets/common.dart`):`RemoteImage`loading/error 兜底)、`SectionCard``TagPill``EmptyState`
- **加载**`CircularProgressIndicator`(主壳启动、创作页、档案页已用)。
---
## 2. 登录页规范
### 2.1 布局结构
单列居中布局,无 AppBar,无底部 TabBar。页面背景 `canvas`,可在顶部叠加一层由 `surfaceTint` 到透明的极浅径向渐变作氛围(可选装饰,非必需)。
```text
┌──────────────────────────────────────┐
│ SafeArea + SingleChildScrollView │ 键盘弹出时可滚动,防溢出
│ 水平 padding 24 │
│ │
│ ↑ 弹性空间 (flex, min 48) │
│ │
│ [ 品牌区 ] │
│ 🐾 logo 图形 72×72 │ v1 可用 Icons.pets 于
│ Patbond │ brandGradient 圆底(radiusXl)代替
│ 和毛孩子在一起的每一天 │ 字标 32/w700 primary
│ │ slogan 14 muted,居中
│ 间距 48 │
│ │
│ ┌────────────────────────────────┐ │
│ │ 用户名 / 手机号 │ │ 输入框①,高 52
│ └────────────────────────────────┘ │
│ 间距 16 │
│ ┌────────────────────────────────┐ │
│ │ 密码 [👁] │ │ 输入框②,高 52,可见性切换
│ └────────────────────────────────┘ │
│ ⚠ 字段级错误显示在对应输入框下方 │
│ │
│ ┌ 表单级错误横幅(条件显示)───────┐ │ 见 §4.3
│ └────────────────────────────────┘ │
│ 间距 24 │
│ ┌────────────────────────────────┐ │
│ │ 登 录 │ │ 主按钮,高 52,全宽
│ └────────────────────────────────┘ │
│ 间距 16 │
│ 还没有账号? 立即注册 │ 行内文字链接,居中
│ │
│ ↑ 弹性空间 │
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │
│ ┆ [预留区] 其他登录方式 ┆ │ v1 不渲染,见 2.4
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │
│ 登录即代表同意《用户协议》《隐私政策》 │ bodySmall muted,底部 24
└──────────────────────────────────────┘
```
### 2.2 组件清单
| 组件 | 规格 |
| --- | --- |
| 品牌区 | logo 72×72;字标 32/w700 `primary`slogan 14 `muted`;整体居中 |
| 账号输入框 | label "用户名 / 手机号"`keyboardType: text``textInputAction: next``AutofillHints.username`,前缀图标 `person_outline_rounded``muted` 色) |
| 密码输入框 | label "密码"`obscureText` 默认开,后缀 `visibility_off/visibility` 切换按钮(点击目标 ≥44×44),`textInputAction: done`(提交表单),`AutofillHints.password`,前缀图标 `lock_outline_rounded` |
| 主按钮「登录」 | 全宽 × 52 高,圆角 `radiusMd`(16),填充 `primaryStrong`,文字白色 15/w700。状态见 §4.4 |
| 注册入口 | "还没有账号?"`muted`+ "立即注册"`primaryStrong`/w700,点击目标 ≥44 高),push 到注册页 |
| 协议行 | bodySmall,链接词 `primaryStrong`;v1 若协议页未就绪可先不渲染整行 |
### 2.3 表单字段与校验规则
| 字段 | 客户端校验(失焦 + 提交时) | 错误文案 |
| --- | --- | --- |
| 用户名/手机号 | 非空;去首尾空格 | "请输入用户名或手机号" |
| 密码 | 非空 | "请输入密码" |
登录页刻意不做格式强校验(用户名还是手机号由服务端判定),失败统一走服务端错误映射(§4.2)。
### 2.4 预留区(未确认功能,v1 不实现)
开发计划 §12 明确:手机号短信验证码登录、第三方登录**尚未确认**。设计上预留、代码上不渲染:
- **位置**:主按钮与协议行之间。展开后结构为:分隔线 + 居中文字"其他登录方式"12 `muted`)+ 一排 44×44 圆形图标按钮(白底、`border` 描边),间距 24。
- **短信验证码**:确认后以账号输入框上方的"密码登录 / 验证码登录"分段切换(SegmentedButton 或双 Tab 文字)接入,不改变整体布局。
- 布局采用居中弹性结构,预留区展开不会挤压表单——实现时无需为此留白占位。
---
## 3. 注册页规范
### 3.1 布局结构
从登录页 push 进入,有返回能力。与登录页同一视觉框架,改为顶部对齐(字段多,不做垂直居中)。
```text
┌──────────────────────────────────────┐
│ ← 返回(AppBar 透明,仅返回箭头) │
│ SafeArea + Scroll,水平 padding 24 │
│ │
│ 创建账号 │ headlineSmall 22/w800
│ 加入 Patbond,记录毛孩子的每一天 │ bodySmall muted,间距 8
│ 间距 32 │
│ ┌────────────────────────────────┐ │
│ │ 用户名 │ │
│ └────────────────────────────────┘ │
│ 间距 16 │
│ ┌────────────────────────────────┐ │
│ │ 手机号 │ │
│ └────────────────────────────────┘ │
│ ┄┄ [预留] 短信验证码行,v1 不渲染 ┄┄ │
│ 间距 16 │
│ ┌────────────────────────────────┐ │
│ │ 密码 [👁] │ │
│ └────────────────────────────────┘ │
│ 密码 8–32 位,需包含字母和数字 │ helperText 12 muted
│ 间距 16 │
│ ┌────────────────────────────────┐ │
│ │ 确认密码 [👁] │ │
│ └────────────────────────────────┘ │
│ 间距 32 │
│ ┌────────────────────────────────┐ │
│ │ 注 册 │ │ 主按钮同登录页
│ └────────────────────────────────┘ │
│ 间距 16 │
│ 已有账号? 直接登录 │ 返回登录页(pop)
└──────────────────────────────────────┘
```
### 3.2 字段与校验规则
| 字段 | 校验(失焦即校验,提交再总校验) | 错误文案 |
| --- | --- | --- |
| 用户名 | 非空;3–20 字符;字母/数字/下划线,字母开头(最终以 API 契约为准) | "请输入用户名" / "用户名需 3–20 位,字母开头,可含数字和下划线" |
| 手机号 | 非空;11 位大陆手机号 `^1\d{10}$` | "请输入手机号" / "请输入正确的 11 位手机号" |
| 密码 | 8–32 位,含字母和数字(最终以 API 契约为准);helperText 常显规则,出错时被 errorText 替换 | "密码需 8–32 位,且同时包含字母和数字" |
| 确认密码 | 与密码一致 | "两次输入的密码不一致" |
服务端唯一性冲突(M1 已列入范围)映射回字段:用户名已存在 → 用户名字段 errorText "该用户名已被使用";手机号已注册 → 手机号字段 "该手机号已注册,可直接登录",并可附带"去登录"文字链接。
注册成功即建立会话(`POST /auth/register` 注册并创建会话),直接进入首页,不回登录页重新登录。
### 3.3 预留区
- **短信验证码**(未确认):手机号下方一行——验证码输入框(flex)+ 右侧"获取验证码"次级按钮(`OutlinedButton`,高 52,倒计时态显示"59s 后重发"并禁用)。
- 第三方登录预留区同登录页 §2.4。
---
## 4. 状态设计(对应 DoDloading / error / retry;登录表单无 empty 态)
### 4.1 输入框状态
| 状态 | 视觉 |
| --- | --- |
| 默认 | 白底,`border` 1px 描边 |
| 聚焦 | `primary` 1.5px 描边(主题已有) |
| 错误 | `error` 1.5px 描边 + 输入框下方 errorText12`error` 色);聚焦重新输入后即清除该字段错误 |
| 禁用(提交中) | 整体 60% 不透明度,不可编辑 |
错误出现/消失会引起 8–20px 高度变化,可接受;不使用固定高度错误占位(多字段表单会过度稀疏)。
### 4.2 错误展示的层级策略
1. **字段级**(首选):能定位到具体字段的错误一律放该字段 errorText——包括本地校验和服务端 409/422 映射。
2. **表单级**:无法归属单一字段的业务错误(如 401 "用户名或密码错误"、429 "尝试次数过多,请稍后再试")→ 主按钮上方的行内错误横幅:`error` 8% 透明度底、`radiusSm` 圆角、内边距 12、左侧 `error_outline` 图标 18 + 文字 13 `error` 色。用横幅而非 SnackBar,因为错误需要停留在表单上下文里供用户对照修改。
3. **瞬态/网络错误**:请求超时、断网 → floating SnackBar(主题已有圆角 16):"网络异常,请检查网络后重试",附 action "重试"(重放上次提交)。
所有错误文案说人话、给出路,不透传服务端异常文本(契约规范 §6.1 禁止只返回异常文本,客户端同样禁止直接展示错误码)。
### 4.3 提交 loading 态
- 主按钮:文字替换为 20×20 白色 `CircularProgressIndicator`strokeWidth 2.5),按钮尺寸不变、不可再点。
- 两个输入框、注册/登录切换链接同时禁用,防止请求飞行中改动或重复导航。
- 不用全屏遮罩 loading——登录请求是单按钮动作,局部 loading 干扰最小。
### 4.4 主按钮状态汇总
| 状态 | 视觉 |
| --- | --- |
| 默认 | `primaryStrong` 填充,白字 15/w700 |
| 按下 | 填充加深 8%Material ripple 默认即可) |
| 禁用 | 主题 disabledonSurface 12% 底 / 38% 字)。仅在提交中禁用;**不做"表单没填完就置灰"**——允许点击后给出校验错误,比让用户猜为什么按钮是灰的更友好 |
| loading | 见 §4.3 |
---
## 5. 启动过渡:登录态恢复(Splash)
### 5.1 流程
```text
App 启动
└─ Splash 展示(最短 500ms,避免闪跳)
同时并行:从安全存储读 refresh token
├─ 无 token ────────────────→ 淡入登录页
├─ 有 token → POST /auth/refresh(客户端超时 5s
│ ├─ 成功(拿到新 access/refresh)→ 淡入首页(MainShell
│ ├─ 401/会话失效 → 清除本地凭证 → 淡入登录页
│ └─ 网络错误/超时 → Splash 切换为错误态(见 5.3
```
### 5.2 Splash 视觉
- 全屏 `canvas` 底色,品牌区(同登录页 §2.2:logo 72 + 字标 + slogan)垂直水平居中。
- 品牌区下方 32 处放 20×20 `CircularProgressIndicator``primary` 色,strokeWidth 2.5)——**仅当等待超过 300ms 才显示**,快速路径下用户只看到一闪而过的品牌屏。
- 页面切换用 300ms fade`PageRouteBuilder` + `FadeTransition`);Splash 与登录页品牌区位置刻意一致,淡入登录页时品牌区视觉上原地不动、表单浮现,过渡自然。
- 原生层(iOS LaunchScreen / Android launch theme)应配同色 `canvas` 纯色底,避免白屏→Splash 的颜色跳变(可延后到 M6 商店配置一并做)。
### 5.3 Splash 错误态(网络失败且本地有 token 时)
不能让用户卡在无限转圈:
```text
🐾 Patbond(品牌区不动)
网络连接失败,无法恢复登录
┌──────────────┐
│ 重试 │ 次级按钮 OutlinedButton,高 44
└──────────────┘
改用账号登录 文字链接 → 清除凭证进登录页
```
「重试」重新发起 refresh;「改用账号登录」是逃生通道。**注意**:网络失败(非 401)不得自动清除本地 refresh token——只有服务端明确判定会话无效才清除。
---
## 6. Flutter 实现建议
### 6.1 复用现有主题 token
| 现有资产 | 用法 |
| --- | --- |
| `AppColors.*` | 全部沿用;**新增** `primaryStrong``primaryDark``accent``surfaceTint``error``error` 是本迭代硬需求,其余配合暖色迁移时加) |
| `inputDecorationTheme` | 直接满足输入框默认/聚焦态;错误态补 `errorBorder` / `focusedErrorBorder``AppColors.error`1.5px)和 `errorStyle`12px |
| `textTheme` | 页面标题 `headlineSmall`、按钮/label `titleMedium`、辅助文字 `bodySmall` |
| `snackBarTheme` | 网络错误提示直接用 |
| `EmptyState`common.dart | 登录流用不到,但其"图标+文案"模式是 Splash 错误态的参照 |
建议在 `buildAppTheme()` 补充 `filledButtonTheme``minimumSize: Size.fromHeight(52)``RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))``textStyle: 15/w700`——一次定义,登录/注册/后续所有主操作按钮受益。若采纳暖色迁移,同时把 `ColorScheme.fromSeed``seedColor` 换为暖色 `primary` 并显式覆盖 `colorScheme.error`
### 6.2 新增可复用组件(建议放 `lib/widgets/` 或 `lib/features/auth/widgets/`
| 组件 | 职责 |
| --- | --- |
| `BrandMark` | logo + 字标 + slogan`size` 参数;Splash 与登录页共用,保证两处渲染一致(这是 §5.2 淡入过渡成立的前提) |
| `AppTextField` | 封装 `TextFormField`label、前缀图标、errorText、enabled,密码模式内置可见性切换(含 ≥44 点击区)与 obscure 状态 |
| `PrimaryButton` | `FilledButton` + `isLoading`:loading 时换转圈、锁点击、尺寸不变。全 app 提交类按钮通用 |
| `InlineErrorBanner` | §4.2 表单级错误横幅;后续所有网络页面的错误态可复用 |
| `AuthScaffold` | 认证页统一框架:SafeArea + 滚动 + padding 24 + 键盘避让,登录/注册/找回密码共用 |
### 6.3 结构与状态(配合开发计划 §4.2)
- 新建 `lib/features/auth/``login_page.dart``register_page.dart``splash_page.dart``auth_state.dart`(或 controller)。
- 认证状态机:`unknown → authenticating → authenticated | unauthenticated``app.dart` 根据状态切换 `home`(替代现在直接进 `MainShellPage`),配 300ms fade。
- token 存 `flutter_secure_storage`(需新增依赖);开发计划明令禁止把 token 写进 `SharedPreferences`(现有 `AppState` 的持久化方式不可套用到凭证)。
- 表单用 `Form` + `AutofillGroup`(激活系统密码管理器自动填充),提交成功后调 `TextInput.finishAutofillContext()` 触发保存密码提示。
### 6.4 可访问性核对清单
- 触控目标 ≥44×44:主按钮 52、密码可见性切换与文字链接需显式保证点击区。
- 承载文字的色彩组合 ≥4.5:1`primaryStrong`/白 ≈4.5、`ink`/canvas、`error`/白 ≈5.4 均达标;`#FF6F4C` 只作装饰不载文。
- errorText 由 `InputDecoration` 原生渲染,TalkBack/VoiceOver 自动关联朗读;表单级横幅出现时用 `SemanticsService.announce` 播报。
- 键盘流:账号 `next` → 密码 `done` 提交;字体随系统缩放(不写死 `textScaleFactor`)。
---
## 7. 交付验收对照(DoD
- [ ] 登录/注册页具备 loading、错误、重试路径(本规范 §4);Splash 具备网络失败重试(§5.3)。
- [ ] 错误同时区分字段级/表单级/瞬态三层,无裸错误码透出。
- [ ] token 仅存安全存储;退出登录清除凭证并回登录页。
- [ ] 预留区(短信验证码、第三方登录)不渲染任何占位 UI,待 §12 事项确认后按本规范扩展。
- [ ] 若采纳暖色迁移:仅改 `app_theme.dart` 色值映射,全局回归五个既有 Tab 页无布局破坏。