Skip to content
On this page

动态修改主题

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

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

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

关于 element ui 颜色 设计可以阅读这两篇文章 https://element-plus.gitee.io/#/zh-CN/component/colorhttps://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-adminimage.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.pngimage.png

2-1 安装相关依赖

安装 axios

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

bash
npm i axios
1

安装 css-color-function

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

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

javascript
// 下面color函数以及 shade tin是css的color-mod函数
// 了解文档 http://cdn1.w3cplus.com/css4/color-mod.html
'color(#11A983 shade(10%))' => 'rgb(15, 152, 118)'
'color(#11A983 tint(10%))' => 'rgb(41, 178, 143)'
'color(#11A983 tint(20%))' => 'rgb(65, 186, 156)'
'color(#11A983 tint(30%))' => 'rgb(88, 195, 168)'
'color(#11A983 tint(40%))' => 'rgb(112, 203, 181)'
'color(#11A983 tint(50%))' => 'rgb(136, 212, 193)'
'color(#11A983 tint(60%))' => 'rgb(160, 221, 205)'
'color(#11A983 tint(70%))' => 'rgb(184, 229, 218)'
'color(#11A983 tint(80%))' => 'rgb(207, 238, 230)'
'color(#11A983 tint(90%))' => 'rgb(231, 246, 243)'

// 利用css-color-function转换为rgb
import color from 'css-color-function'
color.convert('color(#11A983 shade(10%))') // 'rgb(15, 152, 118)'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash
npm i css-color-function
1

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

手动创建声明文件

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

src/css-color-function.d.ts

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

2-2 Navbar 添加设置图标

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

vue
<template>
  <div class="navbar">
    <hambuger  @toggleClick="toggleSidebar" :is-active="sidebar.opened"/>
    <breadcrumb />
    <div class="right-menu">
      <!-- 设置 -->
      <div @click="openShowSetting" class="setting right-menu-item hover-effect">
        <i class="el-icon-s-tools"></i>
      </div>
      <!-- 全屏 -->
      <screenfull id="screefull" class="right-menu-item hover-effect" />
      <!-- element组件size切换 -->
      <el-tooltip content="Global Size" effect="dark" placement="bottom">
        <size-select class="right-menu-item hover-effect" />
      </el-tooltip>
      <!-- 用户头像 -->
      <avatar />
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'
import Breadcrumb from '@/components/Breadcrumb/index.vue'
import Hambuger from '@/components/Hambuger/index.vue'
import { useStore } from '@/store/index'
import Screenfull from '@/components/Screenfull/index.vue'
import SizeSelect from '@/components/SizeSelect/index.vue'
import Avatar from './avatar/index.vue'

export default defineComponent({
  name: 'Navbar',
  components: {
    Breadcrumb,
    Hambuger,
    Screenfull,
    SizeSelect,
    Avatar
  },
  emits: ['showSetting'],
  setup(props, { emit }) {
    // 使用我们自定义的useStore 具备类型提示
    // store.state.app.sidebar 对于getters里的属性没有类型提示
    const store = useStore()
    const toggleSidebar = () => {
      store.dispatch('app/toggleSidebar')
    }
    // 从getters中获取sidebar
    const sidebar = computed(() => store.getters.sidebar)

    // 打开设置面板
    const openShowSetting = () => {
      emit('showSetting', true)
    }

    return {
      toggleSidebar,
      sidebar,
      openShowSetting
    }
  }
})
</script>

<style lang="scss">
  .navbar {
    display: flex;
    background: #fff;
    border-bottom: 1px solid rgba(0, 21, 41, .08);
    box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
    .right-menu {
      flex: 1;
      display: flex;
      align-items: center;
      justify-content: flex-end;
      padding-right: 15px;
      .setting {
        font-size: 26px;
      }
      &-item {
        padding: 0 8px;
        font-size: 18px;
        color: #5a5e66;
        vertical-align: text-bottom;
        &.hover-effect {
          cursor: pointer;
          transition: background .3s;

          &:hover {
            background: rgba(0, 0, 0, .025);
          }
        }
      }
    }
  }
</style>

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

2-2 封装 RightPanel 组件

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

element.ts 中导入 el-drawer 组件

image.png

src/components/RightPanel/index.vue

