237 lines
9.0 KiB
Markdown
237 lines
9.0 KiB
Markdown
# 「随心」主题(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/跳过/缺画像)
|
||
|