动态修改主题

这里主要通过动态选择主题色,根据element-plus/theme-chalk/index.css主题文件作为模板,用主题色替换变量 动态生成 全新的theme主题样式内容,内嵌到style标签中。

elemen主题涉及到的主题变量为以下几种:

  1. element-plus ui每套主题 共用到以下种颜色 根据白色和主题色进行混合生成渐变色
  2. sasssMix 函数是将两种颜色根据一定的比例混合在一起,生成另一种颜色
  3. $--color-primary: #409EFF !default;
  4. // 下面几种都是根据主题色生成的渐变色 所以我们只关心主题色是什么 然后再生成这九种渐变色 把原始模板替换掉
  5. $--color-primary-light-1: mix($--color-white, $--color-primary, 10%) !default; /* 53a8ff */
  6. $--color-primary-light-2: mix($--color-white, $--color-primary, 20%) !default; /* 66b1ff */
  7. $--color-primary-light-3: mix($--color-white, $--color-primary, 30%) !default; /* 79bbff */
  8. $--color-primary-light-4: mix($--color-white, $--color-primary, 40%) !default; /* 8cc5ff */
  9. $--color-primary-light-5: mix($--color-white, $--color-primary, 50%) !default; /* a0cfff */
  10. $--color-primary-light-6: mix($--color-white, $--color-primary, 60%) !default; /* b3d8ff */
  11. $--color-primary-light-7: mix($--color-white, $--color-primary, 70%) !default; /* c6e2ff */
  12. $--color-primary-light-8: mix($--color-white, $--color-primary, 80%) !default; /* d9ecff */
  13. $--color-primary-light-9: mix($--color-white, $--color-primary, 90%) !default; /* ecf5ff */

关于element ui 颜色 设计可以阅读这两篇文章
https://element-plus.gitee.io/#/zh-CN/component/color
https://juejin.cn/post/6844903960218697741

效果图
默认情况下
image.png
修改主题
image.png
确定后
image.png
主题修改 看下html 动态生成的style
image.png

本章建议

> 建议大家先结合完整源码 先过一遍 别着急写 先看下主线 再细究

看源码时从App.vue里这个hook方法开始看 生成主题主要逻辑就在这个hook方法里
vue3-admin/src/App.vue
目前最新源码 https://gitee.com/brolly/vue3-element-admin
image.png

图标字体文件准备

选择对应版本图标字体

https://unpkg.com/browse/element-plus@1.0.2-beta.33/lib/theme-chalk/fonts/
image.png
下载下来放到 vue3-admin/public下
image.png
如何下载

点击ttf文件和woff文件 进去后点击 view raw下载

image.png
image.png

2-1 安装相关依赖

安装axios

首次我们需要远程获取element-plus/lib/theme-chalk/index.css 主题样式文件,作为原始模板

  1. npm i axios

安装css-color-function

将通过该包提供的convert函数生成将css color-mod函数生成的渐变色 转换成rgb css-color-function使用说明文档

根据css-color-function 的color.convert函数 将颜色函数color CSS字符串转换为RGB颜色字符串。

  1. // 下面color函数以及 shade tin是css的color-mod函数
  2. // 了解文档 http://cdn1.w3cplus.com/css4/color-mod.html
  3. 'color(#11A983 shade(10%))' => 'rgb(15, 152, 118)'
  4. 'color(#11A983 tint(10%))' => 'rgb(41, 178, 143)'
  5. 'color(#11A983 tint(20%))' => 'rgb(65, 186, 156)'
  6. 'color(#11A983 tint(30%))' => 'rgb(88, 195, 168)'
  7. 'color(#11A983 tint(40%))' => 'rgb(112, 203, 181)'
  8. 'color(#11A983 tint(50%))' => 'rgb(136, 212, 193)'
  9. 'color(#11A983 tint(60%))' => 'rgb(160, 221, 205)'
  10. 'color(#11A983 tint(70%))' => 'rgb(184, 229, 218)'
  11. 'color(#11A983 tint(80%))' => 'rgb(207, 238, 230)'
  12. 'color(#11A983 tint(90%))' => 'rgb(231, 246, 243)'
  13. // 利用css-color-function转换为rgb
  14. import color from 'css-color-function'
  15. color.convert('color(#11A983 shade(10%))') // 'rgb(15, 152, 118)'
  1. npm i css-color-function

需要注意该包没有声明文件 需要自己定义下

手动创建声明文件

可能需要重新启动npm run serve 如果还报缺少声明文件 重启下

src/css-color-function.d.ts

  1. declare module 'css-color-function' {
  2. export function convert(color: string): string;
  3. }

2-2 Navbar添加设置图标

image.png
image.png
src/layout/components/Navbar.vue

  1. <template>
  2. <div class="navbar">
  3. <hambuger @toggleClick="toggleSidebar" :is-active="sidebar.opened"/>
  4. <breadcrumb />
  5. <div class="right-menu">
  6. <!-- 设置 -->
  7. <div @click="openShowSetting" class="setting right-menu-item hover-effect">
  8. <i class="el-icon-s-tools"></i>
  9. </div>
  10. <!-- 全屏 -->
  11. <screenfull id="screefull" class="right-menu-item hover-effect" />
  12. <!-- element组件size切换 -->
  13. <el-tooltip content="Global Size" effect="dark" placement="bottom">
  14. <size-select class="right-menu-item hover-effect" />
  15. </el-tooltip>
  16. <!-- 用户头像 -->
  17. <avatar />
  18. </div>
  19. </div>
  20. </template>
  21. <script lang="ts">
  22. import { defineComponent, computed } from 'vue'
  23. import Breadcrumb from '@/components/Breadcrumb/index.vue'
  24. import Hambuger from '@/components/Hambuger/index.vue'
  25. import { useStore } from '@/store/index'
  26. import Screenfull from '@/components/Screenfull/index.vue'
  27. import SizeSelect from '@/components/SizeSelect/index.vue'
  28. import Avatar from './avatar/index.vue'
  29. export default defineComponent({
  30. name: 'Navbar',
  31. components: {
  32. Breadcrumb,
  33. Hambuger,
  34. Screenfull,
  35. SizeSelect,
  36. Avatar
  37. },
  38. emits: ['showSetting'],
  39. setup(props, { emit }) {
  40. // 使用我们自定义的useStore 具备类型提示
  41. // store.state.app.sidebar 对于getters里的属性没有类型提示
  42. const store = useStore()
  43. const toggleSidebar = () => {
  44. store.dispatch('app/toggleSidebar')
  45. }
  46. // 从getters中获取sidebar
  47. const sidebar = computed(() => store.getters.sidebar)
  48. // 打开设置面板
  49. const openShowSetting = () => {
  50. emit('showSetting', true)
  51. }
  52. return {
  53. toggleSidebar,
  54. sidebar,
  55. openShowSetting
  56. }
  57. }
  58. })
  59. </script>
  60. <style lang="scss">
  61. .navbar {
  62. display: flex;
  63. background: #fff;
  64. border-bottom: 1px solid rgba(0, 21, 41, .08);
  65. box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
  66. .right-menu {
  67. flex: 1;
  68. display: flex;
  69. align-items: center;
  70. justify-content: flex-end;
  71. padding-right: 15px;
  72. .setting {
  73. font-size: 26px;
  74. }
  75. &-item {
  76. padding: 0 8px;
  77. font-size: 18px;
  78. color: #5a5e66;
  79. vertical-align: text-bottom;
  80. &.hover-effect {
  81. cursor: pointer;
  82. transition: background .3s;
  83. &:hover {
  84. background: rgba(0, 0, 0, .025);
  85. }
  86. }
  87. }
  88. }
  89. }
  90. </style>

