first commit

This commit is contained in:
2025-08-20 13:31:55 +08:00
commit 3dcd8e5e3b
15 changed files with 1536 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { X } from "lucide-react";
import clsx from "clsx";
// 展台四模块视频配置(请将对应 mp4/webm 文件放入 public/videos/ 下)
const videos = [
{
key: "mechanics",
title: "力学 · 单摆高精度测量",
src: "/videos/mechanics.mp4",
desc: "CV 追踪 + AI 拟合:g 误差压至 0.43%,提取阻尼 γ 与品质因数 Q。探究傅科摆效应。"
},
{
key: "circuit",
title: "电路 · 实物 → 虚拟转换",
src: "/videos/circuit.mp4",
desc: "YOLO 识别元件与连线,自动生成可交互虚拟电路 + AI 问答辅导。"
},
{
key: "electromag",
title: "电磁 · 能量转换优化",
src: "/videos/electromag.mp4",
desc: "视觉 + 单片机测电磁转换效率 + 未来 BO/RL 迭代脉冲参数,提高加速/制动能量效率。"
},
{
key: "optics",
title: "光学 · 分光计智能辅助",
src: "/videos/optics.mp4",
desc: "十字像追踪 + 分步引导 + PID 调节。"
}
];
interface VideoStateRef {
currentTime: number;
wasPlaying: boolean;
}
export default function ExpoPage() {
const [active, setActive] = useState<string | null>(null);
const timeRefs = useRef<Record<string, VideoStateRef>>({});
const containerRefs = useRef<Record<string, HTMLVideoElement | null>>({});
// 记录时间用于在放大时继续播放(非严格同步,够展示用)
const handleBeforeExpand = (key: string) => {
const vid = containerRefs.current[key];
if (vid) {
timeRefs.current[key] = {
currentTime: vid.currentTime,
wasPlaying: !vid.paused
};
}
setActive(key);
};
const handleRestore = () => {
setActive(null);
};
// 在放大的 video 元素上恢复播放进度
const expandedVideoRef = useRef<HTMLVideoElement | null>(null);
useEffect(() => {
if (active && expandedVideoRef.current) {
const state = timeRefs.current[active];
if (state) {
try {
expandedVideoRef.current.currentTime = state.currentTime;
} catch (_) {
/* Safari 某些情况下可能阻止精确跳转,忽略 */
}
if (state.wasPlaying) expandedVideoRef.current.play().catch(() => {});
}
}
}, [active]);
// ESC 关闭
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape" && active) setActive(null);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [active]);
return (
<main className="relative min-h-screen w-full bg-[#05080b] text-slate-100 overflow-hidden select-none">
{/* 2x2 全屏栅格 */}
<div className="grid grid-cols-1 grid-rows-4 md:grid-cols-2 md:grid-rows-2 w-full h-screen">
{videos.map(v => (
<motion.div
key={v.key}
layoutId={`card-${v.key}`}
className={clsx(
"relative group overflow-hidden flex cursor-pointer bg-black",
active && active !== v.key && "opacity-25 scale-[0.99] transition"
)}
onClick={() => handleBeforeExpand(v.key)}
>
<video
ref={el => { containerRefs.current[v.key] = el; }}
className="w-full h-full object-contain"
src={v.src}
playsInline
muted
autoPlay
loop
preload="auto"
/>
{/* 角标信息,默认半透明,悬停/触摸更亮 */}
<div className="absolute left-0 top-0 p-2 md:p-3 bg-black/35 backdrop-blur-sm rounded-br-xl md:rounded-br-2xl text-[10px] md:text-xs leading-snug transition group-hover:bg-black/55">
<p className="font-semibold text-slate-100/95 mb-0.5 md:mb-1 truncate max-w-[10rem] md:max-w-[14rem]">{v.title}</p>
<p className="text-[9px] md:text-[10px] text-slate-300/80 line-clamp-2 md:line-clamp-2 max-w-[12rem] md:max-w-[16rem]">{v.desc}</p>
</div>
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition bg-black/25 flex items-center justify-center text-[11px] tracking-wide font-medium">点击放大</div>
</motion.div>
))}
</div>
<AnimatePresence>
{active && (
<>
<motion.div
key="backdrop"
className="fixed inset-0 bg-black/70 backdrop-blur-sm z-40"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
{videos.filter(v => v.key === active).map(v => (
<motion.div
key={v.key}
layoutId={`card-${v.key}`}
className="fixed z-50 inset-0 flex flex-col overflow-hidden bg-black"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ type: "spring", stiffness: 180, damping: 22 }}
>
<div className="relative flex-1 bg-black">
<video
ref={expandedVideoRef}
className="w-full h-full object-contain bg-black"
src={v.src}
playsInline
muted
autoPlay
loop
controls
/>
<div className="absolute top-0 left-0 right-0 p-3 md:p-5 flex flex-col gap-2 bg-gradient-to-b from-black/70 to-transparent pointer-events-none">
<h2 className="text-base md:text-2xl font-semibold tracking-tight drop-shadow">{v.title}</h2>
<p className="text-[10px] md:text-sm text-slate-300 max-w-4xl leading-relaxed">{v.desc}</p>
</div>
<button
onClick={handleRestore}
className="absolute top-2 right-2 md:top-4 md:right-4 w-9 h-9 rounded-full bg-black/60 hover:bg-black/80 text-slate-200 flex items-center justify-center border border-white/10 backdrop-blur-sm transition"
aria-label="关闭放大"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-2 md:p-3 flex justify-end gap-3 items-center text-[10px] md:text-xs text-slate-400 bg-[#0e1419]/90 border-t border-white/5">
<button
onClick={handleRestore}
className="px-3 py-1 rounded-md bg-sky-600/25 hover:bg-sky-600/40 text-sky-200 font-medium tracking-wide"
>返回</button>
</div>
</motion.div>
))}
</>
)}
</AnimatePresence>
</main>
);
}