# 「随心」主题(Suixin Theme)技术计划(plan) ## 0. 目标回顾 在 Home 现有「风景 / 纯色」主题基础上新增第三种主题「随心」: - **输入**:问卷生成的用户画像 `U`(本地存储) - **输出**:Home 背景推荐颜色(以纯色为主) - **规则**:复用「个性化背景颜色推荐算法」的 Base Theme/Neutral Theme 与 Hard Rules - **计算时机**: - **冷启动(App 进程级)**:计算一次并锁定 Base Theme - **切换文案(Home 上滑切下一条)**:在同一 Base Theme 内更新一次“当前颜色” - **持久化**:主题选择与「随心」计算状态均持久化(避免回到 Home/重进页面时丢失) - **多语言**:TC(zh-TW)+ EN ## 1. 现状梳理(与改动点) ### 1.1 现有主题切换 - `ThemeMode` 当前为 `'scenery' | 'color'` - `ThemeModal` 弹窗提供 2 个卡片切换 - `Home` 根据 `themeMode`: - `scenery`:背景图 + 默认底色 - `color`:从 `THEME_COLORS` 按 `index` 轮换纯色 - `ui.theme.mode` 已在 `AsyncStorage` 持久化 ### 1.2 用户画像输入已就绪 客户端已将问卷映射为 `UserProfileV1_2(_Extended)` 并持久化(`user.profileScoring`),关键字段: - `stage.unknown`(Hard Rule:unknown → Neutral) - `need`(稀疏 one-hot:`{ [needTag]: 1 }` 或 `{}`) - `emotion_score: number | null` - `profile_confidence: number` - `profile_answered` ## 2. 技术方案总览 ### 2.1 「随心」算法在 Home 的落地形态 Home 不具备“长文案阅读页的 scroll”,因此采用**“渐变单点采样”**来复用算法的连续插值模型: - Base Theme 仍按 `need / stage` 选定并锁定(Theme Lock) - 每次切换文案时,生成一个 \(t \in [0, 1]\),并计算: \[ Color(t) = lerp(Color\_top, Color\_bottom, t) \] - 输出为单一 `hex` 纯色,作为 Home `backgroundColor` - 全程 **不跨 need、不跨主题色系**,仅在同主题内移动 ### 2.2 持久化与“只在冷启动/切换文案时计算” 为同时满足“持久化”与“冷启动时计算”: - **持久化内容**:锁定的 `base_theme_id` + 用于生成 \(t\) 的 `seed` + 当前 `step_index` + `last_color` - **冷启动计算**:当检测到“新一轮 App 启动会话”时,重新选择并锁定 `base_theme_id`,并重置/更新 `seed` 与 `step_index` - **切换文案计算**:仅递增 `step_index`,在同一 `base_theme_id` 下更新 `last_color` > 说明:冷启动检测以“进程级首次进入 Home”为准(工程实现阶段会在 `_layout` 或全局单例中生成 boot 标记)。 ## 3. 数据结构与存储设计 ### 3.1 扩展主题枚举 - 将 `ThemeMode` 扩展为:`'scenery' | 'color' | 'suixin'` - 存储 key:沿用 `ui.theme.mode` ### 3.2 新增「随心」状态存储 新增本地存储 key(建议): - `ui.theme.suixin.state` 数据结构(建议): ```ts type SuixinThemeStateV1 = { schema_version: 1; saved_at: string; // ISO8601 base_theme_id: 'neutral' | 'emotional_support' | 'parenting_pressure' | 'self_worth' | 'anxiety_relief' | 'rest_balance'; seed: string; // 用于生成 t 的稳定种子(可由 profile + 日期等派生) step_index: number; // 每切换一条文案 +1 last_color: string; // "#RRGGBB" }; ``` ### 3.3 冷启动会话标记(Boot ID) 为实现“仅冷启动时重置 base theme/seed”,新增一个进程级 boot 标记(实现二选一): - **方案 A(推荐)**:在 `app/_layout.tsx` 首次挂载时生成 `boot_id` 并写入内存单例(不落盘) - **方案 B**:写入 `AsyncStorage`(例如 `app.boot.lastSeenAt`)并结合“本次运行内存标记”判定首次进入 Home 计划优先采用方案 A:逻辑清晰且不污染存储。 ## 4. 颜色算法实现细节(Home 版本) ### 4.1 主题色盘常量 在客户端新增一个颜色模块(例如 `client/src/features/suixinTheme/`),内置: - Base Theme(5 套)+ Neutral(1 套) - 与设计文档保持一致的 `hex` 值 ### 4.2 Base Theme 选择(锁定) 输入:`UserProfileScoring` 输出:`base_theme_id` 规则: - 若 `stage.unknown === 1` → `neutral` - 若 `need` 为空 `{}` → `neutral` - 否则取 `Object.keys(need)[0]`: - 若 key 在枚举内 → 对应 Base Theme - 否则 → `neutral` ### 4.3 t 的生成与“低感知变化” 为了让“切换文案”带来“流动感”但不跳变,采用**小步进**策略: - 定义 `N = 12`(可调):表示从 \(0 \to 1\) 的分段数 - 每次切换文案:`step_index += 1` - 计算:`t = (step_index % N) / (N - 1)` > 该策略保证 \(t\) 在 \([0, 1]\) 内缓慢移动;到达 1 后回到 0 会有一次跳变。为进一步降低跳变,可改为往返波形: > > - `phase = step_index % (2*(N-1))` > - `t = phase <= (N-1) ? phase/(N-1) : (2*(N-1)-phase)/(N-1)` 实现阶段默认采用**往返波形**,避免回卷突跳。 ### 4.4 lerp 计算(严格线性) - `lerp` 仅允许线性插值 - 颜色空间:先使用 sRGB 的逐通道线性插值(实现简单、可控);若后续需要更自然,可升级到线性空间插值,但仍保持线性模型 ### 4.5 emotion/confidence 的约束接入 本期按“安全优先”策略落地: - 若 `emotion_score === null` 或 `emotion_score <= 0.3`:输出不做任何微扰(纯 lerp 结果) - 亮度微扰(\(\Delta L \le \pm 2\%\))与饱和度上限为可选增强;若落地,将以 `profile_confidence` 作为开关条件,并确保不改变色系 ## 5. UI 与交互实现计划 ### 5.1 ThemeModal:新增第三个主题卡片 - 在 `client/components/home/ThemeModal.tsx`: - `ThemeMode` 扩展为包含 `'suixin'` - 新增 `ThemeCard`:标题使用 i18n(如 `t('theme.suixin')`) - 布局改造:由 2 卡横排改为 **3 卡自适应**(`flexWrap` 或减小 gap/宽度),确保小屏不溢出 - 预览图:一期可复用 `theme_color.png` 作为占位;若有设计资源再替换为 `theme_suixin.png` ### 5.2 Home:新增主题分支与颜色计算时机 在 `client/app/(app)/home.tsx`: - 将 `themeMode === 'suixin'` 作为第三分支: - 背景为纯色(`backgroundColor = suixinColor`) - 不显示风景图 - **冷启动**:Home 首次进入时,读取用户画像与 `suixin.state`: - 若检测到新 boot 会话:重算并写入 `suixin.state` - 否则:直接使用持久化的 `last_color` - **切换文案**:在现有 `triggerNextContent` 成功切换索引后: - 若当前主题为 `suixin`:递增 `step_index`,计算新的 `last_color`,并持久化 ### 5.3 收藏(Favorites)背景记录兼容 `FavoriteItem.background` 当前对 `color` 存 `hex`,对 `scenery` 存图片索引。 - `suixin` 同样存 `hex`,与 `color` 分支一致即可 ## 6. i18n 计划(TC / EN) 在 `client/src/i18n/locales/all.json` 增加: - `theme.suixin` - (可选)`theme.suixinDesc`(若 UI 后续展示描述) 英文命名采用语义化方案(本计划建议): - EN:`theme.suixin = "Ease"` - TC:`theme.suixin = "隨心"` > 若后续品牌希望保留音译,也可改为 EN=`Suixin`,不影响技术实现。 ## 7. 兼容性与迁移 - `ThemeMode` 的存储值新增 `'suixin'`: - 旧版本只会存 `'scenery'|'color'`,升级后兼容 - 若读取到未知值,继续回退 `'scenery'` - 新增 `ui.theme.suixin.state`: - 若不存在,首次进入随心主题时初始化 ## 8. 测试计划(最小可回归) ### 8.1 单元测试(推荐) 为颜色算法模块增加用例(可放在 `client/src/features/suixinTheme/__tests__/`): - `stage.unknown=1` → 必选 `neutral` - `need={}` → 必选 `neutral` - `need={rest_balance:1}` → 选 `rest_balance` Base Theme - `step_index` 递增 → `t` 按往返波形变化且始终在 \([0,1]\) - `emotion_score=null` / `<=0.3` → 不触发微扰逻辑 ### 8.2 手动验收(与 spec 对齐) - ThemeModal 能看到第三个主题并可切换 - 冷启动进入 Home:随心背景根据画像选定主题色系 - 上滑切换文案:背景色在同主题内缓慢变化(无跨主题跳色) - `stage.unknown=1` 或 `need` 跳过:背景为 Neutral Theme - 切换语言:主题名称在 TC/EN 下正确显示 ## 9. 风险与对策 - **三卡布局拥挤**:采用 `flexWrap`/缩小卡片尺寸,必要时改为横向滚动 - **“持久化”与“冷启动重算”矛盾**:以“状态落盘 + 冷启动重置 base theme/seed”方式兼容两者 - **颜色可读性风险**:一期先用主题中间色/插值结果,避免过饱和;必要时增加对比度检查(后续迭代) ## 10. 里程碑拆分(实现顺序) - **M1:基础接入** - 扩展 `ThemeMode`,ThemeModal 增加第三项与 i18n - Home 增加 `suixin` 分支,背景可显示(先用 neutral 兜底) - **M2:算法落地 + 持久化** - 新增 suixin 颜色模块(Base/Neutral、pickTheme、lerp、t 生成) - 新增 `suixin.state` 存取与冷启动/切换文案更新 - **M3:回归与体验优化** - 收藏背景记录兼容 - 测试补齐与边界修正(unknown/跳过/缺画像)