Element-Plus主题切换

Element-Plus主题切换

基于 Vue3 + TypeScript + Scss + Element-plus 实现 Element-plus 主题切换。

Element-plus 主题切换涉及了 实验性:Web APICSS伪元素,因此在一些个别的浏览器和老版本的浏览器上无法实现该动画效果。

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)`,
    })
  })
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值