training_data/apps/web/src/components/models/course/detail/CourseSyllabus.tsx

215 lines
6.1 KiB
TypeScript

import { XMarkIcon, BookOpenIcon } from "@heroicons/react/24/outline";
import {
ChevronDownIcon,
ClockIcon,
PlayCircleIcon,
} from "@heroicons/react/24/outline";
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import React, { useState, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { SectionDto } from "@nice/common";
interface CourseSyllabusProps {
sections: SectionDto[];
onLectureClick?: (lectureId: string) => void;
isOpen: boolean;
onToggle: () => void;
}
export const CourseSyllabus: React.FC<CourseSyllabusProps> = ({
sections,
onLectureClick,
isOpen,
onToggle,
}) => {
const [expandedSections, setExpandedSections] = useState<string[]>([]);
const sectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const toggleSection = (sectionId: string) => {
setExpandedSections((prev) =>
prev.includes(sectionId)
? prev.filter((id) => id !== sectionId)
: [...prev, sectionId]
);
// 平滑滚动到选中的章节
setTimeout(() => {
sectionRefs.current[sectionId]?.scrollIntoView({
behavior: "smooth",
block: "start",
});
}, 100);
};
return (
<motion.div
initial={false}
animate={{
width: isOpen ? "33.333333%" : "48px",
right: 0,
}}
className="fixed top-0 bottom-0 z-20 bg-white shadow-xl">
{/* 收起时显示的展开按钮 */}
{!isOpen && (
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onToggle}
className="h-full w-12 flex items-center justify-center hover:bg-gray-100">
<BookOpenIcon className="w-6 h-6 text-gray-600" />
</motion.button>
)}
{/* 展开的课程大纲 */}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="h-full flex flex-col">
{/* 标题栏 */}
<div className="p-4 border-b flex items-center justify-between">
<h2 className="text-xl font-semibold"></h2>
<button
onClick={onToggle}
className="p-2 hover:bg-gray-100 rounded-full">
<XMarkIcon className="w-6 h-6 text-gray-600" />
</button>
</div>
{/* 课程大纲内容 */}
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-4">
{/* 原有的 sections mapping 内容 */}
{sections.map((section) => (
<motion.div
key={section.id}
ref={(el) =>
(sectionRefs.current[section.id] =
el)
}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="border rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-md transition-shadow">
<button
className="w-full flex items-center justify-between p-4 hover:bg-gray-50 transition-colors"
onClick={() =>
toggleSection(section.id)
}>
<div className="flex items-center gap-4">
<span className="text-lg font-medium text-gray-700">
{Math.floor(section.order)}
</span>
<div>
<h3 className="text-left font-medium text-gray-900">
{section.title}
</h3>
<p className="text-sm text-gray-500">
{section.totalLectures}
·{" "}
{Math.floor(
section.totalDuration /
60
)}
</p>
</div>
</div>
<motion.div
animate={{
rotate: expandedSections.includes(
section.id
)
? 180
: 0,
}}
transition={{ duration: 0.2 }}>
<ChevronDownIcon className="w-5 h-5 text-gray-500" />
</motion.div>
</button>
<AnimatePresence>
{expandedSections.includes(
section.id
) && (
<motion.div
initial={{
height: 0,
opacity: 0,
}}
animate={{
height: "auto",
opacity: 1,
}}
exit={{
height: 0,
opacity: 0,
}}
transition={{
duration: 0.3,
}}
className="border-t">
{section.lectures.map(
(lecture) => (
<motion.button
key={lecture.id}
initial={{
opacity: 0,
x: -20,
}}
animate={{
opacity: 1,
x: 0,
}}
className="w-full flex items-center gap-4 p-4 hover:bg-gray-50 text-left transition-colors"
onClick={() =>
onLectureClick?.(
lecture.id
)
}>
<PlayCircleIcon className="w-5 h-5 text-blue-500 flex-shrink-0" />
<div className="flex-grow">
<h4 className="font-medium text-gray-800">
{
lecture.title
}
</h4>
{lecture.description && (
<p className="text-sm text-gray-500 mt-1">
{
lecture.description
}
</p>
)}
</div>
<div className="flex items-center gap-1 text-sm text-gray-500">
<ClockIcon className="w-4 h-4" />
<span>
{
lecture.duration
}
</span>
</div>
</motion.button>
)
)}
</motion.div>
)}
</AnimatePresence>
</motion.div>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};