主题切换

思路:

前端主题切换,其实就是切换 css

而且,css 中变换最大的,就是颜色相关的属性,图片,icon....

前端工程中样式切换的解决方案

1、css 变量 + css 样式【data-set [data-name='dark']】class='dark'

1.primary{
2
3  --vt-c-bg:#fff;
4}
5
6.dark{
7  --vt-c-bg:#000
8}
9
10可以使用js,点击某个按钮之后,切换根元素的css样式,样式切换之后,意味着css变量的值改变了
11.layer{
12  background-color:var(--vt-c-bg);
13}

css 变量,是当我们点击某个按钮的时候做切换,可能如果变换比较大内容较多,会出现白屏的情况

2、css 预处理器(scss,less,styuls...) + css 样式 [data-set]

就是使用 css 预处理器强大的预编译功能,简单来说,就是把 css 先写好

1html.primary .layer{
2  color:#fff
3}
4
5html.dark .layer{
6  color:#000
7}
8html.primary .container{}
9
10html.dark .container{}
11
12html.primary .link:hover{}
13html.dark .link:hover{}

css 预编译器,一开始,把所有的主题样式涉及到的内容全部写好,当切换的时候,就直接切换了

这种的方式,可能牺牲的是,一开始的白屏时间

通过 css 变量实现主题切换及工程化处理

使用持久化存储

1、安装 pinia 和持久化插件

1npm install pinia pinia-plugin-persistedstate

2、创建 pinia

1//stores/index.ts
2import { createPinia } from "pinia";
3import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
4
5// pinia persist
6const pinia = createPinia();
7pinia.use(piniaPluginPersistedstate);
8
9export default pinia;

3、main.ts 中引用

1import { createApp } from "vue";
2import "./style.css";
3import App from "./App.vue";
4// pinia store
5import pinia from "@/stores";
6
7createApp(App).use(pinia).mount("#app");

4、设置 pinia 持久化配置

1// config/piniaPersist.ts
2import { PersistedStateOptions } from "pinia-plugin-persistedstate";
3
4/**
5 * @description pinia 持久化参数配置
6 * @param {String} key 存储到持久化的 name
7 * @param {Array} paths 需要持久化的 state name
8 * @return persist
9 * */
10const piniaPersistConfig = (key: string, paths?: string[]) => {
11  const persist: PersistedStateOptions = {
12    key,
13    storage: localStorage,
14    // storage: sessionStorage,
15    paths,
16  };
17  return persist;
18};
19
20export default piniaPersistConfig;

5、创建 global.ts

1import { defineStore } from "pinia";
2import piniaPersistConfig from "@/config/piniaPersist";
3import { GlobalState, ObjToKeyValArray } from "@/stores/interface";
4
5export const useGlobalStore = defineStore({
6  id: "dy-global",
7  // 修改默认值之后,需清除 localStorage 数据
8  state: (): GlobalState => ({
9    // 深色模式
10    isDark: false,
11  }),
12  persist: piniaPersistConfig("dy-global"),
13});

6、设置 actions 统一的的函数

1import { defineStore } from "pinia";
2import piniaPersistConfig from "@/config/piniaPersist";
3import { GlobalState,ObjToKeyValArray } from "@/stores/interface";
4
5export const useGlobalStore = defineStore({
6  id: "dy-global",
7  // 修改默认值之后,需清除 localStorage 数据
8  state: ():GlobalState => ({
9    // 深色模式
10    isDark: false,
11  }),
12+  actions: {
13+    // Set GlobalState
14+    setGlobalState(...args: ObjToKeyValArray<GlobalState>) {
15+      this.$patch({ [args[0]]: args[1] });
16+    }
17+  },
18  persist: piniaPersistConfig("dy-global")
19});

7、setGlobalState 函数参数的设定

1export interface GlobalState {
2  isDark: boolean;
3}
4export type ObjToKeyValArray<T> = {
5  [K in keyof T]: [K, T[K]];
6}[keyof T];

8、修改 useTheme

1import { useGlobalStore } from "@/stores/modules/global";
2import { storeToRefs } from "pinia";
3
4export const useTheme = () => {
5
6  const globalStore = useGlobalStore();
7  const { isDark } = storeToRefs(globalStore);
8
9  const switchTheme = (init?: Boolean) => {
10    if (!init) {
11      globalStore.setGlobalState("isDark", !isDark.value);
12    }
13    const html = document.documentElement as HTMLElement;
14    console.log(isDark.value)
15    if (isDark.value) html.setAttribute("class", "dark");
16    else html.setAttribute("class", "primary");
17  }
18
19  const initTheme = () => {
20    switchTheme(true);
21  };
22
23
24  return {
25    switchTheme,
26    initTheme
27  }
28}

9、界面调用

1import { useTheme } from "@/hooks/useTheme";
2const { initTheme, switchTheme } = useTheme();
3
4initTheme();
5
6const modeSwitch = () => {
7  // const theme = document.documentElement.className
8  // document.documentElement.className = theme === 'primary' ? 'dark' : 'primary'
9  switchTheme();
10};

跟随系统主题

首先,使用媒体查询,可以实现跟随系统主题的效果,其实只有light和dark两种颜色

1body{
2  margin: 0;
3  padding: 0;
4}
5
6.container{
7  position: fixed;
8  width: 100%;
9  height: 100%;
10  overflow: auto;
11  transition:color .5s, background-color .5s;
12  color:#000;
13  background-color:#fff;
14}
15.layout{
16  display: flex;
17  justify-content: center;
18}
19
20.layer{
21  width: 300px;
22  height: 300px;
23  padding: 8px;
24  margin: 10px;
25  border:1px solid #000;
26}
27@media (prefers-color-scheme: dark) {
28  .container{
29    color:#fff;
30    background-color:#000;
31  }
32  .layer{
33    border:1px solid #fff;
34  }
35}