vue
<template>
  <div class="right-panel">
    <el-drawer
      :model-value="modelValue"
      :direction="direction"
      :show-close="showClose"
      :custom-class="customClass"
      :with-header="withHeader"
      :title="title"
      :size="size"
      @close="handleClose"
    >
      <slot />
    </el-drawer>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'RightPanel',
  props: {
    modelValue: {
      type: Boolean,
      default: true
    },
    direction: {
      type: String,
      validator(val: string) {
        return ['rtl', 'ltr', 'ttb', 'btt'].includes(val)
      },
      default: 'rtl'
    },
    title: {
      type: String,
      default: '自定义title'
    },
    size: {
      type: [String, Number]
    },
    customClass: {
      type: String,
      default: 'setting-panel'
    },
    showClose: {
      type: Boolean,
      default: true
    },
    withHeader: {
      type: Boolean,
      default: true
    }
  },
  // 在组件上使用modelValue文档说明
  // 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
  emits: ['update:modelValue', 'close'],
  setup(props, { emit }) {
    const handleClose = () => {
      emit('update:modelValue', false)
      emit('close')
    }

    return {
      handleClose
    }
  }
})
</script>

<style lang="scss" scoped>
</style>

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

layout 组件中导入 rightPanel

image.png src/layout/index.vue

vue
<template>
  <div class="app-wrapper">
    <div class="sidebar-container">
      <Sidebar />
    </div>
    <div class="main-container">
      <div class="header">
        <navbar @showSetting="openSetting" />
        <tags-view />
      </div>
      <!-- AppMain router-view -->
      <app-main />
    </div>
    <right-panel
      v-model="showSetting"
      title="样式风格设置"
      :size="SettingsPanelWidth"
    >
      <!-- settings 面板设置组件 -->
      <settings />
    </right-panel>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'
import Sidebar from './components/Sidebar/index.vue'
import AppMain from './components/AppMain.vue'
import Navbar from './components/Navbar.vue'
import TagsView from './components/TagsView/index.vue'
import RightPanel from '@/components/RightPanel/index.vue'
import Settings from './components/Settings/index.vue'
import varibalse from '@/styles/variables.scss'

export default defineComponent({
  components: {
    Sidebar,
    AppMain,
    Navbar,
    TagsView,
    RightPanel,
    Settings
  },
  setup() {
    // rightPanel显示隐藏状态
    const showSetting = ref(false)

    const openSetting = () => {
      showSetting.value = true
    }

    return {
      showSetting,
      openSetting,
      // 调整panel宽度
      SettingsPanelWidth: varibalse.settingPanelWidth
    }
  }
})
</script>

<style lang="scss" scoped>
  .app-wrapper {
    display: flex;
    width: 100%;
    height: 100%;
    .main-container {
      flex: 1;
      display: flex;
      flex-direction: column;
      overflow: hidden;
      .app-main {
        /* 50= navbar  50  如果有tagsview + 34  */
        min-height: calc(100vh - 84px);
      }
    }
  }
</style>

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

调整样式修改 scss 变量

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

css
// base color
$blue:#324157;
$light-blue:#3A71A8;
$red:#C03639;
$pink: #E65D6E;
$green: #30B08F;
$tiffany: #4AB7BD;
$yellow:#FEC171;
$panGreen: #30B08F;

// sidebar
$menuText:#bfcbd9;
$menuActiveText:#409EFF;
$subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951

$menuBg:#304156;
$menuHover:#263445;

$subMenuBg:#1f2d3d;
$subMenuHover:#001528;

$sideBarWidth: 210px;

$settingPanelWidth: 260px;
// 默认主题色
$theme: #409EFF;
// The :export directive is the magic sauce for webpack
// https://mattferderer.com/use-sass-variables-in-typescript-and-javascript
:export {
  menuText: $menuText;
  menuActiveText: $menuActiveText;
  subMenuActiveText: $subMenuActiveText;
  menuBg: $menuBg;
  menuHover: $menuHover;
  subMenuBg: $subMenuBg;
  subMenuHover: $subMenuHover;
  sideBarWidth: $sideBarWidth;
  theme: $theme;
  settingPanelWidth: $settingPanelWidth;
}

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
29
30
31
32
33
34
35
36
37
38
39
40
41

src/styles/variables.scss.d.ts

typescript
export interface ScssVariables {
  menuText: string;
  menuActiveText: string;
  subMenuActiveText: string;
  menuBg: string;
  menuHover: string;
  subMenuBg: string;
  subMenuHover: string;
  sideBarWidth: string;
  theme: string;
  settingPanelWidth: string;
}

export const variables: ScssVariables

export default variables

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

2-3 创建 settings 组件

src/store/modules/settings.ts

vue
<template>
  <div class="drawer-container">
    <div class="drawer-item">
      <span>主题色</span>
       <!-- 主题组件 -->
      <theme-picker />
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import ThemePicker from '@/components/ThemePicker/index.vue'

export default defineComponent({
  name: 'Settings',
  components: {
    ThemePicker
  }
})
</script>

<style lang="scss" scoped>
  .drawer-container {
    padding: 24px;
    font-size: 14px;
    line-height: 1.5;
    word-wrap: break-word;

    .drawer-item {
      display: flex;
      justify-content: space-between;
      padding: 12px 0;
      font-size: 16px;
      color: rgba(0, 0, 0, .65);
    }
  }
</style>

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
29
30
31
32
33
34
35
36
37
38
39

2-4 创建 ThemePicker 组件

颜色面板组件

image.png

element.ts 导入 color picker 组件

image.png

typescript
import { App } from 'vue'
import {
  locale,
  ElButton,
  ElMessage,
  ElNotification,
  ElMessageBox,
  ElMenu,
  ElMenuItem,
  ElSubmenu,
  ElRow,
  ElCol,
  ElBreadcrumb,
  ElBreadcrumbItem,
  ElTooltip,
  ElDropdown,
  ElDropdownMenu,
  ElDropdownItem,
  ElScrollbar,
  ElDrawer,
  ElColorPicker
} from 'element-plus'
// 默认主题
import 'element-plus/lib/theme-chalk/index.css'

// Element Plus 组件内部默认使用英语
// https://element-plus.gitee.io/#/zh-CN/component/i18n
import lang from 'element-plus/lib/locale/lang/zh-cn'
// Element Plus 直接使用了 Day.js 项目的时间日期国际化设置, 并且会自动全局设置已经导入的 Day.js 国际化配置。
import 'dayjs/locale/zh-cn'

// $ELEMENT size属性类型
export type Size = 'default' | 'medium' | 'small' | 'mini'

interface ElementOptions {
  size: Size
}

export default (app: App, options: ElementOptions): void => {
  locale(lang)

  // 按需导入组件列表
  const components = [
    ElButton,
    ElMessage,
    ElNotification,
    ElMessageBox,
    ElMenu,
    ElMenuItem,
    ElSubmenu,
    ElRow,
    ElCol,
    ElBreadcrumb,
    ElBreadcrumbItem,
    ElTooltip,
    ElDropdown,
    ElDropdownMenu,
    ElDropdownItem,
    ElScrollbar,
    ElDrawer,
    ElColorPicker
  ]

  components.forEach(component => {
    app.component(component.name, component)
  })

  // Vue.prototype 替换为 config.globalProperties
  // 文档说明 https://v3.cn.vuejs.org/guide/migration/global-api.html#vue-prototype-%E6%9B%BF%E6%8D%A2%E4%B8%BA-config-globalproperties
  app.config.globalProperties.$message = ElMessage
  app.config.globalProperties.$notify = ElNotification
  app.config.globalProperties.$confirm = ElMessageBox.confirm
  app.config.globalProperties.$alert = ElMessageBox.alert
  app.config.globalProperties.$prompt = ElMessageBox.prompt

  // 全局配置 https://element-plus.gitee.io/#/zh-CN/component/quickstart#quan-ju-pei-zhi
  // 该对象目前支持 size 与 zIndex 字段。size 用于改变组件的默认尺寸 small,zIndex 设置弹框的初始 z-index(默认值:2000)。
  app.config.globalProperties.$ELEMENT = {
    size: options.size
  }
}

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

src/components/ThemePicker/index.vue

vue
<template>
  <el-color-picker
    v-model="theme"
    class="theme-picker"
    :predefine="themeColor"
    popper-class="theme-picker-dropdown"
  />
</template>

<script lang='ts'>
import { defineComponent, ref, computed, watch } from 'vue'
import { useStore } from '@/store'
import { useGenerateTheme } from '@/hooks/useGenerateTheme'

