一站式搞定品牌风格:TinyRobot 主题定制从入门到精通
每个品牌都有自己的视觉风格------颜色、圆角、间距、字号,这些看似琐碎的细节,共同构成了用户对产品的第一印象。TinyRobot 的主题系统正是为此而生:通过 CSS 变量 + ThemeProvider + useTheme 的三层架构,开发者可以一站式搞定从全局主题到单组件微调的全部定制需求。
本文将从核心概念出发,逐步深入到高级用法和实战案例,带你从入门到精通 TinyRobot 的主题定制。
主题系统核心概念
TinyRobot 的主题系统基于 CSS 变量实现,核心组件是 ThemeProvider 和 useTheme 组合式函数。
两个核心概念
- 主题(Theme):定义应用的整体视觉风格,如品牌色、圆角、间距等。一个主题可以包含多组 CSS 变量值,覆盖亮色和暗色两种颜色模式。
- 颜色模式(Color Mode) :控制亮色/暗色模式,支持
light、dark和auto(跟随系统)三种模式。
主题和颜色模式是独立的概念------你可以在同一个主题下切换亮色/暗色模式,也可以在不同主题下使用相同的颜色模式。
typescript
// 颜色模式类型
type ColorMode = 'light' | 'dark' | 'auto'
light:亮色模式dark:暗色模式auto:自动模式,跟随系统prefers-color-scheme设置
ThemeProvider 基础用法
包裹应用
使用 ThemeProvider 包裹你的应用,它会自动在目标元素上添加 data-tr-theme 和 data-tr-color-mode 属性:
vue
<template>
<ThemeProvider>
<YourApp />
</ThemeProvider>
</template>
<script setup>
import { ThemeProvider } from '@opentiny/tiny-robot'
</script>
包裹后,html 元素(默认 targetElement)会被添加:
html
<html data-tr-theme="" data-tr-color-mode="light">
<!-- 或 auto 模式下跟随系统 -->
<html data-tr-theme="" data-tr-color-mode="dark">
</html>
ThemeProvider Props
| 属性 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
colorMode |
ColorMode |
否 | 'auto' |
颜色模式,支持 v-model 双向绑定 |
targetElement |
string |
否 | 'html' |
应用主题属性选择器的目标元素 |
theme |
string |
否 | '' |
主题名称,支持 v-model 双向绑定 |
storage |
ThemeStorage |
否 | - | 主题数据存储实现,用于持久化 |
storageKey |
string |
否 | 'tiny-robot-theme-data' |
存储键名 |
CSS 属性选择器覆写
ThemeProvider 设置的 data-tr-theme 和 data-tr-color-mode 属性,是 CSS 变量覆写的核心锚点。开发者使用属性选择器来定义不同主题和颜色模式下的样式:
按颜色模式覆写
css
/* 亮色模式 */
[data-tr-color-mode='light'] {
--tr-primary-color: #5e7ce0;
--tr-background-color: #ffffff;
--tr-sender-bg-color: #ffffff;
--tr-sender-text-color: #333333;
}
/* 暗色模式 */
[data-tr-color-mode='dark'] {
--tr-primary-color: #7693f5;
--tr-background-color: #1a1a1a;
--tr-sender-bg-color: #1e1e2e;
--tr-sender-text-color: #cdd6f4;
}
按主题名称覆写
css
/* 默认主题(theme="") */
[data-tr-theme=''] {
--tr-border-radius: 8px;
}
/* 自定义主题(theme="brand") */
[data-tr-theme='brand'] {
--tr-border-radius: 16px;
--tr-sender-bg-color: #f0f7ff;
--tr-sender-border-radius: 12px;
}
组合选择器
css
/* brand 主题 + 亮色模式 */
[data-tr-theme='brand'][data-tr-color-mode='light'] {
--tr-sender-bg-color: #f0f7ff;
--tr-container-bg-color: #ebf5ff;
}
/* brand 主题 + 暗色模式 */
[data-tr-theme='brand'][data-tr-color-mode='dark'] {
--tr-sender-bg-color: #1a365d;
--tr-container-bg-color: #0d1b3e;
}
主题切换
通过 Props 控制
ThemeProvider 的 theme 和 colorMode 属性支持 v-model 双向绑定:
vue
<template>
<ThemeProvider
v-model:theme="currentTheme"
v-model:color-mode="colorMode"
>
<YourApp />
</ThemeProvider>
</template>
<script setup>
import { ref } from 'vue'
const currentTheme = ref('') // 默认主题
const colorMode = ref('auto') // 自动模式
</script>
通过 useTheme 控制
在 ThemeProvider 包裹的组件中使用 useTheme 组合式函数,可以通过代码动态切换主题:
vue
<script setup>
import { useTheme } from '@opentiny/tiny-robot'
const {
theme, // 当前主题名称(Ref<string>)
colorMode, // 当前颜色模式(Ref<ColorMode>)
resolvedColorMode, // 解析后的颜色模式(Readonly<Ref<'light' | 'dark'>>)
systemColorMode, // 系统颜色模式(Readonly<Ref<'light' | 'dark'>>)
setTheme, // 设置主题
toggleColorMode, // 切换亮暗模式
setColorMode // 设置颜色模式
} = useTheme()
// 设置主题
setTheme('brand')
// 设置颜色模式
setColorMode('dark')
// 切换亮暗模式
toggleColorMode()
</script>
useTheme 返回值详解:
| 属性 | 类型 | 说明 |
|---|---|---|
theme |
Ref<string> |
当前主题名称 |
colorMode |
Ref<ColorMode> |
当前颜色模式 |
resolvedColorMode |
`Readonly<Ref<'light' | 'dark'>>` |
systemColorMode |
`Readonly<Ref<'light' | 'dark'>>` |
setTheme |
(newTheme: string) => boolean |
设置主题,返回是否成功 |
toggleColorMode |
() => boolean |
切换亮暗模式 |
setColorMode |
(mode: ColorMode) => boolean |
设置颜色模式 |
重要提示 :
useTheme只能在ThemeProvider包裹的组件中使用。如果在没有 ThemeProvider 的组件中使用,相关方法会返回false并在控制台输出警告信息。
实战:主题切换面板
vue
<template>
<ThemeProvider v-model:theme="currentTheme" v-model:color-mode="colorMode">
<div class="chat-app">
<!-- 主题切换按钮 -->
<div class="theme-controls">
<button @click="toggleColorMode()">
{{ resolvedColorMode === 'dark' ? '☀️' : '🌙' }}
</button>
<select v-model="currentTheme">
<option value="">默认</option>
<option value="brand">品牌</option>
<option value="minimal">极简</option>
</select>
</div>
<tr-container title="AI 助手">
<tr-bubble-list :messages="messages" />
<template #footer>
<tr-sender v-model="inputText" @submit="handleSubmit" />
</template>
</tr-container>
</div>
</ThemeProvider>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ThemeProvider, useTheme } from '@opentiny/tiny-robot'
const currentTheme = ref('')
const colorMode = ref('auto')
const { resolvedColorMode, toggleColorMode } = useTheme()
</script>
嵌套主题
ThemeProvider 可以嵌套使用,子组件会往上查找最近的 ThemeProvider:
vue
<template>
<ThemeProvider theme="default" color-mode="light">
<div>外层:默认主题(亮色)</div>
<!-- 主题预览区 -->
<ThemeProvider theme="brand" color-mode="dark">
<div>内层:品牌主题(暗色)</div>
<!-- 这里的 Sender 使用 brand + dark 主题 -->
<tr-sender />
</ThemeProvider>
</ThemeProvider>
</template>
嵌套主题的适用场景:
- 特定区域使用不同主题:如左侧导航使用品牌主题,右侧内容使用极简主题
- 主题预览功能:在一个页面中同时展示多个主题的效果
- 组件级主题隔离:某些组件使用独立主题,不受全局主题影响
嵌套的 ThemeProvider 会在其对应的 targetElement 上设置独立的属性,子组件自动匹配最近的 ThemeProvider。
主题持久化
使用 storage 和 storageKey 属性持久化主题设置,用户切换主题后,刷新页面仍能恢复之前的选择:
typescript
// ThemeStorage 接口
type ThemeStorage = Pick<Storage, 'getItem' | 'setItem'>
localStorage 持久化
vue
<template>
<ThemeProvider
v-model:theme="currentTheme"
v-model:color-mode="colorMode"
:storage="localStorage"
storage-key="my-app-theme"
>
<YourApp />
</ThemeProvider>
</template>
sessionStorage 持久化
vue
<template>
<ThemeProvider
v-model:theme="currentTheme"
v-model:color-mode="colorMode"
:storage="sessionStorage"
storage-key="my-app-theme"
>
<YourApp />
</ThemeProvider>
</template>
自定义存储实现
可以实现自定义存储策略,如将主题数据保存到远程服务器:
typescript
const customStorage: ThemeStorage = {
getItem: (key: string) => {
// 从远程服务器获取
return serverFetchThemeData(key)
},
setItem: (key: string, value: string) => {
// 保存到远程服务器
serverSaveThemeData(key, value)
}
}
vue
<ThemeProvider :storage="customStorage" storage-key="user-theme-preference">
<YourApp />
</ThemeProvider>
自定义目标元素:targetElement
默认情况下,ThemeProvider 在 html 元素上设置属性,影响整个页面。通过 targetElement 可以指定主题应用的根元素:
vue
<template>
<div id="app-container">
<ThemeProvider target-element="#app-container">
<YourApp />
</ThemeProvider>
</div>
</template>
此时,data-tr-theme 和 data-tr-color-mode 属性会被设置在 #app-container 元素上,主题只会影响该元素及其子元素。
适用场景:
- 页面中只有部分区域使用 TinyRobot 组件
- 多个独立的聊天窗口需要不同主题
- 避免主题属性污染全局
html元素
从 v0.3 迁移:theme prop → ThemeProvider
v0.3 的 Sender 使用 theme prop 设置主题:
vue
<!-- v0.3 -->
<tr-sender theme="dark" />
v0.4 移除了 theme prop,改用 ThemeProvider 包裹:
vue
<!-- v0.4 -->
<ThemeProvider color-mode="dark">
<tr-sender />
</ThemeProvider>
迁移优势:
- 全局生效:一个 ThemeProvider 包裹所有组件,无需每个组件单独设置
- 自动继承:子组件自动继承父级 ThemeProvider 的配置
- 双向绑定:v-model:color-mode 支持动态切换
- 持久化:内置 storage 支持,无需手动保存主题偏好
Sender 主题定制实战
结合 CSS 变量和 ThemeProvider,定制 Sender 的完整外观:
实战1:品牌主题 Sender
css
/* brand 主题变量定义 */
[data-tr-theme='brand'] {
--tr-sender-bg-color: #f0f7ff;
--tr-sender-text-color: #1a365d;
--tr-sender-placeholder-color: #a0aec0;
--tr-sender-button-hover-bg: #bee3f8;
--tr-sender-border-radius: 16px;
--tr-sender-padding: 14px 16px;
--tr-sender-gap: 10px;
--tr-sender-footer-gap: 10px;
--tr-sender-footer-padding: 10px 16px;
--tr-sender-button-size: 36px;
--tr-sender-button-size-submit: 44px;
--tr-container-bg-color: #ebf5ff;
--tr-container-border-color: #bee3f8;
--tr-prompt-bg: #bee3f8;
--tr-prompt-bg-hover: #90cdf4;
--tr-prompt-border-radius: 12px;
--tr-prompt-title-color: #1a365d;
}
vue
<template>
<ThemeProvider v-model:theme="currentTheme">
<tr-container title="AI 助手">
<tr-bubble-list :messages="messages" />
<template #footer>
<tr-sender @submit="handleSubmit">
<template #footer="{ disabled, loading }">
<tr-voice-button :disabled="disabled || loading" />
</template>
</tr-sender>
</template>
</tr-container>
</ThemeProvider>
</template>
<script setup>
import { ref } from 'vue'
const currentTheme = ref('brand')
</script>
实战2:亮暗自适应 Sender
css
/* 亮色模式 */
[data-tr-color-mode='light'] {
--tr-sender-bg-color: #ffffff;
--tr-sender-text-color: #333333;
--tr-sender-placeholder-color: #999999;
--tr-sender-button-hover-bg: #f5f5f5;
--tr-bubble-box-bg: #f8f9fa;
}
/* 暗色模式 */
[data-tr-color-mode='dark'] {
--tr-sender-bg-color: #1e1e2e;
--tr-sender-text-color: #cdd6f4;
--tr-sender-placeholder-color: #585b70;
--tr-sender-button-hover-bg: #313244;
--tr-bubble-box-bg: #313244;
}
vue
<template>
<!-- 使用 auto 模式,自动跟随系统 -->
<ThemeProvider v-model:color-mode="colorMode" :storage="localStorage">
<div class="theme-toggle">
<button @click="toggleColorMode()">
{{ resolvedColorMode === 'dark' ? '切换到亮色' : '切换到暗色' }}
</button>
</div>
<tr-sender @submit="handleSubmit" />
</ThemeProvider>
</template>
<script setup>
import { ref } from 'vue'
import { ThemeProvider, useTheme } from '@opentiny/tiny-robot'
const colorMode = ref('auto')
const { resolvedColorMode, toggleColorMode } = useTheme()
</script>
实战3:多主题切换面板
css
/* 默认主题 */
[data-tr-theme=''] {
--tr-sender-bg-color: #ffffff;
--tr-sender-border-radius: 8px;
--tr-container-border-color: #e5e7eb;
}
/* 品牌主题 */
[data-tr-theme='brand'] {
--tr-sender-bg-color: #f0f7ff;
--tr-sender-border-radius: 16px;
--tr-container-border-color: #bee3f8;
--tr-container-bg-color: #ebf5ff;
}
/* 极简主题 */
[data-tr-theme='minimal'] {
--tr-sender-bg-color: #fafafa;
--tr-sender-border-radius: 4px;
--tr-container-border-color: #e0e0e0;
}
vue
<template>
<ThemeProvider v-model:theme="currentTheme" v-model:color-mode="colorMode">
<div class="theme-panel">
<div class="theme-options">
<button @click="currentTheme = ''">默认</button>
<button @click="currentTheme = 'brand'">品牌</button>
<button @click="currentTheme = 'minimal'">极简</button>
</div>
<div class="color-mode-options">
<button @click="colorMode = 'light'">亮色</button>
<button @click="colorMode = 'dark'">暗色</button>
<button @click="colorMode = 'auto'">自动</button>
</div>
</div>
<tr-container title="AI 助手">
<tr-bubble-list :messages="messages" />
<template #footer>
<tr-sender @submit="handleSubmit" />
</template>
</tr-container>
</ThemeProvider>
</template>
<script setup>
import { ref } from 'vue'
const currentTheme = ref('')
const colorMode = ref('auto')
</script>
最佳实践
1. 使用语义化 CSS 变量名
不要只覆写"视觉层"的变量,而要考虑变量的语义含义:
css
/* 不推荐------只关注视觉 */
[data-tr-color-mode='dark'] {
--tr-sender-bg-color: #1e1e2e;
}
/* 推荐------语义化命名 */
[data-tr-color-mode='dark'] {
--tr-sender-bg-color: var(--app-surface-color, #1e1e2e);
}
2. 为所有 CSS 变量提供默认值
确保未自定义时也有良好显示,使用 var() 的 fallback 值:
css
.my-sender {
--tr-sender-bg-color: var(--my-brand-bg, #f0f7ff);
}
3. 启用主题持久化
用户切换主题后,刷新页面应该恢复之前的选择:
vue
<ThemeProvider :storage="localStorage" storage-key="my-app-theme">
<YourApp />
</ThemeProvider>
4. 使用 auto 模式
默认使用 auto 颜色模式,自动适配用户的系统设置。大多数用户期望应用跟随系统的亮暗偏好:
vue
<ThemeProvider v-model:color-mode="colorMode">
<!-- 默认 auto,但允许用户手动切换 -->
</ThemeProvider>
<script setup>
const colorMode = ref('auto')
</script>
5. 避免复杂的 CSS 计算
CSS 变量中避免使用 calc() 等复杂计算,保持主题切换流畅:
css
/* 不推荐 */
[data-tr-theme='brand'] {
--tr-sender-padding: calc(var(--base-padding) * 1.5);
}
/* 推荐 */
[data-tr-theme='brand'] {
--tr-sender-padding: 18px;
}
6. 集中管理主题变量
将所有主题变量定义集中在一个或少数几个 CSS 文件中,便于维护:
css
/* themes/light.css */
[data-tr-color-mode='light'] {
/* 全局变量 */
--tr-primary-color: #5e7ce0;
/* Sender 变量 */
--tr-sender-bg-color: #ffffff;
--tr-sender-text-color: #333333;
/* Container 变量 */
--tr-container-bg-color: #ffffff;
--tr-container-border-color: #e5e7eb;
/* Bubble 变量 */
--tr-bubble-box-bg: #f8f9fa;
--tr-bubble-text-color: #333333;
}
/* themes/dark.css */
[data-tr-color-mode='dark'] {
--tr-primary-color: #7693f5;
--tr-sender-bg-color: #1e1e2e;
--tr-sender-text-color: #cdd6f4;
--tr-container-bg-color: #181825;
--tr-bubble-box-bg: #313244;
--tr-bubble-text-color: #cdd6f4;
}
小结
TinyRobot 的主题系统三层架构各司其职:
| 层级 | 机制 | 职责 |
|---|---|---|
| CSS 变量 | --tr-* 变量 |
定义具体的视觉值 |
| ThemeProvider | data-tr-* 属性 |
选择变量集合(主题 + 颜色模式) |
| useTheme | 组合式函数 | 动态切换主题和颜色模式 |
这种三层架构的优势:
- 一致性:所有组件使用相同的变量前缀和命名规范
- 灵活性:CSS 变量可以精确到单个组件的某个属性
- 可组合:ThemeProvider 嵌套、持久化、自定义目标元素等高级功能
- 零运行时开销:CSS 变量的切换由浏览器原生处理,不涉及 JavaScript 计算
从快速入门(一个 ThemeProvider 包裹应用),到高级定制(嵌套主题 + 持久化 + 自定义存储),TinyRobot 的主题系统覆盖了从简单到复杂的全部场景。记住一句话:定义变量,选择集合,动态切换------这就是 TinyRobot 主题定制的全部秘密。
🔗 TinyRobot 官网 :tiny-robot.opentiny.design
🔗 GitHub 仓库 :github.com/opentiny/ti...