前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。

文章目录
- [新手入门:实现聚焦式主题切换动效(Vue3 + Pinia + View Transitions)](#新手入门:实现聚焦式主题切换动效(Vue3 + Pinia + View Transitions))
-
- 一、功能介绍
-
- 核心依赖技术
-
- [1. 核心 API:View Transitions API(视图过渡 API)](#1. 核心 API:View Transitions API(视图过渡 API))
- [2. 核心 CSS 技术:`clip-path` 裁剪路径](#2. 核心 CSS 技术:
clip-path裁剪路径) - [3. 辅助技术:CSS 伪元素(`::view-transition-new`/`::view-transition-old`)](#3. 辅助技术:CSS 伪元素(
::view-transition-new/::view-transition-old))
- 二、前置准备
-
- [1. 技术栈要求](#1. 技术栈要求)
- [2. 环境搭建](#2. 环境搭建)
- 三、分步实现(新手友好)
-
- 第一步:创建主题状态管理(Pinia)
- [第二步:创建主题样式文件(CSS 变量)](#第二步:创建主题样式文件(CSS 变量))
- 第三步:实现主题切换按钮(页面组件)
- 第四步:补充说明(图标与兼容性)
- 四、核心原理拆解(新手必懂)
-
- [1. 状态管理(Pinia)](#1. 状态管理(Pinia))
- [2. 动画核心(View Transitions API)](#2. 动画核心(View Transitions API))
- [3. 主题切换逻辑](#3. 主题切换逻辑)
- 五、效果演示
- 六、常见问题排查
- 七、扩展功能(可选)
新手入门:实现聚焦式主题切换动效(Vue3 + Pinia + View Transitions)
一、功能介绍
我们要实现的是「聚焦式主题切换动效」------ 点击主题切换按钮时,以点击位置为中心,像水波纹一样扩散 / 收缩切换亮 / 暗两种主题,同时兼容不支持新 API 的浏览器,确保功能稳定。
核心亮点:
- 动画跟随点击位置,交互感强
- 基于原生 View Transitions API,性能优异
- 用 Pinia 管理主题状态,全局响应
- 支持主题持久化(刷新不丢失)
- 完善的降级方案,兼容性好
核心依赖技术
1. 核心 API:View Transitions API(视图过渡 API)
- 定位:Web 原生 API(Chrome 111+、Edge 111+、Opera 97+ 支持),用于在 DOM 状态变化(如主题切换、页面跳转)时创建 "新旧状态快照" 的过渡动画。
- 作用:替代传统的 "手动隐藏 / 显示元素""添加过渡类" 等繁琐逻辑,浏览器自动处理快照捕获、动画过渡、快照销毁,简化开发。
2. 核心 CSS 技术:clip-path 裁剪路径
- 定位:CSS 基础属性,用于 "裁剪" 元素的可见区域,仅显示指定路径内的内容。
- 作用 :实现 "圆形扩散 / 收缩" 效果的核心 ------ 通过
circle()函数定义圆形裁剪区域,动态修改圆形的半径和中心点,模拟 "从点击位置扩散 / 收缩" 的视觉效果。
3. 辅助技术:CSS 伪元素(::view-transition-new/::view-transition-old)
-
定位:View Transitions API 自动生成的 "快照伪元素",用于承载新旧状态的 DOM 快照。
-
作用:
::view-transition-old(root):承载 "旧状态" 的快照(如切换前的亮色主题);::view-transition-new(root):承载 "新状态" 的快照(如切换后的暗色主题);- 开发者通过控制这两个伪元素的层级、透明度、动画,实现复杂过渡效果。
二、前置准备
1. 技术栈要求
- Vue3(Composition API +
<script setup>) - Pinia(状态管理)
- CSS 变量(主题配色)
- 可选:Element Plus(UI 组件)、IconFont(图标)
2. 环境搭建
如果是新项目,先初始化 Vue3 项目并安装依赖:
bash
# 创建Vue3项目
npm create vue@latest theme-demo
cd theme-demo
npm install
# 安装必要依赖
npm install pinia # 状态管理
npm install element-plus # 可选,UI组件
三、分步实现(新手友好)
第一步:创建主题状态管理(Pinia)
首先创建 Pinia 的主题模块,负责主题的切换、存储、初始化。
- 在
src/store/modules/目录下新建theme.ts(如果没有modules文件夹就手动创建):
typescript
// src/store/modules/theme.ts
import { defineStore } from 'pinia'
// 定义主题类型:仅支持light/dark两种
type ThemeType = 'light' | 'dark'
// 创建并导出主题Store
export const useThemeStore = defineStore('theme', {
state: () => ({
currentTheme: 'light' as ThemeType, // 默认亮色主题
isChanging: false, // 标记是否正在切换主题(防止重复点击)
clickPosition: { x: 0, y: 0 }, // 记录点击坐标(用于动画起始位置)
}),
actions: {
// 1. 记录点击位置(点击主题按钮时调用)
setClickPosition(x: number, y: number) {
this.clickPosition = { x, y }
},
// 2. 核心:切换主题并触发动画
setTheme(theme: ThemeType) {
// 如果当前就是目标主题,直接返回(避免重复切换)
if (this.currentTheme === theme) return
this.isChanging = true // 标记开始切换
// 检查浏览器是否支持View Transitions API(现代浏览器支持)
if (typeof document !== 'undefined' && document.startViewTransition) {
// 启动原生过渡动画
const transition = document.startViewTransition(() => {
// 动画期间执行:更新主题、存储到本地、同步根元素属性
this.currentTheme = theme
this.syncThemeToStorage(theme)
this.updateRootElementTheme(theme)
})
// 动画准备就绪后,添加聚焦扩散/收缩效果
transition.ready.then(() => {
const { x, y } = this.clickPosition
// 计算从点击点到屏幕对角的最大距离(确保动画覆盖全屏)
const radius = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y)
)
// 裁剪路径:从0px扩散到最大半径(或反向收缩)
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${radius}px at ${x}px ${y}px)`
]
// 暗色主题:新主题扩散;亮色主题:旧主题收缩
// 变暗时使用扩散动画,变亮时使用收缩动画
const isDark = theme === 'dark'
const pseudoElementSelector = isDark
? '::view-transition-new(root)' // 暗色主题用新元素扩散
: '::view-transition-old(root)' // 亮色主题用旧元素收缩
// 动画关键帧:扩散或收缩
const keyframes = isDark
? [{ clipPath: clipPath[0] }, { clipPath: clipPath[1] }] // 扩散
: [{ clipPath: clipPath[1] }, { clipPath: clipPath[0] }] // 收缩
// 👇 修改后代码(翻转逻辑)
// 变暗时使用收缩动画,变亮时使用扩散动画(注意:需要跟css配合才能做到真正的切换)
// const isDark = theme === 'dark'
// const pseudoElementSelector = isDark
// ? '::view-transition-old(root)' // 暗色用旧元素收缩
// : '::view-transition-new(root)' // 亮色用新元素扩散
// const keyframes = isDark
// ? [{ clipPath: clipPath[1] }, { clipPath: clipPath[0] }] // 暗色:收缩(最大半径→0)
// : [{ clipPath: clipPath[0] }, { clipPath: clipPath[1] }] // 亮色:扩散(0→最大半径)
// 执行动画
document.documentElement.animate(keyframes, {
duration: 600, // 动画时长600ms
easing: 'cubic-bezier(0.4, 0, 0.2, 1)', // 缓动效果(自然过渡)
fill: 'both', // 保持动画结束状态
pseudoElement: pseudoElementSelector // 作用于过渡伪元素
})
}).catch(err => {
console.error('动画准备失败:', err)
})
// 动画结束后,重置切换状态
transition.finished.then(() => {
this.isChanging = false
}).catch(err => {
console.error('动画执行失败:', err)
this.isChanging = false // 即使失败也重置状态
})
} else {
// 浏览器不支持View Transitions:降级方案(直接切换无动画)
console.log('浏览器不支持,使用降级方案')
this.currentTheme = theme
this.syncThemeToStorage(theme)
this.updateRootElementTheme(theme)
this.isChanging = false
}
},
// 3. 辅助:将主题存储到本地(刷新不丢失)
syncThemeToStorage(theme: ThemeType) {
try {
// uni-app用uni.setStorageSync,普通Vue用localStorage
if (typeof uni !== 'undefined') {
uni.setStorageSync('app_theme', theme)
} else {
localStorage.setItem('app_theme', theme)
}
} catch (e) {
console.error('主题存储失败:', e)
}
},
// 4. 辅助:更新根元素的data-theme属性(用于CSS主题切换)
updateRootElementTheme(theme: ThemeType) {
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-theme', theme)
}
},
// 5. 初始化主题(页面加载时调用)
initTheme() {
try {
// 从本地存储读取主题
let storedTheme: ThemeType = 'light'
if (typeof uni !== 'undefined') {
storedTheme = uni.getStorageSync('app_theme') as ThemeType
} else {
storedTheme = localStorage.getItem('app_theme') as ThemeType
}
// 验证存储的主题是否合法,合法则使用,否则默认亮色
if (['light', 'dark'].includes(storedTheme)) {
this.currentTheme = storedTheme
this.updateRootElementTheme(storedTheme)
} else {
this.currentTheme = 'light'
this.syncThemeToStorage('light')
this.updateRootElementTheme('light')
}
} catch (e) {
console.error('主题初始化失败:', e)
this.currentTheme = 'light' // 异常时默认亮色
this.syncThemeToStorage('light')
this.updateRootElementTheme('light')
}
}
}
})
- 初始化 Pinia:在
src/main.ts中引入并使用 Pinia:
typescript
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 引入Pinia
import App from './App.vue'
import './styles/theme.css' // 后续会创建的主题样式文件
const app = createApp(App)
app.use(createPinia()) // 使用Pinia
app.mount('#app')
第二步:创建主题样式文件(CSS 变量)
创建全局主题样式,用 CSS 变量定义亮 / 暗两种主题的配色,配合 View Transitions 实现动画。
在src/styles/目录下新建theme.css:
css
/* src/styles/theme.css */
/* 1. 关键:启用View Transitions快照容器(必须) */
:root {
view-transition-name: root; /* 指定根元素为过渡快照目标 */
}
/* 2. 配置过渡伪元素基础样式(避免动画异常) */
::view-transition-new(root),
::view-transition-old(root) {
animation: none !important; /* 禁用默认动画,用自定义裁剪 */
mix-blend-mode: normal; /* 关闭颜色混合,避免错乱 */
opacity: 1 !important; /* 强制不透明 */
width: 100vw !important; /* 占满全屏 */
height: 100vh !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
}
/* 3. 主题层级控制(确保动画方向正确) */
/* 变暗时使用扩散动画,变亮时使用收缩动画 */
/* 暗色主题:新主题(深底)在上扩散,旧主题(白底)在下 */
:root[data-theme="dark"]::view-transition-new(root) {
z-index: 100 !important;
background-color: #121212 !important;
}
:root[data-theme="dark"]::view-transition-old(root) {
z-index: 99 !important;
background-color: #ffffff !important;
}
/* 亮色主题:旧主题(深底)在上收缩,新主题(白底)在下 */
:root[data-theme="light"]::view-transition-old(root) {
z-index: 100 !important;
background-color: #121212 !important;
}
:root[data-theme="light"]::view-transition-new(root) {
z-index: 99 !important;
background-color: #ffffff !important;
}
/* 👇 修改后代码(翻转层级) */
/* 变暗时使用收缩动画,变亮时使用扩散动画 */
/* 暗色主题:旧主题(白底)在上收缩,新主题(深底)在下 */
:root[data-theme="dark"]::view-transition-old(root) {
z-index: 100 !important; /* 旧主题(亮色)在上,收缩消失 */
background-color: #ffffff !important;
}
:root[data-theme="dark"]::view-transition-new(root) {
z-index: 99 !important; /* 新主题(暗色)在下,等待露出 */
background-color: #121212 !important;
}
/* 亮色主题:新主题(白底)在上扩散,旧主题(深底)在下 */
:root[data-theme="light"]::view-transition-new(root) {
z-index: 100 !important; /* 新主题(亮色)在上,扩散覆盖 */
background-color: #ffffff !important;
}
:root[data-theme="light"]::view-transition-old(root) {
z-index: 99 !important; /* 旧主题(暗色)在下,被覆盖 */
background-color: #121212 !important;
}
/* 4. 亮色主题配色(默认) */
:root {
/* 主题色(绿色系) */
--primary-color: #43de7e;
--primary-hover: #36eb9d;
--primary-active: #2fc46d;
/* 背景色 */
--bg-main: #ffffff;
--bg-sidebar: #f0f9f2;
--bg-menu-hover: #e6f7ed;
/* 文字色 */
--text-main: #2d3748;
--text-secondary: #4a5568;
/* 顶部栏背景 */
--header-bg: #f0f9f2;
/* 覆盖Element Plus默认样式(可选) */
--el-color-primary: var(--primary-color);
--el-bg-color: var(--bg-main);
--el-text-color-primary: var(--text-main);
}
/* 5. 暗色主题配色 */
:root[data-theme="dark"] {
--primary-color: #43de7e; /* 保持主题色不变,更突出 */
--primary-hover: #36eb9d;
--primary-active: #2fc46d;
--bg-main: #121212;
--bg-sidebar: #16251f;
--bg-menu-hover: #23372b;
--text-main: #f0fdf4;
--text-secondary: #d1fae5;
--header-bg: #16251f;
--el-color-primary: var(--primary-color);
--el-bg-color: var(--bg-main);
--el-text-color-primary: var(--text-main);
}
/* 6. 动画过渡(确保主题切换时样式平滑变化) */
:root {
transition:
--primary-color 0.3s ease,
--bg-main 0.3s ease,
--text-main 0.3s ease;
}
第三步:实现主题切换按钮(页面组件)
在需要添加主题切换的页面(比如首页index.vue)中,添加亮 / 暗切换按钮并绑定事件。
vue
<template>
<!-- 主题切换过渡遮罩(优化视觉效果) -->
<div class="theme-transition-mask" :class="{ active: isThemeChanging }"></div>
<!-- 页面布局(仅展示核心部分,其他布局可保留) -->
<div class="header flex items-center justify-between p-4">
<h1>我的应用</h1>
<!-- 主题切换按钮区域 -->
<div class="theme-switcher">
<!-- 亮/暗色切换按钮 -->
<i
v-if="themeStore.currentTheme === 'light'"
class="iconfont icon-anse cursor-pointer"
@click.stop="handleThemeChange($event, 'dark')"
title="切换暗色主题"
></i>
<i
v-else
class="iconfont icon-a-Frame48 cursor-pointer"
@click.stop="handleThemeChange($event, 'light')"
title="切换亮色主题"
></i>
</div>
</div>
</template>
<script setup lang="ts">
// 1. 引入必要依赖
import { ref, watch } from 'vue'
import { useThemeStore } from '@/store/modules/theme'
// 2. 初始化主题Store
const themeStore = useThemeStore()
// 页面加载时初始化主题(从本地存储读取)
themeStore.initTheme()
// 3. 主题切换相关状态
const isThemeChanging = ref(false) // 控制过渡遮罩显示
// 4. 核心:处理主题切换事件
const handleThemeChange = (event: MouseEvent, theme: 'light' | 'dark') => {
// 记录点击坐标(用于动画起始位置)
themeStore.setClickPosition(event.clientX, event.clientY)
// 触发主题切换(Pinia中的setTheme方法)
themeStore.setTheme(theme)
}
// 5. 监听主题变化,显示过渡遮罩
watch(
() => themeStore.currentTheme,
(newTheme, oldTheme) => {
if (newTheme !== oldTheme) {
isThemeChanging.value = true
// 600ms后隐藏遮罩(与动画时长一致)
setTimeout(() => {
isThemeChanging.value = false
}, 600)
}
}
)
</script>
<style scoped>
/* 主题切换过渡遮罩样式 */
.theme-transition-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.1);
opacity: 0;
pointer-events: none; /* 不阻止页面交互 */
transition: opacity 0.6s ease;
z-index: 9999; /* 确保在最上层 */
}
.theme-transition-mask.active {
opacity: 1;
}
/* 按钮基础样式(可根据需求调整) */
.iconfont {
font-size: 20px;
color: var(--text-secondary);
transition: color 0.3s ease;
}
.iconfont:hover {
color: var(--primary-color);
}
</style>
第四步:补充说明(图标与兼容性)
- 图标替换 :示例中用了 IconFont 图标(
icon-anse、icon-a-Frame48),你需要:- 去IconFont选择自己喜欢的亮 / 暗色图标(比如太阳、月亮图标)
- 下载并引入项目,替换图标类名
- 兼容性处理 :
- 现代浏览器(Chrome 115+、Edge 115+、Safari 16.4+)支持 View Transitions API,会显示聚焦动画
- 旧浏览器会自动使用降级方案(直接切换主题,无动画),不影响功能使用
- uni-app 适配 :示例中兼容了 uni-app 的存储 API(
uni.setStorageSync),如果是普通 Vue 项目,会自动使用localStorage,无需额外修改。
四、核心原理拆解(新手必懂)
1. 状态管理(Pinia)
currentTheme:存储当前主题(light/dark)clickPosition:记录点击坐标,让动画从点击点开始isChanging:防止动画期间重复点击
2. 动画核心(View Transitions API)
document.startViewTransition():原生 API,会对当前页面拍快照(::view-transition-old),对新状态拍快照(::view-transition-new)clip-path: circle():通过圆形裁剪路径实现扩散 / 收缩效果- 计算最大半径:确保动画能覆盖整个屏幕
3. 主题切换逻辑
- 点击按钮 → 记录点击坐标
- 调用
setTheme→ 标记isChanging: true - 启动 View Transitions → 切换主题并存储到本地
- 动画就绪 → 执行聚焦扩散 / 收缩动画
- 动画结束 → 重置
isChanging: false
五、效果演示
- 点击亮色主题按钮(太阳图标)→ 以点击点为中心,白色收缩,黑色扩散(切换暗色)
- 点击暗色主题按钮(月亮图标)→ 以点击点为中心,黑色收缩,白色扩散(切换亮色)
六、常见问题排查
- 动画不显示 :
- 检查浏览器版本是否支持 View Transitions API
- 确认
theme.css中是否添加了view-transition-name: root - 检查控制台是否有报错(比如伪元素样式错误)
- 主题切换后样式异常 :
- 确保所有页面样式都使用了 CSS 变量(
var(--bg-main)、var(--text-main)等),而不是固定颜色 - 检查
updateRootElementTheme方法是否正确设置了data-theme属性
- 确保所有页面样式都使用了 CSS 变量(
- 刷新后主题重置 :
- 确认
initTheme方法在页面加载时被调用(示例中在组件 setup 里调用了themeStore.initTheme())
- 确认
七、扩展功能(可选)
- 自定义动画时长:修改
duration: 600(单位 ms),数值越大动画越慢 - 动画缓动效果:修改
easing属性(比如ease、linear、cubic-bezier) - 主题切换 loading:利用
isChanging状态显示 loading 图标,优化等待体验 - 系统主题适配:添加监听系统亮暗主题的逻辑,自动同步(可参考
window.matchMedia('(prefers-color-scheme: dark)'))
通过以上步骤,你就成功实现了仅支持亮 / 暗两种主题的聚焦式切换动效!这个功能不仅交互友好,而且代码结构清晰,新手也能轻松理解和复用~