This commit is contained in:
ditiqi 2025-02-26 10:19:29 +08:00
parent eb2df7d2eb
commit 7f2cec371b
11 changed files with 135 additions and 90 deletions

View File

@ -2,7 +2,7 @@ import { useContext, useState } from "react";
import { Input, Layout, Avatar, Button, Dropdown } from "antd"; import { Input, Layout, Avatar, Button, Dropdown } from "antd";
import { EditFilled, SearchOutlined, UserOutlined } from "@ant-design/icons"; import { EditFilled, SearchOutlined, UserOutlined } from "@ant-design/icons";
import { useAuth } from "@web/src/providers/auth-provider"; import { useAuth } from "@web/src/providers/auth-provider";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { UserMenu } from "./UserMenu/UserMenu"; import { UserMenu } from "./UserMenu/UserMenu";
import { NavigationMenu } from "./NavigationMenu"; import { NavigationMenu } from "./NavigationMenu";
import { useMainContext } from "./MainProvider"; import { useMainContext } from "./MainProvider";
@ -10,6 +10,7 @@ const { Header } = Layout;
export function MainHeader() { export function MainHeader() {
const { isAuthenticated, user } = useAuth(); const { isAuthenticated, user } = useAuth();
const { id } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const { searchValue, setSearchValue } = useMainContext(); const { searchValue, setSearchValue } = useMainContext();
return ( return (
@ -52,10 +53,15 @@ export function MainHeader() {
{isAuthenticated && ( {isAuthenticated && (
<> <>
<Button <Button
onClick={() => navigate("/course/editor")} onClick={() => {
const url = id
? `/course/${id}/editor`
: "/course/editor";
navigate(url);
}}
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all" className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
icon={<EditFilled />}> icon={<EditFilled />}>
{id ? "编辑课程" : "创建课程"}
</Button> </Button>
</> </>
)} )}

View File

@ -28,9 +28,15 @@ const CollapsibleContent: React.FC<CollapsibleContentProps> = ({
{/* 包装整个内容区域的容器 */} {/* 包装整个内容区域的容器 */}
<div <div
ref={contentWrapperRef} ref={contentWrapperRef}
style={{
maxHeight:
shouldCollapse && !isExpanded
? maxHeight
: undefined,
}}
className={`duration-300 ${ className={`duration-300 ${
shouldCollapse && !isExpanded shouldCollapse && !isExpanded
? `max-h-[${maxHeight}px] overflow-hidden relative` ? ` overflow-hidden relative`
: "" : ""
}`}> }`}>
{/* 内容区域 */} {/* 内容区域 */}

View File

@ -7,7 +7,7 @@ import {
} from "@nice/common"; } from "@nice/common";
import { useAuth } from "@web/src/providers/auth-provider"; import { useAuth } from "@web/src/providers/auth-provider";
import React, { createContext, ReactNode, useEffect, useState } from "react"; import React, { createContext, ReactNode, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
interface CourseDetailContextType { interface CourseDetailContextType {
editId?: string; // 添加 editId editId?: string; // 添加 editId
@ -33,6 +33,7 @@ export function CourseDetailProvider({
const navigate = useNavigate(); const navigate = useNavigate();
const { read } = useVisitor(); const { read } = useVisitor();
const { user } = useAuth(); const { user } = useAuth();
const { lectureId } = useParams();
const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } =
(api.post as any).findFirst.useQuery( (api.post as any).findFirst.useQuery(
{ {
@ -47,7 +48,7 @@ export function CourseDetailProvider({
const [selectedLectureId, setSelectedLectureId] = useState< const [selectedLectureId, setSelectedLectureId] = useState<
string | undefined string | undefined
>(undefined); >(lectureId || undefined);
const { data: lecture, isLoading: lectureIsLoading } = ( const { data: lecture, isLoading: lectureIsLoading } = (
api.post as any api.post as any
).findFirst.useQuery( ).findFirst.useQuery(

View File

@ -61,7 +61,6 @@ export const CourseDetailDescription: React.FC = () => {
expandable: true, expandable: true,
symbol: "展开", symbol: "展开",
onExpand: () => console.log("展开"), onExpand: () => console.log("展开"),
// collapseText: "收起",
}}> }}>
{course?.content} {course?.content}
</Paragraph> </Paragraph>

View File

@ -51,7 +51,7 @@ export const CourseDetailDisplayArea: React.FC = () => {
<div className="w-full bg-white shadow-md rounded-lg border border-gray-200 p-6 "> <div className="w-full bg-white shadow-md rounded-lg border border-gray-200 p-6 ">
<CollapsibleContent <CollapsibleContent
content={lecture?.content || ""} content={lecture?.content || ""}
maxHeight={150} // Optional, defaults to 150 maxHeight={500} // Optional, defaults to 150
/> />
</div> </div>
</div> </div>

View File

@ -7,7 +7,7 @@ import {
UserOutlined, UserOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useAuth } from "@web/src/providers/auth-provider"; import { useAuth } from "@web/src/providers/auth-provider";
import { useNavigate } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu"; import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu";
import { CourseDetailContext } from "../CourseDetailContext"; import { CourseDetailContext } from "../CourseDetailContext";
@ -15,7 +15,8 @@ const { Header } = Layout;
export function CourseDetailHeader() { export function CourseDetailHeader() {
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const { isAuthenticated, user } = useAuth(); const { id } = useParams();
const { isAuthenticated, user, hasSomePermissions } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const { course } = useContext(CourseDetailContext); const { course } = useContext(CourseDetailContext);
@ -51,10 +52,15 @@ export function CourseDetailHeader() {
{isAuthenticated && ( {isAuthenticated && (
<> <>
<Button <Button
onClick={() => navigate("/course/editor")} onClick={() => {
const url = id
? `/course/${id}/editor`
: "/course/editor";
navigate(url);
}}
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all" className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
icon={<EditFilled />}> icon={<EditFilled />}>
{"编辑课程"}
</Button> </Button>
</> </>
)} )}

View File

@ -1,7 +1,7 @@
// components/CourseSyllabus/LectureItem.tsx // components/CourseSyllabus/LectureItem.tsx
import { Lecture, LectureType } from "@nice/common"; import { Lecture, LectureType, LessonTypeLabel } from "@nice/common";
import React from "react"; import React, { useMemo } from "react";
import { import {
ClockCircleOutlined, ClockCircleOutlined,
FileTextOutlined, FileTextOutlined,
@ -19,15 +19,24 @@ export const LectureItem: React.FC<LectureItemProps> = ({
onClick, onClick,
}) => { }) => {
const { lectureId } = useParams(); const { lectureId } = useParams();
const isReading = useMemo(() => {
return lecture?.id === lectureId;
}, [lectureId, lecture]);
return ( return (
<div <div
className="w-full flex items-center gap-4 p-4 hover:bg-gray-50 text-left transition-colors cursor-pointer" className="w-full flex items-center gap-4 p-4 hover:bg-gray-200 text-left transition-colors cursor-pointer"
onClick={() => onClick(lecture.id)}> onClick={() => onClick(lecture.id)}>
{lecture.type === LectureType.VIDEO && ( {lecture?.meta?.type === LectureType.VIDEO && (
<PlayCircleOutlined className="w-5 h-5 text-blue-500 flex-shrink-0" /> <div className="text-blue-500 flex items-center">
<PlayCircleOutlined className="w-5 h-5 flex-shrink-0" />
<span>{LessonTypeLabel[lecture?.meta?.type]}</span>
</div>
)} )}
{lecture.type === LectureType.ARTICLE && ( {lecture?.meta?.type === LectureType.ARTICLE && (
<FileTextOutlined className="w-5 h-5 text-blue-500 flex-shrink-0" /> // 为文章类型添加图标 <div className="text-blue-500 flex items-center">
<FileTextOutlined className="w-5 h-5 text-blue-500 flex-shrink-0" />{" "}
<span>{LessonTypeLabel[lecture?.meta?.type]}</span>
</div>
)} )}
<div className="flex-grow"> <div className="flex-grow">
<h4 className="font-medium text-gray-800">{lecture.title}</h4> <h4 className="font-medium text-gray-800">{lecture.title}</h4>
@ -37,10 +46,6 @@ export const LectureItem: React.FC<LectureItemProps> = ({
</p> </p>
)} )}
</div> </div>
{/* <div className="flex items-center gap-1 text-sm text-gray-500">
<ClockCircleOutlined className="w-4 h-4" />
<span>{lecture.duration}</span>
</div> */}
</div> </div>
); );
}; };

View File

@ -1,10 +1,9 @@
import { ChevronDownIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon } from "@heroicons/react/24/outline";
import { SectionDto } from "@nice/common"; import { SectionDto } from "@nice/common";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import React from "react"; import React, { useMemo } from "react";
import { LectureItem } from "./LectureItem"; import { LectureItem } from "./LectureItem";
import { useParams } from "react-router-dom";
// components/CourseSyllabus/SectionItem.tsx
interface SectionItemProps { interface SectionItemProps {
section: SectionDto; section: SectionDto;
index?: number; index?: number;
@ -13,57 +12,68 @@ interface SectionItemProps {
onLectureClick: (lectureId: string) => void; onLectureClick: (lectureId: string) => void;
ref: React.RefObject<HTMLDivElement>; ref: React.RefObject<HTMLDivElement>;
} }
export const SectionItem = React.forwardRef<HTMLDivElement, SectionItemProps>( export const SectionItem = React.forwardRef<HTMLDivElement, SectionItemProps>(
({ section, index, isExpanded, onToggle, onLectureClick }, ref) => ( ({ section, index, isExpanded, onToggle, onLectureClick }, ref) => {
<div const { lectureId } = useParams();
ref={ref} const isReading = useMemo(() => {
// initial={{ opacity: 0, y: 20 }} return (section?.lectures || [])
// animate={{ opacity: 1, y: 0 }} ?.map((lecture) => lecture?.id)
// transition={{ duration: 0.3 }} .includes(lectureId);
className="border rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-md transition-shadow"> }, [lectureId, section]);
<button return (
className="w-full flex items-center justify-between p-4 hover:bg-gray-50 transition-colors" <div
onClick={() => onToggle(section.id)}> ref={ref}
<div className="flex items-center gap-4"> className="border rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-md transition-shadow">
<span className="text-lg font-medium text-gray-700"> <div
{index} className="w-full flex items-center justify-between p-4 hover:bg-gray-50 transition-colors"
</span> onClick={() => onToggle(section.id)}>
<div className="flex flex-col items-start"> <div className="flex items-center gap-4">
<h3 className="text-left font-medium text-gray-900"> <span className="text-lg font-medium text-gray-700">
{section.title} {index}
</h3> </span>
<p className="text-sm text-gray-500"> <div className="flex flex-col items-start">
{section?.lectures?.length} ·{" "} <h3 className="text-left font-medium text-gray-900">
{/* {Math.floor(section?.totalDuration / 60)}分钟 */} {section.title}
</p> </h3>
<p className="text-sm text-gray-500">
{section?.lectures?.length} ·
</p>
</div>
</div>
<div className=" flex justify-end gap-2">
{isReading && (
<span className="text-primary text-sm">
</span>
)}
<motion.div
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.2 }}>
<ChevronDownIcon className="w-5 h-5 text-gray-500" />
</motion.div>
</div> </div>
</div> </div>
<motion.div
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.2 }}>
<ChevronDownIcon className="w-5 h-5 text-gray-500" />
</motion.div>
</button>
<AnimatePresence> <AnimatePresence>
{isExpanded && ( {isExpanded && (
<motion.div <motion.div
initial={{ height: 0, opacity: 0 }} initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }} animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }} exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="border-t"> className="border-t">
{section.lectures.map((lecture) => ( {section.lectures.map((lecture) => (
<LectureItem <LectureItem
key={lecture.id} key={lecture.id}
lecture={lecture} lecture={lecture}
onClick={onLectureClick} onClick={onLectureClick}
/> />
))} ))}
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
</div> </div>
) );
}
); );

View File

@ -237,7 +237,10 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
{isContentVisible && {isContentVisible &&
!editing && // Conditionally render content based on type !editing && // Conditionally render content based on type
(field?.meta?.type === LectureType.ARTICLE ? ( (field?.meta?.type === LectureType.ARTICLE ? (
<CollapsibleContent content={field?.content} /> <CollapsibleContent
maxHeight={200}
content={field?.content}
/>
) : ( ) : (
<VideoPlayer src={field?.meta?.videoUrl} /> <VideoPlayer src={field?.meta?.videoUrl} />
))} ))}

View File

@ -25,7 +25,7 @@ export default function CourseEditorLayout() {
const navigate = useNavigate(); const navigate = useNavigate();
const handleNavigation = (item: NavItem, index: number) => { const handleNavigation = (item: NavItem, index: number) => {
setSelectedSection(index); setSelectedSection(index);
navigate(item.path, { replace: true }); navigate(item.path);
}; };
return ( return (
<CourseFormProvider editId={id}> <CourseFormProvider editId={id}>

View File

@ -14,7 +14,7 @@ interface CollapsibleSectionProps {
interface MenuItem { interface MenuItem {
key: string; key: string;
link?: string; link?: string;
blank?: boolean blank?: boolean;
icon?: React.ReactNode; icon?: React.ReactNode;
label: string; label: string;
children?: Array<MenuItem>; children?: Array<MenuItem>;
@ -69,7 +69,10 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
const isChildCollapsed = !expandedSections[item.key]; const isChildCollapsed = !expandedSections[item.key];
return ( return (
<div key={item.key} className="flex flex-col mb-2 select-none" style={{ color: token.colorTextLightSolid }}> <div
key={item.key}
className="flex flex-col mb-2 select-none"
style={{ color: token.colorTextLightSolid }}>
<motion.div <motion.div
className={`flex items-center justify-between px-4 py-2 rounded-full ${hasChildren ? "cursor-pointer" : ""} `} className={`flex items-center justify-between px-4 py-2 rounded-full ${hasChildren ? "cursor-pointer" : ""} `}
onClick={() => { onClick={() => {
@ -78,7 +81,7 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
} }
if (item.link) { if (item.link) {
if (!item.blank) { if (!item.blank) {
navigate(item.link, { replace: true }); navigate(item.link);
} else { } else {
window.open(item.link, "_blank"); window.open(item.link, "_blank");
} }
@ -86,12 +89,20 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
}} }}
initial={false} initial={false}
animate={{ animate={{
backgroundColor: isActive ? token.colorPrimaryBorder : token.colorPrimary, backgroundColor: isActive
? token.colorPrimaryBorder
: token.colorPrimary,
}} }}
whileHover={{ backgroundColor: token.colorPrimaryHover }} whileHover={{
transition={{ type: "spring", stiffness: 300, damping: 25, duration: 0.3 }} backgroundColor: token.colorPrimaryHover,
style={{ marginLeft: `${level * 16}px` }} }}
> transition={{
type: "spring",
stiffness: 300,
damping: 25,
duration: 0.3,
}}
style={{ marginLeft: `${level * 16}px` }}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className=" items-center flex gap-2"> <div className=" items-center flex gap-2">
{item.icon && <span>{item.icon}</span>} {item.icon && <span>{item.icon}</span>}
@ -100,8 +111,7 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
{hasChildren && ( {hasChildren && (
<Icon <Icon
name={"caret-right"} name={"caret-right"}
className={`ml-1 transition-transform duration-300 ${!isChildCollapsed ? "rotate-90" : ""}`} className={`ml-1 transition-transform duration-300 ${!isChildCollapsed ? "rotate-90" : ""}`}></Icon>
></Icon>
)} )}
</div> </div>
{item.extra && <div className="ml-4">{item.extra}</div>} {item.extra && <div className="ml-4">{item.extra}</div>}
@ -115,11 +125,10 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
// stiffness: 200, // stiffness: 200,
// damping: 20, // damping: 20,
type: "tween", type: "tween",
duration: 0.2 duration: 0.2,
}} }}
style={{ overflow: "hidden" }} style={{ overflow: "hidden" }}
className="mt-1" className="mt-1">
>
{renderItems(item.children, level + 1)} {renderItems(item.children, level + 1)}
</motion.div> </motion.div>
)} )}