vue3笔记(38-1)vue3项目换肤

新需求,项目需要换肤和多语言支持。本篇介绍如何在vue3项目中实现换肤功能。

背景

通常,前端一键换肤功能需要通过使用 CSS 样式表来定义不同的主题样式,然后通过 JavaScript 来控制切换不同的样式表,以达到换肤的效果。用户在点击换肤按钮或者选择不同的主题选项后,页面会立即应用新的样式,从而改变界面的外观。这种功能在很多网站、应用中都有广泛的应用,特别是一些内容丰富、用户群体广泛的平台,以满足不同用户对于外观风格的偏好。

思路

切换主题和皮肤,主要是背景色、字体颜色的替换。分别设置黑暗和明亮两个主题文件夹,当触发主题变化时,调用不同的样式文件。
当前项目中,文件结构如下:
|– assets/scss
|– dark
|– index.scss
|– element-variables.scss
|– light
|– index.scss
|– element-variables.scss
|– common.scss
|– common-ev.scss
|– transition.scss

其中,common.scss 用于全局样式,common-ev.scss用于全局 element 组件样式修改配置,transition.scss 用于动画效果。dark 和 light 目录下分别存放了黑暗和明亮主题样式和变量文件。index.scss 用于主题样式,element-variables.scss 用于对应主题 element 组件特殊配置。

实现

  1. 分别按照设计图设置黑暗和明亮主题的颜色,在 dark 文件下的所有样式都用 .dark 作为最外层包裹。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    .dark {
    :root {
    --el-text-color-primary: #ffffff;
    --el-text-color-regular: #ffffff;
    --el-table-text-color: #ffffff;
    }
    .el-table {
    --el-table-header-text-color: rgba(173, 202, 255, 0.8) !important;
    --el-table-header-bg-color: #2a3144 !important;
    --el-table-row-hover-bg-color: #1f202f !important;
    }
    }
  2. 分别在index.scss中引入对应的主题变量

    1
    @use './element-variables.scss' as *;

    common.scss中引入其他全局样式文件

    1
    2
    @use './common-ev.scss' as *;
    @use './transition.scss' as *;

    main.ts中引入通用样式文件和两个主题样式文件。

    1
    2
    3
    import '@/assets/scss/common.scss'
    import '@/assets/scss/dark/index.scss'
    import '@/assets/scss/light/index.scss'
  3. App.vue中初始化主题,本项目初始化为明亮。主题色会影响到全局样式(侧栏导航)和element组件样式(el-tabs、el-tabs、el-pagination)。

    1
    2
    3
    4
    5
    import { handleBackgroundStyle, handleThemeStyle } from './utils/theme'

    handleBackgroundStyle(['#EEEEEE', '#cccccc', '#bbbbbb'])
    handleThemeStyle('#2196f3')

    /utils/theme.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 处理背景色
    export function handleBackgroundStyle(background: any) {
    document.documentElement.style.setProperty('--el-bg-color', background[0])
    document.documentElement.style.setProperty('--el-bg-color-1', background[1])
    document.documentElement.style.setProperty('--el-bg-color-2', background[2])
    }

    // 处理主题样式
    export function handleThemeStyle(theme: any) {
    document.documentElement.style.setProperty('--el-color-primary', theme)
    }

    document.documentElement代表的是文档对象模型(DOM)中的根元素,即 HTML 文档中的元素。style.setProperty用于在 JavaScript 中设置元素样式。 具体来说,document.documentElement.style.setProperty 方法可以用于动态地设置根元素的 CSS 样式属性。它接受两个参数:
    属性名: 第一个参数是要设置的 CSS 属性名称,例如 “color”, “font-size”, “background-color” 等等。
    属性值: 第二个参数是要为属性设置的值,可以是字符串或者变量,表示对应样式属性的值。
    这种方法可以用于动态改变页面的整体样式,在实现一键换肤功能时非常有用。通过 JavaScript 动态地调用setProperty方法,可以实现在用户操作后改变整个页面的外观,从而实现换肤的效果。

  4. 设置一个按钮,点击切换主题。通过在body标签上添加class=“dark”或“”来切换主题。
    layout/components/navbar/ThemeSwitch.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <template>
    <button
    type="button"
    role="switch"
    aria-label="切换深色模式"
    aria-checked="false"
    @click="toggleDarkMode"
    >
    </button>
    </template>

    <script setup lang="ts">
    import { handleBackgroundStyle, handleThemeStyle } from '@/utils/theme'

    const toggleDarkMode = () => {
    const body = document.documentElement as HTMLElement
    const classValue = body.getAttribute('class')
    if (classValue === 'dark') {
    body.setAttribute('class', '')
    handleBackgroundStyle(['#EEEEEE', '#cccccc', '#000000'])
    handleThemeStyle('#000000')
    } else {
    body.setAttribute('class', 'dark')
    handleBackgroundStyle(['#151723', '#2a3144', '#313955'])
    handleThemeStyle('#ff7f02')
    }
    }
    </script>
  5. 其他替换项目
    (1)所有的按钮图标需要替换,深色模式为白色,明亮模式为黑色。
    (2)tooltip、dialog、message等组件的样式需要替换。.el-button–primary
    (3)图表颜色替换。components/echart/src/template/index.ts 中,修改getEchartsColor判断

