You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Netflix风格卡片延迟hover缩放效果及状态异常问题求助

问题:Netflix卡片悬停状态异常

我想复刻Netflix的UI效果:鼠标悬停在卡片上超过1秒时,显示视频图片区域,同时触发缩放和从opacity 0到1的淡入动画。目前动画已经实现,但存在UI问题:即使鼠标离开卡片,isHovered状态仍会被自动设为true。

现有React组件代码

export default function Card({ movieData, isLiked = false }) {
  const [isHovered, setIsHovered] = useState(false);
  console.log("movdata");
  const nav = useNavigate();
  let timeoutId = null;

  const handleMouseEnter = () => {
    timeoutId = setTimeout(() => {
      setIsHovered(true);
    }, 1000);
  };

  const handleMouseLeave = () => {
    clearTimeout(timeoutId);
    setIsHovered(false);
  };

  return (
    <Container
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      className={isHovered ? "hovered" : ""}
    >
      <img
        src={`https://image.tmdb.org/t/p/w500${movieData.image}`}
        alt="age"
      />

      <div
        className="hover rr"
        style={{
          visibility: `${isHovered === true ? "visible" : "hidden"}`,
          opacity: `${isHovered === true ? "1" : "0"}`,
        }}
      >
        {
          <>
            <div className="image-video">
              <img
                src={`https://image.tmdb.org/t/p/w500${movieData.image}`}
                alt="age"
                onClick={() => nav("/player")}
              />
              <video
                src={op}
                autoPlay
                loop
                muted
                onClick={() => nav("/player")}
              >
                <div className="mute icons flex j-between">
                  <div className="controls flex">
                    <IoVolumeHighSharp />
                    <IoVolumeMuteSharp />
                  </div>
                </div>
              </video>
            </div>
            <div className="info-container flex column">
              <h3 className="name" onClick={() => nav("/player")}>
                {movieData.name}
              </h3>
              <div className="icons flex j-between">
                <div className="controls flex">
                  <IoPlayCircleSharp
                    title="Play"
                    onClick={() => nav("/player")}
                  />
                  <BsHandThumbsUp />
                  {isLiked ? (
                    <BsCheck title="Remove From Fav" />
                  ) : (
                    <AiOutlinePlus title="Add to Fav" />
                  )}
                </div>
                <div className="info">
                  <BiChevronDown title="More" />
                </div>
              </div>
              <div className="genres flex">
                <ul className="flex">
                  {movieData.genres.map((genre) => (
                    <li key={genre}>{genre}</li>
                  ))}
                </ul>
              </div>
            </div>
          </>
        }
      </div>
    </Container>
  );
}  

现有CSS样式

max-width: 16.66666667%;
  width: 16.66666667%;
  height: 100%;
  cursor: pointer;
  padding: 0 0.2vw;
  position: relative;
  &.hovered {
    transition-delay: 0.5s;
    transform: scale(1.3);
    transition: transform 600ms ease-in;
  }
  img {
    position: relative;
    border-radius: 0.2rem;
    width: 227px;
    border-radius: 5px;
    max-width: 227px;
    height: 100%;
    z-index: -1;
  }
  .hover {
    z-index: 999;
    height: max-content;
    position: absolute;
    top: -10vh;
    border-radius: 0.3rem;
    box-shadow: #000000c4 0px 3px 10px;
    width: 250px;
    height: 290px;
    background-color: #181818;
    .image-video {
      position: relative;
      height: 140px;
      img {
        width: 227px;
        height: 140px;
        object-fit: cover;
        border-radius: 0.3rem;
        top: 0;
        z-index: 4; 
        position: absolute;
      }
      video {
        width: 100%;
        height: 140px;
        object-fit: cover;
        border-radius: 0.3rem;
        top: 0;
        z-index: 5;
        position: absolute;
      }
    }
    .info-container {
      padding: 1rem;
      gap: 0.5rem;

      font-size: 0.8vw;
    }

问题原因与解决方案

核心问题

  1. timeoutId是普通变量,组件每次重渲染都会重置为null,导致旧定时器无法被清除,到期后仍会触发setIsHovered(true)。
  2. 缩放后的.hover悬浮层z-index极高,鼠标移到上面时会触发原卡片的handleMouseLeave,但实际用户仍在卡片交互区域内。

修复步骤

  1. 用useRef保存定时器ID
    替换普通变量为useRef,确保定时器ID在组件生命周期内不丢失:

    import { useState, useRef, useEffect } from 'react'; // 补充导入useRef和useEffect
    
    // ...
    const timeoutId = useRef(null); // 替换let timeoutId = null;
    
    const handleMouseEnter = () => {
      timeoutId.current = setTimeout(() => {
        setIsHovered(true);
      }, 1000);
    };
    
    const handleMouseLeave = () => {
      clearTimeout(timeoutId.current);
      timeoutId.current = null;
      setIsHovered(false);
    };
    
  2. 添加组件卸载时的定时器清理
    避免组件卸载后定时器仍运行导致内存泄漏:

    useEffect(() => {
      return () => {
        clearTimeout(timeoutId.current);
      };
    }, []);
    
  3. 处理悬浮层鼠标事件穿透
    给.hover层添加pointer-events: none,让鼠标事件穿透到原卡片,同时恢复内部可点击元素的事件响应:

    .hover {
      z-index: 999;
      height: max-content;
      position: absolute;
      top: -10vh;
      border-radius: 0.3rem;
      box-shadow: #000000c4 0px 3px 10px;
      width: 250px;
      height: 290px;
      background-color: #181818;
      pointer-events: none; /* 添加此行 */
      .image-video, .info-container {
        pointer-events: auto; /* 内部可点击元素恢复事件响应 */
      }
      /* 原有样式保留 */
    }
    

内容的提问来源于stack exchange,提问作者Fam fas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 12:05:20