2-2 封装RightPanel组件

点击设置右边出来设置面板
image.png
image.png

element.ts中导入el-drawer组件

image.png

src/components/RightPanel/index.vue

  1. <template>
  2. <div class="right-panel">
  3. <el-drawer
  4. :model-value="modelValue"
  5. :direction="direction"
  6. :show-close="showClose"
  7. :custom-class="customClass"
  8. :with-header="withHeader"
  9. :title="title"
  10. :size="size"
  11. @close="handleClose"
  12. >
  13. <slot />
  14. </el-drawer>
  15. </div>
  16. </template>
  17. <script lang="ts">
  18. import { defineComponent } from 'vue'
  19. export default defineComponent({
  20. name: 'RightPanel',
  21. props: {
  22. modelValue: {
  23. type: Boolean,
  24. default: true
  25. },
  26. direction: {
  27. type: String,
  28. validator(val: string) {
  29. return ['rtl', 'ltr', 'ttb', 'btt'].includes(val)
  30. },
  31. default: 'rtl'
  32. },
  33. title: {
  34. type: String,
  35. default: '自定义title'
  36. },
  37. size: {
  38. type: [String, Number]
  39. },
  40. customClass: {
  41. type: String,
  42. default: 'setting-panel'
  43. },
  44. showClose: {
  45. type: Boolean,
  46. default: true
  47. },
  48. withHeader: {
  49. type: Boolean,
  50. default: true
  51. }
  52. },
  53. // 在组件上使用modelValue文档说明
  54. // https://v3.cn.vuejs.org/guide/component-basics.html#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-v-model
  55. emits: ['update:modelValue', 'close'],
  56. setup(props, { emit }) {
  57. const handleClose = () => {
  58. emit('update:modelValue', false)
  59. emit('close')
  60. }
  61. return {
  62. handleClose
  63. }
  64. }
  65. })
  66. </script>
  67. <style lang="scss" scoped>
  68. </style>

layout组件中导入rightPanel

image.png
src/layout/index.vue

  1. <template>
  2. <div class="app-wrapper">
  3. <div class="sidebar-container">
  4. <Sidebar />
  5. </div>
  6. <div class="main-container">
  7. <div class="header">
  8. <navbar @showSetting="openSetting" />
  9. <tags-view />
  10. </div>
  11. <!-- AppMain router-view -->
  12. <app-main />
  13. </div>
  14. <right-panel
  15. v-model="showSetting"
  16. title="样式风格设置"
  17. :size="SettingsPanelWidth"
  18. >
  19. <!-- settings 面板设置组件 -->
  20. <settings />
  21. </right-panel>
  22. </div>
  23. </template>
  24. <script lang="ts">
  25. import { defineComponent, ref } from 'vue'
  26. import Sidebar from './components/Sidebar/index.vue'
  27. import AppMain from './components/AppMain.vue'
  28. import Navbar from './components/Navbar.vue'
  29. import TagsView from './components/TagsView/index.vue'
  30. import RightPanel from '@/components/RightPanel/index.vue'
  31. import Settings from './components/Settings/index.vue'
  32. import varibalse from '@/styles/variables.scss'
  33. export default defineComponent({
  34. components: {
  35. Sidebar,
  36. AppMain,
  37. Navbar,
  38. TagsView,
  39. RightPanel,
  40. Settings
  41. },
  42. setup() {
  43. // rightPanel显示隐藏状态
  44. const showSetting = ref(false)
  45. const openSetting = () => {
  46. showSetting.value = true
  47. }
  48. return {
  49. showSetting,
  50. openSetting,
  51. // 调整panel宽度
  52. SettingsPanelWidth: varibalse.settingPanelWidth
  53. }
  54. }
  55. })
  56. </script>
  57. <style lang="scss" scoped>
  58. .app-wrapper {
  59. display: flex;
  60. width: 100%;
  61. height: 100%;
  62. .main-container {
  63. flex: 1;
  64. display: flex;
  65. flex-direction: column;
  66. overflow: hidden;
  67. .app-main {
  68. /* 50= navbar 50 如果有tagsview + 34 */
  69. min-height: calc(100vh - 84px);
  70. }
  71. }
  72. }
  73. </style>

调整样式修改scss变量

src/styles/variables.scss
image.png
scss变量类型声明不要忘了
image.png
src/styles/variables.scss

  1. // base color
  2. $blue:#324157;
  3. $light-blue:#3A71A8;
  4. $red:#C03639;
  5. $pink: #E65D6E;
  6. $green: #30B08F;
  7. $tiffany: #4AB7BD;
  8. $yellow:#FEC171;
  9. $panGreen: #30B08F;
  10. // sidebar
  11. $menuText:#bfcbd9;
  12. $menuActiveText:#409EFF;
  13. $subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951
  14. $menuBg:#304156;
  15. $menuHover:#263445;
  16. $subMenuBg:#1f2d3d;
  17. $subMenuHover:#001528;
  18. $sideBarWidth: 210px;
  19. $settingPanelWidth: 260px;
  20. // 默认主题色
  21. $theme: #409EFF;
  22. // The :export directive is the magic sauce for webpack
  23. // https://mattferderer.com/use-sass-variables-in-typescript-and-javascript
  24. :export {
  25. menuText: $menuText;
  26. menuActiveText: $menuActiveText;
  27. subMenuActiveText: $subMenuActiveText;
  28. menuBg: $menuBg;
  29. menuHover: $menuHover;
  30. subMenuBg: $subMenuBg;
  31. subMenuHover: $subMenuHover;
  32. sideBarWidth: $sideBarWidth;
  33. theme: $theme;
  34. settingPanelWidth: $settingPanelWidth;
  35. }

