This commit is contained in:
Rao 2025-02-26 10:20:11 +08:00
commit 35c9fab24b
12 changed files with 137 additions and 92 deletions

4
.gitignore vendored
View File

@ -67,5 +67,5 @@ yarn-error.log*
# Ignore .idea files in the Expo monorepo
**/.idea/
uploads
uploads
packages/mind-elixir-core

View File

@ -2,7 +2,7 @@ import { useContext, useState } from "react";
import { Input, Layout, Avatar, Button, Dropdown } from "antd";
import { EditFilled, SearchOutlined, UserOutlined } from "@ant-design/icons";
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 { NavigationMenu } from "./NavigationMenu";
import { useMainContext } from "./MainProvider";
@ -10,6 +10,7 @@ const { Header } = Layout;
export function MainHeader() {
const { isAuthenticated, user } = useAuth();
const { id } = useParams();
const navigate = useNavigate();
const { searchValue, setSearchValue } = useMainContext();
return (
@ -52,10 +53,15 @@ export function MainHeader() {
{isAuthenticated && (
<>
<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"
icon={<EditFilled />}>
{id ? "编辑课程" : "创建课程"}
</Button>
</>
)}

View File

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

View File

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

View File

@ -61,7 +61,6 @@ export const CourseDetailDescription: React.FC = () => {
expandable: true,
symbol: "展开",
onExpand: () => console.log("展开"),
// collapseText: "收起",
}}>
{course?.content}
</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 ">
<CollapsibleContent
content={lecture?.content || ""}
maxHeight={150} // Optional, defaults to 150
maxHeight={500} // Optional, defaults to 150
/>
</div>
</div>

View File

@ -7,7 +7,7 @@ import {
UserOutlined,
} from "@ant-design/icons";
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 { CourseDetailContext } from "../CourseDetailContext";
@ -15,7 +15,8 @@ const { Header } = Layout;
export function CourseDetailHeader() {
const [searchValue, setSearchValue] = useState("");
const { isAuthenticated, user } = useAuth();
const { id } = useParams();
const { isAuthenticated, user, hasSomePermissions } = useAuth();
const navigate = useNavigate();
const { course } = useContext(CourseDetailContext);
@ -51,10 +52,15 @@ export function CourseDetailHeader() {
{isAuthenticated && (
<>
<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"
icon={<EditFilled />}>
{"编辑课程"}
</Button>
</>
)}

View File

@ -1,7 +1,7 @@
// components/CourseSyllabus/LectureItem.tsx
import { Lecture, LectureType } from "@nice/common";
import React from "react";
import { Lecture, LectureType, LessonTypeLabel } from "@nice/common";
import React, { useMemo } from "react";
import {
ClockCircleOutlined,
FileTextOutlined,
@ -19,15 +19,24 @@ export const LectureItem: React.FC<LectureItemProps> = ({
onClick,
}) => {
const { lectureId } = useParams();
const isReading = useMemo(() => {
return lecture?.id === lectureId;
}, [lectureId, lecture]);
return (
<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)}>
{lecture.type === LectureType.VIDEO && (
<PlayCircleOutlined className="w-5 h-5 text-blue-500 flex-shrink-0" />
{lecture?.meta?.type === LectureType.VIDEO && (
<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 && (
<FileTextOutlined className="w-5 h-5 text-blue-500 flex-shrink-0" /> // 为文章类型添加图标
{lecture?.meta?.type === LectureType.ARTICLE && (
<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">
<h4 className="font-medium text-gray-800">{lecture.title}</h4>
@ -37,10 +46,6 @@ export const LectureItem: React.FC<LectureItemProps> = ({
</p>
)}
</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>
);
};

View File

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

View File

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

View File

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

View File

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