9.0 KiB
「随心」主题(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 | nullprofile_confidence: numberprofile_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纯色,作为 HomebackgroundColor - 全程 不跨 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
数据结构(建议):
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
- 若检测到新 boot 会话:重算并写入
- 切换文案:在现有
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→ 必选neutralneed={}→ 必选neutralneed={rest_balance:1}→ 选rest_balanceBase Themestep_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/跳过/缺画像)