export default defineComponent({
  name: 'ThemePicker',
  setup() {
    const store = useStore()
    // 预设可选颜色
    // eslint-disable-next-line comma-spacing, comma-dangle
    const themeColor = ['#409EFF', '#1890ff', '#304156', '#212121', '#11a983', '#13c2c2', '#6959CD', '#f5222d',]
    // store中获取默认主题色
    const defaultTheme = computed(() => store.state.settings.theme)
    const theme = ref('')

    // 主题生成方法 稍后
    const { generateTheme } = useGenerateTheme()

    // 监听默认样式
    watch(defaultTheme, value => {
      theme.value = value
    }, {
      immediate: true
    })


    // 根据theme选择变化 重新生成主题
    watch(theme, (value) => {
      // 同步store
      store.dispatch('settings/changeSetting', { key: 'theme', value })
       // 根据theme选择变化 重新生成主题
      generateTheme(value)
    })

    return {
      themeColor,
      theme
    }
  }
})
</script>

<style lang="scss">
  .theme-picker {
    height: 26px !important;
    margin-right: 8px;
    .el-color-picker__trigger {
      height: 26px !important;
      width: 26px !important;
      padding: 2px;
    }
  }

  .theme-message,
  .theme-picker-dropdown {
    z-index: 99999 !important;
  }

  .theme-picker-dropdown .el-color-dropdown__link-btn {
    display: none;
  }
</style>

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

2-5 store 中存储 theme

创建 settings module

src/store/modules/settings.ts

typescript
import { MutationTree, ActionTree } from 'vuex'
import variables from '@/styles/variables.scss'
import { IRootState } from '@/store'

export interface ISettingsState {
  theme: string;
  originalStyle: string;
}

// 定义state
const state: ISettingsState = {
  theme: variables.theme,
  originalStyle: '' // 保存element 主题样式文件内容 作为替换模板
}

// 动态key的情况下 根据不同的key 约束对应value
// http://www.voidcn.com/article/p-wtmkdcie-byz.html
type ValueOf<T> = T[keyof T];
interface ISettings { // 约束payload类型
  key: keyof ISettingsState; // 约束为ISettingsState中key
  value: ValueOf<ISettingsState>; // 约束为ISettingsState中value的类型
}
// 定义mutations 通用muation
const mutations: MutationTree<ISettingsState> = {
  CHANGE_SETTING(state, { key, value }: ISettings) {
    if (key in state) {
      (state[key] as ValueOf<ISettingsState>) = value
    }
  }
}

const actions: ActionTree<ISettingsState, IRootState> = {
  changeSetting({ commit }, data) {
    commit('CHANGE_SETTING', data)
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

store 中缓存 settings.theme 和 settings.originalStyle

image.png src/store/index.ts

typescript
import { InjectionKey } from 'vue'
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import app, { IAppState } from '@/store/modules/app'
import tagsView, { ITagsViewState } from '@/store/modules/tagsView'
import settings, { ISettingsState } from '@/store/modules/settings'
import getters from './getters'

// 模块声明在根状态下
export interface IRootState {
  app: IAppState;
  tagsView: ITagsViewState;
  settings: ISettingsState;
}

// 通过下面方式使用 TypeScript 定义 store 能正确地为 store 提供类型声明。
// https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage
// eslint-disable-next-line symbol-description
export const key: InjectionKey<Store<IRootState>> = Symbol()

// 对于getters在组件使用时没有类型提示
// 有人提交了pr #1896 为getters创建泛型 应该还未发布
// https://github.com/vuejs/vuex/pull/1896
// 代码pr内容详情
// https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce

// vuex store持久化 默认使用localstorage持久化
const persisteAppState = createPersistedState({
  storage: window.sessionStorage, // 指定storage 也可自定义
  key: 'vuex_app', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  // paths: ['app'] // 针对app这个模块持久化
  // 只针对app模块下sidebar.opened状态持久化
  paths: ['app.sidebar.opened', 'app.size'] // 通过点连接符指定state路径
})

const persisteSettingsState = createPersistedState({
  storage: window.sessionStorage, // 指定storage 也可自定义
  key: 'vuex_setting', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  // paths: ['app'] // 针对app这个模块持久化
  // 只针对app模块下sidebar.opened状态持久化
  paths: ['settings.theme', 'settings.originalStyle'] // 通过点连接符指定state路径
})

export default createStore<IRootState>({
  plugins: [
    persisteAppState,
    persisteSettingsState
  ],
  getters,
  modules: {
    app,
    tagsView,
    settings
  }
})

// 定义自己的 `useStore` 组合式函数
// https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function useStore () {
  return baseUseStore(key)
}

// vuex持久化 vuex-persistedstate文档说明
// https://www.npmjs.com/package/vuex-persistedstate

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

getters 添加 theme

image.png src/store/getters.ts

typescript
import { GetterTree } from 'vuex'
import { IRootState } from './index'

// 定义全局getters
const getters: GetterTree<IRootState, IRootState> = {
  sidebar: (state) => state.app.sidebar,
  size: state => state.app.size,
  themeColor: state => state.settings.theme
}

export default getters

1
2
3
4
5
6
7
8
9
10
11
12

2-6 主题生成逻辑

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

src/App.vue

vue
<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { useGenerateTheme } from '@/hooks/useGenerateTheme'

export default defineComponent({
  name: 'App',
  setup() {
    // 根据此时store中主题色生成
    useGenerateTheme()
  }
})
</script>

<style>
#app {
  height: 100%;
}
</style>

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

