77 lines
1.7 KiB
TypeScript
77 lines
1.7 KiB
TypeScript
"use client";
|
|
|
|
import React, { useCallback, useRef, useState } from "react";
|
|
|
|
type HoverVideoProps = {
|
|
videoUrl: string;
|
|
coverUrl?: string | null;
|
|
className?: string;
|
|
style?: React.CSSProperties;
|
|
};
|
|
|
|
/**
|
|
* 鼠标移入才加载视频并自动播放,移出暂停并释放资源;默认显示封面。
|
|
*/
|
|
export default function HoverVideo({
|
|
videoUrl,
|
|
coverUrl,
|
|
className,
|
|
style,
|
|
}: HoverVideoProps) {
|
|
const [active, setActive] = useState(false);
|
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
|
|
const onEnter = useCallback(() => {
|
|
setActive(true);
|
|
// 播放在下一帧触发,避免 ref 尚未赋值
|
|
requestAnimationFrame(() => {
|
|
const v = videoRef.current;
|
|
if (!v) return;
|
|
v.play().catch(() => {});
|
|
});
|
|
}, [videoUrl]);
|
|
|
|
const onLeave = useCallback(() => {
|
|
const v = videoRef.current;
|
|
if (v) {
|
|
try {
|
|
v.pause();
|
|
v.load();
|
|
} catch {}
|
|
}
|
|
setActive(false);
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
style={style}
|
|
onMouseEnter={onEnter}
|
|
onMouseLeave={onLeave}
|
|
>
|
|
{/* 封面始终渲染在底层 */}
|
|
<img
|
|
src={coverUrl || "/placeholder.svg"}
|
|
alt="cover"
|
|
className="absolute inset-0 w-full h-full object-cover"
|
|
draggable={false}
|
|
loading="lazy"
|
|
/>
|
|
|
|
{/* 仅在激活后渲染视频;初始不设置 src,防止提前加载 */}
|
|
{active ? (
|
|
<video
|
|
ref={videoRef}
|
|
muted
|
|
playsInline
|
|
loop
|
|
autoPlay
|
|
src={videoUrl}
|
|
preload="none"
|
|
className="absolute inset-0 w-full h-full object-cover"
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|