"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { ChevronLeft, ChevronRight, Pause, Play, Volume2, VolumeX, Maximize, Minimize2, MessageSquare, ThumbsUp, MessageSquareText, RotateCcw, RotateCw, } from "lucide-react"; type User = { nickname: string; avatar_url: string | null }; type Comment = { cid: string; text: string; digg_count: number; created_at: string | Date; user: User }; type VideoData = { type: "video"; aweme_id: string; desc: string; created_at: string | Date; duration_ms?: number | null; video_url: string; width?: number | null; height?: number | null; author: User; comments: Comment[]; }; type ImageData = { type: "image"; aweme_id: string; desc: string; created_at: string | Date; images: { id: string; url: string; width?: number; height?: number }[]; music_url?: string | null; author: User; comments: Comment[]; }; const SEGMENT_MS = 5000; // 图文每段 5s export default function AwemeDetailClient(props: { data: VideoData | ImageData }) { const { data } = props; const isVideo = data.type === "video"; // ====== 布局 & 评论 ====== const [open, setOpen] = useState(false); // 评论是否展开(竖屏为 bottom sheet;横屏为并排分栏) const comments = useMemo(() => data.comments ?? [], [data]); // ====== 媒体引用 ====== const mediaContainerRef = useRef(null); const videoRef = useRef(null); const audioRef = useRef(null); // ====== 统一控制状态 ====== const [isPlaying, setIsPlaying] = useState(true); // 视频=播放;图文=是否自动切换 const [isFullscreen, setIsFullscreen] = useState(false); const [volume, setVolume] = useState(1); // 视频音量 / 图文BGM音量 const [rate, setRate] = useState(1); // 仅视频使用 const [progress, setProgress] = useState(0); // 0..1 总进度 const [rotation, setRotation] = useState(0); // 视频旋转角度:0/90/180/270 // ====== 图文专用(分段) ====== const images = (data as any).images as ImageData["images"] | undefined; const totalSegments = images?.length ?? 0; const [idx, setIdx] = useState(0); // 当前图片索引 const scrollerRef = useRef(null); // 用 ref 解决“闪回” const segStartRef = useRef(null); // 当前段开始时间戳 const idxRef = useRef(0); const rafRef = useRef(null); const [segProgress, setSegProgress] = useState(0); // 段内 0..1 useEffect(() => { idxRef.current = idx; }, [idx]); // ====== 视频:进度/播放/倍速/音量 ====== useEffect(() => { if (!isVideo) return; const v = videoRef.current; if (!v) return; const onTime = () => { if (!v.duration || Number.isNaN(v.duration)) return; setProgress(v.currentTime / v.duration); }; const onPlay = () => setIsPlaying(true); const onPause = () => setIsPlaying(false); v.addEventListener("timeupdate", onTime); v.addEventListener("loadedmetadata", onTime); v.addEventListener("play", onPlay); v.addEventListener("pause", onPause); return () => { v.removeEventListener("timeupdate", onTime); v.removeEventListener("loadedmetadata", onTime); v.removeEventListener("play", onPlay); v.removeEventListener("pause", onPause); }; }, [isVideo]); useEffect(() => { if (!isVideo) return; const v = videoRef.current; if (v) v.volume = volume; }, [volume, isVideo]); useEffect(() => { if (!isVideo) return; const v = videoRef.current; if (v) v.playbackRate = rate; }, [rate, isVideo]); // ====== 图文:BGM & 初次自动播放尝试 ====== useEffect(() => { if (isVideo) return; const el = audioRef.current; if (!el) return; el.volume = volume; if (isPlaying) { el.play().catch(() => {/* 被策略阻止无妨,用户点播放即可 */ }); } else { el.pause(); } }, [isVideo]); // 初次挂载 useEffect(() => { if (isVideo) return; const el = audioRef.current; if (el) el.volume = volume; }, [volume, isVideo]); // ====== 全屏状态监听 ====== useEffect(() => { const onFsChange = () => setIsFullscreen(!!document.fullscreenElement); document.addEventListener("fullscreenchange", onFsChange); return () => document.removeEventListener("fullscreenchange", onFsChange); }, []); // ====== 图文:自动切页(消除“闪回”)====== useEffect(() => { if (isVideo || !images?.length) return; if (segStartRef.current == null) segStartRef.current = performance.now(); const tick = (ts: number) => { if (!images?.length) return; let start = segStartRef.current!; let localIdx = idxRef.current; // 暂停时只更新 UI,不推进时间 if (!isPlaying) { const elapsed = Math.max(0, ts - start); const localSeg = Math.min(1, elapsed / SEGMENT_MS); setSegProgress(localSeg); setProgress((localIdx + localSeg) / images.length); rafRef.current = requestAnimationFrame(tick); return; } // 前进时间:处理跨多段情况(极少见,但更稳妥) let elapsed = ts - start; while (elapsed >= SEGMENT_MS) { elapsed -= SEGMENT_MS; localIdx = (localIdx + 1) % images.length; } segStartRef.current = ts - elapsed; if (localIdx !== idxRef.current) { idxRef.current = localIdx; setIdx(localIdx); const el = scrollerRef.current; if (el) el.scrollTo({ left: localIdx * el.clientWidth, behavior: "smooth" }); } const localSeg = Math.max(0, Math.min(1, elapsed / SEGMENT_MS)); setSegProgress(localSeg); setProgress((localIdx + localSeg) / images.length); rafRef.current = requestAnimationFrame(tick); }; rafRef.current = requestAnimationFrame(tick); return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); rafRef.current = null; }; }, [isVideo, images?.length, isPlaying]); // 横向滚动同步 idx(且重置段起点) useEffect(() => { const el = scrollerRef.current; if (!el) return; const onScroll = () => { const i = Math.round(el.scrollLeft / el.clientWidth); if (i !== idxRef.current) { idxRef.current = i; setIdx(i); segStartRef.current = performance.now(); setSegProgress(0); setProgress(images && images.length ? i / images.length : 0); } }; el.addEventListener("scroll", onScroll, { passive: true }); return () => el.removeEventListener("scroll", onScroll); }, [images?.length]); // ====== 统一操作 ====== const seekTo = (ratio: number) => { ratio = Math.min(1, Math.max(0, ratio)); if (isVideo) { const v = videoRef.current; if (!v || !v.duration) return; v.currentTime = v.duration * ratio; return; } if (!images?.length) return; const total = images.length; const exact = ratio * total; const targetIdx = Math.min(total - 1, Math.floor(exact)); const remainder = exact - targetIdx; idxRef.current = targetIdx; setIdx(targetIdx); segStartRef.current = performance.now() - remainder * SEGMENT_MS; setSegProgress(remainder); setProgress((targetIdx + remainder) / total); const el = scrollerRef.current; if (el) el.scrollTo({ left: targetIdx * el.clientWidth, behavior: "smooth" }); }; const togglePlay = async () => { if (isVideo) { const v = videoRef.current; if (!v) return; if (v.paused) await v.play(); else v.pause(); return; } const el = audioRef.current; if (!isPlaying) { setIsPlaying(true); try { await el?.play(); } catch { } } else { setIsPlaying(false); el?.pause(); } }; const toggleFullscreen = () => { const el = mediaContainerRef.current; if (!el) return; if (!document.fullscreenElement) { el.requestFullscreen().catch(() => { }); } else { document.exitFullscreen().catch(() => { }); } }; const prevImg = () => { if (!images?.length) return; const next = Math.max(0, idxRef.current - 1); idxRef.current = next; setIdx(next); segStartRef.current = performance.now(); setSegProgress(0); const el = scrollerRef.current; if (el) el.scrollTo({ left: next * el.clientWidth, behavior: "smooth" }); }; const nextImg = () => { if (!images?.length) return; const next = Math.min(images.length - 1, idxRef.current + 1); idxRef.current = next; setIdx(next); segStartRef.current = performance.now(); setSegProgress(0); const el = scrollerRef.current; if (el) el.scrollTo({ left: next * el.clientWidth, behavior: "smooth" }); }; // ====== 侧栏(横屏)/ 抽屉(竖屏)样式(Tailwind) const asideClasses = [ "z-30 flex flex-col bg-[rgba(22,22,22,0.92)] text-white", // 竖屏:bottom sheet,从下向上弹出 "portrait:fixed portrait:inset-x-0 portrait:bottom-0 portrait:w-full portrait:h-[min(80vh,88dvh)]", "portrait:transition-transform portrait:duration-200 portrait:ease-out", open ? "portrait:translate-y-0" : "portrait:translate-y-full", "portrait:border-t portrait:border-white/10", // 横屏:并排分栏,宽度过渡 "landscape:relative landscape:h-full landscape:overflow-hidden", "landscape:transition-[width] landscape:duration-200 landscape:ease-out", open ? "landscape:w-[min(420px,36vw)] landscape:border-l landscape:border-white/10" : "landscape:w-0", ].join(" "); return (
{/* 横屏下使用并排布局;竖屏下为单栏(评论为 bottom sheet) */}
{/* 主媒体区域 */}
{isVideo ? (
{/* 评论面板:竖屏 bottom sheet;横屏并排分栏 */}
); }