50天50个小项目 (React19 + Tailwindcss V4) ✨ | DoubleClickHeart(双击爱心)

📅 我们继续 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)(持久且不触发重渲染)

为什么用 useRefclickTime

因为它是临时计时器,不需要触发 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.tsxchildren数组中添加子路由

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

每天造一个轮子,码力暴涨不是梦!🚀

相关推荐
梦6502 小时前
React 封装 UEditor 富文本编辑器
前端·react.js·前端框架
qq. 28040339842 小时前
react 编写规范
前端·react.js·前端框架
qq. 28040339842 小时前
react 基本语法
前端·react.js·前端框架
小程故事多_802 小时前
重读ReAct,LLM Agent的启蒙之光,从“空想”到“实干”的范式革命
前端·人工智能·aigc
懒人村杂货铺2 小时前
前端步入全栈第一步
前端·docker·fastapi
小码过河.2 小时前
vue-office使用指南
前端·javascript·vue.js
wuhen_n2 小时前
LeetCode -- 349. 两个数组的交集(简单)
前端·javascript·算法·leetcode
cypking2 小时前
三、NestJS 开发实战文档-->集成 MySQL(TypeORM)
前端·数据库·mysql·adb·node.js
dreams_dream2 小时前
Element UI菜单折叠后的el-menu-item属性无法修改问题解决
前端·vue