From 0afb73a45820f082f1fa971c7640eb9c18e46f94 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Thu, 20 Feb 2025 20:02:27 +0800 Subject: [PATCH 01/12] add --- .../server/src/models/post/post.controller.ts | 3 +- apps/server/src/models/post/utils.ts | 27 + .../main/courses/components/FilterSection.tsx | 92 +-- apps/web/src/app/main/courses/page.tsx | 161 +++-- .../course/detail/CourseDetailContext.tsx | 5 +- .../course/detail/CourseDetailLayout.tsx | 3 +- .../editor/context/CourseEditorContext.tsx | 23 +- .../course/editor/form/CourseBasicForm.tsx | 3 +- .../editor/form/CourseContentForm copy.tsx | 604 ------------------ .../form/CourseContentForm/LectureList.tsx | 18 +- .../CourseContentForm/SortableSection.tsx | 1 + .../components/models/term/term-editor.tsx | 2 +- .../video-player/ControlButtons/Speed.tsx | 2 +- packages/common/src/constants.ts | 9 +- packages/common/src/models/post.ts | 11 +- 15 files changed, 226 insertions(+), 738 deletions(-) delete mode 100644 apps/web/src/components/models/course/editor/form/CourseContentForm copy.tsx diff --git a/apps/server/src/models/post/post.controller.ts b/apps/server/src/models/post/post.controller.ts index e5731af..1e9eb77 100755 --- a/apps/server/src/models/post/post.controller.ts +++ b/apps/server/src/models/post/post.controller.ts @@ -6,6 +6,5 @@ import { db } from '@nice/common'; @Controller('post') export class PostController { - constructor(private readonly postService: PostService) { } - + constructor(private readonly postService: PostService) {} } diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 91af13e..e2fb89b 100644 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -123,3 +123,30 @@ export async function updateCourseEnrollmentStats(courseId: string) { }, }); } +export async function setCourseInfo(data: Post) { + if (data?.type === PostType.COURSE) { + const ancestries = await db.postAncestry.findMany({ + where: { + ancestorId: data.id, + }, + select: { + id: true, + descendant: true, + }, + }); + const descendants = ancestries.map((ancestry) => ancestry.descendant); + const sections = descendants.filter((descendant) => { + return ( + descendant.type === PostType.SECTION && descendant.parentId === data.id + ); + }); + const lectures = descendants.filter((descendant) => { + return ( + descendant.type === PostType.LECTURE && + sections.map((section) => section.id).includes(descendant.parentId) + ); + }); + } + + Object.assign(data, {}); +} diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index 93e0e6f..678c227 100644 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -1,54 +1,54 @@ -import { Checkbox, Divider, Radio, Space } from 'antd'; -import { categories, levels } from '../mockData'; +import { Checkbox, Divider, Radio, Space } from "antd"; +import { categories, levels } from "../mockData"; +import { api } from "@nice/client"; interface FilterSectionProps { - selectedCategory: string; - selectedLevel: string; - onCategoryChange: (category: string) => void; - onLevelChange: (level: string) => void; + selectedCategory: string; + selectedLevel: string; + onCategoryChange: (category: string) => void; + onLevelChange: (level: string) => void; } export default function FilterSection({ - selectedCategory, - selectedLevel, - onCategoryChange, - onLevelChange, + selectedCategory, + selectedLevel, + onCategoryChange, + onLevelChange, }: FilterSectionProps) { - return ( -
-
-

课程分类

- onCategoryChange(e.target.value)} - className="flex flex-col space-y-3" - > - 全部课程 - {categories.map(category => ( - - {category} - - ))} - -
+ // const { data } = api.term; + return ( +
+
+

课程分类

+ onCategoryChange(e.target.value)} + className="flex flex-col space-y-3"> + 全部课程 + {categories.map((category) => ( + + {category} + + ))} + +
- + -
-

难度等级

- onLevelChange(e.target.value)} - className="flex flex-col space-y-3" - > - 全部难度 - {levels.map(level => ( - - {level} - - ))} - -
-
- ); -} \ No newline at end of file +
+

难度等级

