返回资源库
Hover Card
UI组件
预览
卡片标题
这是一个测试预览实例,11111111
代码
组件代码
'use client';
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import './HoverCard.scss';
interface CardProps {
dataImage: string;
headerContent?: React.ReactNode;
contentContent?: React.ReactNode;
}
const HoverCard: React.FC<CardProps> = ({ dataImage, headerContent, contentContent }) => {
const [mouseX, setMouseX] = useState(0);
const [mouseY, setMouseY] = useState(0);
const cardRef = useRef<HTMLDivElement>(null);
const mouseLeaveDelayRef = useRef<NodeJS.Timeout | null>(null);
const rafRef = useRef<number>();
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
if (!cardRef.current) return;
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
rafRef.current = requestAnimationFrame(() => {
const rect = cardRef.current!.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
setMouseX(x);
setMouseY(y);
});
}, []);
const handleMouseEnter = useCallback(() => {
if (mouseLeaveDelayRef.current) {
clearTimeout(mouseLeaveDelayRef.current);
mouseLeaveDelayRef.current = null;
}
}, []);
const handleMouseLeave = useCallback(() => {
mouseLeaveDelayRef.current = setTimeout(() => {
setMouseX(0);
setMouseY(0);
}, 1000);
}, []);
const cardStyle = useMemo(() => {
const mousePX = mouseX / (cardRef.current?.offsetWidth || 1);
const mousePY = mouseY / (cardRef.current?.offsetHeight || 1);
return {
transform: `rotateY(${mousePX * 30}deg) rotateX(${mousePY * -30}deg)`
};
}, [mouseX, mouseY]);
const cardBgTransform = useMemo(() => {
const mousePX = mouseX / (cardRef.current?.offsetWidth || 1);
const mousePY = mouseY / (cardRef.current?.offsetHeight || 1);
return {
transform: `translateX(${mousePX * -40}px) translateY(${mousePY * -40}px)`
};
}, [mouseX, mouseY]);
const cardBgImage = useMemo(() => ({
backgroundImage: `url(${dataImage})`,
}), [dataImage]);
useEffect(() => {
return () => {
if (mouseLeaveDelayRef.current) {
clearTimeout(mouseLeaveDelayRef.current);
}
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
}
};
}, []);
return (
<div
className="card-wrap hover-card-wrap"
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={cardRef}
>
<div className="card" style={cardStyle}>
<div
className="card-bg"
style={{...cardBgTransform, ...cardBgImage}}
></div>
<div className="card-info">
{headerContent}
{contentContent}
</div>
</div>
</div>
);
};
export default React.memo(HoverCard);
SCSS 样式代码
$hoverEasing: cubic-bezier(0.23, 1, 0.32, 1);
$returnEasing: cubic-bezier(0.445, 0.05, 0.55, 0.95);
.card-wrap {
margin: 10px;
transform: perspective(800px);
transform-style: preserve-3d;
cursor: pointer;
font-family: "Raleway";
font-size: 14px;
font-weight: 500;
-webkit-font-smoothing: antialiased;
// background-color: #fff;
&:hover {
.card-info {
transform: translateY(0);
}
.card-info p {
opacity: 1;
}
.card-info, .card-info p {
transition: 0.6s $hoverEasing;
}
.card-info:after {
transition: 5s $hoverEasing;
opacity: 1;
transform: translateY(0);
}
.card-bg {
transition:
0.6s $hoverEasing,
opacity 5s $hoverEasing;
opacity: 0.8;
}
.card {
transition:
0.6s $hoverEasing,
box-shadow 2s $hoverEasing;
box-shadow:
rgba(white, 0.2) 0 0 40px 5px,
rgba(white, 1) 0 0 0 1px,
rgba(black, 0.66) 0 30px 60px 0,
inset #333 0 0 0 5px,
inset white 0 0 0 6px;
}
}
}
.card {
position: relative;
flex: 0 0 240px;
width: 240px;
height: 320px;
background-color: #333;
overflow: hidden;
border-radius: 10px;
box-shadow:
rgba(black, 0.66) 0 30px 60px 0,
inset #333 0 0 0 5px,
inset rgba(white, 0.5) 0 0 0 6px;
transition: 1s $returnEasing;
}
.card-bg {
opacity: 0.5;
position: absolute;
top: -20px; left: -20px;
box-sizing: content-box;
width: 100%;
height: 100%;
padding: 20px;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
transition:
1s $returnEasing,
opacity 5s 1s $returnEasing;
pointer-events: none;
}
.card-info {
padding: 20px;
position: absolute;
bottom: 0;
color: #fff;
transform: translateY(40%);
transition: 0.6s 1.6s cubic-bezier(0.215, 0.61, 0.355, 1);
p {
opacity: 0;
text-shadow: rgba(black, 1) 0 2px 3px;
transition: 0.6s 1.6s cubic-bezier(0.215, 0.61, 0.355, 1);
}
* {
position: relative;
z-index: 1;
}
&:after {
content: '';
position: absolute;
top: 0; left: 0;
z-index: 0;
width: 100%;
height: 100%;
background-image: linear-gradient(to bottom, transparent 0%, rgba(#000, 0.6) 100%);
background-blend-mode: overlay;
opacity: 0;
transform: translateY(100%);
transition: 5s 1s $returnEasing;
}
}
.card-info h1 {
font-family: "Playfair Display";
font-size: 36px;
font-weight: 700;
text-shadow: rgba(black, 0.5) 0 10px 10px;
}