「每日一诗」小程序换肤系统实战:从 CSS 变量到节日皮肤自动切换的完整实现

0 阅读8分钟

每日一诗.jpeg

## 前言

「每日一诗」是我开发的一款诗词打卡微信小程序,每天推荐一首古诗词,配合打卡签到、徽章成就等玩法。随着用户增长,个性化的需求越来越强烈——有人喜欢春天的绿色,有人偏爱夜晚的深紫,春节时又该有一抹喜庆的红。

于是,一套完整的换肤系统应运而生:9款皮肤(6通用 + 3节日)、右上角调色盘一键切换、节日当天自动换肤。本文分享这套系统的设计思路与核心实现。

技术选型:为什么是 CSS Variables?

小程序换肤常见三种方案:

方案原理优点缺点
多 WXSS 文件切换样式表引用完全隔离需重新渲染,闪烁明显
CSS 类名切换.theme-dark .card { ... }简单代码量大,耦合高
CSS Variables运行时修改变量值无需重载,一套代码小程序中需注意注入方式

最终选择 CSS Variables,三个核心理由:

  1. 运行时动态切换:修改 style 属性即可换肤,页面不闪烁、不重载
  2. 一套代码适配所有主题:所有样式统一用 var(--xxx) 引用,新增皮肤零代码改动
  3. 配置化驱动:皮肤定义存数据库 JSONB,后台可增减皮肤,无需发版

小程序中 CSS Variables 的关键限制:不支持 :root 定义,只能通过容器节点的行内 style 注入,子节点通过 var() 继承。

架构总览

┌─────────────────────────────────────┐
│         微信小程序前端               │
│   skinManager.js ← 皮肤管理器       │
│   pages/index/    ← 主页+调色盘面板  │
│   pages/skin-store/ ← 皮肤商店页面   │
└──────────────┬──────────────────────┘
               │ HTTP
┌──────────────▼──────────────────────┐
│      Supabase Edge Functions        │
│   get-user-skin 获取今日皮肤        │
│   set-user-preferred-skin 保存偏好  │
│   get-unlocked-skins 获取已解锁列表  │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│          PostgreSQL                  │
│   skins 皮肤定义(9款)             │
│   user_skin_states 用户皮肤状态     │
│   skin_usage_history 皮肤使用历史   │
└─────────────────────────────────────┘

前端只做两件事:注入 CSS 变量 + 交互展示;所有皮肤选择逻辑在后端 Edge Function 中完成,前端仅消费结果。

一、主题变量设计:10 个语义化 CSS 变量

核心思路是用最少的变量覆盖全页面颜色,分四层:

层级变量语义
品牌色--primary-color / --primary-dark按钮、标签、强调
页面背景--bg-gradient-start / --bg-gradient-end容器渐变
卡片背景--card-bg-start / --card-bg-end诗词卡片渐变
文字层级--text-primary / --text-secondary / --text-muted / --text-card标题、副文、提示、卡片内

10 个变量,不多不少,刚好覆盖一个内容型小程序的所有色彩需求。

每个皮肤的配置存储为 JSONB,直接存在数据库 skins 表中。好处是:新增皮肤只需插入一行 SQL,前端零改动。前端拿到配置后,遍历字段拼接成 CSS 变量字符串,注入到页面根容器的 style 属性即可。

二、皮肤获取优先级算法——系统的核心

这是整个换肤系统最关键的设计。每天用户打开小程序,看到哪款皮肤?答案由 get-user-skin Edge Function 的三级优先级链路决定:

优先级 1: 用户有偏好皮肤 (preferred_skin_id)  直接返回
优先级 2: 节日当天  自动返回节日皮肤
优先级 3: 其他  轮播算法选择

2.1 偏好皮肤优先

用户手动选择皮肤后,preferred_skin_id 保存到 user_skin_states 表。每次获取皮肤时,优先检查偏好设置。这尊重了用户的选择——如果你选了「静谧夜空」,即使春节也不会被强制切换(当然可以调整策略让节日皮肤优先级更高)。

2.2 节日皮肤自动切换——难点在农历

春节、中秋是农历节日,不能简单用公历日期判断。这里需要一个公历转农历算法

实现思路:

  • 经典的 lunarInfo 查表法,用一段 hex 数组编码 1900-2100 年每月大小月和闰月信息
  • 输入公历年月日 → 逐月减去天数 → 定位农历月日
  • 输出格式 "01-01" 表示正月初一,"08-15" 表示八月十五

数据库中节日皮肤的 lunar_date 字段存储农历日期,checkFestivalSkin() 将当天公历转农历后逐个匹配。

皮肤名称lunar_date含义
spring_festival新春佳节01-01正月初一
mid_autumn中秋月圆08-15八月十五
qingming清明时节NULL清明按公历,前端处理

清明节是节气,固定在公历 4 月 4-6 日,不走农历匹配逻辑,而是在前端 festival.js 中按公历判断。

2.3 日常轮播算法

