EasyStarter logoEasyStarter

主题系统

配置 App 的外观模式(亮色/暗色/跟随系统)与主题系列

主题系统

App 端的主题由两个独立维度组合而成:外观模式(亮色 / 暗色 / 跟随系统)和主题系列(Theme Family)。两者组合后生成一个活跃主题名称,由 Uniwind 驱动实际样式渲染。


两个维度

外观模式(ThemeModePreference)

控制应用使用亮色还是暗色配色:

说明
system跟随设备系统设置(默认)
light固定亮色模式
dark固定暗色模式

主题系列(ThemeFamily)

控制应用的整体色调风格,内置 4 套:

风格
alpha默认主题系列
lavender薰衣草紫
mint薄荷绿
sky天空蓝

活跃主题名称

两个维度组合后生成活跃主题名:{themeFamily}-{resolvedThemeMode}

例如,用户选择 lavender 系列 + 深色模式,则活跃主题为 lavender-dark。该名称通过 Uniwind.setTheme() 驱动组件库的样式切换。


核心文件

文件说明
apps/native/providers/theme-provider.tsx主题状态管理、持久化读写、Uniwind 同步
apps/native/configs/app-config.ts存储键名配置

用户偏好存储

主题选择通过 AsyncStorage 持久化到设备本地:

存储键(来自 app-config.ts内容
{AppName}_theme_preference外观模式:system / light / dark
{AppName}_theme_family主题系列:alpha / lavender / mint / sky

其中 AppName 来自 packages/app-config 中配置的应用名称。


更改默认主题

ThemeProvider 初始化时,若 AsyncStorage 中没有存储值,则使用代码中定义的默认值:

apps/native/providers/theme-provider.tsx
const [themeModePreference, setThemeModePreferenceState] =
  useState<ThemeModePreference>("system");   // 默认:跟随系统

const [themeFamily, setThemeFamilyState] = useState<ThemeFamily>("alpha");  // 默认:alpha

修改这两行的初始值,即可更改新用户首次打开 App 时的默认主题。


新增主题系列

在类型中添加新系列

编辑 theme-provider.tsx,将新系列加入 THEME_FAMILIES 常量和 ThemeFamily 类型:

apps/native/providers/theme-provider.tsx
const THEME_FAMILIES = ["alpha", "lavender", "mint", "sky", "ocean"] as const;
//                                                              ↑ 新增
export type ThemeFamily = "alpha" | "lavender" | "mint" | "sky" | "ocean";

在 Uniwind 中注册对应主题

参照项目中已有的 alphalavender 等系列,新系列需要三步:新建 CSS 文件、在 global.css 中导入、在 metro.config.js 中注册。

1. 新建 apps/native/themes/ocean.css

参照 themes/alpha.css 的结构,为亮色和暗色各写一套变量:

apps/native/themes/ocean.css
@layer theme {
  :root {
    @variant ocean-light {
      --radius: 0.5rem;

      --background: oklch(0.97 0.01 220);
      --foreground: oklch(0.15 0.03 220);

      --surface: oklch(0.97 0.01 220);
      --surface-foreground: var(--foreground);
      --surface-secondary: oklch(0.93 0.02 220);
      --surface-secondary-foreground: var(--foreground);
      --surface-tertiary: oklch(0.90 0.02 220);
      --surface-tertiary-foreground: var(--foreground);

      --overlay: oklch(0.97 0.01 220);
      --overlay-foreground: var(--foreground);

      --muted: var(--color-neutral-500);
      --default: oklch(0.92 0.02 220);
      --default-foreground: oklch(0.15 0.03 220);

      --accent: oklch(0.45 0.15 220);
      --accent-foreground: var(--snow);

      --field-background: var(--default);
      --field-foreground: var(--foreground);
      --field-placeholder: var(--muted);
      --field-border: transparent;

      --success: oklch(0.55 0.12 154);
      --success-foreground: var(--snow);
      --warning: oklch(0.72 0.15 65);
      --warning-foreground: var(--eclipse);
      --danger: oklch(0.63 0.19 29);
      --danger-foreground: var(--snow);

      --segment: oklch(0.97 0.01 220);
      --segment-foreground: var(--eclipse);

      --border: oklch(0.85 0.03 220);
      --separator: oklch(0.75 0.03 220);
      --focus: var(--accent);
      --link: var(--foreground);

      --surface-shadow:
        0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
        0 0 1px 0 rgba(0, 0, 0, 0.06);
      --overlay-shadow:
        0 2px 8px 0 rgba(0, 0, 0, 0.02), 0 14px 28px 0 rgba(0, 0, 0, 0.03);
      --field-shadow:
        0 2px 4px 0 rgba(0, 0, 0, 0.04), 0 1px 2px 0 rgba(0, 0, 0, 0.06),
        0 0 1px 0 rgba(0, 0, 0, 0.06);
    }

    @variant ocean-dark {
      --radius: 0.5rem;

      --background: oklch(0.12 0.03 220);
      --foreground: oklch(0.92 0.02 220);

      --surface: oklch(0.17 0.03 220);
      --surface-foreground: var(--foreground);
      --surface-secondary: oklch(0.22 0.03 220);
      --surface-secondary-foreground: var(--foreground);
      --surface-tertiary: oklch(0.25 0.03 220);
      --surface-tertiary-foreground: var(--foreground);

      --overlay: oklch(0.20 0.03 220);
      --overlay-foreground: var(--foreground);

      --muted: var(--color-neutral-400);
      --default: oklch(0.20 0.03 220);
      --default-foreground: var(--snow);

      --accent: oklch(0.65 0.15 220);
      --accent-foreground: var(--eclipse);

      --field-background: var(--default);
      --field-foreground: var(--foreground);
      --field-placeholder: var(--muted);
      --field-border: transparent;

      --success: oklch(0.55 0.12 154);
      --success-foreground: var(--snow);
      --warning: oklch(0.85 0.14 78);
      --warning-foreground: var(--eclipse);
      --danger: oklch(0.58 0.16 31);
      --danger-foreground: var(--snow);

      --segment: oklch(0.20 0.03 220);
      --segment-foreground: var(--foreground);

      --border: oklch(0.25 0.03 220);
      --separator: oklch(0.35 0.03 220);
      --focus: var(--accent);
      --link: var(--foreground);

      --surface-shadow: 0 0 0 0 transparent inset;
      --overlay-shadow: 0 0 1px 0 rgba(255, 255, 255, 0.2) inset;
      --field-shadow: 0 0 0 0 transparent inset;
    }
  }
}

每个主题必须声明完全相同的变量名,参照 alpha.css 核对变量列表。

2. 在 global.css 中导入

apps/native/global.css
@import "./themes/alpha.css";
@import "./themes/lavander.css";
@import "./themes/mint.css";
@import "./themes/sky.css";
@import "./themes/ocean.css";   /* 新增 */

3. 在 metro.config.js 中注册

apps/native/metro.config.js
module.exports = withUniwindConfig(config, {
  cssEntryFile: "./global.css",
  dtsFile: "./uniwind-types.d.ts",
  extraThemes: [
    "alpha-light",   "alpha-dark",
    "lavender-light", "lavender-dark",
    "mint-light",    "mint-dark",
    "sky-light",     "sky-dark",
    "ocean-light",   "ocean-dark",   // 新增
  ],
});

修改 metro.config.js 后需要重启 Metro。如遇缓存异常,运行 npx expo start --clear

参考:Uniwind 自定义主题文档

添加 i18n 翻译(可选)

packages/i18n/messages/native/ 下的各语言文件中,为新系列补充显示名称和描述:

packages/i18n/messages/native/zh.json
{
  "settings": {
    "themeFamilyOptions": {
      "ocean": "海洋"
    },
    "themeFamilyDescriptions": {
      "ocean": "深邃的海洋蓝调"
    }
  }
}