src/styles/variables.scss.d.ts

  1. export interface ScssVariables {
  2. menuText: string;
  3. menuActiveText: string;
  4. subMenuActiveText: string;
  5. menuBg: string;
  6. menuHover: string;
  7. subMenuBg: string;
  8. subMenuHover: string;
  9. sideBarWidth: string;
  10. theme: string;
  11. settingPanelWidth: string;
  12. }
  13. export const variables: ScssVariables
  14. export default variables

2-3 创建settings组件

src/store/modules/settings.ts

  1. <template>
  2. <div class="drawer-container">
  3. <div class="drawer-item">
  4. <span>主题色</span>
  5. <!-- 主题组件 -->
  6. <theme-picker />
  7. </div>
  8. </div>
  9. </template>
  10. <script lang="ts">
  11. import { defineComponent } from 'vue'
  12. import ThemePicker from '@/components/ThemePicker/index.vue'
  13. export default defineComponent({
  14. name: 'Settings',
  15. components: {
  16. ThemePicker
  17. }
  18. })
  19. </script>
  20. <style lang="scss" scoped>
  21. .drawer-container {
  22. padding: 24px;
  23. font-size: 14px;
  24. line-height: 1.5;
  25. word-wrap: break-word;
  26. .drawer-item {
  27. display: flex;
  28. justify-content: space-between;
  29. padding: 12px 0;
  30. font-size: 16px;
  31. color: rgba(0, 0, 0, .65);
  32. }
  33. }
  34. </style>

2-4 创建ThemePicker组件

颜色面板组件

image.png

element.ts导入color picker组件

image.png

  1. import { App } from 'vue'
  2. import {
  3. locale,
  4. ElButton,
  5. ElMessage,
  6. ElNotification,
  7. ElMessageBox,
  8. ElMenu,
  9. ElMenuItem,
  10. ElSubmenu,
  11. ElRow,
  12. ElCol,
  13. ElBreadcrumb,
  14. ElBreadcrumbItem,
  15. ElTooltip,
  16. ElDropdown,
  17. ElDropdownMenu,
  18. ElDropdownItem,
  19. ElScrollbar,
  20. ElDrawer,
  21. ElColorPicker
  22. } from 'element-plus'
  23. // 默认主题
  24. import 'element-plus/lib/theme-chalk/index.css'
  25. // Element Plus 组件内部默认使用英语
  26. // https://element-plus.gitee.io/#/zh-CN/component/i18n
  27. import lang from 'element-plus/lib/locale/lang/zh-cn'
  28. // Element Plus 直接使用了 Day.js 项目的时间日期国际化设置, 并且会自动全局设置已经导入的 Day.js 国际化配置。
  29. import 'dayjs/locale/zh-cn'
  30. // $ELEMENT size属性类型
  31. export type Size = 'default' | 'medium' | 'small' | 'mini'
  32. interface ElementOptions {
  33. size: Size
  34. }
  35. export default (app: App, options: ElementOptions): void => {
  36. locale(lang)
  37. // 按需导入组件列表
  38. const components = [
  39. ElButton,
  40. ElMessage,
  41. ElNotification,
  42. ElMessageBox,
  43. ElMenu,
  44. ElMenuItem,
  45. ElSubmenu,
  46. ElRow,
  47. ElCol,
  48. ElBreadcrumb,
  49. ElBreadcrumbItem,
  50. ElTooltip,
  51. ElDropdown,
  52. ElDropdownMenu,
  53. ElDropdownItem,
  54. ElScrollbar,
  55. ElDrawer,
  56. ElColorPicker
  57. ]
  58. components.forEach(component => {
  59. app.component(component.name, component)
  60. })
  61. // Vue.prototype 替换为 config.globalProperties
  62. // 文档说明 https://v3.cn.vuejs.org/guide/migration/global-api.html#vue-prototype-%E6%9B%BF%E6%8D%A2%E4%B8%BA-config-globalproperties
  63. app.config.globalProperties.$message = ElMessage
  64. app.config.globalProperties.$notify = ElNotification
  65. app.config.globalProperties.$confirm = ElMessageBox.confirm
  66. app.config.globalProperties.$alert = ElMessageBox.alert
  67. app.config.globalProperties.$prompt = ElMessageBox.prompt
  68. // 全局配置 https://element-plus.gitee.io/#/zh-CN/component/quickstart#quan-ju-pei-zhi
  69. // 该对象目前支持 size 与 zIndex 字段。size 用于改变组件的默认尺寸 small,zIndex 设置弹框的初始 z-index(默认值:2000)。
  70. app.config.globalProperties.$ELEMENT = {
  71. size: options.size
  72. }
  73. }

src/components/ThemePicker/index.vue

  1. <template>
  2. <el-color-picker
  3. v-model="theme"
  4. class="theme-picker"
  5. :predefine="themeColor"
  6. popper-class="theme-picker-dropdown"
  7. />
  8. </template>
  9. <script lang='ts'>
  10. import { defineComponent, ref, computed, watch } from 'vue'
  11. import { useStore } from '@/store'
  12. import { useGenerateTheme } from '@/hooks/useGenerateTheme'
  13. export default defineComponent({
  14. name: 'ThemePicker',
  15. setup() {
  16. const store = useStore()
  17. // 预设可选颜色
  18. // eslint-disable-next-line comma-spacing, comma-dangle
  19. const themeColor = ['#409EFF', '#1890ff', '#304156', '#212121', '#11a983', '#13c2c2', '#6959CD', '#f5222d',]
  20. // store中获取默认主题色
  21. const defaultTheme = computed(() => store.state.settings.theme)
  22. const theme = ref('')
  23. // 主题生成方法 稍后
  24. const { generateTheme } = useGenerateTheme()
  25. // 监听默认样式
  26. watch(defaultTheme, value => {
  27. theme.value = value
  28. }, {
  29. immediate: true
  30. })
  31. // 根据theme选择变化 重新生成主题
  32. watch(theme, (value) => {
  33. // 同步store
  34. store.dispatch('settings/changeSetting', { key: 'theme', value })
  35. // 根据theme选择变化 重新生成主题
  36. generateTheme(value)
  37. })
  38. return {
  39. themeColor,
  40. theme
  41. }
  42. }
  43. })
  44. </script>
  45. <style lang="scss">
  46. .theme-picker {
  47. height: 26px !important;
  48. margin-right: 8px;
  49. .el-color-picker__trigger {
  50. height: 26px !important;
  51. width: 26px !important;
  52. padding: 2px;
  53. }
  54. }
  55. .theme-message,
  56. .theme-picker-dropdown {
  57. z-index: 99999 !important;
  58. }
  59. .theme-picker-dropdown .el-color-dropdown__link-btn {
  60. display: none;
  61. }
  62. </style>