优化

每次点击时切换主题,同时要在页面刷新时保持原有主题,所以我们做一个简单的缓存优化。
utils/cookie.ts

1
2
3
4
5
6
7
8
const themeKey = `${casdoor.appName}-theme`
// 设置主题
export const setTheme = (theme: string) => {
const expires = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000)
if (theme) Cookies.set(themeKey, theme, { expires: expires })
}
// 获取主题
export const getTheme = () => Cookies.get(themeKey)

src/store/modules/app.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const useAppStore = defineStore('app', {
state: (): IAppState => ({
theme: getTheme() || ''
}),
getters: {
getTheme(): string {
return this.theme
}
},
actions: {
toggleTheme(theme: string) {
this.theme = theme
setTheme(theme)
}
}
})

layout/components/navbar/ThemeSwitch.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useAppStoreWithOut } from '@/store/modules/app'

const appStore = useAppStoreWithOut()
const toggleDarkMode = () => {
const body = document.documentElement as HTMLElement
const classValue = body.getAttribute('class')
if (classValue === 'dark') {
appStore.toggleTheme('light')
body.setAttribute('class', '')
handleBackgroundStyle(['#EEEEEE', '#cccccc', '#bbbbbb'])
handleThemeStyle('#2196f3')
} else {
appStore.toggleTheme('dark')
body.setAttribute('class', 'dark')
handleBackgroundStyle(['#151723', '#2a3144', '#313955'])
handleThemeStyle('#ff7f02')
}
}

App.vue中从缓存中获取初始化主题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { handleBackgroundStyle, handleThemeStyle } from './utils/theme'

const appStore = useAppStoreWithOut()
const currentTheme = appStore.theme
console.log('currentTheme=', currentTheme)
const body = document.documentElement as HTMLElement
if (currentTheme === 'dark') {
body.setAttribute('class', 'dark')
handleBackgroundStyle(['#151723', '#2a3144', '#313955'])
handleThemeStyle('#ff7f02')
} else {
body.setAttribute('class', '')
handleBackgroundStyle(['#EEEEEE', '#cccccc', '#bbbbbb'])
handleThemeStyle('#2196f3')
}

问题

  1. echarts 图表在切换主题时需要重绘,这里尝试了components/echart/src/echart.vue

    1
    2
    3
    4
    5
    6
    watch(
    () => appStore.theme,
    (theme) => {
    initChart(theme)
    }
    )

    没有成功,感觉这个theme和颜色不是同一个。

  2. components/echart/src/template/index.ts中,观察 theme 变化,修改getEchartsColor判断,切换主题时,图表颜色也会变化。但是仔细一看,变化只是跟着背景变了,实际的颜色配置还是没获取成功。。。原因是图表色卡或者失败。。。
    若将 lineOptions: any = ref({}),colors.value 改变时,背景颜色无法跟着变。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const colors = ref<EchartsTheme>()
    watch(
    () => appStore.theme,
    (theme) => {
    colors.value = getEchartsColor(theme === 'dark')
    console.log(
    colors.value.bgColor,
    lineOptions.value.backgroundColor
    ) // 颜色变化,但是背景颜色没有变化
    }
    )

按需加载

对于需要按需加载的项目,尝试直接将darklight文件夹引入,原vite.config.ts配置如下:

1
2
3
4
5
6
7
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/assets/scss/index.scss";`
}
}
}

执行后,有如下报错[sass] This module was already loaded, so it can't be configured using "with".
当只引入一个文件夹内容时,报错消失,初步判断是有重复调用。

总结

  1. 为了避免后期改动麻烦,建议在项目初始阶段(或任一新模块开发初期)就按照配色规范来编写 css 代码。
  2. 避免在 html 中直接编写 style=”” 此类代码,以防止后期修改样式时出现问题。
  3. 模块中尽量不写和颜色相关的 !important,以防止样式修改时造成意想不到的问题。
1
2
3
4
5

:root {
--el-color-primary: #ff7f02;
}

附录
vue3如何实现换肤效果(精简版)
两种最简单的方式教会你如何实现前端一键换肤!
CSS — BEM 命名规范
element-plus 主题色配置