0821-1
This commit is contained in:
parent
8ba6a2643e
commit
862cb594ae
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { X } from "lucide-react";
|
import { X, Maximize2, Minimize2 } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
// 展台四模块视频配置(请将对应 mp4/webm 文件放入 public/videos/ 下)
|
// 展台四模块视频配置(请将对应 mp4/webm 文件放入 public/videos/ 下)
|
||||||
@ -39,8 +39,10 @@ interface VideoStateRef {
|
|||||||
|
|
||||||
export default function ExpoPage() {
|
export default function ExpoPage() {
|
||||||
const [active, setActive] = useState<string | null>(null);
|
const [active, setActive] = useState<string | null>(null);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const timeRefs = useRef<Record<string, VideoStateRef>>({});
|
const timeRefs = useRef<Record<string, VideoStateRef>>({});
|
||||||
const containerRefs = useRef<Record<string, HTMLVideoElement | null>>({});
|
const containerRefs = useRef<Record<string, HTMLVideoElement | null>>({});
|
||||||
|
const mainRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
// 记录时间用于在放大时继续播放(非严格同步,够展示用)
|
// 记录时间用于在放大时继续播放(非严格同步,够展示用)
|
||||||
const handleBeforeExpand = (key: string) => {
|
const handleBeforeExpand = (key: string) => {
|
||||||
@ -83,8 +85,53 @@ export default function ExpoPage() {
|
|||||||
return () => window.removeEventListener("keydown", onKey);
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
}, [active]);
|
}, [active]);
|
||||||
|
|
||||||
|
// Fullscreen 监听
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = () => {
|
||||||
|
const fsEl = document.fullscreenElement || (document as any).webkitFullscreenElement;
|
||||||
|
setIsFullscreen(!!fsEl);
|
||||||
|
};
|
||||||
|
document.addEventListener("fullscreenchange", handler);
|
||||||
|
document.addEventListener("webkitfullscreenchange", handler as any);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("fullscreenchange", handler);
|
||||||
|
document.removeEventListener("webkitfullscreenchange", handler as any);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const enterFs = async () => {
|
||||||
|
const el = mainRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
try {
|
||||||
|
if (el.requestFullscreen) await el.requestFullscreen();
|
||||||
|
else if ((el as any).webkitRequestFullscreen) (el as any).webkitRequestFullscreen();
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const exitFs = async () => {
|
||||||
|
try {
|
||||||
|
if (document.exitFullscreen) await document.exitFullscreen();
|
||||||
|
else if ((document as any).webkitExitFullscreen) (document as any).webkitExitFullscreen();
|
||||||
|
} catch (_) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const toggleFs = () => {
|
||||||
|
if (isFullscreen) exitFs(); else enterFs();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="relative min-h-screen w-full bg-[#05080b] text-slate-100 overflow-hidden select-none">
|
<main ref={mainRef} className="relative min-h-screen w-full bg-[#05080b] text-slate-100 overflow-hidden select-none">
|
||||||
|
|
||||||
|
{/* 全屏按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={toggleFs}
|
||||||
|
className="fixed z-[60] top-2 left-2 md:top-3 md:left-3 w-9 h-9 md:w-10 md:h-10 flex items-center justify-center rounded-full bg-black/60 hover:bg-black/80 border border-white/10 text-slate-200 backdrop-blur-sm transition"
|
||||||
|
aria-label={isFullscreen ? "退出全屏" : "进入全屏"}
|
||||||
|
>
|
||||||
|
{isFullscreen ? <Minimize2 className="w-4 h-4 md:w-5 md:h-5" /> : <Maximize2 className="w-4 h-4 md:w-5 md:h-5" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* 2x2 全屏栅格 */}
|
{/* 2x2 全屏栅格 */}
|
||||||
<div className="grid grid-cols-1 grid-rows-4 md:grid-cols-2 md:grid-rows-2 w-full h-screen">
|
<div className="grid grid-cols-1 grid-rows-4 md:grid-cols-2 md:grid-rows-2 w-full h-screen">
|
||||||
@ -109,9 +156,9 @@ export default function ExpoPage() {
|
|||||||
preload="auto"
|
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">
|
<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-[12px] 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="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>
|
<p className="text-[10px] 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>
|
||||||
<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>
|
<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>
|
</motion.div>
|
||||||
@ -138,10 +185,11 @@ export default function ExpoPage() {
|
|||||||
exit={{ opacity: 0, scale: 0.95 }}
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
transition={{ type: "spring", stiffness: 180, damping: 22 }}
|
transition={{ type: "spring", stiffness: 180, damping: 22 }}
|
||||||
>
|
>
|
||||||
<div className="relative flex-1 bg-black">
|
<div className="relative flex-1 bg-black flex items-center justify-center min-h-0">
|
||||||
|
{/* 居中容器:使用 max-* 避免视频固有尺寸撑大父容器 */}
|
||||||
<video
|
<video
|
||||||
ref={expandedVideoRef}
|
ref={expandedVideoRef}
|
||||||
className="w-full h-full object-contain bg-black"
|
className="max-w-full max-h-full w-auto h-auto object-contain bg-black select-none"
|
||||||
src={v.src}
|
src={v.src}
|
||||||
playsInline
|
playsInline
|
||||||
muted
|
muted
|
||||||
@ -149,24 +197,19 @@ export default function ExpoPage() {
|
|||||||
loop
|
loop
|
||||||
controls
|
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">
|
{/* 叠加标题描述层,置于上方 */}
|
||||||
|
<div className="absolute top-0 left-0 right-0 z-10 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>
|
<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>
|
<p className="text-[10px] md:text-sm text-slate-300 max-w-4xl leading-relaxed">{v.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleRestore}
|
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"
|
className="absolute z-10 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="关闭放大"
|
aria-label="关闭放大"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -12,8 +12,8 @@ const Scene = dynamic(() => import("@/components/Scene"), { ssr: false });
|
|||||||
const moduleLinks = [
|
const moduleLinks = [
|
||||||
{ title: "力学实验模块", href: "#mechanics", icon: Waves, desc: "单摆精密测量 / 大摆角与阻尼修正 / 傅科摆进动 / 多参数智能拟合" },
|
{ title: "力学实验模块", href: "#mechanics", icon: Waves, desc: "单摆精密测量 / 大摆角与阻尼修正 / 傅科摆进动 / 多参数智能拟合" },
|
||||||
{ title: "电路实验模块", href: "#circuit", icon: CircuitBoard, desc: "YOLO 电路识别 → 拓扑重建 → 虚拟仿真 → AI 智能辅导闭环" },
|
{ title: "电路实验模块", href: "#circuit", icon: CircuitBoard, desc: "YOLO 电路识别 → 拓扑重建 → 虚拟仿真 → AI 智能辅导闭环" },
|
||||||
{ title: "电磁学实验模块", href: "#electromag", icon: Atom, desc: "气垫导轨 + 视觉测量 + BO/RL 迭代优化电磁能量转换效率" },
|
{ title: "电磁学实验模块", href: "#electromag", icon: Atom, desc: "气垫导轨 + 视觉测量 + 后续 BO/RL 迭代优化电磁能量转换效率" },
|
||||||
{ title: "光学实验模块", href: "#optics", icon: Camera, desc: "分光计视觉十字像追踪 + PID/步骤引导提升调节精度与效率" },
|
{ title: "光学实验模块", href: "#optics", icon: Camera, desc: "分光计视觉十字像追踪 + 步骤引导提升调节精度与效率" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|||||||
BIN
public/videos/optics.mp4
Normal file
BIN
public/videos/optics.mp4
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user