2-5 store中存储theme

创建settings module

src/store/modules/settings.ts

  1. import { MutationTree, ActionTree } from 'vuex'
  2. import variables from '@/styles/variables.scss'
  3. import { IRootState } from '@/store'
  4. export interface ISettingsState {
  5. theme: string;
  6. originalStyle: string;
  7. }
  8. // 定义state
  9. const state: ISettingsState = {
  10. theme: variables.theme,
  11. originalStyle: '' // 保存element 主题样式文件内容 作为替换模板
  12. }
  13. // 动态key的情况下 根据不同的key 约束对应value
  14. // http://www.voidcn.com/article/p-wtmkdcie-byz.html
  15. type ValueOf<T> = T[keyof T];
  16. interface ISettings { // 约束payload类型
  17. key: keyof ISettingsState; // 约束为ISettingsState中key
  18. value: ValueOf<ISettingsState>; // 约束为ISettingsState中value的类型
  19. }
  20. // 定义mutations 通用muation
  21. const mutations: MutationTree<ISettingsState> = {
  22. CHANGE_SETTING(state, { key, value }: ISettings) {
  23. if (key in state) {
  24. (state[key] as ValueOf<ISettingsState>) = value
  25. }
  26. }
  27. }
  28. const actions: ActionTree<ISettingsState, IRootState> = {
  29. changeSetting({ commit }, data) {
  30. commit('CHANGE_SETTING', data)
  31. }
  32. }
  33. export default {
  34. namespaced: true,
  35. state,
  36. mutations,
  37. actions
  38. }

store中缓存settings.theme和settings.originalStyle

image.png
src/store/index.ts

  1. import { InjectionKey } from 'vue'
  2. import { createStore, Store, useStore as baseUseStore } from 'vuex'
  3. import createPersistedState from 'vuex-persistedstate'
  4. import app, { IAppState } from '@/store/modules/app'
  5. import tagsView, { ITagsViewState } from '@/store/modules/tagsView'
  6. import settings, { ISettingsState } from '@/store/modules/settings'
  7. import getters from './getters'
  8. // 模块声明在根状态下
  9. export interface IRootState {
  10. app: IAppState;
  11. tagsView: ITagsViewState;
  12. settings: ISettingsState;
  13. }
  14. // 通过下面方式使用 TypeScript 定义 store 能正确地为 store 提供类型声明。
  15. // https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage
  16. // eslint-disable-next-line symbol-description
  17. export const key: InjectionKey<Store<IRootState>> = Symbol()
  18. // 对于getters在组件使用时没有类型提示
  19. // 有人提交了pr #1896 为getters创建泛型 应该还未发布
  20. // https://github.com/vuejs/vuex/pull/1896
  21. // 代码pr内容详情
  22. // https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce
  23. // vuex store持久化 默认使用localstorage持久化
  24. const persisteAppState = createPersistedState({
  25. storage: window.sessionStorage, // 指定storage 也可自定义
  26. key: 'vuex_app', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  27. // paths: ['app'] // 针对app这个模块持久化
  28. // 只针对app模块下sidebar.opened状态持久化
  29. paths: ['app.sidebar.opened', 'app.size'] // 通过点连接符指定state路径
  30. })
  31. const persisteSettingsState = createPersistedState({
  32. storage: window.sessionStorage, // 指定storage 也可自定义
  33. key: 'vuex_setting', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  34. // paths: ['app'] // 针对app这个模块持久化
  35. // 只针对app模块下sidebar.opened状态持久化
  36. paths: ['settings.theme', 'settings.originalStyle'] // 通过点连接符指定state路径
  37. })
  38. export default createStore<IRootState>({
  39. plugins: [
  40. persisteAppState,
  41. persisteSettingsState
  42. ],
  43. getters,
  44. modules: {
  45. app,
  46. tagsView,
  47. settings
  48. }
  49. })
  50. // 定义自己的 `useStore` 组合式函数
  51. // https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95
  52. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  53. export function useStore () {
  54. return baseUseStore(key)
  55. }
  56. // vuex持久化 vuex-persistedstate文档说明
  57. // https://www.npmjs.com/package/vuex-persistedstate

getters添加theme

image.png
src/store/getters.ts

  1. import { GetterTree } from 'vuex'
  2. import { IRootState } from './index'
  3. // 定义全局getters
  4. const getters: GetterTree<IRootState, IRootState> = {
  5. sidebar: (state) => state.app.sidebar,
  6. size: state => state.app.size,
  7. themeColor: state => state.settings.theme
  8. }
  9. export default getters

2-6 主题生成逻辑

最开始需要在App.vue中调用生成主题hooks useGenerateTheme ThemePicker组件里也要调用 useGenerateTheme 更好选择的主题颜色 生成主题

src/App.vue

  1. <template>
  2. <div id="app">
  3. <router-view />
  4. </div>
  5. </template>
  6. <script lang="ts">
  7. import { defineComponent } from 'vue'
  8. import { useGenerateTheme } from '@/hooks/useGenerateTheme'
  9. export default defineComponent({
  10. name: 'App',
  11. setup() {
  12. // 根据此时store中主题色生成
  13. useGenerateTheme()
  14. }
  15. })
  16. </script>
  17. <style>
  18. #app {
  19. height: 100%;
  20. }
  21. </style>

创建useGenerateTheme Hook函数

