实现炫酷的主题切换功能

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

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

前言

在现代 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,我们不仅实现了主题切换功能,还为用户提供了一个更加炫酷、流畅的体验。希望这篇文章能为你在实际开发中提供帮助!

相关推荐
小爬菜6 分钟前
Django学习笔记(项目默认文件)-02
前端·数据库·笔记·python·学习·django
장숙혜9 分钟前
JavaScript正则表达式解析:模式、方法与实战案例
开发语言·javascript·正则表达式
Channing Lewis1 小时前
如何实现网页不用刷新也能更新
前端
努力搬砖的程序媛儿2 小时前
uniapp广告飘窗
前端·javascript·uni-app
dfh00l3 小时前
firefox屏蔽debugger()
前端·firefox
张人玉3 小时前
小白误入(需要一定的vue基础 )使用node建立服务器——vue前端登录注册页面连接到数据库
服务器·前端·vue.js
大大。3 小时前
element el-table合并单元格
前端·javascript·vue.js
一纸忘忧3 小时前
Bun 1.2 版本重磅更新,带来全方位升级体验
前端·javascript·node.js
杨.某某3 小时前
若依 v-hasPermi 自定义指令失效场景
前端·javascript·vue.js