Element Plus全局主题修改最佳实践

243 阅读5分钟

Element Plus全局主题修改最佳实践

下载

百度网盘

pan.baidu.com/s/1ep85Bs3f…

蓝奏云

wwrh.lanzoul.com/iW2Yq30fywu…

分析

  • 目标:实现 Element Plus 组件库的全局主题切换(如明暗模式、主色调自定义等),并支持用户个性化设置,且能持久化保存用户选择。

  • 适用场景:后台管理系统、企业级应用、需要多主题切换的前端项目。

使用技术

  • UI 框架:Element Plus

  • 前端框架:Vue 3(推荐配合 Vite)

  • 状态管理:Pinia 或 Vuex(用于主题状态管理)

  • 持久化:localStorage(保存用户主题偏好)

  • 样式变量:CSS 变量(:root),Element Plus 提供的主题变量

实现步骤

  • 准备主题变量:整理 Element Plus 支持的主题变量,定义默认主题和暗黑主题。
  • 封装主题切换逻辑:编写工具函数,动态修改 CSS 变量。
  • 集成状态管理:用 Pinia/Vuex 管理主题状态,提供切换方法。
  • 实现主题切换组件:开发 UI 组件,用户可交互切换主题。
  • 持久化用户选择:切换时保存到 localStorage,初始化时读取。
  • 全局生效:确保主题颜色切换能影响所有 Element Plus 组件和自定义样式。

第一步创建项目

从初始化创建项目开始,更好的理解如何使用

image.png

项目结构如下,反正只要是vue3+element plus项目即可,这里使用vite+vue+js+element plus初始化一个项目为例

image.png

第二步安装依赖

我使用的依赖如下

image.png

第三步注册依赖初始化主题

首先肯定要在main.js中注册和初始化主题和elemnet plus和pinia

image.png

第四步组件编写

LightAndDarkSelectionButton.vue组件

白天模式

image.png

暗夜模式

image.png

过渡动效

image.png

代码
<style scoped>
/* The switch - the box around the slider */
.switch {
  display: block;
  --width-of-switch: 3.5em;
  --height-of-switch: 2em;
  /* size of sliding icon -- sun and moon */
  --size-of-icon: 1.4em;
  /* it is like a inline-padding of switch */
  --slider-offset: 0.3em;
  position: relative;
  width: var(--width-of-switch);
  height: var(--height-of-switch);
}

/* Hide default HTML checkbox */
.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

/* The slider */
.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #f4f4f5;
  transition: .4s;
  border-radius: 30px;
}

.slider:before {
  position: absolute;
  content: "";
  height: var(--size-of-icon, 1.4em);
  width: var(--size-of-icon, 1.4em);
  border-radius: 20px;
  left: var(--slider-offset, 0.3em);
  top: 50%;
  transform: translateY(-50%);
  background: linear-gradient(40deg, #ff0080, #ff8c00 70%);;
  transition: .4s;
}

input:checked + .slider {
  background-color: #303136;
}

input:checked + .slider:before {
  left: calc(100% - (var(--size-of-icon, 1.4em) + var(--slider-offset, 0.3em)));
  background: #303136;
  /* change the value of second inset in box-shadow to change the angle and direction of the moon  */
  box-shadow: inset -3px -2px 5px -2px #8983f7, inset -10px -4px 0 0 #a3dafb;
}

</style>

<template>
  <label class="switch">
    <input @click="change" type="checkbox" v-model="changeDark">
    <span class="slider"></span>
  </label>
</template>
<script>
export default {
  data() {
    return {
      isDark: localStorage.getItem('vueuse-color-scheme') === 'dark',
      changeDark: false
    };
  },
  created() {
    this.changeDark = localStorage.getItem('vueuse-color-scheme') === 'dark'
    if (this.changeDark === true) {
      document.documentElement.classList.add('dark');
    }
  },
  methods: {
    toggleDark() {
      this.isDark = !this.isDark;
      if (this.isDark) {
        document.documentElement.classList.add('dark');
        localStorage.setItem('vueuse-color-scheme', 'dark');
      } else {
        document.documentElement.classList.remove('dark');
        localStorage.setItem('vueuse-color-scheme', 'light');
      }
    },
    async change(e) {
      await new Promise(resolve => setTimeout(resolve, 300));
      this.toggleTheme(e);
    },
    toggleTheme(e) {
      const transition = document.startViewTransition(() => {
        this.toggleDark();
      });
      transition.ready.then(() => {
        const x = e.clientX;
        const y = e.clientY;
        const radius = Math.sqrt(
            Math.max(x, window.innerWidth - x) ** 2 + Math.max(y, window.innerHeight - y) ** 2
        );
        const clipPath = [`circle(0 at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`];
        document.documentElement.animate(
            {
              clipPath: this.isDark ? clipPath.reverse() : clipPath,
            },
            {
              duration: 500,
              easing: 'ease-in',
              pseudoElement: this.isDark ? '::view-transition-old(root)' : '::view-transition-new(root)',
            }
        );
      });
    },
  },
};
</script>
<style>
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
}

.dark::view-transition-old(root) {
  z-index: 9999;
}
</style>
实现功能
  • 自定义 Switch 外观:通过 .switch.slider 等类自定义了一个美观的开关按钮,模拟了原生 checkbox 的切换效果。

  • 隐藏原生 checkbox.switch input 设置为透明且不可见,只用作状态控制。

  • 滑块动画.slider:before 通过 leftbackground 属性的变化,实现了滑块在明暗模式间的平滑移动和样式切换。

  • 暗黑模式样式:当 input 被选中(即暗黑模式),滑块和背景颜色会发生变化,模拟月亮和夜晚的效果。

  • 用户选择的持久化(localStorage)

  • 动画过渡(圆形扩散,提升体验)

  • 响应式 UI 状态(v-model 绑定)

ThemeSettings.vue组件

主题颜色切换

image.png

自定义主题选择

image.png

代码
<script>
import {Check} from '@element-plus/icons-vue'
import LightAndDarkSelectionButton from "./LightAndDarkSelectionButton.vue";
import {colorList, applyTheme, defaultColor} from '../../utils/themes.js'

export default {
  name: 'ThemeSettings',
  components: {
    LightAndDarkSelectionButton,
    Check,

  },

  data() {
    return {
      primaryColor: defaultColor,
      colorList: colorList,
      custom_color: "#FF8B00"
    }
  },

  mounted() {
    this.primaryColor = localStorage.getItem('theme-color') || defaultColor
  },

  methods: {
    handleColorChange(color) {
      this.primaryColor = color
      applyTheme(color)
    },
    random_Color() {
      if (this.colorInterval) {
        // 已经在运行,点击则停止
        clearInterval(this.colorInterval)
        this.colorInterval = null
      } else {
        // 没有运行,点击则开始
        this.colorInterval = setInterval(() => {
          const randomColor = '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0')
          this.handleColorChange(randomColor)
        }, 500)
      }
    }
  }
}
</script>


<template>
  <div class="theme-page">
    <div class="theme-content">
      <div class="theme-section">
        <h4>主题颜色</h4>
        <div class="color-palette">
          <div
              v-for="color in colorList"
              :key="color"
              class="color-item"
              :style="{ backgroundColor: color }"
              @click="handleColorChange(color)"
          >
            <el-icon v-if="color === primaryColor">
              <Check/>
            </el-icon>
          </div>

          <el-divider direction="vertical"/>
        </div>

        <h4>自定义</h4>
        <div class="color-palette">
          <div class="color-item"
               :key="custom_color"
               :style="{ backgroundColor: custom_color }"
               @click="handleColorChange(custom_color)"
          >

            <el-icon v-if="custom_color === primaryColor">
              <Check/>
            </el-icon>
          </div>

          <el-color-picker v-model="custom_color" size="large" :predefine="colorList"/>
        </div>


      </div>
      <div class="theme-section">
        <h4>暗黑模式</h4>
        <div class="color-palette">
          <LightAndDarkSelectionButton/>
        </div>
      </div>


      <el-button @click="random_Color()">五颜六色(滑稽脸)</el-button>


    </div>

  </div>
</template>
<style scoped>
.theme-sidebar {
  position: fixed;
  top: 0;
  right: 0;
  width: 300px;
  height: 100vh;
  background: var(--el-bg-color);
  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
  padding: 20px;
  z-index: 999;
  overflow-y: auto;
}

.theme-header {
  font-size: 18px;
  font-weight: bold;
  color: var(--el-text-color-primary);
  margin-bottom: 20px;
}