src目录下创建hooks文件夹
image.png
src/utils/useGenerateTheme.ts

  1. import { computed } from 'vue'
  2. import { useThemeFiles } from '@/hooks/useThemeFiles'
  3. import { getStyleTemplate } from '@/utils/getStyleTemplate'
  4. import { generateColors } from '@/utils/color'
  5. import { writeNewStyle } from '@/utils/writeNewStyle'
  6. import { useStore } from '@/store'
  7. export const useGenerateTheme = () => {
  8. const store = useStore()
  9. // 从store获取中获取 theme主题色
  10. const defaultTheme = computed(() => store.state.settings.theme)
  11. // 获取element-ui 主题文件内容 通过axios获取的 作为变量替换模板
  12. const originalStyle = computed(() => store.state.settings.originalStyle)
  13. // 生成主题
  14. // 了解element ui 设计 https://juejin.cn/post/6844903960218697741
  15. const generateTheme = (color: string) => {
  16. const colors = Object.assign({
  17. primary: defaultTheme.value
  18. }, generateColors(color))
  19. // 写入新的css样式
  20. writeNewStyle(originalStyle.value, colors)
  21. }
  22. // 第一步 远程获取element-ui 主题文件作为模板 然后进行变量替换 替换成我们所选主题色
  23. const { getThemeChalkStyle } = useThemeFiles()
  24. // 如果主题模板不存在 就要发送请求获取
  25. if (!originalStyle.value) {
  26. // axios请求后去 主题模板
  27. getThemeChalkStyle().then(data => {
  28. // data是获取到主题文件的css内容
  29. // 生成样式模板 将里面css内容里默认主题颜色值 替换成变量标记 如 '#409eff' => 'primary',
  30. // 方便我们后续 把primary字符标记 换成我们的主题色
  31. const styleValue = getStyleTemplate(data as string)
  32. // 把模板保存到 store中缓存起来 不用每次重新获取
  33. store.dispatch('settings/changeSetting', { key: 'originalStyle', value: styleValue })
  34. // 根据主题色生成主题 插入到html中
  35. generateTheme(defaultTheme.value)
  36. })
  37. } else {
  38. generateTheme(defaultTheme.value)
  39. }
  40. return {
  41. generateTheme
  42. }
  43. }

创建useThemeFiles hooks函数

主要是为了远程获取 elment-ui theme index.css

image.png
src/hooks/useThemeFiles.ts

  1. import { useFetch } from './useFetch'
  2. import pkgJson from 'element-plus/package.json'
  3. // 获取element-plus版本
  4. const version = pkgJson.version
  5. interface ReturnFn {
  6. getThemeChalkStyle: () => Promise<unknown>
  7. }
  8. export const useThemeFiles = ():ReturnFn => {
  9. const getThemeChalkStyle = async (): Promise<unknown> => {
  10. // 返回获取到的指定版本的element主题css内容
  11. // return await useFetch('//unpkg.com/element-plus@1.0.2-beta.33/lib/theme-chalk/index.css')
  12. return await useFetch(`//unpkg.com/element-plus@${version}/lib/theme-chalk/index.css`)
  13. }
  14. return {
  15. getThemeChalkStyle
  16. }
  17. }

上面ts中导入了json文件 需要配置tsconfig

添加”resolveJsonModule”: true即可

image.png

创建useFetch请求hook函数

image.png
src/hooks/useFetch.ts

  1. import axios from 'axios'
  2. const useFetch = async (url: string): Promise<unknown> => {
  3. return await new Promise((resolve, reject) => {
  4. axios({
  5. url,
  6. method: 'get'
  7. }).then(res => {
  8. if (res.status === 200) {
  9. resolve(res.data)
  10. } else {
  11. reject(new Error(res.statusText))
  12. }
  13. }).catch(err => {
  14. reject(new Error(err.message))
  15. })
  16. })
  17. }
  18. export {
  19. useFetch
  20. }

生成最终的样式模板

主要就是把获取到默认主题css内容 替换成变量标记,后续作为模板替换为我们想要的主题色和渐变色

  1. export interface IObject {
  2. [prop: string]: string
  3. }
  4. // element ui 关于以下颜色设计阅读资料
  5. // https://juejin.cn/post/6844903960218697741
  6. // 官方文档说明
  7. // https://element-plus.gitee.io/#/zh-CN/component/custom-theme
  8. // element-plus ui每套主题 共用到以下多种颜色 根据根据白色和主题色进行混合生成渐变色
  9. // Mix 函数是将两种颜色根据一定的比例混合在一起,生成另一种颜色
  10. // $--color-primary: #409EFF !default;
  11. // $--color-primary-light-1: mix($--color-white, $--color-primary, 10%) !default; /* 53a8ff */
  12. // $--color-primary-light-2: mix($--color-white, $--color-primary, 20%) !default; /* 66b1ff */
  13. // $--color-primary-light-3: mix($--color-white, $--color-primary, 30%) !default; /* 79bbff */
  14. // $--color-primary-light-4: mix($--color-white, $--color-primary, 40%) !default; /* 8cc5ff */
  15. // $--color-primary-light-5: mix($--color-white, $--color-primary, 50%) !default; /* a0cfff */
  16. // $--color-primary-light-6: mix($--color-white, $--color-primary, 60%) !default; /* b3d8ff */
  17. // $--color-primary-light-7: mix($--color-white, $--color-primary, 70%) !default; /* c6e2ff */
  18. // $--color-primary-light-8: mix($--color-white, $--color-primary, 80%) !default; /* d9ecff */
  19. // $--color-primary-light-9: mix($--color-white, $--color-primary, 90%) !default; /* ecf5ff */
  20. // 根据样式内容生成样式模板
  21. export const getStyleTemplate = (data: string): string => {
  22. // 这些是我们要把key也就是css内容中颜色值 替换成右边value作为变量标记 后续我们之关系右边value这些变量标记
  23. const colorMap: IObject = {
  24. '#3a8ee6': 'shade-1',
  25. '#409eff': 'primary',
  26. '#53a8ff': 'light-1',
  27. '#66b1ff': 'light-2',
  28. '#79bbff': 'light-3',
  29. '#8cc5ff': 'light-4',
  30. '#a0cfff': 'light-5',
  31. '#b3d8ff': 'light-6',
  32. '#c6e2ff': 'light-7',
  33. '#d9ecff': 'light-8',
  34. '#ecf5ff': 'light-9'
  35. }
  36. Object.keys(colorMap).forEach(key => {
  37. const value = colorMap[key]
  38. // 将key对应的颜色值 替换成 右边对应value primary list-1这些 后续生成主题 作为替换变量标记使用
  39. data = data.replace(new RegExp(key, 'ig'), value)
  40. })
  41. return data // 返回css字符串模板 之后主要靠它用我们所选的主题色 渐变色 把里面 变量标记替换掉生成主题css
  42. }

