feat: 添加引导页、视频播放和触摸交互功能

- 新增引导页组件(Guide.tsx)及相关资源
- 添加视频播放功能和相关资源文件
- 实现useTouch自定义hook支持触摸交互
- 添加store状态管理
- 更新多个页面组件(Content, Menu, Publicity)
- 优化图片组件和背景图片组件
- 更新项目依赖
This commit is contained in:
zhongyidian committed 2025-10-31 17:49:13 +08:00
1 parent 4ce7520066
commit 53d0eeac6b
24 files changed
+1390 -84

No files matched your search

+6 -6
View File
@@ -206,10 +206,10 @@ const TabContent = ({
<table className="w-full">
<thead>
<tr className="bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
<th className="px-6 py-4 text-left text-lg font-semibold w-[70%]">
<th className="px-6 py-4 text-left text-3xl font-semibold w-[70%]">
活动名称
</th>
<th className="px-6 py-4 text-left text-lg font-semibold w-[30%]">
<th className="px-6 py-4 text-left text-3xl font-semibold w-[30%]">
时间
</th>
</tr>
@@ -220,7 +220,7 @@ const TabContent = ({
key={activity.id}
onClick={() => handleActivityClick(activity)}
className={clsx(
"border-b border-gray-200 cursor-pointer transition-all duration-200",
"border-b border-gray-200 cursor-pointer transition-all duration-200 text-3xl",
"hover:bg-[#82B2D6]/10 hover:shadow-md",
index % 2 === 0 ? "bg-white" : "bg-gray-50"
)}
@@ -259,10 +259,10 @@ const TabContent = ({
<table className="w-full">
<thead>
<tr className="bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
<th className="px-6 py-4 text-left text-lg font-semibold w-[70%]">
<th className="px-6 py-4 text-left text-3xl font-semibold w-[70%]">
活动名称
</th>
<th className="px-6 py-4 text-left text-lg font-semibold w-[30%]">
<th className="px-6 py-4 text-left text-3xl font-semibold w-[30%]">
时间
</th>
</tr>
@@ -276,7 +276,7 @@ const TabContent = ({
handleActivitiesClick(activity)
}
className={clsx(
"border-b border-gray-200 cursor-pointer transition-all duration-200",
"border-b border-gray-200 cursor-pointer transition-all duration-200 text-3xl",
"hover:bg-[#82B2D6]/10 hover:shadow-md",
index % 2 === 0 ? "bg-white" : "bg-gray-50"
)}
+77
View File
@@ -0,0 +1,77 @@
import { useNavigate } from "react-router-dom";
import { BackgroundImage, Image } from "@/components";
import { getStaticImage, getPublicPath } from "@/utils";
import { useState, useRef, useEffect } from "react";
export default function Guide() {
const navigate = useNavigate();
const [showVideo, setShowVideo] = useState(true);
const videoRef = useRef<HTMLVideoElement>(null);
// 监听视频播放完成
const handleVideoEnd = () => {
setShowVideo(false);
};
// 跳过引导,直接进入宣传页
const handleSkip = () => {
navigate("/publicity");
};
return (
<BackgroundImage
src={getStaticImage("guide/引导页背景.png")}
className="h-full w-full relative"
backgroundSize="cover"
backgroundPosition="center"
>
{/* 跳过按钮 */}
<Image
src={getStaticImage("components/按钮-跳过.png")}
alt="跳过"
button
className="absolute top-0 right-0"
onClick={handleSkip}
/>
{/* 标题 */}
<Image
src={getStaticImage("publicity/名师工作室-标题.png")}
alt="标题"
className="absolute left-0 top-30"
/>
{/* 引导视频 - 仅在首次显示 */}
{
<div className="absolute w-[500px] left-20 -bottom-20 z-50">
<video
ref={videoRef}
className="w-full h-full object-contain"
src={getPublicPath("video/5_alpha channel.webm")}
autoPlay
playsInline
onEnded={handleVideoEnd}
/>
</div>
}
{/* 引导内容区域 */}
{
<div className="absolute right-10 bottom-0 z-10">
{/* 发言底板 */}
<BackgroundImage
className="p-20"
width={1220}
height={763}
src={getStaticImage("guide/框.png")}
>
<p className="text-white text-3xl tracking-wide leading-15 text-left">
各位朋友好!我是曹君,智慧商旅系主任、网络营销与直播电商专业带头人。我的名师工作室聚焦产教融合,紧扣新兴行业需求,对接虹口北外滩发展战略。
带教以实战为核心,依托全国行业资源,让学员深入企业、乡村参与直播全流程;同时兼顾实践与教学能力双提升,致力培养复合型数字技能人才,期待与大家共成长!
</p>
</BackgroundImage>
</div>
}
</BackgroundImage>
);
}
+89 -58
View File
@@ -1,8 +1,8 @@
import { useNavigate } from "react-router-dom";
import { BackgroundImage, Image, Alert } from "@/components";
import { getStaticImage } from "@/utils";
import { useState } from "react";
import { IframeModal } from "@/components/Modal";
import { useState, Fragment, useRef, useEffect } from "react";
import { useTouch } from "@/hooks";
/**
* 卢航-未选中.png
@@ -16,8 +16,8 @@ const teachers = [
avatar: getStaticImage("menu/卢航-未选中.png"),
selectedAvatar: getStaticImage("menu/卢航-选中.png"),
position: {
top: "20%",
left: "12%",
top: "20.5%",
left: "15%",
},
},
{
@@ -36,8 +36,8 @@ const teachers = [
avatar: getStaticImage("menu/龚丹-未选中.png"),
selectedAvatar: getStaticImage("menu/龚丹-选中.png"),
position: {
top: "20%",
left: "65%",
top: "20.5%",
left: "63%",
},
},
];
@@ -45,9 +45,73 @@ const teachers = [
export default function Home() {
const navigate = useNavigate();
const [selectedTeacherId, setSelectedTeacherId] = useState(0);
const [isModalOpen, setIsModalOpen] = useState(false);
const [showTipModal, setShowTipModal] = useState(false);
// 旋转相关状态
const [rotation, setRotation] = useState(0);
const [isAutoRotating, setIsAutoRotating] = useState(true);
const autoRotateTimerRef = useRef<number | null>(null);
const resumeTimerRef = useRef<NodeJS.Timeout | null>(null);
const startRotationRef = useRef(0);
// 自动旋转效果
useEffect(() => {
if (isAutoRotating) {
const animate = () => {
setRotation((prev) => prev + 0.2);
autoRotateTimerRef.current = requestAnimationFrame(animate);
};
autoRotateTimerRef.current = requestAnimationFrame(animate);
}
return () => {
if (autoRotateTimerRef.current !== null) {
cancelAnimationFrame(autoRotateTimerRef.current);
autoRotateTimerRef.current = null;
}
};
}, [isAutoRotating]);
// 组件卸载时清理定时器
useEffect(() => {
return () => {
if (autoRotateTimerRef.current !== null) {
cancelAnimationFrame(autoRotateTimerRef.current);
}
if (resumeTimerRef.current !== null) {
clearTimeout(resumeTimerRef.current);
}
};
}, []);
// 使用 useTouch 处理触摸手势
const { handlers } = useTouch({
onTouchStart: () => {
// 停止自动旋转
setIsAutoRotating(false);
// 清除恢复定时器
if (resumeTimerRef.current) {
clearTimeout(resumeTimerRef.current);
resumeTimerRef.current = null;
}
// 记录开始时的旋转角度
startRotationRef.current = rotation;
},
onTouchMove: (data) => {
// 根据水平移动距离计算旋转角度(灵敏度 0.5)
const deltaRotation = data.deltaX * 0.5;
setRotation(startRotationRef.current + deltaRotation);
},
onTouchEnd: () => {
// 1秒后恢复自动旋转
resumeTimerRef.current = setTimeout(() => {
setIsAutoRotating(true);
}, 1000);
},
});
const handleSelectTeacher = (teacherId: number) => {
if (teacherId !== 2) {
// 点击卢航或龚丹时显示提示
@@ -55,20 +119,7 @@ export default function Home() {
return;
}
setSelectedTeacherId(teacherId);
navigate("/Publicity");
};
const handleStartExperiment = () => {
console.log("点击开启实验");
// navigate("/experiment");
};
const handleAvatarClick = () => {
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
navigate("/guide");
};
return (
@@ -92,36 +143,6 @@ export default function Home() {
className="absolute bottom-0 left-1/2 transform -translate-x-1/2"
/>
{/* 数字人头像 */}
{!isModalOpen && (
<>
<div
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-20"
onClick={handleAvatarClick}
>
<Image
src={getStaticImage("publicity/数字人-头像框.png")}
alt="数字人头像"
/>
<span className="text-white text-2xl font-bold tracking-wide">
点击我随时为你解答
</span>
</div>
</>
)}
{/* 嵌入式客服对话框 */}
<IframeModal
isOpen={isModalOpen}
onClose={handleCloseModal}
url="https://www.edutech.host/digitalHuman/"
title="数字人助手"
mode="embedded"
position="bottom-right"
width="420px"
height="650px"
/>
{/* 提示弹窗 */}
<Alert
isOpen={showTipModal}
@@ -132,21 +153,30 @@ export default function Home() {
<BackgroundImage
src={getStaticImage("menu/转盘合.png")}
className="absolute w-full h-500 -bottom-1/5 left-0"
width={2058}
height={2046}
className="absolute -bottom-1/5 -left-14.5 cursor-grab active:cursor-grabbing select-none"
backgroundSize="cover"
backgroundPosition="center"
{...handlers}
style={{
transform: `rotate(${rotation}deg)`,
transition: "none",
}}
>
{teachers.map((teacher) => (
<>
{teachers.map((teacher, _index) => (
<Fragment key={teacher.id}>
<Image
key={teacher.id}
src={
selectedTeacherId === teacher.id
? teacher.selectedAvatar
: teacher.avatar
}
alt={teacher.name}
onClick={() => handleSelectTeacher(teacher.id)}
onClick={(e) => {
e.stopPropagation();
handleSelectTeacher(teacher.id);
}}
className="absolute"
style={{
top:
@@ -157,7 +187,8 @@ export default function Home() {
selectedTeacherId === teacher.id
? "36%"
: teacher.position.left,
transition: "transform 0.2s ease, z-index 0.2s ease",
transform: `rotate(${-rotation}deg)`,
transition: "none",
}}
button={selectedTeacherId !== teacher.id}
/>
@@ -168,7 +199,7 @@ export default function Home() {
<div className="absolute top-[25%] left-[65%] text-gray-500 text-3xl font-bold cursor-not-allowed">
该名师工作室数字睿教正在筹划中
</div> */}
</>
</Fragment>
))}
</BackgroundImage>
</BackgroundImage>
+36 -8
View File
@@ -1,8 +1,9 @@
import { BackgroundImage, Image } from "@/components";
import { IframeModal } from "@/components/Modal";
import { getStaticImage } from "@/utils";
import { useState } from "react";
import { getPublicPath, getStaticImage } from "@/utils";
import { useState, useRef, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useFirstVisitStore } from "@/store";
// 解答弹窗组件
interface DialogBoxProps {
@@ -116,6 +117,33 @@ export default function PublicityPage() {
const [selectedFunctionId, setSelectedFunctionId] = useState(0);
const [isModalOpen, setIsModalOpen] = useState(false);
// 首次访问状态管理
const { hasSeenIntro, setHasSeenIntro, isExpired, reset } =
useFirstVisitStore();
const [showIntroVideo, setShowIntroVideo] = useState(false);
const videoRef = useRef<HTMLVideoElement>(null);
// 检查是否显示引导视频
useEffect(() => {
// 如果存储已过期,重置状态
if (isExpired()) {
reset();
setShowIntroVideo(true);
return;
}
if (!hasSeenIntro) {
setShowIntroVideo(true);
}
}, [hasSeenIntro, isExpired, reset]);
// 监听视频播放完成
const handleVideoEnd = () => {
setShowIntroVideo(false);
setHasSeenIntro();
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleOpenDialog = () => {
setIsDialogOpen(true);
};
@@ -170,7 +198,7 @@ export default function PublicityPage() {
className="absolute left-0 top-30"
/>
{/* 数字人 */}
{/* 数字人 - 未播放引导视频时显示 */}
<Image
src={getStaticImage("publicity/曹君名师数字人分身.png")}
alt="数字人"
@@ -178,7 +206,7 @@ export default function PublicityPage() {
/>
{/* 数字人头像 */}
{!isModalOpen && (
{/* {!isModalOpen && (
<>
<div
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-10"
@@ -193,7 +221,7 @@ export default function PublicityPage() {
</span>
</div>
</>
)}
)} */}
{/* 提示 */}
{/* <Image
src={getStaticImage("publicity/点击提示.png")}
@@ -204,10 +232,10 @@ export default function PublicityPage() {
/> */}
{/* 解答弹窗 */}
<DialogBox isOpen={isDialogOpen} onClose={handleCloseDialog} />
{/* <DialogBox isOpen={isDialogOpen} onClose={handleCloseDialog} /> */}
{/* 嵌入式客服对话框 */}
<IframeModal
{/* <IframeModal
isOpen={isModalOpen}
onClose={handleCloseModal}
url="https://www.edutech.host/digitalHuman/"
@@ -216,7 +244,7 @@ export default function PublicityPage() {
position="bottom-right"
width="420px"
height="650px"
/>
/> */}
</BackgroundImage>
);
}