【项目实践】页面布局和主题色切换

3,650 阅读4分钟

vue-element-admin 在设置中可以对页面布局和主题进行切换,vue-element-plus-admin在这方面比前者更加复杂,通过理解源码总结关于页面布局和主题色切换

页面布局切换

页面布局切换本质上就是通过根据配置动态调整元素的位置或者显示,难点在于代码怎么写才能实现低耦合和可维护。

控制元素的显示和隐藏

先看下 vue-element-admin 的效果:

GIF1.gif

实现了3个功能:sidebar logo,头部是否固定,路由标签栏的显示和隐藏。查看源码实现方案如下:用户修改完配置后,store 中的值将会改变,相应的引入了该数据的页面将会发生改变。

image.png 核心代码如下:

默认配置设置

// 默认配置
module.exports = {
  title: 'Vue Element Admin',

  showSettings: true,

  tagsView: true,

  fixedHeader: false,

  sidebarLogo: false,
}

// store中的代码,只是对数据的修改操作

// store中的代码
const { showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings

const state = {
  theme: variables.theme,
  showSettings: showSettings,
  tagsView: tagsView,
  fixedHeader: fixedHeader,
  sidebarLogo: sidebarLogo
}

const mutations = {
  CHANGE_SETTING: (state, { key, value }) => {
    // eslint-disable-next-line no-prototype-builtins
    if (state.hasOwnProperty(key)) {
      state[key] = value
    }
  }
}

const actions = {
  changeSetting({ commit }, data) {
    commit('CHANGE_SETTING', data)
  }
}

配置组件的代码

<template>
      <div class="drawer-item">
        <span>Open Tags-View</span>
        <el-switch v-model="tagsView" class="drawer-switch" />
      </div>

      <div class="drawer-item">
        <span>Fixed Header</span>
        <el-switch v-model="fixedHeader" class="drawer-switch" />
      </div>

      <div class="drawer-item">
        <span>Sidebar Logo</span>
        <el-switch v-model="sidebarLogo" class="drawer-switch" />
      </div>
</template>
<script>
  computed: {
    fixedHeader: {
      get() {
        return this.$store.state.settings.fixedHeader
      },
      set(val) {
        this.$store.dispatch('settings/changeSetting', {
          key: 'fixedHeader',
          value: val
        })
      }
    },
    tagsView: {
      get() {
        return this.$store.state.settings.tagsView
      },
      set(val) {
        this.$store.dispatch('settings/changeSetting', {
          key: 'tagsView',
          value: val
        })
      }
    },
    sidebarLogo: {
      get() {
        return this.$store.state.settings.sidebarLogo
      },
      set(val) {
        this.$store.dispatch('settings/changeSetting', {
          key: 'sidebarLogo',
          value: val
        })
      }
    }
  },
</script>
<template>
    <logo v-if="showLogo" :collapse="isCollapse" />
</template>
<script>
computed: {
    showLogo() {
      return this.$store.state.settings.sidebarLogo
    }
}
</script>

控制页面的显示和隐藏

vue-element-plus-admin 的效果则相对复杂:

GIF2.gif

选择不同布局时,页面的结构发生改变。对于这种情况最简单的实现方式就是每种布局使用不同的 HTML 结构,布局改变后选择对应的结构动态生成就行。查看源码实现方案如下:页面布局配置保存在 store 中,每当用户修改完配置后,store 中的值将会改变,相应的引入了对应的页面。

vue-element-plus-admin 使用的是 tsx 来完成,核心代码如下:

export const useRenderLayout = () => {
     const renderClassic = () => {
         // 具体代码省略
     }
     const renderTopLeft = () => {
         // 具体代码省略
     }
      const renderTop = () => {
         // 具体代码省略
     }
      const renderCutMenu = () => {
         // 具体代码省略
     }
}
const renderLayout = () => {
  switch (unref(layout)) {
    case 'classic':
      const { renderClassic } = useRenderLayout()
      return renderClassic()
    case 'topLeft':
      const { renderTopLeft } = useRenderLayout()
      return renderTopLeft()
    case 'top':
      const { renderTop } = useRenderLayout()
      return renderTop()
    case 'cutMenu':
      const { renderCutMenu } = useRenderLayout()
      return renderCutMenu()
    default:
      break
  }
}

export default defineComponent({
  name: 'Layout',
  setup() {
    return () => (
      <section>
        {renderLayout()}
        <Backtop></Backtop>
        <Setting></Setting>
      </section>
    )
  }
})

如果不熟悉 tsx ,也可以换成 h 函数 + 组件的形式,核心代码如下:

import Classic from "@/Layout/Classic.vue";
import TopLeft from "@/Layout/TopLeft.vue";
import Top from "@/Layout/Top.vue";
import appStore from "@/store/index";
setup() {
    const { configState } = appStore.configStore
    return () => {
        if (configState.layoutType == "classic") {
            return h(Classic)
        } else if (configState.layoutType == "top-left") {
            return h(TopLeft)
        } else if (configState.layoutType == "top") {
            return h(Top)
        }
    }
}

源码地址

主题色切换

对于主题色的切换的各种方法可以看 前端主题切换方案 - 掘金 (juejin.cn) 这篇文章。这边只说明下我在不同的布局风格下主题色切换,主要是在以下几方面做的思考:

  1. 同一个组件,怎么保证上一个布局风格下的修改设置会在这次的布局风格下生效
  2. 同一个组件,在不同的布局风格下其颜色必须和其所在的位置的颜色保持一致。例如logo组件在 classic 布局其主题颜色要和侧边栏保持一致,在 top-left 布局下其主题颜色要和 header 保持一致。

对于第1点,实现思路也是利用 store 保存配置信息,每种布局用的是同一份数据。简化代码如下:

:root{
    --color:#fff
}
// pinia 的 setup 写法
function setCssVar(key: string, value: any, dom = document.documentElement) {
    dom.style.setProperty(key, value)
}
export const useConfigStore = defineStore('config', () => {
     const configState = reactive({
         theme:{
             --color:"#fff"
         }
     })
     const setThemeAction = (value) => {
         setCssVar("--color",value)
     }
     return { configState, setThemeAction}
})
<template>
    <div class="color" @click="handler"></div>
</template>
<script>
const useConfig = useConfigStore();
const { configState,setThemeAction } = storeToRefs(useConfig);
const handler = ()=>{
    setThemeAction("#666")
}
</script>
<style>
.color{
    color:var(--color)
}
</style>

对于第2点,重点是明确相同的组件组件在不同的布局下,其主题色需要跟随谁改变,在组件切换的时候给定准确的主题色就行

这是 classic 布局 image.png

这是 top-left 布局 image.png

很明显,logo组件在 classic 布局下主题色和菜单栏保持一致,在 top-left 布局和 header 保持一致,所以在组件切换的时候设定好对应的主题色就行。代码如下:

const setThemeAction = (theme: Theme) => {
  Object.assign(configState.theme, theme);
  Object.keys(configState.theme).forEach(key => setCssVar(key, configState.theme[key]))
}
// classic 布局
setThemeAction({
    "--base-logo-background": configState.theme["--el-menu-bg-color"],
    "--base-logo-text-color": configState.theme["--el-menu-text-color"],
    "--el-menu-bg-color": configState.theme["--el-menu-bg-color"],
    "--el-menu-text-color": configState.theme["--el-menu-text-color"],
});
// top-left 布局
setThemeAction({
    "--base-logo-background": configState.theme["--base-navbar-background"],
    "--base-logo-text-color": configState.theme["--base-navbar-text-color"],
});

最后的效果如图: GIF3.gif