generateTheme生成主题函数

这个函数直接定义在了useGenerateTheme Hook中 并返回到外面 供其他组件使用

image.png
generateTheme函数

  1. // 生成主题
  2. // element ui 设计 https://juejin.cn/post/6844903960218697741
  3. const generateTheme = (color: string) => {
  4. const colors = Object.assign({
  5. primary: defaultTheme.value
  6. }, generateColors(color))
  7. // 动态创建style标签挂载到html中 并写入新的css样式
  8. writeNewStyle(originalStyle.value, colors)
  9. }

src/utils/useGenerateTheme.ts

  1. import { computed } from 'vue'
  2. import { useThemeFiles } from '@/hooks/useThemeFiles'
  3. import { getStyleTemplate } from '@/utils/getStyleTemplate'
  4. import { generateColors } from '@/utils/color'
  5. import { writeNewStyle } from '@/utils/writeNewStyle'
  6. import { useStore } from '@/store'
  7. export const useGenerateTheme = () => {
  8. const store = useStore()
  9. const defaultTheme = computed(() => store.state.settings.theme)
  10. const originalStyle = computed(() => store.state.settings.originalStyle)
  11. // 生成主题
  12. // element ui 设计 https://juejin.cn/post/6844903960218697741
  13. const generateTheme = (color: string) => {
  14. const colors = Object.assign({
  15. primary: defaultTheme.value
  16. }, generateColors(color))
  17. // 写入新的css样式
  18. writeNewStyle(originalStyle.value, colors)
  19. }
  20. // 第一步 远程获取element-ui 主题文件作为模板 然后进行变量替换 替换成我们所选主题色
  21. const { getThemeChalkStyle } = useThemeFiles()
  22. if (!originalStyle.value) {
  23. getThemeChalkStyle().then(data => {
  24. // data是主题文件的css内容
  25. const styleValue = getStyleTemplate(data as string)
  26. store.dispatch('settings/changeSetting', { key: 'originalStyle', value: styleValue })
  27. generateTheme(defaultTheme.value)
  28. })
  29. } else {
  30. generateTheme(defaultTheme.value)
  31. }
  32. return {
  33. generateTheme
  34. }
  35. }

generateColors函数

generateTheme函数 里会调用此函数

image.png
src/utils/color.ts

  1. import color from 'css-color-function'
  2. import { formula, IObject } from './constants'
  3. // 转换成不同色调的rgb颜色值
  4. // https://www.w3cplus.com/css/the-power-of-rgba.html
  5. export const generateColors = (primary: string): IObject => {
  6. const colors = {} as IObject
  7. Object.keys(formula).forEach(key => {
  8. // element ui 主题色 渐变色设计 https://juejin.cn/post/6844903960218697741
  9. // 根据主题色生成渐变色 将formula对象中字符primary 替换成我们所选的主题色
  10. const value = formula[key].replace(/primary/g, primary)
  11. colors[key] = color.convert(value) // 转换成rgba颜色值
  12. })
  13. return colors
  14. }
  15. // 主题色的渐变色设计 https://juejin.cn/post/6844903960218697741
  16. // color-mod css颜色函数
  17. // https://www.w3cplus.com/css4/color-mod.html
  18. // export const formula: IObject = {
  19. // 'shade-1': 'color(primary shade(10%))',
  20. // 'light-1': 'color(primary tint(10%))',
  21. // 'light-2': 'color(primary tint(20%))',
  22. // 'light-3': 'color(primary tint(30%))',
  23. // 'light-4': 'color(primary tint(40%))',
  24. // 'light-5': 'color(primary tint(50%))',
  25. // 'light-6': 'color(primary tint(60%))',
  26. // 'light-7': 'color(primary tint(70%))',
  27. // 'light-8': 'color(primary tint(80%))',
  28. // 'light-9': 'color(primary tint(90%))'
  29. // }

constants变量

src/utils/constants.ts

  1. export interface IObject {
  2. [prop: string]: string;
  3. }
  4. // 主题色的渐变色设计 https://juejin.cn/post/6844903960218697741
  5. // color-mod css颜色函数
  6. // https://www.w3cplus.com/css4/color-mod.html
  7. export const formula: IObject = {
  8. 'shade-1': 'color(primary shade(10%))',
  9. 'light-1': 'color(primary tint(10%))',
  10. 'light-2': 'color(primary tint(20%))',
  11. 'light-3': 'color(primary tint(30%))',
  12. 'light-4': 'color(primary tint(40%))',
  13. 'light-5': 'color(primary tint(50%))',
  14. 'light-6': 'color(primary tint(60%))',
  15. 'light-7': 'color(primary tint(70%))',
  16. 'light-8': 'color(primary tint(80%))',
  17. 'light-9': 'color(primary tint(90%))'
  18. }

writeNewStyle函数

generateTheme函数 里会调用此函数 插入style到html中

image.png
src/utils/writeNewStyle.ts

  1. import { IObject } from './constants'
  2. // 写入新的css样式
  3. export const writeNewStyle = (originalStyle: string, colors: IObject): void => {
  4. Object.keys(colors).forEach(key => {
  5. // 根据模板将之前变量标记替换成颜色值
  6. const reg = new RegExp('(:|\\s+)' + key, 'g')
  7. originalStyle = originalStyle.replace(reg, '$1' + colors[key])
  8. })
  9. // 之前有插入过id名为chalk-theme-style style元素就直接重新里面内容 没有就动态创建style并加上id
  10. const chalkStyle = document.getElementById('chalk-theme-style')
  11. if (!chalkStyle) {
  12. const style = document.createElement('style')
  13. style.innerText = originalStyle
  14. style.id = 'chalk-theme-style'
  15. // 插入到head中
  16. document.head.appendChild(style)
  17. } else {
  18. (chalkStyle as HTMLElement).innerText = originalStyle
  19. }
  20. }

2-7 修改tagviews组件使用主题色

