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

23 KiB
Raw Blame History

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 命名(primarysurfaceink…),与 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) 一致),卡片内边距 1824SectionCard 默认 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):RemoteImageloading/error 兜底)、SectionCardTagPillEmptyState
  • 加载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 primaryslogan 14 muted;整体居中
账号输入框 label "用户名 / 手机号"keyboardType: texttextInputAction: nextAutofillHints.username,前缀图标 person_outline_roundedmuted 色)
密码输入框 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,链接词 primaryStrongv1 若协议页未就绪可先不渲染整行

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. 状态设计(对应 DoDloading / error / retry;登录表单无 empty 态)

4.1 输入框状态

状态 视觉
默认 白底,border 1px 描边
聚焦 primary 1.5px 描边(主题已有)
错误 error 1.5px 描边 + 输入框下方 errorText12error 色);聚焦重新输入后即清除该字段错误
禁用(提交中) 整体 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 白色 CircularProgressIndicatorstrokeWidth 2.5),按钮尺寸不变、不可再点。
  • 两个输入框、注册/登录切换链接同时禁用,防止请求飞行中改动或重复导航。
  • 不用全屏遮罩 loading——登录请求是单按钮动作,局部 loading 干扰最小。

4.4 主按钮状态汇总

状态 视觉
默认 primaryStrong 填充,白字 15/w700
按下 填充加深 8%Material ripple 默认即可)
禁用 主题 disabledonSurface 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 CircularProgressIndicatorprimary 色,strokeWidth 2.5)——仅当等待超过 300ms 才显示,快速路径下用户只看到一闪而过的品牌屏。
  • 页面切换用 300ms fadePageRouteBuilder + 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.* 全部沿用;新增 primaryStrongprimaryDarkaccentsurfaceTinterrorerror 是本迭代硬需求,其余配合暖色迁移时加)
inputDecorationTheme 直接满足输入框默认/聚焦态;错误态补 errorBorder / focusedErrorBorderAppColors.error1.5px)和 errorStyle12px
textTheme 页面标题 headlineSmall、按钮/label titleMedium、辅助文字 bodySmall
snackBarTheme 网络错误提示直接用
EmptyStatecommon.dart 登录流用不到,但其"图标+文案"模式是 Splash 错误态的参照

建议在 buildAppTheme() 补充 filledButtonThememinimumSize: Size.fromHeight(52)RoundedRectangleBorder(borderRadius: BorderRadius.circular(16))textStyle: 15/w700——一次定义,登录/注册/后续所有主操作按钮受益。若采纳暖色迁移,同时把 ColorScheme.fromSeedseedColor 换为暖色 primary 并显式覆盖 colorScheme.error

6.2 新增可复用组件(建议放 lib/widgets/lib/features/auth/widgets/

组件 职责
BrandMark logo + 字标 + slogansize 参数;Splash 与登录页共用,保证两处渲染一致(这是 §5.2 淡入过渡成立的前提)
AppTextField 封装 TextFormFieldlabel、前缀图标、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.dartregister_page.dartsplash_page.dartauth_state.dart(或 controller)。
  • 认证状态机:unknown → authenticating → authenticated | unauthenticatedapp.dart 根据状态切换 home(替代现在直接进 MainShellPage),配 300ms fade。
  • token 存 flutter_secure_storage(需新增依赖);开发计划明令禁止把 token 写进 SharedPreferences(现有 AppState 的持久化方式不可套用到凭证)。
  • 表单用 Form + AutofillGroup(激活系统密码管理器自动填充),提交成功后调 TextInput.finishAutofillContext() 触发保存密码提示。

6.4 可访问性核对清单

  • 触控目标 ≥44×44:主按钮 52、密码可见性切换与文字链接需显式保证点击区。
  • 承载文字的色彩组合 ≥4.5:1primaryStrong/白 ≈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 页无布局破坏。