1. 主题系统设计与动态切换
在现代前端应用中,主题定制功能已成为提升用户体验的重要部分。vue-element-plus-admin 项目实现了一套完善的主题系统,支持系统主题、菜单主题和头部主题的独立配置,以及暗黑模式的切换。
1.1 主题变量设计
项目采用 CSS 变量实现主题定制,核心样式变量定义在 src/styles/var.css
中:
css
:root {
--login-bg-color: #293146;
/* left menu start */
--left-menu-max-width: 200px;
--left-menu-min-width: 64px;
--left-menu-bg-color: #001529;
--left-menu-bg-light-color: #0f2438;
--left-menu-bg-active-color: var(--el-color-primary);
--left-menu-text-color: #bfcbd9;
--left-menu-text-active-color: #fff;
--left-menu-collapse-bg-active-color: var(--el-color-primary);
/* left menu end */
/* logo start */
--logo-height: 50px;
--logo-title-text-color: #fff;
/* logo end */
/* header start */
--top-header-bg-color: '#fff';
--top-header-text-color: 'inherit';
--top-header-hover-color: #f6f6f6;
--top-tool-height: var(--logo-height);
--top-tool-p-x: 0;
--tags-view-height: 35px;
/* header start */
/* tab menu start */
--tab-menu-max-width: 80px;
--tab-menu-min-width: 30px;
--tab-menu-collapse-height: 36px;
/* tab menu end */
--app-content-padding: 20px;
--app-content-bg-color: #f5f7f9;
--app-footer-height: 50px;
--transition-time-02: 0.2s;
}
.dark {
--app-content-bg-color: var(--el-bg-color);
}
这些 CSS 变量形成了一套完整的主题体系,通过修改这些变量可以轻松实现主题的切换。
1.2 主题状态管理
项目使用 Pinia 进行主题状态的集中管理,主题相关的状态定义在 src/store/modules/app.ts
中:
typescript
// 主题相关的状态
theme: {
// 主题色
elColorPrimary: '#409eff',
// 左侧菜单边框颜色
leftMenuBorderColor: 'inherit',
// 左侧菜单背景颜色
leftMenuBgColor: '#001529',
// 左侧菜单浅色背景颜色
leftMenuBgLightColor: '#0f2438',
// 左侧菜单选中背景颜色
leftMenuBgActiveColor: 'var(--el-color-primary)',
// 左侧菜单收起选中背景颜色
leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
// 左侧菜单字体颜色
leftMenuTextColor: '#bfcbd9',
// 左侧菜单选中字体颜色
leftMenuTextActiveColor: '#fff',
// logo字体颜色
logoTitleTextColor: '#fff',
// logo边框颜色
logoBorderColor: 'inherit',
// 头部背景颜色
topHeaderBgColor: '#fff',
// 头部字体颜色
topHeaderTextColor: 'inherit',
// 头部悬停颜色
topHeaderHoverColor: '#f6f6f6',
// 头部边框颜色
topToolBorderColor: '#eee'
}
这些状态可以通过 Pinia store 的 actions 进行修改,实现主题的动态切换:
typescript
// 设置系统主题
setTheme(theme: ThemeTypes) {
this.theme = Object.assign(this.theme, theme)
},
// 设置 CSS 变量
setCssVarTheme() {
for (const key in this.theme) {
setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
}
this.setPrimaryLight()
},
// 设置菜单主题
setMenuTheme(color: string) {
const primaryColor = useCssVar('--el-color-primary', document.documentElement)
const isDarkColor = colorIsDark(color)
const theme: Recordable = {
// 左侧菜单边框颜色
leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
// 左侧菜单背景颜色
leftMenuBgColor: color,
// ... 其他菜单相关样式
}
this.setTheme(theme)
this.setCssVarTheme()
},
// 设置头部主题
setHeaderTheme(color: string) {
const isDarkColor = colorIsDark(color)
const textColor = isDarkColor ? '#fff' : 'inherit'
const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
const topToolBorderColor = isDarkColor ? color : '#eee'
setCssVar('--top-header-bg-color', color)
setCssVar('--top-header-text-color', textColor)
setCssVar('--top-header-hover-color', textHoverColor)
// ... 设置其他头部相关样式
}
1.3 主题色处理工具
项目中实现了一系列颜色处理工具函数,位于 src/utils/color.ts
,用于辅助主题色的计算和转换:
typescript
// 判断颜色是否是深色
export const colorIsDark = (color: string) => {
if (!isHexColor(color)) return
const [r, g, b] = hexToRGB(color)
.replace(/(?:\(|\)|rgb|RGB)*/g, '')
.split(',')
.map((item) => Number(item))
return r * 0.299 + g * 0.578 + b * 0.114 < 192
}
// 减淡颜色
export const lighten = (color: string, amount: number) => {
color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color
amount = Math.trunc((255 * amount) / 100)
return `#${addLight(color.substring(0, 2), amount)}${addLight(
color.substring(2, 4),
amount
)}${addLight(color.substring(4, 6), amount)}`
}
// 混合两种颜色
export const mix = (color1: string, color2: string, weight: number = 0.5): string => {
let color = '#'
for (let i = 0; i <= 2; i++) {
const c1 = parseInt(color1.substring(1 + i * 2, 3 + i * 2), 16)
const c2 = parseInt(color2.substring(1 + i * 2, 3 + i * 2), 16)
const c = Math.round(c1 * weight + c2 * (1 - weight))
color += c.toString(16).padStart(2, '0')
}
return color
}
这些工具函数使得主题色的处理更加灵活和精确,特别是在自动计算颜色的明暗度和生成色阶时非常有用。
1.4 主题设置组件
项目实现了一个专门的设置面板组件(src/components/Setting/src/Setting.vue
),用于用户交互式地修改主题:
vue
<template>
<ElDrawer v-model="drawer" title="项目配置" size="300px">
<!-- 主题色设置 -->
<ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
<ColorRadioPicker
v-model="systemTheme"
@change="setSystemTheme"
:colorList="defaultSystemThemes"
/>
<!-- 头部主题设置 -->
<ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
<ColorRadioPicker
v-model="headerTheme"
@change="setHeaderTheme"
:colorList="defaultHeaderThemes"
/>
<!-- 菜单主题设置 -->
<ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
<ColorRadioPicker
v-model="menuTheme"
@change="setMenuTheme"
:colorList="defaultMenuThemes"
/>
<!-- 其他设置... -->
</ElDrawer>
</template>
<script setup>
// 设置系统主题色
const setSystemTheme = (color: string) => {
setCssVar('--el-color-primary', color)
appStore.setTheme({ elColorPrimary: color })
// 更新菜单样式,确保颜色协调
const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement)
setMenuTheme(trim(unref(leftMenuBgColor) as string))
}
// 设置头部主题
const setHeaderTheme = (color: string) => {
appStore.setHeaderTheme(color)
}
// 设置菜单主题
const setMenuTheme = (color: string) => {
appStore.setMenuTheme(color)
}
</script>
通过这个设置面板,用户可以轻松地自定义系统的主题色、头部主题和菜单主题。
2. 暗黑模式实现
暗黑模式是现代应用的标准功能,vue-element-plus-admin 项目通过结合 Element Plus 的暗黑模式支持和自定义 CSS 变量实现了完整的暗黑模式切换功能。
2.1 暗黑模式的状态管理
暗黑模式的状态同样存储在 app store 中:
typescript
// src/store/modules/app.ts
interface AppState {
// ... 其他状态
isDark: boolean
// ... 其他状态
}
// state 初始值
isDark: false
// actions
setIsDark(isDark: boolean) {
this.isDark = isDark
if (this.isDark) {
document.documentElement.classList.add('dark')
document.documentElement.classList.remove('light')
} else {
document.documentElement.classList.add('light')
document.documentElement.classList.remove('dark')
}
this.setPrimaryLight()
}
2.2 主题色明暗度调整
暗黑模式下,需要调整主题色的明暗度,以确保良好的对比度和视觉效果:
typescript
// 设置主题色的不同明暗度
setPrimaryLight() {
if (this.theme.elColorPrimary) {
const elColorPrimary = this.theme.elColorPrimary
const color = this.isDark ? '#000000' : '#ffffff'
const lightList = [3, 5, 7, 8, 9]
lightList.forEach((v) => {
setCssVar(`--el-color-primary-light-${v}`, mix(color, elColorPrimary, v / 10))
})
setCssVar(`--el-color-primary-dark-2`, mix(color, elColorPrimary, 0.2))
}
}
2.3 暗黑模式切换组件
项目实现了一个专门的暗黑模式切换组件(src/components/ThemeSwitch/src/ThemeSwitch.vue
):
vue
<script setup lang="ts">
import { computed } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { ElSwitch } from 'element-plus'
import { useIcon } from '@/hooks/web/useIcon'
import { getCssVar } from '@/utils'
const emit = defineEmits(['change'])
const Sun = useIcon({ icon: 'vi-emojione-monotone:sun', color: '#fde047' })
const CrescentMoon = useIcon({ icon: 'vi-emojione-monotone:crescent-moon', color: '#fde047' })
const appStore = useAppStore()
// 初始化获取是否是暗黑主题
const isDark = computed({
get() {
return appStore.getIsDark
},
set(val: boolean) {
appStore.setIsDark(val)
const color = getCssVar('--el-bg-color')
appStore.setMenuTheme(color)
appStore.setHeaderTheme(color)
emit('change', val)
}
})
</script>
<template>
<ElSwitch
v-model="isDark"
inline-prompt
:border-color="blackColor"
:inactive-color="blackColor"
:active-color="blackColor"
:active-icon="Sun"
:inactive-icon="CrescentMoon"
/>
</template>
这个组件使用 Element Plus 的 Switch 组件,配合太阳和月亮图标,提供了一个直观的暗黑模式切换控件。
2.4 暗黑模式样式适配
暗黑模式需要调整各个组件和页面的样式,项目在 var.css
中定义了暗黑模式特定的样式变量:
css
.dark {
--app-content-bg-color: var(--el-bg-color);
/* 其他暗黑模式特定的变量 */
}
项目还通过 VueUse 库的 useDark
hook 实现了系统主题的自动检测:
typescript
// 初始化主题模式
initTheme() {
const isDark = useDark({
valueDark: 'dark',
valueLight: 'light'
})
isDark.value = this.getIsDark
// ... 其他初始化逻辑
}
3. 国际化解决方案
vue-element-plus-admin 项目采用了 vue-i18n 实现多语言支持,覆盖了界面文本、错误提示、表单验证等各个方面。
3.1 国际化架构设计
项目的国际化架构主要包括以下部分:
- 语言文件 :
src/locales
目录下的语言文件 - 语言状态管理 :
src/store/modules/locale.ts
中的语言状态 - i18n 插件集成 :
src/plugins/vueI18n
目录下的配置和辅助函数 - 国际化 Hook :
src/hooks/web/useLocale.ts
提供的便捷函数 - 语言切换组件 :
src/components/LocaleDropdown
提供的语言切换下拉菜单
3.2 语言文件设计
项目定义了标准化的语言文件结构,以 zh-CN.ts
和 en.ts
为例:
typescript
// src/locales/zh-CN.ts
export default {
common: {
inputText: '请输入',
selectText: '请选择',
// ... 其他通用文本
},
login: {
welcome: '欢迎使用本系统',
message: '开箱即用的中后台管理系统',
// ... 登录相关文本
},
router: {
login: '登录',
dashboard: '首页',
// ... 路由名称文本
},
// ... 其他模块文本
}
// src/locales/en.ts
export default {
common: {
inputText: 'Please input',
selectText: 'Please select',
// ... 其他通用文本
},
// ... 其他模块文本
}
这种模块化的语言文件设计使得多语言内容维护更加清晰。
3.3 语言状态管理
项目使用 Pinia 管理语言状态,定义在 src/store/modules/locale.ts
中:
typescript
interface LocaleState {
currentLocale: LocaleDropdownType
localeMap: LocaleDropdownType[]
}
export const useLocaleStore = defineStore('locales', {
state: (): LocaleState => {
return {
currentLocale: {
lang: getStorage('lang') || 'zh-CN',
elLocale: elLocaleMap[getStorage('lang') || 'zh-CN']
},
// 多语言列表
localeMap: [
{
lang: 'zh-CN',
name: '简体中文'
},
{
lang: 'en',
name: 'English'
}
]
}
},
// getters 和 actions...
})
通过这个 store,应用可以全局管理当前语言和支持的语言列表。
3.4 i18n 插件配置
项目在 src/plugins/vueI18n/index.ts
中配置了 vue-i18n 插件:
typescript
export const setupI18n = async (app: App<Element>) => {
const options = await createI18nOptions()
i18n = createI18n(options) as I18n
app.use(i18n)
}
const createI18nOptions = async (): Promise<I18nOptions> => {
const localeStore = useLocaleStoreWithOut()
const locale = localeStore.getCurrentLocale
const localeMap = localeStore.getLocaleMap
const defaultLocal = await import(`../../locales/${locale.lang}.ts`)
const message = defaultLocal.default ?? {}
setHtmlPageLang(locale.lang)
localeStore.setCurrentLocale({
lang: locale.lang
})
return {
legacy: false,
locale: locale.lang,
fallbackLocale: locale.lang,
messages: {
[locale.lang]: message
},
availableLocales: localeMap.map((v) => v.lang),
sync: true,
silentTranslationWarn: true,
missingWarn: false,
silentFallbackWarn: true
}
}
这个配置实现了:
- 动态加载当前语言文件
- 设置文档的 lang 属性
- 配置 i18n 的行为选项
3.5 useLocale Hook
项目封装了 useLocale
hook 简化国际化操作:
typescript
// src/hooks/web/useLocale.ts
export const useLocale = () => {
// 切换语言
const changeLocale = async (locale: LocaleType) => {
const globalI18n = i18n.global
// 动态加载语言文件
const langModule = await import(`../../locales/${locale}.ts`)
globalI18n.setLocaleMessage(locale, langModule.default)
setI18nLanguage(locale)
}
return {
changeLocale
}
}
// 设置 i18n 语言
const setI18nLanguage = (locale: LocaleType) => {
const localeStore = useLocaleStoreWithOut()
if (i18n.mode === 'legacy') {
i18n.global.locale = locale
} else {
(i18n.global.locale as any).value = locale
}
localeStore.setCurrentLocale({
lang: locale
})
setHtmlPageLang(locale)
}
这个 hook 提供了动态切换语言的功能,包括加载语言文件和更新语言设置。
3.6 语言切换组件
项目实现了一个多语言切换下拉菜单组件(src/components/LocaleDropdown/src/LocaleDropdown.vue
):
vue
<script setup lang="ts">
import { computed, unref } from 'vue'
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
import { useLocaleStore } from '@/store/modules/locale'
import { useLocale } from '@/hooks/web/useLocale'
const localeStore = useLocaleStore()
const langMap = computed(() => localeStore.getLocaleMap)
const currentLang = computed(() => localeStore.getCurrentLocale)
const setLang = (lang: LocaleType) => {
if (lang === unref(currentLang).lang) return
// 需要重新加载页面让整个语言多初始化
window.location.reload()
localeStore.setCurrentLocale({ lang })
const { changeLocale } = useLocale()
changeLocale(lang)
}
</script>
<template>
<ElDropdown trigger="click" @command="setLang">
<Icon
:size="18"
icon="vi-ion:language-sharp"
class="cursor-pointer !p-0"
:class="$attrs.class"
:color="color"
/>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem v-for="item in langMap" :key="item.lang" :command="item.lang">
{{ item.name }}
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</template>
这个组件提供了一个简洁的界面,让用户可以轻松切换系统语言。
4. CSS 变量与样式模块化
vue-element-plus-admin 项目采用了基于 CSS 变量的样式系统,实现了高度可定制和模块化的样式管理。
4.1 CSS 变量系统
如前所述,项目在 src/styles/var.css
中定义了全局 CSS 变量,这些变量覆盖了布局、颜色、尺寸等各个方面。
项目还使用了 Element Plus 的 CSS 变量系统,与自定义 CSS 变量结合,形成了完整的主题变量系统。
4.2 Less 模块化样式
除了 CSS 变量外,项目还使用了 Less 预处理器和样式模块化技术:
vue
<style lang="less" scoped>
@prefix-cls: ~'@{adminNamespace}-icon';
.@{prefix-cls} {
// 样式定义
:deep(svg) {
&:hover {
color: v-bind(hoverColor) !important;
}
}
}
</style>
这种方式的优点:
- 命名空间隔离:通过前缀避免样式冲突
- 变量复用:Less 变量可以在样式中复用
- 作用域限制:scoped 样式确保样式只影响当前组件
- 深度选择器 :使用
:deep
选择器修改子组件样式
4.3 样式工具函数
项目封装了一系列样式辅助函数,用于动态操作 CSS 变量:
typescript
// src/utils/index.ts
/**
* 设置 css 变量
* @param prop 变量名
* @param val 变量值
* @param dom 元素
*/
export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {
dom.style.setProperty(prop, val)
}
/**
* 获取 css 变量
* @param prop 变量名
* @param dom 元素
*/
export const getCssVar = (prop: string, dom = document.documentElement) => {
return getComputedStyle(dom).getPropertyValue(prop)
}
这些工具函数使得在 JavaScript 中操作 CSS 变量变得简单直观。
4.4 useDesign Hook
项目提供了 useDesign
hook,用于在组件中生成带有前缀的类名:
typescript
// src/hooks/web/useDesign.ts
export const useDesign = () => {
const getPrefixCls = (scope: string) => {
return `${adminNamespace}-${scope}`
}
return {
getPrefixCls
}
}
在组件中使用:
typescript
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('icon')
这种方式确保了样式类名的一致性和可维护性。
5. 自定义指令与插件开发
vue-element-plus-admin 项目开发了多个自定义指令和插件,扩展了 Vue 的基础功能。
5.1 权限指令
项目实现了权限控制指令 v-hasPermi
,定义在 src/directives/permission/hasPermi.ts
中:
typescript
import { useUserStoreWithOut } from '@/store/modules/user'
import { DirectiveBinding } from 'vue'
const hasPermission = (value: string | string[]): boolean => {
if (!value) {
return false
}
const userStore = useUserStoreWithOut()
const permissions = userStore.getPermissions
if (!permissions || !permissions.length) {
return false
}
if (permissions.includes('*:*:*')) {
return true
}
if (Array.isArray(value)) {
return permissions.some((item) => value.includes(item))
}
return permissions.includes(value)
}
export const hasPermi = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
const hasAuth = hasPermission(value)
if (!hasAuth) {
el.parentNode?.removeChild(el)
}
}
}
这个指令可以在模板中直接使用:
vue
<button v-hasPermi="'system:user:add'">添加用户</button>
当用户没有相应权限时,按钮会被自动移除。
5.2 可调整大小的弹窗指令
项目为可调整大小的弹窗实现了 v-resize
指令:
typescript
// src/components/Dialog/src/ResizeDialog.vue
const vResize = {
mounted(el) {
const observer = new MutationObserver(() => {
const elDialog = el.querySelector('.el-dialog')
if (elDialog) {
setupDrag(elDialog, el)
}
})
observer.observe(el, { childList: true, subtree: true })
}
}
// 注册指令
const initDirective = () => {
const directives = instance?.appContext?.app._context?.directives
if (!directives || !directives['resize']) {
instance?.appContext?.app.directive('resize', vResize)
}
}
这个指令使得 Dialog 组件可以通过拖拽边缘调整大小,提升了用户体验。
5.3 插件集成与扩展
项目在 src/plugins
目录下实现了多个插件,包括:
- animate.css:提供动画效果
- echarts:集成图表库
- elementPlus:Element Plus 组件库配置
- svgIcon:SVG 图标支持
- unocss:集成原子化 CSS
- vueI18n:国际化插件
以 Element Plus 插件为例:
typescript
// src/plugins/elementPlus/index.ts
import { App, Component } from 'vue'
import {
ElTag,
ElButton,
ElInput,
// ... 其他组件
} from 'element-plus'
import 'element-plus/theme-chalk/index.css'
const components = [
ElTag,
ElButton,
ElInput,
// ... 其他组件
]
const plugins = [
ElLoading,
ElMessage,
ElMessageBox,
ElNotification
]
export const setupElementPlus = (app: App<Component>) => {
components.forEach((component) => {
app.component(component.name, component)
})
plugins.forEach((plugin) => {
app.use(plugin)
})
}
这种方式可以按需注册和配置第三方库,提高应用性能和灵活性。
6. 总结与最佳实践
通过对 vue-element-plus-admin 项目主题定制与国际化功能的分析,我们可以总结出以下最佳实践:
6.1 主题设计最佳实践
- CSS 变量为基础:使用 CSS 变量构建主题系统,方便动态切换和扩展
- 集中状态管理:使用 Pinia store 集中管理主题状态
- 颜色计算工具:实现颜色处理函数,自动计算色阶和明暗度
- 组件化设计:将主题切换功能封装为独立组件
- 适配暗黑模式:考虑明亮模式和暗黑模式的样式差异
6.2 国际化最佳实践
- 模块化语言文件:按功能模块组织语言文件内容
- 统一的语言键名:保持语言键名的一致性,避免遗漏翻译
- 运行时语言切换:支持无需刷新页面的语言切换
- 类型安全:使用 TypeScript 提供类型安全的国际化支持
- HTML 语言属性:切换语言时同步设置 HTML 的 lang 属性
6.3 样式模块化最佳实践
- 命名空间隔离:使用前缀避免样式冲突
- Scoped 样式:使用 Vue 的 scoped 样式限制作用域
- 辅助函数封装:封装样式相关的辅助函数,简化操作
- 响应式设计:确保在不同设备和主题下的样式一致性
- 样式复用:使用混合(mixins)和变量提高样式复用性
vue-element-plus-admin 项目的主题定制和国际化实现展现了现代前端应用的最佳实践,通过组件化、模块化和工具函数的合理使用,打造了一个高度可定制、用户友好的管理系统框架。这些实践对于构建大型企业级应用具有重要的参考价值。