image.png
src/layout/components/TagsView/index.vue

  1. <template>
  2. <div class="tags-view-container">
  3. <scroll-panel>
  4. <div class="tags-view-wrapper">
  5. <router-link
  6. class="tags-view-item"
  7. :class="{
  8. active: isActive(tag)
  9. }"
  10. :style="{
  11. backgroundColor: isActive(tag) ? themeColor : '',
  12. borderColor: isActive(tag) ? themeColor : ''
  13. }"
  14. v-for="(tag, index) in visitedTags"
  15. :key="index"
  16. :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
  17. tag="span"
  18. >
  19. <el-dropdown
  20. trigger="contextmenu"
  21. @command="command => handleTagCommand(command, tag)">
  22. <span>
  23. {{ tag.meta.title }}
  24. <!-- affix固定的路由tag是无法删除 -->
  25. <span
  26. v-if="!isAffix(tag)"
  27. class="el-icon-close"
  28. @click.prevent.stop="closeSelectedTag(tag)"
  29. ></span>
  30. </span>
  31. <template #dropdown>
  32. <el-dropdown-menu>
  33. <el-dropdown-item command="refresh">刷新</el-dropdown-item>
  34. <el-dropdown-item command="all">关闭所有</el-dropdown-item>
  35. <el-dropdown-item command="other">关闭其他</el-dropdown-item>
  36. <el-dropdown-item command="self" v-if="!tag.meta || !tag.meta.affix">关闭</el-dropdown-item>
  37. </el-dropdown-menu>
  38. </template>
  39. </el-dropdown>
  40. </router-link>
  41. </div>
  42. </scroll-panel>
  43. </div>
  44. </template>
  45. <script lang="ts">
  46. import path from 'path'
  47. import { defineComponent, computed, watch, onMounted, nextTick } from 'vue'
  48. import { useRoute, RouteRecordRaw, useRouter } from 'vue-router'
  49. import { useStore } from '@/store'
  50. import { RouteLocationWithFullPath } from '@/store/modules/tagsView'
  51. import { routes } from '@/router'
  52. import ScrollPanel from './ScrollPanel.vue'
  53. // 右键菜单
  54. enum TagCommandType {
  55. All = 'all',
  56. Other = 'other',
  57. Self = 'self',
  58. Refresh = 'refresh'
  59. }
  60. export default defineComponent({
  61. name: 'TagsView',
  62. components: {
  63. ScrollPanel
  64. },
  65. setup() {
  66. const store = useStore()
  67. const router = useRouter()
  68. const route = useRoute()
  69. // 可显示的tags view
  70. const visitedTags = computed(() => store.state.tagsView.visitedViews)
  71. // 从路由表中过滤出要affixed tagviews
  72. const fillterAffixTags = (routes: Array<RouteLocationWithFullPath | RouteRecordRaw>, basePath = '/') => {
  73. let tags: RouteLocationWithFullPath[] = []
  74. routes.forEach(route => {
  75. if (route.meta && route.meta.affix) {
  76. // 把路由路径解析成完整路径,路由可能是相对路径
  77. const tagPath = path.resolve(basePath, route.path)
  78. tags.push({
  79. name: route.name,
  80. path: tagPath,
  81. fullPath: tagPath,
  82. meta: { ...route.meta }
  83. } as RouteLocationWithFullPath)
  84. }
  85. // 深度优先遍历 子路由(子路由路径可能相对于route.path父路由路径)
  86. if (route.children) {
  87. const childTags = fillterAffixTags(route.children, route.path)
  88. if (childTags.length) {
  89. tags = [...tags, ...childTags]
  90. }
  91. }
  92. })
  93. return tags
  94. }
  95. // 初始添加affix的tag
  96. const initTags = () => {
  97. const affixTags = fillterAffixTags(routes)
  98. for (const tag of affixTags) {
  99. if (tag.name) {
  100. store.dispatch('tagsView/addVisitedView', tag)
  101. }
  102. }
  103. }
  104. // 添加tag
  105. const addTags = () => {
  106. const { name } = route
  107. if (name) {
  108. store.dispatch('tagsView/addView', route)
  109. }
  110. }
  111. // 路径发生变化追加tags view
  112. watch(() => route.path, () => {
  113. addTags()
  114. })
  115. // 最近当前router到tags view
  116. onMounted(() => {
  117. initTags()
  118. addTags()
  119. })
  120. // 当前是否是激活的tag
  121. const isActive = (tag: RouteRecordRaw) => {
  122. return tag.path === route.path
  123. }
  124. // 让删除后tags view集合中最后一个为选中状态
  125. const toLastView = (visitedViews: RouteLocationWithFullPath[], view: RouteLocationWithFullPath) => {
  126. // 得到集合中最后一个项tag view 可能没有
  127. const lastView = visitedViews[visitedViews.length - 1]
  128. if (lastView) {
  129. router.push(lastView.fullPath as string)
  130. } else { // 集合中都没有tag view时
  131. // 如果刚刚删除的正是Dashboard 就重定向回Dashboard(首页)
  132. if (view.name === 'Dashboard') {
  133. router.replace({ path: '/redirect' + view.fullPath as string })
  134. } else {
  135. // tag都没有了 删除的也不是Dashboard 只能跳转首页
  136. router.push('/')
  137. }
  138. }
  139. }
  140. // 关闭当前右键的tag路由
  141. const closeSelectedTag = (view: RouteLocationWithFullPath) => {
  142. // 关掉并移除view
  143. store.dispatch('tagsView/delView', view).then(() => {
  144. // 如果移除的view是当前选中状态view, 就让删除后的集合中最后一个tag view为选中态
  145. if (isActive(view)) {
  146. toLastView(visitedTags.value, view)
  147. }
  148. })
  149. }
  150. // 是否是始终固定在tagsview上的tag
  151. const isAffix = (tag: RouteLocationWithFullPath) => {
  152. return tag.meta && tag.meta.affix
  153. }
  154. // 右键菜单
  155. const handleTagCommand = (command: TagCommandType, view: RouteLocationWithFullPath) => {
  156. switch (command) {
  157. case TagCommandType.All: // 右键删除标签导航所有tag 除了affix为true的
  158. handleCloseAllTag(view)
  159. break
  160. case TagCommandType.Other: // 关闭其他tag 除了affix为true的和当前右键的tag
  161. handleCloseOtherTag(view)
  162. break
  163. case TagCommandType.Self: // 关闭当前右键的tag affix为true的tag下拉菜单中无此项
  164. closeSelectedTag(view)
  165. break
  166. case TagCommandType.Refresh: // 刷新当前右键选中tag对应的路由
  167. refreshSelectedTag(view)
  168. }
  169. }
  170. // 删除所有tag 除了affix为true的
  171. const handleCloseAllTag = (view: RouteLocationWithFullPath) => {
  172. // 对于是affix的tag是不会被删除的
  173. store.dispatch('tagsView/delAllView').then(() => {
  174. // 关闭所有后 就让切换到剩下affix中最后一个tag
  175. toLastView(visitedTags.value, view)
  176. })
  177. }
  178. // 删除其他tag 除了当前右键的tag
  179. const handleCloseOtherTag = (view: RouteLocationWithFullPath) => {
  180. store.dispatch('tagsView/delOthersViews', view).then(() => {
  181. if (!isActive(view)) { // 删除其他tag后 让该view路由激活
  182. router.push(view.path)
  183. }
  184. })
  185. }
  186. // 右键刷新 清空当前对应路由缓存
  187. const refreshSelectedTag = (view: RouteLocationWithFullPath) => {
  188. // 刷新前 将该路由名称从缓存列表中移除
  189. store.dispatch('tagsView/delCachedView', view).then(() => {
  190. const { fullPath } = view
  191. nextTick(() => {
  192. router.replace('/redirect' + fullPath)
  193. })
  194. })
  195. }
  196. // 获取主题色
  197. const themeColor = computed(() => store.getters.themeColor)
  198. return {
  199. visitedTags,
  200. isActive,
  201. closeSelectedTag,
  202. isAffix,
  203. handleTagCommand,
  204. themeColor
  205. }
  206. }
  207. })
  208. </script>
  209. <style lang="scss" scoped>
  210. .tags-view-container {
  211. height: 34px;
  212. background: #fff;
  213. border-bottom: 1px solid #d8dce5;
  214. box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
  215. overflow: hidden;
  216. .tags-view-wrapper {
  217. .tags-view-item {
  218. display: inline-block;
  219. height: 26px;
  220. line-height: 26px;
  221. border: 1px solid #d8dce5;
  222. background: #fff;
  223. color: #495060;
  224. padding: 0 8px;
  225. box-sizing: border-box;
  226. font-size: 12px;
  227. margin-left: 5px;
  228. margin-top: 4px;
  229. &:first-of-type {
  230. margin-left: 15px;
  231. }
  232. &:last-of-type {
  233. margin-right: 15px;
  234. }
  235. &.active {
  236. background-color: #409EFF;
  237. color: #fff;
  238. border-color: #409EFF;
  239. ::v-deep {
  240. .el-dropdown {
  241. color: #fff;
  242. }
  243. }
  244. &::before {
  245. position: relative;
  246. display: inline-block;
  247. content: '';
  248. width: 8px;
  249. height: 8px;
  250. border-radius: 50%;
  251. margin-right: 5px;
  252. background: #fff;
  253. }
  254. }
  255. }
  256. }
  257. }
  258. </style>
  259. <style lang="scss">
  260. .tags-view-container {
  261. .el-icon-close {
  262. width: 16px;
  263. height: 16px;
  264. vertical-align: 2px;
  265. border-radius: 50%;
  266. text-align: center;
  267. transition: all .3s cubic-bezier(.645, .045, .355, 1);
  268. transform-origin: 100% 50%;
  269. &:before {
  270. transform: scale(.6);
  271. display: inline-block;
  272. vertical-align: -3px;
  273. }
  274. &:hover {
  275. background-color: #b4bccc;
  276. color: #fff;
  277. }
  278. }
  279. }
  280. </style>

