feat: 添加引导页、视频播放和触摸交互功能
- 新增引导页组件(Guide.tsx)及相关资源 - 添加视频播放功能和相关资源文件 - 实现useTouch自定义hook支持触摸交互 - 添加store状态管理 - 更新多个页面组件(Content, Menu, Publicity) - 优化图片组件和背景图片组件 - 更新项目依赖
This commit is contained in:
1 parent
4ce7520066
commit
53d0eeac6b
24 files changed
+1390
-84
No files matched your search
@@ -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"
|
||||
)}
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user