# 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,默认描边 `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. 状态设计(对应 DoD:loading / error / retry;登录表单无 empty 态) ### 4.1 输入框状态 | 状态 | 视觉 | | --- | --- | | 默认 | 白底,`border` 1px 描边 | | 聚焦 | `primary` 1.5px 描边(主题已有) | | 错误 | `error` 1.5px 描边 + 输入框下方 errorText(12,`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 默认即可) | | 禁用 | 主题 disabled(onSurface 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 页无布局破坏。