+ onLevelChange(e.target.value)} + className="flex flex-col space-y-3"> + 全部难度 + {levels.map((level) => ( + + {level} + + ))} + +
+
+ ); +} diff --git a/apps/web/src/app/main/courses/page.tsx b/apps/web/src/app/main/courses/page.tsx index f852e00..cf2162e 100644 --- a/apps/web/src/app/main/courses/page.tsx +++ b/apps/web/src/app/main/courses/page.tsx @@ -1,73 +1,100 @@ -import { useState, useMemo } from 'react'; -import { mockCourses } from './mockData'; -import FilterSection from './components/FilterSection'; -import CourseList from './components/CourseList'; +import { useState, useMemo } from "react"; +import { mockCourses } from "./mockData"; +import FilterSection from "./components/FilterSection"; +import CourseList from "./components/CourseList"; +import { api } from "@nice/client"; +import { LectureType, PostType } from "@nice/common"; export default function CoursesPage() { - const [currentPage, setCurrentPage] = useState(1); - const [selectedCategory, setSelectedCategory] = useState(''); - const [selectedLevel, setSelectedLevel] = useState(''); - const pageSize = 12; + const [currentPage, setCurrentPage] = useState(1); + const [selectedCategory, setSelectedCategory] = useState(""); + const [selectedLevel, setSelectedLevel] = useState(""); + const pageSize = 12; + const { data, isLoading } = api.post.findManyWithPagination.useQuery({ + where: { + type: PostType.COURSE, + terms: { + some: { + AND: [ + ...(selectedCategory + ? [ + { + name: selectedCategory, + }, + ] + : []), + ...(selectedLevel + ? [ + { + name: selectedLevel, + }, + ] + : []), + ], + }, + }, + }, + }); + const filteredCourses = useMemo(() => { + return mockCourses.filter((course) => { + const matchCategory = + !selectedCategory || course.category === selectedCategory; + const matchLevel = !selectedLevel || course.level === selectedLevel; + return matchCategory && matchLevel; + }); + }, [selectedCategory, selectedLevel]); - const filteredCourses = useMemo(() => { - return mockCourses.filter(course => { - const matchCategory = !selectedCategory || course.category === selectedCategory; - const matchLevel = !selectedLevel || course.level === selectedLevel; - return matchCategory && matchLevel; - }); - }, [selectedCategory, selectedLevel]); + const paginatedCourses = useMemo(() => { + const startIndex = (currentPage - 1) * pageSize; + return filteredCourses.slice(startIndex, startIndex + pageSize); + }, [filteredCourses, currentPage]); - const paginatedCourses = useMemo(() => { - const startIndex = (currentPage - 1) * pageSize; - return filteredCourses.slice(startIndex, startIndex + pageSize); - }, [filteredCourses, currentPage]); + const handlePageChange = (page: number) => { + setCurrentPage(page); + window.scrollTo({ top: 0, behavior: "smooth" }); + }; - const handlePageChange = (page: number) => { - setCurrentPage(page); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }; + return ( +
+
+
+ {/* 左侧筛选区域 */} +
+
+ { + setSelectedCategory(category); + setCurrentPage(1); + }} + onLevelChange={(level) => { + setSelectedLevel(level); + setCurrentPage(1); + }} + /> +
+
- return ( -
-
-
- {/* 左侧筛选区域 */} -
-
- { - setSelectedCategory(category); - setCurrentPage(1); - }} - onLevelChange={level => { - setSelectedLevel(level); - setCurrentPage(1); - }} - /> -
-
- - {/* 右侧课程列表区域 */} -
-
-
- - 共找到 {filteredCourses.length} 门课程 - -
- -
-
-
-
-
- ); -} \ No newline at end of file + {/* 右侧课程列表区域 */} +
+
+
+ + 共找到 {filteredCourses.length} 门课程 + +
+ +
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 3458a1f..dbb7b87 100644 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -1,8 +1,7 @@ -import { api,} from "@nice/client"; +import { api } from "@nice/client"; import { courseDetailSelect, CourseDto } from "@nice/common"; import React, { createContext, ReactNode, useState } from "react"; - interface CourseDetailContextType { editId?: string; // 添加 editId course?: CourseDto; @@ -23,7 +22,7 @@ export function CourseDetailProvider({ editId, }: CourseFormProviderProps) { const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = - api.course.findFirst.useQuery( + (api.post as any).findFirst.useQuery( { where: { id: editId }, include: { diff --git a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx index 4f4ecd5..d1fd37c 100644 --- a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx @@ -16,7 +16,8 @@ export default function CourseDetailLayout() { const [isSyllabusOpen, setIsSyllabusOpen] = useState(false); return (
- {/* 添加 Header 组件 */} + + {/* 添加 Header 组件 */} {/* 主内容区域 */} {/* 为了防止 Header 覆盖内容,添加上边距 */}
diff --git a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx index 26c9e09..fc3dc51 100644 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -43,7 +43,12 @@ export function CourseFormProvider({ const [form] = Form.useForm(); const { create, update, createCourse } = usePost(); const { data: course }: { data: CourseDto } = api.post.findFirst.useQuery( - { where: { id: editId } }, + { + where: { id: editId }, + include: { + terms: true, + }, + }, { enabled: Boolean(editId) } ); const { @@ -51,7 +56,7 @@ export function CourseFormProvider({ }: { data: Taxonomy[]; } = api.taxonomy.getAll.useQuery({ - // type: ObjectType.COURSE, + type: ObjectType.COURSE, }); const navigate = useNavigate(); @@ -65,6 +70,9 @@ export function CourseFormProvider({ requirements: course?.meta?.requirements, objectives: course?.meta?.objectives, }; + course.terms?.forEach((term) => { + formData[term.taxonomyId] = term.id; // 假设 taxonomyName 是您在 Form.Item 中使用的 name + }); form.setFieldsValue(formData); } }, [course, form]); @@ -72,13 +80,24 @@ export function CourseFormProvider({ const onSubmit = async (values: CourseFormData) => { console.log(values); const sections = values?.sections || []; + const termIds = taxonomies + .map((tax) => values[tax.id]) // 获取每个 taxonomy 对应的选中值 + .filter((id) => id); // 过滤掉空值 + const formattedValues = { ...values, meta: { requirements: values.requirements, objectives: values.objectives, }, + terms: { + connect: termIds.map((id) => ({ id })), // 转换成 connect 格式 + }, }; + // 删除原始的 taxonomy 字段 + taxonomies.forEach((tax) => { + delete formattedValues[tax.name]; + }); delete formattedValues.requirements; delete formattedValues.objectives; delete formattedValues.sections; diff --git a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx index 754004d..14eae28 100644 --- a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx @@ -14,6 +14,7 @@ export function CourseBasicForm() { value: key as CourseLevel, }) ); + const { form, taxonomies } = useCourseEditor(); return (
@@ -50,7 +51,7 @@ export function CourseBasicForm() { }, ]} label={tax.name} - name={tax.name} + name={tax.id} key={index}> diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm copy.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm copy.tsx deleted file mode 100644 index 8ab2ad2..0000000 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm copy.tsx +++ /dev/null @@ -1,604 +0,0 @@ -import { - PlusOutlined, - DragOutlined, - DeleteOutlined, - CaretRightOutlined, - SaveOutlined, -} from "@ant-design/icons"; -import { - Form, - Alert, - Button, - Input, - Select, - Space, - Collapse, - message, -} from "antd"; -import React, { useCallback, useEffect, useState } from "react"; -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - DragEndEvent, -} from "@dnd-kit/core"; -import { api, emitDataChange } from "@nice/client"; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - useSortable, - verticalListSortingStrategy, -} from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import QuillEditor from "../../../../common/editor/quill/QuillEditor"; -import { TusUploader } from "../../../../common/uploader/TusUploader"; -import { Lecture, LectureType, PostType } from "@nice/common"; -import { useCourseEditor } from "../context/CourseEditorContext"; -import { usePost } from "@nice/client"; -import toast from "react-hot-toast"; -interface SectionData { - id: string; - title: string; - content?: string; - courseId?: string; -} - -interface LectureData { - id: string; - title: string; - meta?: { - type?: LectureType; - fieldIds?: []; - }; - content?: string; - sectionId?: string; -} -const CourseContentFormHeader = () => ( - -

通过组织清晰的章节和课时,帮助学员更好地学习。建议:

-
    -
  • 将相关内容组织到章节中
  • -
  • 每个章节建议包含 3-7 个课时
  • -
  • 课时可以是视频、文章或测验
  • -
- - } - className="mb-8" - /> -); - -const CourseSectionEmpty = () => ( -
-
- -

开始创建您的课程内容

-

点击下方按钮添加第一个章节

-
-
-); - -interface SortableSectionProps { - courseId?: string; - field: SectionData; - remove: () => void; - children: React.ReactNode; -} - -const SortableSection: React.FC = ({ - field, - remove, - courseId, - children, -}) => { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: field?.id }); - - const [form] = Form.useForm(); - const [editing, setEditing] = useState(field.id ? false : true); - const [loading, setLoading] = useState(false); - const { create, update } = usePost(); - - const handleSave = async () => { - if (!courseId) { - toast.error("课程未创建,请先填写课程基本信息完成创建"); - return; - } - try { - setLoading(true); - const values = await form.validateFields(); - let result; - try { - if (!field?.id) { - result = await create.mutateAsync({ - data: { - title: values?.title, - type: PostType.SECTION, - parentId: courseId, - }, - }); - } else { - result = await update.mutateAsync({ - data: { - title: values?.title, - }, - }); - } - } catch (err) { - console.log(err); - } - - field.id = result.id; - setEditing(false); - message.success("保存成功"); - } catch (error) { - console.log(error); - message.error("保存失败"); - } finally { - setLoading(false); - } - }; - - const style = { - transform: CSS.Transform.toString(transform), - transition, - backgroundColor: isDragging ? "#f5f5f5" : undefined, - }; - - return ( -
- - - - - - - - - - - ) : ( -
- - - {field.title || "未命名章节"} - - - - - -
- ) - } - key={field.id || "new"}> - {children} -
-
-
- ); -}; - -interface SortableLectureProps { - field: LectureData; - remove: () => void; - sectionFieldKey: string; -} - -const SortableLecture: React.FC = ({ - field, - remove, - sectionFieldKey, -}) => { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: field?.id }); - const { create, update } = usePost(); - const [form] = Form.useForm(); - const [editing, setEditing] = useState(field?.id ? false : true); - const [loading, setLoading] = useState(false); - const lectureType = - Form.useWatch(["meta", "type"], form) || LectureType.ARTICLE; - const handleSave = async () => { - try { - setLoading(true); - const values = await form.validateFields(); - let result; - try { - if (!field.id) { - result = await create.mutateAsync({ - data: { - parentId: sectionFieldKey, - type: PostType.LECTURE, - title: values?.title, - meta: { - type: values?.meta?.type, - fileIds: values?.meta?.fileIds, - }, - resources: { - connect: (values?.meta?.fileIds || []).map( - (fileId) => ({ - fileId, - }) - ), - }, - content: values?.content, - }, - }); - } else { - result = await update.mutateAsync({ - where: { - id: field?.id, - }, - data: { - title: values?.title, - meta: { - type: values?.meta?.type, - fieldIds: values?.meta?.fileIds, - }, - resources: { - connect: (values?.meta?.fileIds || []).map( - (fileId) => ({ - fileId, - }) - ), - }, - content: values?.content, - }, - }); - } - } catch (err) { - console.log(err); - } - - field.id = result.id; - setEditing(false); - message.success("保存成功"); - } catch (error) { - message.error("保存失败"); - } finally { - setLoading(false); - } - }; - - const style = { - transform: CSS.Transform.toString(transform), - transition, - borderBottom: "1px solid #f0f0f0", - backgroundColor: isDragging ? "#f5f5f5" : undefined, - }; - - return ( -
- {editing ? ( -
-
- - - - - } - placeholder="搜索课程" - className="w-72 rounded-full" - value={searchValue} - onChange={(e) => setSearchValue(e.target.value)} - /> -
- {isAuthenticated ? ( - } - trigger={['click']} - placement="bottomRight" - > - - {(user?.showname || user?.username || '')[0]?.toUpperCase()} - - - ) : ( - - )} -
-
- - ); -} \ No newline at end of file + return ( +
+
+
+
navigate("/")} + className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent hover:scale-105 transition-transform cursor-pointer"> + 烽火慕课 +
+ +
+
+
+ + } + placeholder="搜索课程" + className="w-72 rounded-full" + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} + /> +
+ {isAuthenticated && ( + <> + + + )} + {isAuthenticated ? ( + } + trigger={["click"]} + placement="bottomRight"> + + {(user?.showname || + user?.username || + "")[0]?.toUpperCase()} + + + ) : ( + + )} +
+
+
+ ); +} diff --git a/apps/web/src/components/models/course/detail/CourseDetail.tsx b/apps/web/src/components/models/course/detail/CourseDetail.tsx index 402fbd7..353ec4f 100644 --- a/apps/web/src/components/models/course/detail/CourseDetail.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetail.tsx @@ -1,7 +1,13 @@ import { CourseDetailProvider } from "./CourseDetailContext"; import CourseDetailLayout from "./CourseDetailLayout"; -export default function CourseDetail({ id }: { id?: string }) { +export default function CourseDetail({ + id, + lectureId, +}: { + id?: string; + lectureId?: string; +}) { const iframeStyle = { width: "50%", height: "100vh", diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index dbb7b87..60d1bd3 100644 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -21,12 +21,13 @@ export function CourseDetailProvider({ children, editId, }: CourseFormProviderProps) { + const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = (api.post as any).findFirst.useQuery( { where: { id: editId }, include: { - sections: { include: { lectures: true } }, + // sections: { include: { lectures: true } }, enrollments: true, }, }, diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx index 58212a1..aef7064 100644 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx @@ -7,11 +7,12 @@ import { import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; import React, { useState, useRef, useContext } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import { SectionDto } from "@nice/common"; +import { SectionDto, TaxonomySlug } from "@nice/common"; import { SyllabusHeader } from "./SyllabusHeader"; import { SectionItem } from "./SectionItem"; import { CollapsedButton } from "./CollapsedButton"; import { CourseDetailContext } from "../CourseDetailContext"; +import { api } from "@nice/client"; interface CourseSyllabusProps { sections: SectionDto[]; @@ -29,7 +30,11 @@ export const CourseSyllabus: React.FC = ({ const { isHeaderVisible } = useContext(CourseDetailContext); const [expandedSections, setExpandedSections] = useState([]); const sectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); - + // api.term.findMany.useQuery({ + // where: { + // taxonomy: { slug: TaxonomySlug.CATEGORY }, + // }, + // }); const toggleSection = (sectionId: string) => { setExpandedSections((prev) => prev.includes(sectionId) diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx index 9a87d75..0999f3c 100644 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -1,9 +1,8 @@ // components/CourseSyllabus/LectureItem.tsx -import { Lecture } from "@nice/common"; +import { Lecture, LectureType } from "@nice/common"; import React from "react"; -import { motion } from "framer-motion"; -import { ClockIcon, PlayCircleIcon } from "@heroicons/react/24/outline"; +import { ClockCircleOutlined, FileTextOutlined, PlayCircleOutlined } from "@ant-design/icons"; // 使用 Ant Design 图标 interface LectureItemProps { lecture: Lecture; @@ -14,23 +13,24 @@ export const LectureItem: React.FC = ({ lecture, onClick, }) => ( - onClick(lecture.id)}> - + {lecture.type === LectureType.VIDEO && ( + + )} + {lecture.type === LectureType.ARTICLE && ( + // 为文章类型添加图标 + )}

{lecture.title}

- {lecture.description && ( -

- {lecture.description} -

+ {lecture.subTitle && ( +

{lecture.subTitle}

)}
- + {lecture.duration}分钟
-
+
); diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx index 100fb6e..95c61db 100644 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx @@ -33,8 +33,8 @@ export const SectionItem = React.forwardRef( {section.title}

- {section.totalLectures}节课 ·{" "} - {Math.floor(section.totalDuration / 60)}分钟 + {section?.lectures?.length}节课 ·{" "} + {/* {Math.floor(section?.totalDuration / 60)}分钟 */}

diff --git a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx index fc3dc51..efa4962 100644 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -96,7 +96,7 @@ export function CourseFormProvider({ }; // 删除原始的 taxonomy 字段 taxonomies.forEach((tax) => { - delete formattedValues[tax.name]; + delete formattedValues[tax.id]; }); delete formattedValues.requirements; delete formattedValues.objectives; diff --git a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx index 14eae28..f038144 100644 --- a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx @@ -53,7 +53,9 @@ export function CourseBasicForm() { label={tax.name} name={tax.id} key={index}> - + ))} {/* diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index cd51fcf..cee94f9 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -56,11 +56,10 @@ export const routes: CustomRouteObject[] = [ { index: true, element: , - }, { path: "paths", - element: + element: , }, { path: "courses", @@ -125,7 +124,7 @@ export const routes: CustomRouteObject[] = [ ], }, { - path: ":id?/detail", // 使用 ? 表示 id 参数是可选的 + path: ":id?/detail/:lectureId?", // 使用 ? 表示 id 参数是可选的 element: , }, ], diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 8ad7da3..103ea10 100644 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -198,7 +198,8 @@ model Post { terms Term[] @relation("post_term") order Float? @default(0) @map("order") duration Int? - + rating Int? @default(0) + // 索引 // 日期时间类型字段 createdAt DateTime @default(now()) @map("created_at") publishedAt DateTime? @map("published_at") // 发布时间 @@ -242,7 +243,6 @@ model PostAncestry { relDepth Int @map("rel_depth") ancestor Post? @relation("AncestorPosts", fields: [ancestorId], references: [id]) descendant Post @relation("DescendantPosts", fields: [descendantId], references: [id]) - // 复合索引优化 // 索引建议 @@index([ancestorId]) // 针对祖先的查询 diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index c642ca9..9598a55 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -6,8 +6,8 @@ export enum PostType { POST_COMMENT = "post_comment", COURSE_REVIEW = "course_review", COURSE = "couse", - LECTURE = "lecture", SECTION = "section", + LECTURE = "lecture", } export enum LectureType { VIDEO = "video", From fb026e1fde3f08a9a216ab9a8a56e1344bd12210 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Fri, 21 Feb 2025 13:20:13 +0800 Subject: [PATCH 07/12] add --- .../models/course/detail/CourseDetailContext.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 60d1bd3..8540e54 100644 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -1,6 +1,7 @@ import { api } from "@nice/client"; import { courseDetailSelect, CourseDto } from "@nice/common"; -import React, { createContext, ReactNode, useState } from "react"; +import React, { createContext, ReactNode, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; interface CourseDetailContextType { editId?: string; // 添加 editId @@ -21,7 +22,7 @@ export function CourseDetailProvider({ children, editId, }: CourseFormProviderProps) { - + const navigate = useNavigate(); const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = (api.post as any).findFirst.useQuery( { @@ -36,6 +37,10 @@ export function CourseDetailProvider({ const [selectedLectureId, setSelectedLectureId] = useState< string | undefined >(undefined); + useEffect(() => { + navigate(``) + + }, [selectedLectureId, editId]); const [isHeaderVisible, setIsHeaderVisible] = useState(true); // 新增 return ( Date: Fri, 21 Feb 2025 13:20:15 +0800 Subject: [PATCH 08/12] 02211320 --- packages/common/prisma/schema.prisma | 125 +++++++++++++++------------ 1 file changed, 71 insertions(+), 54 deletions(-) diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 8ad7da3..1042412 100644 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -44,7 +44,8 @@ model Term { createdBy String? @map("created_by") depts Department[] @relation("department_term") hasChildren Boolean? @default(false) @map("has_children") - posts Post[] @relation("post_term") + posts Post[] @relation("post_term") + @@index([name]) // 对name字段建立索引,以加快基于name的查找速度 @@index([parentId]) // 对parentId字段建立索引,以加快基于parentId的查找速度 @@map("term") @@ -87,14 +88,14 @@ model Staff { deletedAt DateTime? @map("deleted_at") officerId String? @map("officer_id") - watchedPost Post[] @relation("post_watch_staff") + watchedPost Post[] @relation("post_watch_staff") visits Visit[] posts Post[] - sentMsgs Message[] @relation("message_sender") - receivedMsgs Message[] @relation("message_receiver") + sentMsgs Message[] @relation("message_sender") + receivedMsgs Message[] @relation("message_receiver") registerToken String? enrollments Enrollment[] - teachedPosts PostInstructor[] + teachedPosts PostInstructor[] ownedResources Resource[] @@index([officerId]) @@ -186,41 +187,42 @@ model AppConfig { model Post { // 字符串类型字段 - id String @id @default(cuid()) // 帖子唯一标识,使用 cuid() 生成默认值 + id String @id @default(cuid()) // 帖子唯一标识,使用 cuid() 生成默认值 type String? // Post类型,课程、章节、小节、讨论都用Post实现 - level String? - state String? + level String? + state String? title String? // 帖子标题,可为空 - subTitle String? + subTitle String? content String? // 帖子内容,可为空 - important Boolean? //是否重要/精选/突出 - domainId String? @map("domain_id") - terms Term[] @relation("post_term") - order Float? @default(0) @map("order") - duration Int? - + important Boolean? //是否重要/精选/突出 + domainId String? @map("domain_id") + terms Term[] @relation("post_term") + order Float? @default(0) @map("order") + duration Int? + // 日期时间类型字段 - createdAt DateTime @default(now()) @map("created_at") - publishedAt DateTime? @map("published_at") // 发布时间 - updatedAt DateTime @updatedAt @map("updated_at") - deletedAt DateTime? @map("deleted_at") // 删除时间,可为空 - instructors PostInstructor[] + createdAt DateTime @default(now()) @map("created_at") + publishedAt DateTime? @map("published_at") // 发布时间 + updatedAt DateTime @updatedAt @map("updated_at") + deletedAt DateTime? @map("deleted_at") // 删除时间,可为空 + instructors PostInstructor[] // 关系类型字段 - authorId String? @map("author_id") - author Staff? @relation(fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型 - enrollments Enrollment[] // 学生报名记录 - visits Visit[] // 访问记录,关联 Visit 模型 - parentId String? @map("parent_id") - parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型 - children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型 - hasChildren Boolean? @default(false) @map("has_children") - // 闭包表关系 - ancestors PostAncestry[] @relation("DescendantPosts") - descendants PostAncestry[] @relation("AncestorPosts") - resources Resource[] // 附件列表 - watchableStaffs Staff[] @relation("post_watch_staff") // 可观看的员工列表,关联 Staff 模型 - watchableDepts Department[] @relation("post_watch_dept") // 可观看的部门列表,关联 Department 模型 - meta Json? // 封面url 视频url objectives具体的学习目标 rating评分Int + authorId String? @map("author_id") + author Staff? @relation(fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型 + enrollments Enrollment[] // 学生报名记录 + visits Visit[] // 访问记录,关联 Visit 模型 + parentId String? @map("parent_id") + parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型 + children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型 + hasChildren Boolean? @default(false) @map("has_children") + // 闭包表关系 + ancestors PostAncestry[] @relation("DescendantPosts") + descendants PostAncestry[] @relation("AncestorPosts") + resources Resource[] // 附件列表 + watchableStaffs Staff[] @relation("post_watch_staff") // 可观看的员工列表,关联 Staff 模型 + watchableDepts Department[] @relation("post_watch_dept") // 可观看的部门列表,关联 Department 模型 + meta Json? // 封面url 视频url objectives具体的学习目标 rating评分Int + // 索引 @@index([type, domainId]) @@index([authorId, type]) @@ -236,15 +238,15 @@ model Post { } model PostAncestry { - id String @id @default(cuid()) - ancestorId String? @map("ancestor_id") - descendantId String @map("descendant_id") - relDepth Int @map("rel_depth") - ancestor Post? @relation("AncestorPosts", fields: [ancestorId], references: [id]) - descendant Post @relation("DescendantPosts", fields: [descendantId], references: [id]) + id String @id @default(cuid()) + ancestorId String? @map("ancestor_id") + descendantId String @map("descendant_id") + relDepth Int @map("rel_depth") + ancestor Post? @relation("AncestorPosts", fields: [ancestorId], references: [id]) + descendant Post @relation("DescendantPosts", fields: [descendantId], references: [id]) // 复合索引优化 - // 索引建议 + // 索引建议 @@index([ancestorId]) // 针对祖先的查询 @@index([descendantId]) // 针对后代的查询 @@index([ancestorId, descendantId]) // 组合索引,用于查询特定的祖先-后代关系 @@ -292,18 +294,16 @@ model Visit { // totalWatchTime Int? @default(0) @map("total_watch_time") // 总观看时长(秒) // // 时间记录 // lastWatchedAt DateTime? @map("last_watched_at") // 最后观看时间 - createdAt DateTime @default(now()) @map("created_at") // 创建时间 - updatedAt DateTime @updatedAt @map("updated_at") // 更新时间 + createdAt DateTime @default(now()) @map("created_at") // 创建时间 + updatedAt DateTime @updatedAt @map("updated_at") // 更新时间 deletedAt DateTime? @map("deleted_at") // 删除时间,可为空 - meta Json? + meta Json? @@index([postId, type, visitorId]) @@index([messageId, type, visitorId]) @@map("visit") } - - model Enrollment { id String @id @default(cuid()) @map("id") status String @map("status") @@ -316,8 +316,8 @@ model Enrollment { // 关联关系 student Staff @relation(fields: [studentId], references: [id]) studentId String @map("student_id") - post Post @relation(fields: [postId], references: [id]) - postId String @map("post_id") + post Post @relation(fields: [postId], references: [id]) + postId String @map("post_id") @@unique([studentId, postId]) @@index([status]) @@ -326,14 +326,14 @@ model Enrollment { } model PostInstructor { - postId String @map("post_id") + postId String @map("post_id") instructorId String @map("instructor_id") role String @map("role") createdAt DateTime @default(now()) @map("created_at") order Float? @default(0) @map("order") - post Post @relation(fields: [postId], references: [id]) - instructor Staff @relation(fields: [instructorId], references: [id]) + post Post @relation(fields: [postId], references: [id]) + instructor Staff @relation(fields: [instructorId], references: [id]) @@id([postId, instructorId]) @@map("post_instructor") @@ -347,7 +347,7 @@ model Resource { fileId String? @unique url String? // 元数据 - meta Json? @map("meta") + meta Json? @map("meta") // 处理状态控制 status String? createdAt DateTime? @default(now()) @map("created_at") @@ -360,7 +360,7 @@ model Resource { ownerId String? @map("owner_id") post Post? @relation(fields: [postId], references: [id]) postId String? @map("post_id") - + // 索引 @@index([type]) @@index([createdAt]) @@ -404,3 +404,20 @@ model NodeEdge { @@index([targetId]) @@map("node_edge") } + +model Animal { + id String @id @default(cuid()) + name String + age Int + gender Boolean + personId String? + person Person? @relation(fields: [personId], references: [id]) +} + +model Person { + id String @id @default(cuid()) + name String + age Int + gender Boolean + animals Animal[] +} From 92983baf527b76cba98a2a222237193bf2854f64 Mon Sep 17 00:00:00 2001 From: longdayi <13477510+longdayilongdayi@user.noreply.gitee.com> Date: Fri, 21 Feb 2025 16:10:14 +0800 Subject: [PATCH 09/12] 02211610 --- apps/web/.env.example | 4 +- apps/web/src/app/error.tsx | 40 ++++++++++++++++--- apps/web/src/app/main/home/page.tsx | 2 + apps/web/src/app/main/paths/page.tsx | 2 +- .../common/uploader/TusUploader.tsx | 26 ++++-------- .../form/CourseContentForm/LectureList.tsx | 6 --- apps/web/src/env.ts | 8 ++-- apps/web/src/hooks/useTusUpload.ts | 12 ++---- package.json | 3 +- packages/common/prisma/schema.prisma | 1 - 10 files changed, 56 insertions(+), 48 deletions(-) diff --git a/apps/web/.env.example b/apps/web/.env.example index 2d66081..4d30872 100755 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,7 +1,5 @@ -VITE_APP_TUS_URL=http://localhost:8080 -VITE_APP_API_URL=http://localhost:3000 VITE_APP_SERVER_IP=192.168.252.239 VITE_APP_SERVER_PORT=3000 -VITE_APP_UPLOAD_PORT=80 +VITE_APP_FILE_PORT=80 VITE_APP_VERSION=0.3.0 VITE_APP_APP_NAME=MOOC diff --git a/apps/web/src/app/error.tsx b/apps/web/src/app/error.tsx index c32c11b..c51af76 100755 --- a/apps/web/src/app/error.tsx +++ b/apps/web/src/app/error.tsx @@ -1,11 +1,41 @@ +/** + * 错误处理模块 - 全局路由级错误展示组件 + * 功能: 捕获React Router路由层级错误并展示可视化错误信息 + * 特性: + * - 自动解析路由错误对象 + * - 自适应错误信息展示 + * - 响应式布局设计 + */ import { useRouteError } from "react-router-dom"; +/** + * 错误展示页面组件 + * @核心功能 呈现标准化错误界面,用于处理应用程序的路由层级错误 + * @设计模式 采用展示型组件模式,完全解耦业务逻辑实现纯UI展示 + * @使用示例 在React Router的RouterProvider中配置errorElement={} + */ export default function ErrorPage() { + // 使用React Router提供的Hook获取路由错误对象 + // 类型定义为any以兼容React Router不同版本的类型差异 const error: any = useRouteError(); - return
-
-
哦?页面似乎出错了...
-
{error?.statusText || error?.message}
+ + return ( + // 主容器: 基于Flex的垂直水平双居中布局 + // pt-64: 顶部留白实现视觉层次结构 +
+ {/* 内容区块: 采用纵向弹性布局控制内部元素间距 */} +
+ {/* 主标题: 强调性文字样式配置 */} +
+ 哦?页面似乎出错了... +
+ + {/* 错误详情: 动态渲染错误信息,实现优雅降级策略 */} + {/* 使用可选链操作符防止未定义错误,信息优先级: statusText > message */} +
+ {error?.statusText || error?.message} +
+
-
+ ) } \ No newline at end of file diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index 0225772..62f9d64 100644 --- a/apps/web/src/app/main/home/page.tsx +++ b/apps/web/src/app/main/home/page.tsx @@ -2,6 +2,7 @@ import HeroSection from './components/HeroSection'; import CategorySection from './components/CategorySection'; import CoursesSection from './components/CoursesSection'; import FeaturedTeachersSection from './components/FeaturedTeachersSection'; +import { TusUploader } from '@web/src/components/common/uploader/TusUploader'; const HomePage = () => { const mockCourses = [ { @@ -105,6 +106,7 @@ const HomePage = () => { return (
+ + return } diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx index 27b120c..e9fa45b 100644 --- a/apps/web/src/components/common/uploader/TusUploader.tsx +++ b/apps/web/src/components/common/uploader/TusUploader.tsx @@ -5,12 +5,8 @@ import { DeleteOutlined, } from "@ant-design/icons"; import { Upload, Progress, Button } from "antd"; -import type { UploadFile } from "antd"; import { useTusUpload } from "@web/src/hooks/useTusUpload"; import toast from "react-hot-toast"; -import { getCompressedImageUrl } from "@nice/utils"; -import { api } from "@nice/client"; - export interface TusUploaderProps { value?: string[]; onChange?: (value: string[]) => void; @@ -30,16 +26,7 @@ export const TusUploader = ({ onChange, multiple = true, }: TusUploaderProps) => { - const { data: files } = api.resource.findMany.useQuery({ - where: { - fileId: { in: value }, - }, - select: { - id: true, - fileId: true, - title: true, - }, - }); + const { handleFileUpload, uploadProgress } = useTusUpload(); const [uploadingFiles, setUploadingFiles] = useState([]); const [completedFiles, setCompletedFiles] = useState( @@ -74,6 +61,7 @@ export const TusUploader = ({ const handleBeforeUpload = useCallback( (file: File) => { + const fileKey = `${file.name}-${Date.now()}`; setUploadingFiles((prev) => [ @@ -151,7 +139,7 @@ export const TusUploader = ({ name="files" multiple={multiple} showUploadList={false} - style={{ background: "transparent", borderStyle: "none" }} + beforeUpload={handleBeforeUpload}>

@@ -177,10 +165,10 @@ export const TusUploader = ({ file.status === "done" ? 100 : Math.round( - uploadProgress?.[ - file.fileKey! - ] || 0 - ) + uploadProgress?.[ + file.fileKey! + ] || 0 + ) } status={ file.status === "error" diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx index 0ef99e7..8395c94 100644 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx @@ -33,15 +33,9 @@ import { useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor"; -import { TusUploader } from "@web/src/components/common/uploader/TusUploader"; import { Lecture, LectureType, PostType } from "@nice/common"; import { useCourseEditor } from "../../context/CourseEditorContext"; import { usePost } from "@nice/client"; -import toast from "react-hot-toast"; -import { CourseContentFormHeader } from "./CourseContentFormHeader"; -import { CourseSectionEmpty } from "./CourseSectionEmpty"; import { LectureData, SectionData } from "./interface"; import { SortableLecture } from "./SortableLecture"; diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index 30278b3..956a74b 100755 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -2,7 +2,7 @@ export const env: { APP_NAME: string; SERVER_IP: string; VERSION: string; - UPLOAD_PORT: string; + FILE_PORT: string; SERVER_PORT: string; } = { APP_NAME: import.meta.env.PROD @@ -11,9 +11,9 @@ export const env: { SERVER_IP: import.meta.env.PROD ? (window as any).env.VITE_APP_SERVER_IP : import.meta.env.VITE_APP_SERVER_IP, - UPLOAD_PORT: import.meta.env.PROD - ? (window as any).env.VITE_APP_UPLOAD_PORT - : import.meta.env.VITE_APP_UPLOAD_PORT, + FILE_PORT: import.meta.env.PROD + ? (window as any).env.VITE_APP_FILE_PORT + : import.meta.env.VITE_APP_FILE_PORT, SERVER_PORT: import.meta.env.PROD ? (window as any).env.VITE_APP_SERVER_PORT : import.meta.env.VITE_APP_SERVER_PORT, diff --git a/apps/web/src/hooks/useTusUpload.ts b/apps/web/src/hooks/useTusUpload.ts index 8c855fa..0b99a1d 100644 --- a/apps/web/src/hooks/useTusUpload.ts +++ b/apps/web/src/hooks/useTusUpload.ts @@ -2,11 +2,6 @@ import { useState } from "react"; import * as tus from "tus-js-client"; import { env } from "../env"; import { getCompressedImageUrl } from "@nice/utils"; -// useTusUpload.ts -interface UploadProgress { - fileId: string; - progress: number; -} interface UploadResult { compressedUrl: string; @@ -35,9 +30,7 @@ export function useTusUpload() { if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) { throw new Error("Invalid upload URL format"); } - console.log(env.UPLOAD_PORT); - const resUrl = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`; - + const resUrl = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`; return resUrl; }; const handleFileUpload = async ( @@ -46,11 +39,13 @@ export function useTusUpload() { onError: (error: Error) => void, fileKey: string // 添加文件唯一标识 ) => { + // console.log() setIsUploading(true); setUploadProgress((prev) => ({ ...prev, [fileKey]: 0 })); setUploadError(null); try { + console.log(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`) const upload = new tus.Upload(file, { endpoint: `http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`, retryDelays: [0, 1000, 3000, 5000], @@ -97,6 +92,7 @@ export function useTusUpload() { onError: (error) => { setIsUploading(false); setUploadError(error.message); + console.log(error) onError(error); }, }); diff --git a/package.json b/package.json index 40e6ac1..8a85a11 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "pnpm run --parallel dev" + "dev": "pnpm run --parallel dev", + "db:clear": "pnpm --filter common run db:clear" }, "keywords": [], "author": "insiinc", diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 061c73e..01bb99e 100644 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -413,7 +413,6 @@ model Animal { personId String? person Person? @relation(fields: [personId], references: [id]) } - model Person { id String @id @default(cuid()) name String From 32516187b21296468fddb0c1c18f17262ec35a37 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Fri, 21 Feb 2025 16:11:02 +0800 Subject: [PATCH 10/12] add --- .../course/detail/CourseDetailContext.tsx | 15 +++++++++--- .../course/detail/CourseDetailDisplayArea.tsx | 23 ++++++++++--------- .../CourseDetailHeader/CourseDetailHeader.tsx | 9 +++++++- .../course/detail/CourseDetailLayout.tsx | 17 ++++++++++---- apps/web/src/hooks/useTusUpload.ts | 1 - 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 8540e54..c67a896 100644 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -1,11 +1,12 @@ import { api } from "@nice/client"; -import { courseDetailSelect, CourseDto } from "@nice/common"; +import { courseDetailSelect, CourseDto, Lecture } from "@nice/common"; import React, { createContext, ReactNode, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; interface CourseDetailContextType { editId?: string; // 添加 editId course?: CourseDto; + lecture?: Lecture; selectedLectureId?: string | undefined; setSelectedLectureId?: React.Dispatch>; isLoading?: boolean; @@ -37,9 +38,16 @@ export function CourseDetailProvider({ const [selectedLectureId, setSelectedLectureId] = useState< string | undefined >(undefined); + const { data: lecture, isLoading: lectureIsLoading } = ( + api.post as any + ).findFirst.useQuery( + { + where: { id: selectedLectureId }, + }, + { enabled: Boolean(editId) } + ); useEffect(() => { - navigate(``) - + navigate(`/course/${editId}/detail/${selectedLectureId}`); }, [selectedLectureId, editId]); const [isHeaderVisible, setIsHeaderVisible] = useState(true); // 新增 return ( @@ -47,6 +55,7 @@ export function CourseDetailProvider({ value={{ editId, course, + lecture, selectedLectureId, setSelectedLectureId, isLoading, diff --git a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx index 2db3ffa..4f956b5 100644 --- a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx @@ -1,21 +1,23 @@ // components/CourseDetailDisplayArea.tsx import { motion, useScroll, useTransform } from "framer-motion"; -import React from "react"; +import React, { useContext } from "react"; import { VideoPlayer } from "@web/src/components/presentation/video-player/VideoPlayer"; import { CourseDetailDescription } from "./CourseDetailDescription/CourseDetailDescription"; -import { Course } from "@nice/common"; +import { Course, PostType } from "@nice/common"; +import { CourseDetailContext } from "./CourseDetailContext"; interface CourseDetailDisplayAreaProps { - course: Course; + // course: Course; videoSrc?: string; videoPoster?: string; - isLoading?: boolean; + // isLoading?: boolean; } export const CourseDetailDisplayArea: React.FC< CourseDetailDisplayAreaProps -> = ({ course, videoSrc, videoPoster, isLoading = false }) => { +> = ({ videoSrc, videoPoster }) => { // 创建滚动动画效果 + const { course, isLoading, lecture } = useContext(CourseDetailContext); const { scrollY } = useScroll(); const videoScale = useTransform(scrollY, [0, 200], [1, 0.8]); const videoOpacity = useTransform(scrollY, [0, 200], [1, 0.8]); @@ -25,16 +27,15 @@ export const CourseDetailDisplayArea: React.FC< {/* 固定的视频区域 */} {/* 移除 sticky 定位,让视频区域随页面滚动 */} -

- -
+ {lecture.type === PostType.LECTURE && ( +
+ +
+ )} {/* 课程内容区域 */} diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx index 4ef5e54..338d428 100644 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -2,12 +2,13 @@ import { motion, useScroll, useTransform } from "framer-motion"; import { useContext, useEffect, useState } from "react"; import { CourseDetailContext } from "../CourseDetailContext"; +import { Button } from "antd"; export const CourseDetailHeader = () => { const { scrollY } = useScroll(); const [lastScrollY, setLastScrollY] = useState(0); - const { course, isHeaderVisible, setIsHeaderVisible } = + const { course, isHeaderVisible, setIsHeaderVisible, lecture } = useContext(CourseDetailContext); useEffect(() => { const updateHeader = () => { @@ -43,6 +44,12 @@ export const CourseDetailHeader = () => {

{course?.title}

+
+ ))} +
+ + {inputValues.length > 1 && ( +
+
+ ); +}; + +export default InputList; diff --git a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx index f038144..333e94d 100644 --- a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx @@ -3,6 +3,7 @@ import { CourseLevel, CourseLevelLabel } from "@nice/common"; import { convertToOptions } from "@nice/client"; import TermSelect from "../../../term/term-select"; import { useCourseEditor } from "../context/CourseEditorContext"; +import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader"; const { TextArea } = Input; @@ -27,14 +28,12 @@ export function CourseBasicForm() { ]}>
- -