sass 实现多主题切换

396 阅读3分钟

sass 实现多主题切换

新建文件以存储各主题对应的样式

//  new-simulation.scss  文件内容如下:(此文件用于存储 new-simulation 主题特有的样式)
$ns-main-color: #ecf0f3;

$new-simulation: (
    main-color: $ns-main-color,
    inset-bg-color: #f0f0f0,
);
//  primary.scss  文件内容如下:(此文件用于存储 primary.scss 主题特有的样式)
$ns-main-color: #505050;

$primary: (
    main-color: $ns-main-color,
    bg-color: #f0f0f0,
);

如上所示,每个主题都放在不一样的文件中,每新增一个主题就可以通过新建文件来实现

新建 theme.scss 文件用于存储与切换主题相关的函数

//  theme.scss  文件内容如下:(此文件用于封装生成各类主题样式的函数,后续开发只需要使用 themeify 和 themed 包裹就可以自动匹配对应主题的样式)

// 引入各主题样式
@import './primary.scss';
@import './new-simulation.scss';

$themes:(
	primary: $primary,
	new-simulation: $new-simulation
);
$current-theme-map: 'primary';
@mixin themeify {
	@each $theme-name, $theme-map in $themes {
		$current-theme-map: $theme-map !global;
        
        // 这里需要根据自己绑定 的主题样式名进行修改
        // 本示例是通过在 body 上增加 data-theme 属性来区分当前是哪个主题
		body[data-theme="#{$theme-name}"] & {
			@content;
		}
	}
}
@function themed($key) {
	@return map-get($current-theme-map, $key);
}

使用 themeify

使用示例:

.test {
	padding: 0;
	margin: 0

	@include themeify {
		color: themed('text-color');
	}
}

// 上述代码编译后的结果如下所示:
body[data-theme=primary] .test {
  color: #000000;
}
body[data-theme=new-simulation] .test {
  color: #696868;
}

可以在 style/index.scss 或 根据其它自定义文件 中 引入 themeify.scss 文件,并在 vite.config.ts 中进行配置,这样在项目的其他文件中可以不引入也能正常使用,如下所示:(本示例是 在 style/index.scss 文件中引入 themeify.scss 文件,并在 vite.config.ts 进行预处理)

// vite.config.ts 文件部分内容:
import { defineConfig } from 'vite'

export default defineConfig(({mode}) => {
  return {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@import "@/styles/theme.scss"',
        },
      },
    },
  }
})

封装自定义 hook 以实现通过切换 body 属性来更改当前显示的样式

React :

useTheme.ts 文件内容:
import { StorageKey, ThemeKey } from "@/types/global"
/**
StorageKey 为自定义的enum, 目的只是为了统一管理 localStorage / sessionStorage 的 key 值, 其部分内容如下:
	const enum StorageKey {
        THEME = 'THEME',
    }
ThemeKey 为自定义的enum, 目的只是为了统一管理主题名, 其部分内容如下所示:
    const enum ThemeKey {
        PRIMARY = 'primary',
        NEWSIMULATION = 'new-simulation'
    }
*/
import { useCallback, useEffect, useState } from "react"

// 多主题切换
let isInit: boolean = false
const useTheme = () => {
    const [theme, setTheme] = useState<ThemeKey>(ThemeKey.NEWSIMULATION)

    const toggleTheme = useCallback((theme: ThemeKey) => {
		document.body.setAttribute('data-theme', theme)
        setTheme(theme)
        localStorage.setItem(StorageKey.THEME, theme)
    }, [])

    useEffect(() => {
        if (!isInit) {
            isInit = true
            const theme = localStorage.getItem(StorageKey.THEME) as ThemeKey || ThemeKey.NEWSIMULATION
            toggleTheme(theme)
        }
    }, [])

    return { theme, toggleTheme }
}

export default useTheme
App.tsx 中使用并注册到 App 的上下文中以方便其他组件使用(如果是 vue, 可以挂载到 app 实例上)
// App.tsx 文件内容:
import { Suspense, useEffect, useState } from 'react';
import { useRoutes } from 'react-router-dom';
import routes from '@/router/index.tsx';
import useTheme from './hooks/useTheme';
import { AppProvider } from './AppContenx';

function App() {
	const { theme, toggleTheme } = useTheme();

	return (
		<AppProvider value={{ theme, toggleTheme }}>
			<div>
				<Suspense fallback="">
					<div className="main">{useRoutes(routes)}</div>
				</Suspense>
			</div>
		</AppProvider>
	);
}

export default App;
// ./AppContenx.ts 文件内容:

import { createContext } from "react";
import { StorageKey, ThemeKey } from "./types/global";

// 创建 app 上下文
export interface AppContextProps {
    theme: ThemeKey;
    toggleTheme: (theme: ThemeKey) => void;
}
const AppContext = createContext<AppContextProps>({} as AppContextProps);

export const AppProvider = AppContext.Provider;
export default AppContext;

后续其他页面中如果需要切换主题,可以直接使用 useContext 获取 toggleTheme 即可,如下所示:

import AppContext from '@/AppContenx'

const { toggleTheme } = useContext(AppContext)

vue3

useTheme.ts 文件内容:
import { onMounted, ref } from "vue"

// 多主题切换
let isInit: boolean = false

export enum ThemeKey {
    PRIMARY = 'primary',
    NEWSIMULATION = 'new-simulation'
}
export enum StorageKey {
    THEME = 'THEME',
}

export const useTheme = () => {
    const theme = ref<ThemeKey>(ThemeKey.NEWSIMULATION)
    const toggleTheme = (_theme: ThemeKey) => {
		document.body.setAttribute('data-theme', _theme)
        theme.value = _theme
        localStorage.setItem(StorageKey.THEME, _theme)
    }

    onMounted(() => {
        if (!isInit) {
            isInit = true
            const _theme = localStorage.getItem(StorageKey.THEME) as ThemeKey || ThemeKey.NEWSIMULATION
            toggleTheme(_theme)
        }
    })

    return { theme, toggleTheme }
}
使用 useTheme 实现点击按钮切换主题功能
<template>
    <div class="layout">
         <button @click.prevent="changeTheme(ThemeKey.NEWSIMULATION)">newSimulation</button>
         <button @click.prevent="changeTheme(ThemeKey.PRIMARY)">primary</button>
    </div>
</template>

<script setup lang="ts">
import { ThemeKey, useTheme } from '@/hooks/useTheme';

const changeTheme = (type: ThemeKey) => {
    toggleTheme(type)
}
</script>