我们可以通过api - window.matchMedia,来获取当前系统主题是深色还是浅色

1window.matchMedia("(prefers-color-scheme: dark)");

有了这个 API,我们完全就没必要修改之前的 css 代码,直接 js 判断

1const media = window.matchMedia("(prefers-color-scheme: dark)");
2
3const followSystem = () => {
4  if (media.matches) {
5    document.documentElement.className = "dark";
6  } else {
7    document.documentElement.className = "primary";
8  }
9};
10
11followSystem();
12
13media.addEventListener("change", followSystem);

直接在 vue3 项目中处理,添加界面系统主题图标,点击激活图标,表示激活跟随系统主题

1、添加图标

1<button>
2  <svg
3    xmlns="http://www.w3.org/2000/svg"
4    viewBox="0 0 16 16"
5    fill="none"
6    role="img"
7  >
8    <circle cx="8" cy="8" r="7.25" stroke="#5B5B66" stroke-width="1.5" />
9    <mask
10      id="a"
11      style="mask-type:alpha"
12      maskUnits="userSpaceOnUse"
13      x="0"
14      y="0"
15      width="16"
16      height="16"
17    >
18      <circle
19        cx="8"
20        cy="8"
21        r="7.25"
22        fill="#5B5B66"
23        stroke="#5B5B66"
24        stroke-width="1.5"
25      />
26    </mask>
27    <g mask="url(#a)">
28      <path fill="#5B5B66" d="M0 0h8v16H0z" />
29    </g>
30  </svg>
31</button>
32
33css .os-default{ background-color: transparent; border: none; padding: 0; color:
34var(--main-color); display: flex; justify-content: center; align-items: center;
35width: 20px; height: 20px; } .os-default-active{ background-color: transparent;
36border: 1px dotted var(--main-color); padding: 0px; color: var(--main-color);
37display: flex; justify-content: center; align-items: center; width: 20px;
38height: 20px; }

2、仓库添加属性

1export const useGlobalStore = defineStore({
2  id: "dy-global",
3  state: () => ({
4    // 深色模式
5    isDark: false,
6+    // 系统颜色是否被激活
7+    osThemeActive: false
8  }),
9  getters: {},
10  actions: {
11    // Set GlobalState
12    setGlobalState(...args: ObjToKeyValArray<GlobalState>) {
13      this.$patch({ [args[0]]: args[1] });
14    }
15  },
16  persist: piniaPersistConfig("dy-global")
17});

添加了属性,就必须修改 GlobalState 的 ts 配置

1export interface GlobalState {
2  isDark: boolean;
3+  osThemeActive: boolean;
4}
5export type ObjToKeyValArray<T> = {
6  [K in keyof T]: [K, T[K]];
7}[keyof T];

3、点击修改 css 样式

1import { useTheme } from '@/hooks/useTheme'
2import { useGlobalStore } from "@/stores/modules/global";
3
4const globalStore = useGlobalStore();
5
6const {initTheme,switchTheme,activeOSTheme} = useTheme();
7
8initTheme();
9
10const modeSwitch = () => {
11  switchTheme()
12}
13
14const osThemeSwitch = () => {
15  activeOSTheme();
16}
17
18//html
19<button :class="globalStore.osThemeActive ? 'os-default-active' : 'os-default'">
20  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" role="img" @click="osThemeSwitch">
21	......省略
22  </svg>
23</button>

4、修改 useTheme.ts

1import { useGlobalStore } from "@/stores/modules/global";
2import { storeToRefs } from "pinia";
3import { watchEffect } from "vue";
4
5const media = window.matchMedia('(prefers-color-scheme: dark)');
6
7export const useTheme = () => {
8  const globalStore = useGlobalStore();
9  const { isDark,osThemeActive } = storeToRefs(globalStore);
10
11  const switchTheme = (init?: Boolean) => {
12    //init参数为true时,表示是初始化页面或者直接跟随系统主题
13    //这个时候不需要手动切换明暗主题,也不需要将跟随系统主题设置为false
14    if (!init) {
15      globalStore.setGlobalState("isDark", !isDark.value);
16      //如果是直接点击切换明暗主题,那么跟随系统主题激活切换为false
17      globalStore.setGlobalState("osThemeActive", false);
18    }
19    const html = document.documentElement as HTMLElement;
20
21    if (isDark.value) html.setAttribute("class", "dark");
22    else html.setAttribute("class", "primary");
23  }
24
25
26  //点击激活需要改变osThemeActive的值
27  const activeOSTheme = () => {
28    globalStore.setGlobalState("osThemeActive", !osThemeActive.value);
29    followSystem();
30  }
31
32  const followSystem = () => {
33    const isOsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
34    //跟随系统主题设置明暗
35    globalStore.setGlobalState("isDark", isOsDark);
36    switchTheme(true);
37  }
38
39  watchEffect(() => {
40    //系统主题切换,需要监听change事件
41    if (osThemeActive.value) {
42      media.addEventListener('change', followSystem);
43    }
44    else {
45      media.removeEventListener('change', followSystem);
46    }
47  })
48
49  const initTheme = () => {
50    switchTheme(true);
51  };
52
53  return {
54    switchTheme,
55    initTheme,
56    activeOSTheme
57  }
58}

示例源码可按本文代码片段自行整理到项目中。