.theme-content {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.theme-section {
  margin-bottom: 20px;
}

.color-palette {
  display: grid;
  grid-template-columns: repeat(auto-fill, 40px);
  gap: 15px;
}

.color-item {
  width: 40px;
  height: 40px;
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.2s;
}

.color-item:hover {
  transform: scale(1.1);
}

</style>
实现功能
  • 用户点击色块或自定义色,主题色立即切换并应用到全局(通过 applyTheme 实现)。

  • 用户选择的主题色会保存到 localStorage,下次进入页面自动恢复。

  • 用户可通过明暗模式按钮切换全局明暗主题。

  • “五颜六色”按钮可让主题色不断随机变化,点击可停止。

  • 主题色切换即刻生效,无需刷新页面

  • 支持自定义色,满足个性化需求

  • 明暗模式无缝切换,与主色搭配

  • 用户偏好持久化,体验友好

  • 彩蛋功能,提升趣味性

UserSettingButton.vue组件

按钮打开组件

image.png

点击打开页面

image.png

代码
<template>
  <div>
    <!-- 触发按钮 -->
    <el-button @click="dialogVisible = true" circle>
      <el-icon>
        <Setting/>
      </el-icon>
    </el-button>

    <teleport to="body">

      <!-- 设置对话框 -->
      <el-dialog
          v-model="dialogVisible"
          title="系统设置"
          width="50%"

          :close-on-click-modal="true"
          :close-on-press-escape="false"
          :show-close="true"
      >
        <ThemeSettings/>

        <template #footer>
          <el-button @click="dialogVisible = false">关闭</el-button>
          <el-button type="primary" @click="handleConfirm">保存设置</el-button>
        </template>
      </el-dialog>


    </teleport>
  </div>
</template>

<script setup>
import {ref} from 'vue'
import {Setting} from '@element-plus/icons-vue'

import ThemeSettings from "./ThemeSettings.vue";

const dialogVisible = ref(false)

const handleConfirm = () => {
  // 这里可以添加保存设置的逻辑
  // 例如调用 SettingMain 组件的保存方法
  dialogVisible.value = false
}
</script>

<style scoped>
/* 可以添加一些自定义样式 */
.el-button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
</style>

App.vue组件

必须要挂载ThemeSettings组件在App上才会生效

<script setup>

import ThemeSettings from "./components/ThemePicker/ThemeSettings.vue";
import {useDrawerStore} from './stores/drawer'

const drawerStore = useDrawerStore()
</script>

<template>
  <router-view/>

  <!-- 抽屉组件放这里,始终生效 -->
  <el-drawer
      v-model="drawerStore.settingsDrawerVisible"
      title="主题设置"
      direction="rtl"
      size="400px"
      :with-header="false"
  >
    <ThemeSettings/>
  </el-drawer>

</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

核心代码js

theme.js实现主题切换的核心代码

// src/utils/theme.js
export const tintColor = (color, tint) => {
    const red = parseInt(color.slice(1, 3), 16)
    const green = parseInt(color.slice(3, 5), 16)
    const blue = parseInt(color.slice(5, 7), 16)
    return `#${[
        Math.round(red + (255 - red) * tint).toString(16).padStart(2, '0'),
        Math.round(green + (255 - green) * tint).toString(16).padStart(2, '0'),
        Math.round(blue + (255 - blue) * tint).toString(16).padStart(2, '0')
    ].join('')}`

}


// src/utils/theme.js
export const initDarkMode = () => {
    const isDark = localStorage.getItem('vueuse-color-scheme') === 'dark'
    if (isDark) {
        document.documentElement.classList.add('dark')
    }
    return isDark
}



export const shadeColor = (color, shade) => {
    const red = parseInt(color.slice(1, 3), 16)
    const green = parseInt(color.slice(3, 5), 16)
    const blue = parseInt(color.slice(5, 7), 16)
    return `#${[
        Math.round(red * (1 - shade)).toString(16).padStart(2, '0'),
        Math.round(green * (1 - shade)).toString(16).padStart(2, '0'),
        Math.round(blue * (1 - shade)).toString(16).padStart(2, '0')
    ].join('')}`
}

export const colorList = [
    '#409EFF',
    '#DAA96E',
    '#0C819F',
    '#27ae60',
    '#ff5c93',
    '#e74c3c',
    '#9b59b6'
]

export const defaultColor = '#409EFF'

export const applyTheme = (color) => {
    const primaryColor = color || localStorage.getItem('theme-color') || defaultColor
    const colors = {
        '--el-color-primary': primaryColor,
        '--el-color-primary-light-2': tintColor(primaryColor, 0.3),
        '--el-color-primary-dark-2': shadeColor(primaryColor, 0.2),
    }

    Object.entries(colors).forEach(([key, value]) => {
        document.documentElement.style.setProperty(key, value)
    })

    localStorage.setItem('theme-color', primaryColor)
}

// 初始化主题
export const initTheme = () => {
    const savedColor = localStorage.getItem('theme-color')
    applyTheme(savedColor)
}

注意事项

记得去掉style.css不然背景颜色可能是白色,切换也不行

可以看到localStorage保存主题颜色和是否是dark模式!

image.png

组件展示

image.png

项目结构

theme/
  ├── index.html
  ├── package-lock.json
  ├── package.json
  ├── public/
  │   └── vite.svg
  ├── README.md
  ├── src/
  │   ├── App.vue
  │   ├── assets/
  │   │   └── vue.svg
  │   ├── components/
  │   │   ├── DemoTestElementPlus.vue
  │   │   ├── HelloWorld.vue
  │   │   └── ThemePicker/
  │   │       ├── LightAndDarkSelectionButton.vue
  │   │       ├── ThemeSettings.vue
  │   │       └── UserSettingButton.vue
  │   ├── main.js
  │   ├── router/
  │   │   └── index.js
  │   ├── stores/
  │   │   └── drawer.js
  │   ├── style.css
  │   └── utils/
  │       └── themes.js
  ├── theme.iml
  └── vite.config.js

drawer.js这个是侧板抽屉,但是我使用的是上面的UserSettingButton组件直接打开即可

实现逻辑

1. 明暗模式切换逻辑

组件:LightAndDarkSelectionButton.vue
  • 状态管理

    • 通过 localStorage 读取和保存当前主题(vueuse-color-scheme,值为 'dark''light')。
    • 组件的 isDarkchangeDark 控制当前是否为暗黑模式。
  • 切换实现

    • 切换时,给 <html> 元素添加或移除 dark 类(document.documentElement.classList.add/remove('dark'))。
    • 同步更新 localStorage,保证刷新后主题不丢失。
    • 使用 document.startViewTransitionclipPath 动画,带来平滑的切换体验。
  • 入口

    • 该按钮被集成在 ThemeSettings.vue 主题设置面板中,供用户点击切换。

2. 主题主色切换逻辑

组件:ThemeSettings.vue
  • 状态管理

    • 主题色(primaryColor)初始值为 defaultColor,挂载时从 localStorage 读取上次选择。
    • 用户可从预设色板或自定义色中选择主题色。
  • 切换实现

    • 选择色块或自定义色时,调用 handleColorChange(color) 方法。
    • 该方法会调用 applyTheme(color)(定义在 src/utils/themes.js),动态修改 Element Plus 的主题色变量。
    • 主题色变化后立即生效,并保存到 localStorage,保证刷新后主题色不丢失。
  • 彩蛋功能

    • “五颜六色”按钮会定时随机切换主题色,点击可停止。

3. 主题变量应用(applyTheme

  • 通过 applyTheme(color),将选中的颜色应用到全局 CSS 变量(如 --el-color-primary),Element Plus 组件会自动响应这些变量的变化,实现全局主题色切换。

4. 持久化与全局生效

  • 明暗模式和主题色都通过 localStorage 持久化,保证用户偏好不会丢失。
  • 主题切换通过修改全局 CSS 变量和 <html> 类名,确保所有页面和组件都能即时响应。

交互入口

  • 用户通过 ThemeSettings.vue 面板进行主题色和明暗模式的切换。
  • 该面板可作为全局设置入口,方便用户随时调整主题。

总结

主题切换实现逻辑是:
通过组件封装 + CSS 变量 + <html> 类名 + localStorage 持久化,实现了 Element Plus 全局主色和明暗模式的动态切换,且切换过程带有动画,用户体验会比较好。

百度网盘

pan.baidu.com/s/1ep85Bs3f…

蓝奏云

wwrh.lanzoul.com/iW2Yq30fywu…