📅 我们继续 50 个小项目挑战!------DoubleClickHeart 组件
仓库地址:https://gitee.com/hhm-hhm/50days50projects.git

创建一个双击点赞动画组件。用户可以双击图片区域触发一个"❤️ 爱心飞出"动画,并统计点赞次数。
这个交互体验非常适用于社交媒体、照片墙、内容点赞等场景。
🌀 组件目标
- 用户双击图片区域时显示爱心动画
- 显示当前点赞总次数
- 动画结束后自动移除爱心元素
- 使用
TailwindCSS快速构建 UI 样式 - 持动态定位和点击时间判断
🔧 DoubleClickHeart.tsx组件实现
TypeScript
import React, { useState, useRef, useEffect } from 'react'
interface Heart {
id: number
x: number
y: number
}
const DoubleClickHeart: React.FC = () => {
const [likes, setLikes] = useState<number>(0)
const [hearts, setHearts] = useState<Heart[]>([])
const containerRef = useRef<HTMLDivElement>(null)
const clickTimeRef = useRef<number>(0)
const imageUrl =
'https://images.unsplash.com/photo-1504215680853-026ed2a45def?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=334&q=80'
const handleClick = (e: React.MouseEvent) => {
// eslint-disable-next-line
const now = Date.now()
if (clickTimeRef.current === 0) {
clickTimeRef.current = now
} else {
if (now - clickTimeRef.current < 800) {
createHeart(e)
clickTimeRef.current = 0
} else {
clickTimeRef.current = now
}
}
}
const createHeart = (e: React.MouseEvent) => {
if (!containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
const xInside = e.clientX - rect.left
const yInside = e.clientY - rect.top
const id = Date.now()
setHearts((prev) => [...prev, { id, x: xInside, y: yInside }])
setLikes((prev) => prev + 1)
// 1秒后移除
setTimeout(() => {
setHearts((prev) => prev.filter((h) => h.id !== id))
}, 1000)
}
// 动态加载外部资源(Oswald 字体 + Font Awesome)
useEffect(() => {
// 加载 Oswald 字体
const fontLink = document.createElement('link')
fontLink.href = 'https://fonts.googleapis.com/css?family=Oswald'
fontLink.rel = 'stylesheet'
document.head.appendChild(fontLink)
// 加载 Font Awesome
const faLink = document.createElement('link')
faLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css'
faLink.rel = 'stylesheet'
faLink.crossOrigin = 'anonymous'
document.head.appendChild(faLink)
// 清理函数(可选,避免重复加载)
return () => {
document.head.removeChild(fontLink)
document.head.removeChild(faLink)
}
}, [])
return (
<div className="flex min-h-screen flex-col items-center justify-center overflow-hidden font-[Oswald] text-white">
<h3 className="mb-0 text-center">
Double click on the image to <i className="fas fa-heart text-red-600"></i> it
</h3>
<small className="mb-5 block text-center">
You liked it <span>{likes}</span> times
</small>
<div
ref={containerRef}
className="relative h-[440px] w-[300px] cursor-pointer overflow-hidden bg-cover bg-center shadow-lg"
style={{ backgroundImage: `url(${imageUrl})` }}
onClick={handleClick}>
{hearts.map((heart) => (
<i
key={heart.id}
className="fas fa-heart heart-anim absolute text-red-600"
style={{
top: `${heart.y}px`,
left: `${heart.x}px`,
}}
/>
))}
</div>
<div className="fixed right-20 bottom-5 z-100 text-2xl text-red-500">
CSDN@Hao_Harrision
</div>
</div>
)
}
export default DoubleClickHeart
🔄 核心转换对照表
| 功能 | Vue 实现 | React 实现 |
|---|---|---|
| 响应式数据 | ref() |
useState |
| DOM 引用 | ref="xxx" |
useRef |
| 临时变量 | 模块变量 | useRef |
| 生命周期 | onMounted |
useEffect(..., []) |
| 局部样式 | <style scoped> |
全局 CSS + className |
| 外部资源加载 | onMounted 中操作 DOM |
useEffect 中操作 DOM |
| 列表渲染 | v-for |
.map() |
| 事件坐标 | e.clientX |
e.clientX(相同) |
1. 状态管理
| Vue | React |
|---|---|
ref(0) → likes |
useState<number>(0) |
ref([]) → hearts |
useState<Heart[]>([]) |
ref(null) → container |
useRef<HTMLDivElement>(null) |
模块变量 clickTime |
useRef<number>(0)(持久且不触发重渲染) |
✅ 为什么用 useRef 存 clickTime?
因为它是临时计时器,不需要触发 UI 更新,useRef 是最佳选择。
2. 事件处理
@click="handleClick"→onClick={handleClick}- 使用
React.MouseEvent类型确保类型安全; - 通过
e.clientX / e.clientY获取点击坐标。
3. DOM 引用
- Vue 的
ref="container"→ React 的ref={containerRef} - 通过
containerRef.current?.getBoundingClientRect()获取容器位置。
4. 动态加载外部资源
- Vue 在
onMounted中创建<link>标签; - React 使用
useEffect(() => { ... }, [])实现相同逻辑; - 添加清理函数(
return () => { ... })防止重复加载或内存泄漏。
⚠️ 生产环境中建议将这些资源通过 HTML 预加载,而非动态注入。
5. 动画实现
- Vue 使用
<style scoped>定义.heart-anim和@keyframes grow; - React 无法在组件内定义 keyframes ,必须将动画样式放入 全局 CSS;
- 动画逻辑完全一致:
scale(0) → scale(10) + opacity:0。
6. 字体使用:font-[Oswald]
- Tailwind 的任意值语法
font-[Oswald]会生成font-family: Oswald; - 前提是 Oswald 字体已加载 (通过
useEffect动态注入); - 若未加载,浏览器会回退到默认字体。
✅ 建议:在
index.html中预加载字体以提升性能:
<link href="https://fonts.googleapis.com/css?family=Oswald" rel="stylesheet">
7. Font Awesome 图标
- 通过 CDN 加载
all.min.css; - 使用
<i className="fas fa-heart">渲染图标; - 确保
crossOrigin="anonymous"避免 CORS 警告。
💡 替代方案:使用
@fortawesome/react-fontawesome组件库(更符合 React 思维),但本例保持与原 Vue 一致。
8. 双击检测逻辑
- 未使用
onDoubleClick,而是手动检测两次点击间隔 < 800ms; - 原因:
onDoubleClick在移动端兼容性差,且可能与缩放手势冲突; - 手动实现更可控,与原 Vue 逻辑一致。
🎯 最终效果
- 用户双击图片 → 在点击位置播放 ❤️ 动画;
- 点赞数递增;
- 动画 0.6 秒后淡出消失;
- 支持多次叠加动画;
- 完全响应式,样式与原 Vue 版本一致。
🎨 TailwindCSS 样式重点讲解
| 类名 | 作用 |
|---|---|
min-h-screen, flex-col, items-center, justify-center |
居中布局 |
font-[Oswald], text-white |
字体与文字颜色 |
relative, absolute |
心形图标的绝对定位 |
h-[440px] w-[300px] |
固定图片容器大小 |
bg-cover bg-center |
设置背景图片居中覆盖 |
cursor-pointer |
鼠标悬停为手型 |
overflow-hidden |
防止爱心动画溢出容器 |
shadow-lg |
添加阴影提升层次感 |
text-red-600 |
爱心颜色设置为红色 |
| [🎯 TailwindCSS 样式说明] |
🦌 路由组件 + 常量定义
router/index.tsx中 children数组中添加子路由
TypeScript
{
path: '/',
element: <App />,
children: [
...
{
path: '/DoubleClickHeart',
lazy: () =>
import('@/projects/DoubleClickHeart').then((mod) => ({
Component: mod.default,
})),
},
],
},
constants/index.tsx 添加组件预览常量``
TypeScript
import demo29Img from '@/assets/pic-demo/demo-29.png'
省略部分....
export const projectList: ProjectItem[] = [
省略部分....
{
id: 29,
title: 'Double ClickHeart',
image: demo29Img,
link: 'DoubleClickHeart',
},
]
🚀 小结
双击点赞动画组件不仅实现了基本的交互功能,它非常适合用于社交平台、图片浏览、内容互动等需要增强用户体验的场景。
你可以进一步扩展此组件的功能包括:
✅ 添加音效反馈(如"滴"一声)
✅ 支持移动端触摸双击识别
✅ 支持本地存储点赞数(使用 localStorage)
✅ 添加粒子爆炸或其他动画效果
✅ 将组件封装为 <DoubleHeartImage /> 可复用组件
📅 明日预告: 我们将完成AutoTextEffect组件,一个类似打字机的组件,可以调节速度。🚀
感谢阅读,欢迎点赞、收藏和分享 😊
原文链接:https://blog.csdn.net/qq_44808710/article/details/149373509
每天造一个轮子,码力暴涨不是梦!🚀