主题系统提供了完整的视觉主题管理功能,支持多主题切换、自定义主题、动态主题生成等,为用户提供个性化的视觉体验。
文件位置: src/styles/theme.css
/* 主题变量定义 */
:root {
/* 主色调 */
--theme-primary: #409eff;
--theme-primary-light: #66b1ff;
--theme-primary-lighter: #a0cfff;
--theme-primary-dark: #3a8ee6;
--theme-primary-darker: #337ecc;
/* 辅助色 */
--theme-success: #67c23a;
--theme-warning: #e6a23c;
--theme-danger: #f56c6c;
--theme-info: #909399;
/* 中性色 */
--theme-text-primary: #303133;
--theme-text-regular: #606266;
--theme-text-secondary: #909399;
--theme-text-placeholder: #c0c4cc;
/* 边框色 */
--theme-border-base: #dcdfe6;
--theme-border-light: #e4e7ed;
--theme-border-lighter: #ebeef5;
--theme-border-extra-light: #f2f6fc;
/* 背景色 */
--theme-bg-primary: #ffffff;
--theme-bg-secondary: #f5f7fa;
--theme-bg-tertiary: #fafafa;
--theme-bg-quaternary: #f0f2f5;
/* 阴影 */
--theme-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
--theme-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
--theme-shadow-dark: 0 4px 12px rgba(0, 0, 0, 0.15);
/* 圆角 */
--theme-border-radius-base: 4px;
--theme-border-radius-small: 2px;
--theme-border-radius-large: 8px;
--theme-border-radius-round: 20px;
--theme-border-radius-circle: 50%;
/* 字体 */
--theme-font-size-extra-small: 12px;
--theme-font-size-small: 13px;
--theme-font-size-base: 14px;
--theme-font-size-medium: 16px;
--theme-font-size-large: 18px;
--theme-font-size-extra-large: 20px;
/* 间距 */
--theme-spacing-xs: 4px;
--theme-spacing-sm: 8px;
--theme-spacing-md: 16px;
--theme-spacing-lg: 24px;
--theme-spacing-xl: 32px;
--theme-spacing-xxl: 48px;
/* 动画 */
--theme-transition-base: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
--theme-transition-fade: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
--theme-transition-md-fade: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}[data-theme="default"] {
--theme-primary: #409eff;
--theme-primary-light: #66b1ff;
--theme-primary-lighter: #a0cfff;
--theme-primary-dark: #3a8ee6;
--theme-primary-darker: #337ecc;
--theme-success: #67c23a;
--theme-warning: #e6a23c;
--theme-danger: #f56c6c;
--theme-info: #909399;
--theme-text-primary: #303133;
--theme-text-regular: #606266;
--theme-text-secondary: #909399;
--theme-text-placeholder: #c0c4cc;
--theme-bg-primary: #ffffff;
--theme-bg-secondary: #f5f7fa;
--theme-bg-tertiary: #fafafa;
--theme-bg-quaternary: #f0f2f5;
--theme-border-base: #dcdfe6;
--theme-border-light: #e4e7ed;
--theme-border-lighter: #ebeef5;
--theme-border-extra-light: #f2f6fc;
}[data-theme="dark"] {
--theme-primary: #409eff;
--theme-primary-light: #66b1ff;
--theme-primary-lighter: #a0cfff;
--theme-primary-dark: #3a8ee6;
--theme-primary-darker: #337ecc;
--theme-success: #67c23a;
--theme-warning: #e6a23c;
--theme-danger: #f56c6c;
--theme-info: #909399;
--theme-text-primary: #e4e7ed;
--theme-text-regular: #cfcfcf;
--theme-text-secondary: #a3a6ad;
--theme-text-placeholder: #6c6e72;
--theme-bg-primary: #1d1e1f;
--theme-bg-secondary: #25262b;
--theme-bg-tertiary: #2c2e33;
--theme-bg-quaternary: #34373c;
--theme-border-base: #4c4d4f;
--theme-border-light: #414243;
--theme-border-lighter: #363637;
--theme-border-extra-light: #2b2b2c;
--theme-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.24), 0 0 6px rgba(0, 0, 0, 0.08);
--theme-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
--theme-shadow-dark: 0 4px 12px rgba(0, 0, 0, 0.3);
}[data-theme="blue"] {
--theme-primary: #1890ff;
--theme-primary-light: #40a9ff;
--theme-primary-lighter: #69c0ff;
--theme-primary-dark: #096dd9;
--theme-primary-darker: #0050b3;
--theme-success: #52c41a;
--theme-warning: #faad14;
--theme-danger: #ff4d4f;
--theme-info: #13c2c2;
--theme-bg-primary: #f0f8ff;
--theme-bg-secondary: #e6f7ff;
--theme-bg-tertiary: #bae7ff;
--theme-bg-quaternary: #91d5ff;
}[data-theme="green"] {
--theme-primary: #52c41a;
--theme-primary-light: #73d13d;
--theme-primary-lighter: #95de64;
--theme-primary-dark: #389e0d;
--theme-primary-darker: #237804;
--theme-success: #52c41a;
--theme-warning: #faad14;
--theme-danger: #ff4d4f;
--theme-info: #13c2c2;
--theme-bg-primary: #f6ffed;
--theme-bg-secondary: #d9f7be;
--theme-bg-tertiary: #b7eb8f;
--theme-bg-quaternary: #95de64;
}[data-theme="purple"] {
--theme-primary: #722ed1;
--theme-primary-light: #9254de;
--theme-primary-lighter: #b37feb;
--theme-primary-dark: #531dab;
--theme-primary-darker: #391085;
--theme-success: #52c41a;
--theme-warning: #faad14;
--theme-danger: #ff4d4f;
--theme-info: #13c2c2;
--theme-bg-primary: #f9f0ff;
--theme-bg-secondary: #efdbff;
--theme-bg-tertiary: #d3adf7;
--theme-bg-quaternary: #b37feb;
}.btn {
background-color: var(--theme-primary);
border-color: var(--theme-primary);
color: var(--theme-bg-primary);
border-radius: var(--theme-border-radius-base);
transition: var(--theme-transition-base);
}
.btn:hover {
background-color: var(--theme-primary-light);
border-color: var(--theme-primary-light);
}
.btn:active {
background-color: var(--theme-primary-dark);
border-color: var(--theme-primary-dark);
}
.btn-secondary {
background-color: var(--theme-bg-secondary);
border-color: var(--theme-border-base);
color: var(--theme-text-primary);
}
.btn-success {
background-color: var(--theme-success);
border-color: var(--theme-success);
color: var(--theme-bg-primary);
}
.btn-warning {
background-color: var(--theme-warning);
border-color: var(--theme-warning);
color: var(--theme-bg-primary);
}
.btn-danger {
background-color: var(--theme-danger);
border-color: var(--theme-danger);
color: var(--theme-bg-primary);
}.card {
background-color: var(--theme-bg-primary);
border: 1px solid var(--theme-border-light);
border-radius: var(--theme-border-radius-large);
box-shadow: var(--theme-shadow-base);
transition: var(--theme-transition-base);
}
.card:hover {
box-shadow: var(--theme-shadow-light);
}
.card-header {
background-color: var(--theme-bg-secondary);
border-bottom: 1px solid var(--theme-border-light);
color: var(--theme-text-primary);
font-size: var(--theme-font-size-medium);
font-weight: 600;
}
.card-body {
color: var(--theme-text-regular);
font-size: var(--theme-font-size-base);
}
.card-footer {
background-color: var(--theme-bg-tertiary);
border-top: 1px solid var(--theme-border-light);
color: var(--theme-text-secondary);
font-size: var(--theme-font-size-small);
}.form-control {
background-color: var(--theme-bg-primary);
border: 1px solid var(--theme-border-base);
border-radius: var(--theme-border-radius-base);
color: var(--theme-text-primary);
font-size: var(--theme-font-size-base);
transition: var(--theme-transition-base);
}
.form-control:focus {
border-color: var(--theme-primary);
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
outline: none;
}
.form-control::placeholder {
color: var(--theme-text-placeholder);
}
.form-label {
color: var(--theme-text-primary);
font-size: var(--theme-font-size-base);
font-weight: 500;
}
.form-help {
color: var(--theme-text-secondary);
font-size: var(--theme-font-size-small);
}
.form-error {
color: var(--theme-danger);
font-size: var(--theme-font-size-small);
}.navbar {
background-color: var(--theme-bg-primary);
border-bottom: 1px solid var(--theme-border-light);
box-shadow: var(--theme-shadow-base);
}
.navbar-brand {
color: var(--theme-primary);
font-size: var(--theme-font-size-large);
font-weight: 700;
}
.navbar-nav .nav-link {
color: var(--theme-text-regular);
font-size: var(--theme-font-size-base);
transition: var(--theme-transition-base);
}
.navbar-nav .nav-link:hover {
color: var(--theme-primary);
}
.navbar-nav .nav-link.active {
color: var(--theme-primary);
font-weight: 600;
}
.sidebar {
background-color: var(--theme-bg-secondary);
border-right: 1px solid var(--theme-border-light);
}
.sidebar-menu .menu-item {
color: var(--theme-text-regular);
transition: var(--theme-transition-base);
}
.sidebar-menu .menu-item:hover {
background-color: var(--theme-bg-tertiary);
color: var(--theme-primary);
}
.sidebar-menu .menu-item.active {
background-color: var(--theme-primary);
color: var(--theme-bg-primary);
}/* 主题切换过渡动画 */
* {
transition:
background-color var(--theme-transition-base),
border-color var(--theme-transition-base),
color var(--theme-transition-base),
box-shadow var(--theme-transition-base);
}
/* 主题切换时的淡入淡出效果 */
.theme-transition-enter-active,
.theme-transition-leave-active {
transition: opacity 0.3s ease;
}
.theme-transition-enter-from,
.theme-transition-leave-to {
opacity: 0;
}
/* 主题切换加载动画 */
.theme-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.theme-loading-spinner {
width: 40px;
height: 40px;
border: 4px solid var(--theme-border-light);
border-top: 4px solid var(--theme-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}/* 移动端主题适配 */
@media (max-width: 767px) {
:root {
--theme-font-size-base: 13px;
--theme-font-size-medium: 15px;
--theme-font-size-large: 17px;
--theme-spacing-md: 12px;
--theme-spacing-lg: 20px;
--theme-border-radius-base: 6px;
--theme-border-radius-large: 10px;
}
.card {
border-radius: var(--theme-border-radius-base);
box-shadow: var(--theme-shadow-light);
}
.btn {
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
font-size: var(--theme-font-size-base);
}
}
/* 平板端主题适配 */
@media (min-width: 768px) and (max-width: 1023px) {
:root {
--theme-font-size-base: 14px;
--theme-font-size-medium: 16px;
--theme-font-size-large: 18px;
--theme-spacing-md: 16px;
--theme-spacing-lg: 24px;
}
}
/* 桌面端主题适配 */
@media (min-width: 1024px) {
:root {
--theme-font-size-base: 14px;
--theme-font-size-medium: 16px;
--theme-font-size-large: 18px;
--theme-spacing-md: 16px;
--theme-spacing-lg: 24px;
--theme-spacing-xl: 32px;
}
}/* 高对比度主题 */
[data-theme="high-contrast"] {
--theme-primary: #0066cc;
--theme-primary-light: #3385d6;
--theme-primary-dark: #004499;
--theme-success: #008000;
--theme-warning: #ff8c00;
--theme-danger: #cc0000;
--theme-info: #0080c7;
--theme-text-primary: #000000;
--theme-text-regular: #333333;
--theme-text-secondary: #666666;
--theme-text-placeholder: #999999;
--theme-bg-primary: #ffffff;
--theme-bg-secondary: #f0f0f0;
--theme-bg-tertiary: #e0e0e0;
--theme-bg-quaternary: #d0d0d0;
--theme-border-base: #000000;
--theme-border-light: #333333;
--theme-border-lighter: #666666;
--theme-border-extra-light: #999999;
--theme-shadow-base: 0 2px 4px rgba(0, 0, 0, 0.3);
--theme-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.2);
--theme-shadow-dark: 0 4px 12px rgba(0, 0, 0, 0.4);
}
/* 高对比度模式下的特殊样式 */
@media (prefers-contrast: high) {
:root {
--theme-primary: #0066cc;
--theme-text-primary: #000000;
--theme-bg-primary: #ffffff;
--theme-border-base: #000000;
}
.btn {
border-width: 2px;
}
.form-control {
border-width: 2px;
}
.card {
border-width: 2px;
}
}/* 打印主题 */
@media print {
:root {
--theme-primary: #000000;
--theme-text-primary: #000000;
--theme-text-regular: #000000;
--theme-text-secondary: #666666;
--theme-bg-primary: #ffffff;
--theme-bg-secondary: #ffffff;
--theme-border-base: #000000;
--theme-shadow-base: none;
--theme-shadow-light: none;
--theme-shadow-dark: none;
}
.card {
box-shadow: none;
border: 1px solid #000000;
}
.btn {
background-color: transparent;
color: #000000;
border: 1px solid #000000;
}
.navbar,
.sidebar,
.footer {
display: none;
}
}/**
* 主题管理器
*/
export class ThemeManager {
constructor() {
this.currentTheme = 'default'
this.themes = ['default', 'dark', 'blue', 'green', 'purple']
this.storageKey = 'app_theme'
this.init()
}
init() {
this.loadTheme()
this.applyTheme(this.currentTheme)
this.observeSystemTheme()
}
setTheme(theme) {
if (!this.themes.includes(theme)) {
console.warn(`Unknown theme: ${theme}`)
return false
}
this.currentTheme = theme
this.applyTheme(theme)
this.saveTheme()
return true
}
applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme)
// 触发主题变更事件
window.dispatchEvent(new CustomEvent('themechange', {
detail: { theme }
}))
}
loadTheme() {
try {
const saved = localStorage.getItem(this.storageKey)
if (saved && this.themes.includes(saved)) {
this.currentTheme = saved
}
} catch (error) {
console.warn('Failed to load theme:', error)
}
}
saveTheme() {
try {
localStorage.setItem(this.storageKey, this.currentTheme)
} catch (error) {
console.warn('Failed to save theme:', error)
}
}
observeSystemTheme() {
if (window.matchMedia) {
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
darkModeQuery.addEventListener('change', (e) => {
if (this.currentTheme === 'auto') {
this.applyTheme(e.matches ? 'dark' : 'default')
}
})
}
}
getCurrentTheme() {
return this.currentTheme
}
getAvailableThemes() {
return [...this.themes]
}
toggleTheme() {
const currentIndex = this.themes.indexOf(this.currentTheme)
const nextIndex = (currentIndex + 1) % this.themes.length
this.setTheme(this.themes[nextIndex])
}
}import { ref, onMounted } from 'vue'
export function useTheme() {
const themeManager = new ThemeManager()
const currentTheme = ref(themeManager.getCurrentTheme())
const availableThemes = ref(themeManager.getAvailableThemes())
const setTheme = (theme) => {
if (themeManager.setTheme(theme)) {
currentTheme.value = theme
}
}
const toggleTheme = () => {
themeManager.toggleTheme()
currentTheme.value = themeManager.getCurrentTheme()
}
onMounted(() => {
window.addEventListener('themechange', (e) => {
currentTheme.value = e.detail.theme
})
})
return {
currentTheme: readonly(currentTheme),
availableThemes: readonly(availableThemes),
setTheme,
toggleTheme
}
}<template>
<div class="theme-switcher">
<el-select v-model="currentTheme" @change="handleThemeChange">
<el-option
v-for="theme in availableThemes"
:key="theme"
:label="getThemeLabel(theme)"
:value="theme"
/>
</el-select>
</div>
</template>
<script setup>
import { useTheme } from '@/utils/theme'
const { currentTheme, availableThemes, setTheme } = useTheme()
const handleThemeChange = (theme) => {
setTheme(theme)
}
const getThemeLabel = (theme) => {
const labels = {
default: '默认',
dark: '暗色',
blue: '蓝色',
green: '绿色',
purple: '紫色'
}
return labels[theme] || theme
}
</script><template>
<div class="custom-component">
<h1 class="title">标题</h1>
<p class="content">内容文本</p>
<button class="btn btn-primary">按钮</button>
</div>
</template>
<style scoped>
.custom-component {
background-color: var(--theme-bg-primary);
border: 1px solid var(--theme-border-light);
border-radius: var(--theme-border-radius-large);
padding: var(--theme-spacing-lg);
}
.title {
color: var(--theme-text-primary);
font-size: var(--theme-font-size-large);
margin-bottom: var(--theme-spacing-md);
}
.content {
color: var(--theme-text-regular);
font-size: var(--theme-font-size-base);
line-height: 1.6;
}
.btn {
background-color: var(--theme-primary);
color: var(--theme-bg-primary);
border: none;
border-radius: var(--theme-border-radius-base);
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
transition: var(--theme-transition-base);
}
.btn:hover {
background-color: var(--theme-primary-light);
}
</style>- 性能优化: 避免过度使用CSS变量,合理组织主题结构
- 兼容性: 确保在不支持CSS变量的浏览器中有降级方案
- 可访问性: 确保所有主题都符合对比度要求
- 一致性: 保持主题间的视觉一致性和用户体验
- 测试覆盖: 在所有主题下充分测试组件和页面
最后更新时间:2025-09-19