没有偏好、也不是节日时,怎么决定今天展示哪款皮肤?答案是确定性轮播

skinIndex = (daysSinceEpoch + hash(openid)) % skins.length

两个关键设计:

  • daysSinceEpoch:距参考日期的天数,保证每天换一个皮肤,避免审美疲劳
  • hash(openid):用户唯一哈希,保证不同用户同一天看到不同皮肤,增加多样性
  • 确定性:相同输入一定得到相同输出,用户刷新页面不会闪烁切换

2.4 当日缓存机制

一旦确定了今日皮肤,就把 skin_rotation_date 设为当天。后续请求发现日期匹配,直接返回已有结果,避免重复计算和频繁查库。

三、前端交互设计

3.1 调色盘快速切换

入口:主页日期栏右侧放置🎨图标,点击弹出底部面板。

面板设计:4列网格,免费皮肤和节日皮肤分组展示。每个皮肤用一个主色渐变色块预览,当前使用的皮肤显示 ✓ 标记。

交互流程:点击 → 确认弹窗 → 应用皮肤 → 本地缓存 + 云端保存偏好

体验细节

  • wx.vibrateShort() 触觉反馈,增强操作质感
  • 重复点击当前皮肤,直接关闭面板,不弹确认
  • 面板滑入使用 cubic-bezier(0.4, 0, 0.2, 1) 缓动,手感流畅

3.2 皮肤商店页面

独立页面,三列网格展示全部皮肤,分「免费」和「节日」两个区域。每个皮肤卡片包含:预览色块 + 名称 + 类型标签。

跨页面状态同步的关键问题:在皮肤商店切换后,主页如何同步更新?这里用了一个简单有效的方案——getCurrentPages() 找到首页实例,直接调用 setData 更新。对于只有两个页面的场景足够好用。

3.3 首屏秒开:缓存先行 + 后台更新

换肤不应该拖慢首屏加载。策略是:

  1. 先读本地缓存,瞬间渲染上次的皮肤
  2. 后台请求云端,获取今日皮肤
  3. 有变化再覆盖,无变化不动

用户感知到的是:打开即展示,几乎无等待。

四、数据库设计要点

4.1 skins 表——皮肤定义

关键字段:

  • category:区分 free / festival,用于查询过滤
  • theme_config:JSONB 存储色彩配置,增删皮肤无需改代码
  • priority:数值越高优先级越高,节日皮肤设为 100+
  • lunar_date:农历日期字符串,节日皮肤专用,普通皮肤为 NULL
  • is_active:软开关,下架皮肤不需删数据

4.2 user_skin_states 表——用户皮肤状态

关键字段:

  • preferred_skin_id:用户手动选择的偏好皮肤
  • current_skin_id:当日实际使用的皮肤
  • skin_rotation_date:当日皮肤确定日期,用于缓存判断
  • unlocked_skins:已解锁皮肤 ID 数组

preferred_skin_id 是后续迁移添加的,初始版本只有 current_skin_id。用 ALTER TABLE ... ADD COLUMN IF NOT EXISTS 实现平滑升级。

4.3 RLS 安全策略

  • 皮肤定义表:公开读取,所有用户可见
  • 用户皮肤状态:仅自己可读写,通过 current_setting('app.current_openid', true) 实现

五、踩坑与经验

5.1 CSS Variables 注入方式

小程序不支持 :root 伪类和 JS 动态修改变量,唯一方式是行内 style 注入。这意味着所有使用变量的元素必须是注入节点的后代,布局时需注意容器层级。

5.2 底部面板滚动穿透

弹出面板内的 scroll-view 需设置固定高度并启用 enhanced="true",否则手指在面板内滑动会穿透到背后页面。

5.3 农历算法的边界

查表法覆盖 1900-2100 年,精度够用。但清明节这类按公历计算的节日,不应走农历匹配,需在 lunar_date 设为 NULL,由前端公历逻辑单独处理。

5.4 跨页面通信的取舍

getCurrentPages() 直接操作其他页面实例,简单粗暴且有效,但不适合复杂场景——如果页面层级不确定或页面可能被销毁,建议改用 getApp() 全局状态或自定义事件中心。

六、未来展望

  • 用户自定义主题色:开放主色选择,自动生成完整配色方案
  • 皮肤使用数据统计:利用 skin_usage_history 表分析用户偏好,指导新皮肤设计
  • AI 生成配色:接入 LLM,根据当日诗词意境自动生成匹配的主题色

总结

这套换肤系统的核心思路可以概括为三句话:

  1. CSS Variables 做主题引擎——10 个语义化变量,配置化驱动,新增皮肤零代码改动
  2. 三级优先级算法做选择逻辑——偏好优先 > 节日自动 > 日常轮播,确定性设计避免闪烁
  3. 农历算法做节日自动换肤——查表法公历转农历,数据库存储农历日期,运行时匹配

整套方案从数据库到前端渲染完全闭环,核心思路可直接迁移到其他小程序项目。希望对你有帮助!