2-8 修改sidebar使用主题色

image.png
src/layout/components/Sidebar/index.vue

  1. <template>
  2. <div>
  3. <el-menu
  4. class="sidebar-container-menu"
  5. mode="vertical"
  6. :default-active="activeMenu"
  7. :background-color="scssVariables.menuBg"
  8. :text-color="scssVariables.menuText"
  9. :active-text-color="themeColor"
  10. :collapse="isCollapse"
  11. :collapse-transition="true"
  12. >
  13. <sidebar-item
  14. v-for="route in menuRoutes"
  15. :key="route.path"
  16. :item="route"
  17. :base-path="route.path"
  18. />
  19. </el-menu>
  20. </div>
  21. </template>
  22. <script lang="ts">
  23. import { defineComponent, computed } from 'vue'
  24. import { useRoute } from 'vue-router'
  25. import variables from '@/styles/variables.scss'
  26. import { routes } from '@/router'
  27. import SidebarItem from './SidebarItem.vue'
  28. import { useStore } from '@/store'
  29. export default defineComponent({
  30. name: 'Sidebar',
  31. components: {
  32. SidebarItem
  33. },
  34. setup() {
  35. const route = useRoute()
  36. const store = useStore()
  37. // 根据路由路径 对应 当前激活的菜单
  38. const activeMenu = computed(() => {
  39. const { path, meta } = route
  40. // 可根据meta.activeMenu指定 当前路由激活时 让哪个菜单高亮选中
  41. if (meta.activeMenu) {
  42. return meta.activeMenu
  43. }
  44. return path
  45. })
  46. // scss变量
  47. const scssVariables = computed(() => variables)
  48. // 展开收起状态 稍后放store 当前是展开就让它收起
  49. const isCollapse = computed(() => !store.getters.sidebar.opened)
  50. // 渲染路由
  51. const menuRoutes = computed(() => routes)
  52. // 获取主题色
  53. const themeColor = computed(() => store.getters.themeColor)
  54. return {
  55. // ...toRefs(variables), // 不有toRefs原因 缺点variables里面变量属性来源不明确
  56. scssVariables,
  57. isCollapse,
  58. activeMenu,
  59. menuRoutes,
  60. themeColor
  61. }
  62. }
  63. })
  64. </script>

2-9 element.ts

可以注释掉了,因为我们会根据store里默认theme值生成内联css

src/plugins/element.ts
image.png

本节参考源码

这里注意 utils/useGenerateTheme.ts移到hooks目录里 文档中是正确的
https://gitee.com/brolly/vue3-element-admin/commit/1a074ecd1e6855241860a287cf1bede38006c684
移动commit
https://gitee.com/brolly/vue3-element-admin/commit/52195d3285cf0aa71919af1bbea34e22e53cc53b
image.png