📅 我们继续 50 个小项目挑战!—— ButtonRippleEffect 组件
仓库地址:https://gitee.com/hhm-hhm/50days50projects.git
构建一个带有点击波纹动画效果的按钮组件。这个按钮在点击时会从点击位置生成一个扩散的“涟漪”动画,增强用户交互体验。
🌀 组件目标
- 创建一个具有点击波纹动画的按钮
- 波纹动画从点击位置开始扩散
- 支持多个连续点击的波纹动画
- 使用 TailwindCSS快速构建现代 UI 界面
🔧 ButtonRippleEffect.tsx组件实现
import React, { useState, useRef } from 'react' const ButtonRippleEffect: React.FC = () => { const [ripples, setRipples] = useState<{ x: number; y: number; size: number }[]>([]) const buttonRef = useRef<HTMLButtonElement>(null) const addRipple = (event: React.MouseEvent) => { const button = buttonRef.current if (!button) return const rect = button.getBoundingClientRect() const x = event.clientX - rect.left const y = event.clientY - rect.top const size = Math.max(button.offsetWidth, button.offsetHeight) setRipples((prevState) => [...prevState, { x, y, size }]) // 600ms 后移除波纹效果,与动画持续时间匹配 setTimeout(() => { setRipples((prevState) => prevState.slice(1)) }, 600) } return ( <div className="flex h-screen items-center justify-center bg-gray-900"> <button ref={buttonRef} className="relative h-20 w-48 cursor-pointer overflow-hidden rounded-lg bg-blue-500 font-bold text-white transition-colors hover:bg-blue-600 active:bg-blue-700" onMouseDown={addRipple}> Click Me {ripples.map((ripple, index) => ( <span key={index} style={{ left: ripple.x + 'px', top: ripple.y + 'px', width: ripple.size + 'px', height: ripple.size + 'px', }} className="animate-ripple pointer-events-none absolute rounded-full bg-white/50 opacity-0"></span> ))} </button> <div className="fixed right-20 bottom-5 text-2xl text-red-500">CSDN@Hao_Harrision</div> </div> ) } export default ButtonRippleEffect🦄样式动画
assets/style.css
... @keyframes ripple { 0% { transform: translate(-50%, -50%) scale(0); opacity: 1; } 100% { transform: translate(-50%, -50%) scale(1.5); /* 根据需要调整scale大小 */ opacity: 0; } } .animate-ripple { animation: ripple 0.6s ease-out; /* 确保这个值与setTimeout中的延迟相匹配 */ }🔧 转换说明
| 功能 / 概念 | Vue 3 (Composition API) | React (TS + Hooks) |
|---|---|---|
| 响应式状态 | const count = ref(0) | const [count, setCount] = useState(0) |
| 事件处理 | @click="handler" | onClick={handler} |
| 模板循环渲染 | <div v-for="(item, i) in list" :key="i"> | {list.map((item, i) => <div key={i}>...)} |
| 动态内联样式 | :style="{ left: x + 'px' }" | style={{ left:${x}px}} |
| 动态 class | :class="[isActive ? 'active' : '']" | className={${isActive ? 'active' : ''}}或使用clsx/classnames |
| 生命周期(挂载) | onMounted(() => { ... }) | useEffect(() => { ... }, []) |
| 副作用清理 | onUnmounted(() => { ... }) | useEffect(() => () => { ... }, []) |
| 引用 DOM 元素 | const el = ref(null)+ref="el" | const elRef = useRef<HTMLButtonElement>(null)+ref={elRef} |
| 监听原生事件 | @mousedown="handler" | onMouseDown={handler} |
| 动画类名 | 手动绑定.animate-ripple | 同样使用className="animate-ripple",配合 CSS |
| CSS scoped 样式 | <style scoped> | 单独.css文件 + 手动引入,或使用 CSS Modules /styled-components |
⚠️ 常见陷阱与注意事项
| 问题 | 解决方案 |
|---|---|
NodeJS.Timeout报错 | 浏览器中setInterval返回number,应使用useRef<number | null> |
setState in useEffect警告 | 首次初始化时调用setState是合理的,可加// eslint-disable-next-line react-hooks/set-state-in-effect忽略 |
| 动态定位偏移 | 使用rotate + translateY + rotate(-angle)时,transformOrigin: 'center'是关键;避免直接用translate(x,y)除非加上中心偏移 |
| Tailwind 不生效 | 确保tailwind.config.js正确配置content包含你的组件路径 |
| TypeScript 类型安全 | 明确标注状态类型,如useState<{x: number, y: number}[]>([]) |
✅ 最佳实践建议
- 状态合并:多个相关状态(如日期+时间)可合并为一个格式化字符串,减少重渲染。
- 定时器管理:始终用
useRef存储setIntervalID,并在useEffect清理函数中清除。 - 事件坐标计算:使用
getBoundingClientRect()获取按钮位置,再计算点击点相对坐标。 - 动画同步:
setTimeout清除 ripple 的时间必须 ≥ CSS 动画持续时间。 - 避免内联回调:若性能敏感,可用
useCallback包裹事件处理器(本例中非必需)。
🎨 TailwindCSS 样式重点讲解
| 类名 | 作用 |
|---|---|
h-screen,items-center,justify-center | 设置全屏高度并垂直居中布局 |
bg-gray-900 | 设置深色背景 |
relative | 为按钮设置相对定位,方便内部绝对定位的波纹元素 |
h-20,w-48 | 设置按钮高度和宽度 |
rounded-lg | 圆角按钮 |
bg-blue-500,hover:bg-blue-600,active:bg-blue-700 | 按钮颜色及悬停、激活状态下的变色效果 |
font-bold,text-white | 文字加粗和白色字体 |
transition-colors | 添加颜色变化的过渡动画 |
absolute,rounded-full,bg-white/50 | 波纹元素的基本样式 |
pointer-events-none | 避免波纹干扰按钮点击事件 |
opacity-0 | 初始隐藏波纹,由动画控制显示 |
这些 Tailwind 工具类快速构建了一个视觉丰富、交互性强的按钮组件。
🦌 路由组件 + 常量定义
router/index.tsx中children数组中添加子路由
{ path: '/', element: <App />, children: [ ... { path: '/ButtonRipple', lazy: () => import('@/projects/ButtonRippleEffect.tsx').then((mod) => ({ Component: mod.default, })), }, ], },constants/index.tsx 添加组件预览常量
import demo20Img from '@/assets/pic-demo/demo-20.png' 省略部分.... export const projectList: ProjectItem[] = [ 省略部分.... { id: 20, title: 'Button Ripple Effect', image: demo20Img, link: 'ButtonRipple', },🚀 小结
扩展的功能推荐:
- 支持不同颜色主题的波纹按钮
- 支持禁用状态下的点击无反应
- 支持自定义波纹颜色或透明度
- 多个按钮共享波纹动画逻辑(封装为可复用组件)
📅 明日预告: 我们将完成DragNDrop组件,一个非常有意思的拖拽组件,可以对元素进行重新排列。🚀
原文链接:https://blog.csdn.net/qq_44808710/article/details/149080035
每天造一个轮子,码力暴涨不是梦!🚀