实现炫酷的主题切换功能

主题切换功能 --派大星的开发日记

个人开发记录笔记,仅个人理解,有错误还请指出

前言

在现代 Web 应用中,主题切换功能几乎成为了标配。为了让用户的体验更上一层楼,加入一些炫酷的过渡动画无疑是一个不错的选择。这篇文章将分享如何结合 Vue3、VueUse、ViewTransition API 和 Web Animations API,实现一个圆形扩散过渡的主题切换功能,类似element-plus的动画,代码简洁,效果丝滑。


效果预览

点击切换主题时,会有一个圆形扩散的动画, 跟随系统不执行动画,页面的主题切换过程更加自然流畅(给不懂技术的领导看效果更佳)。


用到的技术点

在正式进入代码实现之前,先详细介绍一下我们所用到的几个关键技术及其用途:

1. VueUse 的 useColorMode

  • 功能 :VueUse 是一个 Vue 的工具库,其中的 useColorMode 是专门用来管理主题模式的。
  • 特点
    • 可以轻松管理亮色、暗色和自动主题。
    • 自动监听系统主题模式(比如系统从亮色切换到暗色时,应用会自动切换)。
  • 作用:简化主题切换的逻辑,减少我们对主题状态的手动管理。

2. ViewTransition API

  • 功能:这是浏览器的新特性,允许在页面视图变化时添加平滑过渡动画。
  • 特点
    • 能够捕获页面更新前后的 DOM 状态。
    • 配合动画,能让页面切换过程更加平滑和自然。
  • 作用:在主题切换时,配合动画实现圆形扩散效果。

3. Web Animations API

  • 功能:提供了 JavaScript 操控动画的能力,比传统的 CSS 动画更加灵活。
  • 特点
    • 可以动态控制动画的属性(如位置、时间等)。
    • 支持暂停、继续等操作。
  • 作用:用来实现动画的细节,比如圆形扩散的路径和覆盖范围。

4. 几何计算

  • 功能:通过数学公式,动态计算动画的覆盖范围。
  • 公式
    动画的扩散半径需要满足覆盖整个屏幕,可以用以下公式计算:
    radius = √((最大横向距离)² + (最大纵向距离)²)
  • 作用:确保动画能够从点击点扩散到屏幕边缘,避免动画不完整。

代码思路解析

在了解了用到的技术之后,我们再来看实现的逻辑步骤。以下是具体的代码及其详细中文注释,让你快速掌握其中的精髓。

vue 复制代码
<script setup>
import { ref, computed, onMounted } from "vue";
import { useColorMode } from "@vueuse/core";
const { system, store } = useColorMode();
// 计算属性,动态获取设置的 主题色模式
const myColorMode = computed(() =>
  store.value === "auto" ? system.value : store.value
);
let themeSelected = ref(
  store.value == "light" ? 1 : store.value == "dark" ? 2 : 3
);

const changeTheme = (val, e, id) => {
  themeSelected.value = id;
  if (store.value == val) return;

  if (val === "auto") {
    document.startViewTransition(() => {
      store.value = val;
      document.documentElement.classList.remove("light", "dark");
      document.documentElement.classList.add(system.value); // 使用系统当前主题
    });
    return;
  }

  let transition = document.startViewTransition(() => {
    store.value = val;
    document.documentElement.classList.remove("light", "dark");
    document.documentElement.classList.add(val);
  });

  transition.ready.then(() => {
    // 获取选中元素的位置信息
    const x = e.clientX;
    const y = e.clientY;

    // 获取画圆的半径
    const radius = Math.sqrt(
      Math.max(x, window.innerWidth - x) ** 2 +
        Math.max(y, window.innerHeight - y) ** 2
    ); // 勾股定理

    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${radius}px at ${x}px ${y}px)`,
        ],
      }, // 在坐标x,y处从半径为0的圆变为半径为radius的圆
      {
        duration: 500,
        pseudoElement: "::view-transition-new(root)",
      }
    );
  });
};
let themeList = ref([
  {
    id: 1,
    name: "亮色主题",
    click: (e) => changeTheme("light", e, 1),
  },
  {
    id: 2,
    name: "暗色主题",
    click: (e) => changeTheme("dark", e, 2),
  },
  {
    id: 3,
    name: "将主题与电脑同步",
    click: (e) => changeTheme("auto", e, 3),
  },
]);
</script>

<template>
  <div class="interface-wp">
    <div class="theme-box">
      <div class="theme-title">界面主题</div>
      <div
        class="theme-item"
        v-for="item in themeList"
        :key="item.id"
        v-wave="{
          color: 'var(--animation-color)',
          duration: 0.3,
        }"
        @click="item.click"
      >
        <div
          class="theme-item-box"
          :class="{ active: themeSelected == item.id }"
        >
          <article></article>
        </div>
        <div class="theme-item-name">{{ item.name }}</div>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.interface-wp {
  padding-top: 80px;
  box-sizing: border-box;
  .theme-box {
    padding: 20px;
    border-radius: 10px;
    box-sizing: border-box;
    .theme-title {
      font-weight: 600;
    }
    .theme-item {
      margin-top: 10px;
      display: flex;
      align-items: center;
      height: 40px;
      line-height: 40px;
      background: var(--setting-menu-item-bg);
      color: var(--setting-menu-item-active-color);
      border-radius: 10px;
      padding: 0 10px;
      box-sizing: border-box;
      cursor: pointer;
      &:hover {
        background: var(--setting-menu-item-hover-bg);
        & .theme-item-box article {
          background: #bcc2cc;
        }
      }
      .theme-item-box {
        width: 20px;
        height: 20px;
        border-radius: 5px;
        border: 1px solid #586274;

        background: #fff;
        padding: 2px;
        box-sizing: border-box;
        &.active {
          article {
            background: #ffbc00;
          }
        }
        article {
          width: 100%;
          height: 100%;
          background: #fff;
          border-radius: 2px;
        }
      }
      .theme-item-name {
        margin-left: 10px;
        font-size: 14px;
      }
    }
  }
}
</style>
<style>
:root {
  --theme-color: #000;
  --bg-color: #fff;
  background-color: var(--bg-color);
  color: var(--theme-color);
}

:root.dark {
  --bg-color: #303030;
  --theme-color: #fff;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
}
</style>

总结

通过结合 VueUse 的 useColorMode、ViewTransition API 和 Web Animations API,我们不仅实现了主题切换功能,还为用户提供了一个更加炫酷、流畅的体验。希望这篇文章能为你在实际开发中提供帮助!

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax