67 lines
1.7 KiB
TypeScript
67 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>
|
||
);
|
||
}
|