Element-Plus主题切换
基于 Vue3 + TypeScript + Scss + Element-plus 实现 Element-plus 主题切换。
Element-plus 主题切换涉及了 实验性:的 Web API 和 CSS伪元素,因此在一些个别的浏览器和老版本的浏览器上无法实现该动画效果。
Web API:View Transitions API
View Transitions API 提供了一种机制,可以在更新 DOM 内容的同时,轻松地创建不同 DOM 状态之间的动画过渡。同时还可以在单个步骤中更新 DOM 内容。该API的startViewTransition()方法开始一个新的视图过渡
CSS伪元素:::view-transition-image-pair、::view-transition-new、::view-transition-old
::view-transition-image-pair: 表示一个视图过渡的旧视图状态和新视图状态的容器——即过渡前和过渡后的状态。
::view-transition-new: 表示视图过渡的新视图状态——即过渡后新视图的实时表示。
::view-transition-old: 表示视图过渡的旧视图状态——即过渡前旧视图的静态屏幕截图。
html
<script setup lang="ts">
import {Moon, Sunny} from "@element-plus/icons-vue";
// 引入切换主题的hooks
import {theme, toggleTheme} from "@/utils/useTheme";
import type {Theme} from "@/utils/useTheme";
defineOptions({
name: 'ThemeSwitch',
})
const themeValue = ref<Theme>()
const switchRef = ref<HTMLElement>()
watchEffect((): void => {
themeValue.value = theme.value
});
function changeSwitch(val: Theme) {
if (!switchRef.value) return
// 获取按钮的坐标位置
const {offsetLeft: x, offsetHeight: y} = switchRef.value as any;
toggleTheme(val, x, y)
}
</script>
<template>
<div ref="switchRef">
<el-switch
v-model="themeValue"
active-value="dark"
inactive-value="light"
:active-action-icon="Moon"
:inactive-action-icon="Sunny"
style="--el-switch-on-color: #2c2c2c; --el-switch-off-color: #f2f2f2"
@change="changeSwitch"
/>
</div>
</template>
<style scoped lang="scss">
</style>
scss
:root {
...
// 关闭默认的 CSS 动画并防止新旧视图状态以任何方式混合(新状态从旧状态上方“擦除”,而不是过渡)
&::view-transition-image-pair(root) {
isolation: auto;
}
&::view-transition-old(root),
&::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
display: block;
}
}
// 亮色主题
:root {
...
}
// 暗色主题
html[data-theme='dark'] {
...
// 设置旧页面视图的屏幕截图层级
&::view-transition-old(*) {
z-index: 999;
}
}
ts
export type Theme = "light" | "dark";
const LOCAL_KEY: string = "__theme__";
// 检查用户是否偏好深色主题
const darkModeMediaQuery: MediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
// 初始化主题
export const theme = ref<Theme>((localStorage.getItem(LOCAL_KEY) as Theme) || (darkModeMediaQuery.matches ? 'dark' : 'light'));
// 监听主题变化并存储
watchEffect((): void => {
localStorage.setItem(LOCAL_KEY, theme.value);
document.documentElement.dataset.theme = theme.value;
});
// 监听系统主题变化
darkModeMediaQuery.addEventListener('change', (): void => {
setTheme(darkModeMediaQuery.matches ? 'dark' : 'light')
});
// 设置主题
function setTheme(value: Theme): void {
theme.value = value;
}
// 切换主题
export function toggleTheme(value: Theme, x: number, y: number): void {
// 浏览器不支持 View Transitions 时的回退方案
if (!(document as any).startViewTransition) {
setTheme(value);
return
}
// 获取屏幕对角线
const getDiagonal: number = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y));
// 绘画路径
const clipPath: string[] = [`circle(0% at ${x}px ${y}px)`, `circle(${getDiagonal}px at ${x}px ${y}px)`];
// 开始一次视图过渡:
(document as any).startViewTransition((): void => {
setTheme(value);
}).ready.then((): void => {
// 根据不同的主题切换时,动画的方向将发生改变
document.documentElement.animate({clipPath: value === 'dark' ? clipPath.reverse() : clipPath}, {
// 持续时间
duration: 400,
// 指定附加动画的伪元素
pseudoElement: `::view-transition-${value === 'dark' ? 'old' : 'new'}(root)`,
})
})
}