- 新增 development/iterations/iteration-1/:15 份角色报告 + 进展看板(已完成/未闭环/下一步),作为双人协作的进度事实来源 - 新增 ADR-006:测试与交付容器化策略(Testcontainers / 交付 Docker 包 / 本机库仅个人联调) - Git 工作流规范补充:敏感信息只进忽略文件或 sample、测试数据不入库、测试代码限标准测试目录 - 门禁:mkdocs build --strict 通过(零警告)
23 KiB
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 > 卡片 16–18 > 输入框/小卡 14 > 按钮 10–12。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) 一致),卡片内边距 18–24(SectionCard 默认 18)。
1.5 既有组件风格基线
- 输入框(
inputDecorationTheme已定义,直接复用):白底 filled,圆角 18,内边距 H16/V14,默认描边border1px,聚焦描边primary1.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 到透明的极浅径向渐变作氛围(可选装饰,非必需)。
┌──────────────────────────────────────┐
│ 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 进入,有返回能力。与登录页同一视觉框架,改为顶部对齐(字段多,不做垂直居中)。
┌──────────────────────────────────────┐
│ ← 返回(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. 状态设计(对应 DoD:loading / error / retry;登录表单无 empty 态)
4.1 输入框状态
| 状态 | 视觉 |
|---|---|
| 默认 | 白底,border 1px 描边 |
| 聚焦 | primary 1.5px 描边(主题已有) |
| 错误 | error 1.5px 描边 + 输入框下方 errorText(12,error 色);聚焦重新输入后即清除该字段错误 |
| 禁用(提交中) | 整体 60% 不透明度,不可编辑 |
错误出现/消失会引起 8–20px 高度变化,可接受;不使用固定高度错误占位(多字段表单会过度稀疏)。
4.2 错误展示的层级策略
- 字段级(首选):能定位到具体字段的错误一律放该字段 errorText——包括本地校验和服务端 409/422 映射。
- 表单级:无法归属单一字段的业务错误(如 401 "用户名或密码错误"、429 "尝试次数过多,请稍后再试")→ 主按钮上方的行内错误横幅:
error8% 透明度底、radiusSm圆角、内边距 12、左侧error_outline图标 18 + 文字 13error色。用横幅而非 SnackBar,因为错误需要停留在表单上下文里供用户对照修改。 - 瞬态/网络错误:请求超时、断网 → 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 默认即可) |
| 禁用 | 主题 disabled(onSurface 12% 底 / 38% 字)。仅在提交中禁用;不做"表单没填完就置灰"——允许点击后给出校验错误,比让用户猜为什么按钮是灰的更友好 |
| loading | 见 §4.3 |
5. 启动过渡:登录态恢复(Splash)
5.1 流程
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 时)
不能让用户卡在无限转圈:
🐾 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 页无布局破坏。