Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
35c9fab24b
|
@ -67,5 +67,5 @@ yarn-error.log*
|
|||
|
||||
# Ignore .idea files in the Expo monorepo
|
||||
**/.idea/
|
||||
|
||||
uploads
|
||||
uploads
|
||||
packages/mind-elixir-core
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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`
|
||||
: ""
|
||||
}`}>
|
||||
{/* 内容区域 */}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -61,7 +61,6 @@ export const CourseDetailDescription: React.FC = () => {
|
|||
expandable: true,
|
||||
symbol: "展开",
|
||||
onExpand: () => console.log("展开"),
|
||||
// collapseText: "收起",
|
||||
}}>
|
||||
{course?.content}
|
||||
</Paragraph>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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} />
|
||||
))}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue