215 lines
6.1 KiB
TypeScript
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>
|
|
);
|
|
};
|