创建 useGenerateTheme Hook 函数

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

typescript
import { computed } from 'vue'
import { useThemeFiles } from '@/hooks/useThemeFiles'
import { getStyleTemplate } from '@/utils/getStyleTemplate'
import { generateColors } from '@/utils/color'
import { writeNewStyle } from '@/utils/writeNewStyle'
import { useStore } from '@/store'

export const useGenerateTheme = () => {
  const store = useStore()
  // 从store获取中获取 theme主题色
  const defaultTheme = computed(() => store.state.settings.theme)
  // 获取element-ui 主题文件内容 通过axios获取的 作为变量替换模板
  const originalStyle = computed(() => store.state.settings.originalStyle)
  // 生成主题
  // 了解element ui 设计 https://juejin.cn/post/6844903960218697741
  const generateTheme = (color: string) => {
    const colors = Object.assign({
      primary: defaultTheme.value
    }, generateColors(color))
    // 写入新的css样式
    writeNewStyle(originalStyle.value, colors)
  }
  // 第一步 远程获取element-ui 主题文件作为模板 然后进行变量替换 替换成我们所选主题色
  const { getThemeChalkStyle } = useThemeFiles()
  // 如果主题模板不存在 就要发送请求获取
  if (!originalStyle.value) {
    // axios请求后去 主题模板
    getThemeChalkStyle().then(data => {
      // data是获取到主题文件的css内容
      // 生成样式模板 将里面css内容里默认主题颜色值 替换成变量标记 如 '#409eff' => 'primary',
      // 方便我们后续 把primary字符标记 换成我们的主题色
      const styleValue = getStyleTemplate(data as string)
      // 把模板保存到 store中缓存起来 不用每次重新获取
      store.dispatch('settings/changeSetting', { key: 'originalStyle', value: styleValue })
      // 根据主题色生成主题 插入到html中
      generateTheme(defaultTheme.value)
    })
  } else {
    generateTheme(defaultTheme.value)
  }
  return {
    generateTheme
  }
}

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

创建 useThemeFiles hooks 函数

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

image.png src/hooks/useThemeFiles.ts

typescript
import { useFetch } from './useFetch'
import pkgJson from 'element-plus/package.json'

// 获取element-plus版本
const version = pkgJson.version

interface ReturnFn {
  getThemeChalkStyle: () => Promise<unknown>
}
export const useThemeFiles = ():ReturnFn => {
  const getThemeChalkStyle = async (): Promise<unknown> => {
    // 返回获取到的指定版本的element主题css内容
    // return await useFetch('//unpkg.com/element-plus@1.0.2-beta.33/lib/theme-chalk/index.css')
    return await useFetch(`//unpkg.com/element-plus@${version}/lib/theme-chalk/index.css`)
  }

  return {
    getThemeChalkStyle
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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

添加"resolveJsonModule": true 即可

image.png

创建 useFetch 请求 hook 函数

image.png src/hooks/useFetch.ts

typescript
import axios from 'axios'

const useFetch = async (url: string): Promise<unknown> => {
  return await new Promise((resolve, reject) => {
    axios({
      url,
      method: 'get'
    }).then(res => {
      if (res.status === 200) {
        resolve(res.data)
      } else {
        reject(new Error(res.statusText))
      }
    }).catch(err => {
      reject(new Error(err.message))
    })
  })
}

export {
  useFetch
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

生成最终的样式模板

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

typescript
export interface IObject {
  [prop: string]: string
}

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

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

generateTheme 生成主题函数

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

image.png generateTheme 函数

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

src/utils/useGenerateTheme.ts

typescript
import { computed } from 'vue'
import { useThemeFiles } from '@/hooks/useThemeFiles'
import { getStyleTemplate } from '@/utils/getStyleTemplate'
import { generateColors } from '@/utils/color'
import { writeNewStyle } from '@/utils/writeNewStyle'
import { useStore } from '@/store'

export const useGenerateTheme = () => {
  const store = useStore()
  const defaultTheme = computed(() => store.state.settings.theme)
  const originalStyle = computed(() => store.state.settings.originalStyle)
  // 生成主题
  // element ui 设计 https://juejin.cn/post/6844903960218697741
  const generateTheme = (color: string) => {
    const colors = Object.assign({
      primary: defaultTheme.value
    }, generateColors(color))
    // 写入新的css样式
    writeNewStyle(originalStyle.value, colors)
  }
  // 第一步 远程获取element-ui 主题文件作为模板 然后进行变量替换 替换成我们所选主题色
  const { getThemeChalkStyle } = useThemeFiles()
  if (!originalStyle.value) {
    getThemeChalkStyle().then(data => {
      // data是主题文件的css内容
      const styleValue = getStyleTemplate(data as string)
      store.dispatch('settings/changeSetting', { key: 'originalStyle', value: styleValue })
      generateTheme(defaultTheme.value)
    })
  } else {
    generateTheme(defaultTheme.value)
  }
  return {
    generateTheme
  }
}

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
29
30
31
32
33
34
35
36
37

generateColors 函数

generateTheme 函数 里会调用此函数

image.png src/utils/color.ts

typescript
import color from 'css-color-function'
import { formula, IObject } from './constants'

// 转换成不同色调的rgb颜色值
// https://www.w3cplus.com/css/the-power-of-rgba.html
export const generateColors = (primary: string): IObject => {
  const colors = {} as IObject
  Object.keys(formula).forEach(key => {
    // element ui 主题色 渐变色设计 https://juejin.cn/post/6844903960218697741
    // 根据主题色生成渐变色 将formula对象中字符primary 替换成我们所选的主题色
    const value = formula[key].replace(/primary/g, primary)
    colors[key] = color.convert(value) // 转换成rgba颜色值
  })
  return colors
}

// 主题色的渐变色设计 https://juejin.cn/post/6844903960218697741
// color-mod css颜色函数
// https://www.w3cplus.com/css4/color-mod.html
// export const formula: IObject = {
//   'shade-1': 'color(primary shade(10%))',
//   'light-1': 'color(primary tint(10%))',
//   'light-2': 'color(primary tint(20%))',
//   'light-3': 'color(primary tint(30%))',
//   'light-4': 'color(primary tint(40%))',
//   'light-5': 'color(primary tint(50%))',
//   'light-6': 'color(primary tint(60%))',
//   'light-7': 'color(primary tint(70%))',
//   'light-8': 'color(primary tint(80%))',
//   'light-9': 'color(primary tint(90%))'
// }
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
29
30
31

constants 变量

src/utils/constants.ts

typescript
export interface IObject {
  [prop: string]: string;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

writeNewStyle 函数

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

image.png src/utils/writeNewStyle.ts

typescript
import { IObject } from './constants'

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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

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

vue
<template>
  <div class="tags-view-container">
    <scroll-panel>
      <div class="tags-view-wrapper">
          <router-link
            class="tags-view-item"
            :class="{
              active: isActive(tag)
            }"
            :style="{
              backgroundColor: isActive(tag) ? themeColor : '',
              borderColor: isActive(tag) ? themeColor : ''
            }"
            v-for="(tag, index) in visitedTags"
            :key="index"
            :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
            tag="span"
          >
            <el-dropdown
              trigger="contextmenu"
              @command="command => handleTagCommand(command, tag)">
              <span>
                {{ tag.meta.title }}
                <!-- affix固定的路由tag是无法删除 -->
                <span
                  v-if="!isAffix(tag)"
                  class="el-icon-close"
                  @click.prevent.stop="closeSelectedTag(tag)"
                ></span>
              </span>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item command="refresh">刷新</el-dropdown-item>
                  <el-dropdown-item command="all">关闭所有</el-dropdown-item>
                  <el-dropdown-item command="other">关闭其他</el-dropdown-item>
                  <el-dropdown-item command="self" v-if="!tag.meta || !tag.meta.affix">关闭</el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
          </router-link>
      </div>
    </scroll-panel>
  </div>
</template>

<script lang="ts">
import path from 'path'
import { defineComponent, computed, watch, onMounted, nextTick } from 'vue'
import { useRoute, RouteRecordRaw, useRouter } from 'vue-router'
import { useStore } from '@/store'
import { RouteLocationWithFullPath } from '@/store/modules/tagsView'
import { routes } from '@/router'
import ScrollPanel from './ScrollPanel.vue'

// 右键菜单
enum TagCommandType {
  All = 'all',
  Other = 'other',
  Self = 'self',
  Refresh = 'refresh'
}

export default defineComponent({
  name: 'TagsView',
  components: {
    ScrollPanel
  },
  setup() {
    const store = useStore()
    const router = useRouter()
    const route = useRoute()
    // 可显示的tags view
    const visitedTags = computed(() => store.state.tagsView.visitedViews)
    // 从路由表中过滤出要affixed tagviews
    const fillterAffixTags = (routes: Array<RouteLocationWithFullPath | RouteRecordRaw>, basePath = '/') => {
      let tags: RouteLocationWithFullPath[] = []
      routes.forEach(route => {
        if (route.meta && route.meta.affix) {
          // 把路由路径解析成完整路径,路由可能是相对路径
          const tagPath = path.resolve(basePath, route.path)
          tags.push({
            name: route.name,
            path: tagPath,
            fullPath: tagPath,
            meta: { ...route.meta }
          } as RouteLocationWithFullPath)
        }

        // 深度优先遍历 子路由(子路由路径可能相对于route.path父路由路径)
        if (route.children) {
          const childTags = fillterAffixTags(route.children, route.path)
          if (childTags.length) {
            tags = [...tags, ...childTags]
          }
        }
      })
      return tags
    }

    // 初始添加affix的tag
    const initTags = () => {
      const affixTags = fillterAffixTags(routes)
      for (const tag of affixTags) {
        if (tag.name) {
          store.dispatch('tagsView/addVisitedView', tag)
        }
      }
    }

    // 添加tag
    const addTags = () => {
      const { name } = route
      if (name) {
        store.dispatch('tagsView/addView', route)
      }
    }

    // 路径发生变化追加tags view
    watch(() => route.path, () => {
      addTags()
    })

    // 最近当前router到tags view
    onMounted(() => {
      initTags()
      addTags()
    })

    // 当前是否是激活的tag
    const isActive = (tag: RouteRecordRaw) => {
      return tag.path === route.path
    }

    // 让删除后tags view集合中最后一个为选中状态
    const toLastView = (visitedViews: RouteLocationWithFullPath[], view: RouteLocationWithFullPath) => {
      // 得到集合中最后一个项tag view 可能没有
      const lastView = visitedViews[visitedViews.length - 1]
      if (lastView) {
        router.push(lastView.fullPath as string)
      } else { // 集合中都没有tag view时
        // 如果刚刚删除的正是Dashboard 就重定向回Dashboard(首页)
        if (view.name === 'Dashboard') {
          router.replace({ path: '/redirect' + view.fullPath as string })
        } else {
          // tag都没有了 删除的也不是Dashboard 只能跳转首页
          router.push('/')
        }
      }
    }

    // 关闭当前右键的tag路由
    const closeSelectedTag = (view: RouteLocationWithFullPath) => {
      // 关掉并移除view
      store.dispatch('tagsView/delView', view).then(() => {
        // 如果移除的view是当前选中状态view, 就让删除后的集合中最后一个tag view为选中态
        if (isActive(view)) {
          toLastView(visitedTags.value, view)
        }
      })
    }

    // 是否是始终固定在tagsview上的tag
    const isAffix = (tag: RouteLocationWithFullPath) => {
      return tag.meta && tag.meta.affix
    }

    // 右键菜单
    const handleTagCommand = (command: TagCommandType, view: RouteLocationWithFullPath) => {
      switch (command) {
        case TagCommandType.All: // 右键删除标签导航所有tag 除了affix为true的
          handleCloseAllTag(view)
          break
        case TagCommandType.Other: // 关闭其他tag 除了affix为true的和当前右键的tag
          handleCloseOtherTag(view)
          break
        case TagCommandType.Self: // 关闭当前右键的tag affix为true的tag下拉菜单中无此项
          closeSelectedTag(view)
          break
        case TagCommandType.Refresh: // 刷新当前右键选中tag对应的路由
          refreshSelectedTag(view)
      }
    }

    // 删除所有tag 除了affix为true的
    const handleCloseAllTag = (view: RouteLocationWithFullPath) => {
      // 对于是affix的tag是不会被删除的
      store.dispatch('tagsView/delAllView').then(() => {
        // 关闭所有后 就让切换到剩下affix中最后一个tag
        toLastView(visitedTags.value, view)
      })
    }

    // 删除其他tag 除了当前右键的tag
    const handleCloseOtherTag = (view: RouteLocationWithFullPath) => {
      store.dispatch('tagsView/delOthersViews', view).then(() => {
        if (!isActive(view)) { // 删除其他tag后 让该view路由激活
          router.push(view.path)
        }
      })
    }

    // 右键刷新 清空当前对应路由缓存
    const refreshSelectedTag = (view: RouteLocationWithFullPath) => {
      // 刷新前 将该路由名称从缓存列表中移除
      store.dispatch('tagsView/delCachedView', view).then(() => {
        const { fullPath } = view
        nextTick(() => {
          router.replace('/redirect' + fullPath)
        })
      })
    }

    // 获取主题色
    const themeColor = computed(() => store.getters.themeColor)

    return {
      visitedTags,
      isActive,
      closeSelectedTag,
      isAffix,
      handleTagCommand,
      themeColor
    }
  }
})
</script>

<style lang="scss" scoped>
.tags-view-container {
  height: 34px;
  background: #fff;
  border-bottom: 1px solid #d8dce5;
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
  overflow: hidden;
  .tags-view-wrapper {
    .tags-view-item {
      display: inline-block;
      height: 26px;
      line-height: 26px;
      border: 1px solid #d8dce5;
      background: #fff;
      color: #495060;
      padding: 0 8px;
      box-sizing: border-box;
      font-size: 12px;
      margin-left: 5px;
      margin-top: 4px;
      &:first-of-type {
        margin-left: 15px;
      }
      &:last-of-type {
        margin-right: 15px;
      }
      &.active {
        background-color: #409EFF;
        color: #fff;
        border-color: #409EFF;
        ::v-deep {
          .el-dropdown {
            color: #fff;
          }
        }
        &::before {
          position: relative;
          display: inline-block;
          content: '';
          width: 8px;
          height: 8px;
          border-radius: 50%;
          margin-right: 5px;
          background: #fff;
        }
      }
    }
  }
}
</style>

<style lang="scss">
.tags-view-container {
  .el-icon-close {
    width: 16px;
    height: 16px;
    vertical-align: 2px;
    border-radius: 50%;
    text-align: center;
    transition: all .3s cubic-bezier(.645, .045, .355, 1);
    transform-origin: 100% 50%;
    &:before {
      transform: scale(.6);
      display: inline-block;
      vertical-align: -3px;
    }
    &:hover {
      background-color: #b4bccc;
      color: #fff;
    }
  }
}

</style>

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302

2-8 修改 sidebar 使用主题色

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

vue
<template>
  <div>
    <el-menu
      class="sidebar-container-menu"
      mode="vertical"
      :default-active="activeMenu"
      :background-color="scssVariables.menuBg"
      :text-color="scssVariables.menuText"
      :active-text-color="themeColor"
      :collapse="isCollapse"
      :collapse-transition="true"
    >
      <sidebar-item
        v-for="route in menuRoutes"
        :key="route.path"
        :item="route"
        :base-path="route.path"
      />
    </el-menu>
  </div>
</template>

<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useRoute } from 'vue-router'
import variables from '@/styles/variables.scss'
import { routes } from '@/router'
import SidebarItem from './SidebarItem.vue'
import { useStore } from '@/store'

export default defineComponent({
  name: 'Sidebar',
  components: {
    SidebarItem
  },
  setup() {
    const route = useRoute()
    const store = useStore()
    // 根据路由路径 对应 当前激活的菜单
    const activeMenu = computed(() => {
      const { path, meta } = route
      // 可根据meta.activeMenu指定 当前路由激活时 让哪个菜单高亮选中
      if (meta.activeMenu) {
        return meta.activeMenu
      }
      return path
    })
    // scss变量
    const scssVariables = computed(() => variables)
    // 展开收起状态 稍后放store 当前是展开就让它收起
    const isCollapse = computed(() => !store.getters.sidebar.opened)

    // 渲染路由
    const menuRoutes = computed(() => routes)

    // 获取主题色
    const themeColor = computed(() => store.getters.themeColor)

    return {
      // ...toRefs(variables), // 不有toRefs原因 缺点variables里面变量属性来源不明确
      scssVariables,
      isCollapse,
      activeMenu,
      menuRoutes,
      themeColor
    }
  }
})
</script>

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

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/52195d3285cf0aa71919af1bbea34e22e53cc53bimage.png

沪ICP备20006251号-1