diff --git a/apps/server/src/models/base/row-model.service.ts b/apps/server/src/models/base/row-model.service.ts index a0cbb6f..d43769f 100755 --- a/apps/server/src/models/base/row-model.service.ts +++ b/apps/server/src/models/base/row-model.service.ts @@ -21,7 +21,7 @@ export abstract class RowModelService { // 添加更多需要引号的关键词 ]); protected logger = new Logger(this.tableName); - protected constructor(protected tableName: string) {} + protected constructor(protected tableName: string) { } protected async getRowDto(row: any, staff?: UserProfile): Promise { return row; } @@ -52,7 +52,7 @@ export abstract class RowModelService { ]); SQL = await this.getRowsSqlWrapper(SQL, request, staff); - this.logger.debug('getrows', SQL); + // this.logger.debug('getrows', SQL); const results: any[] = (await db?.$queryRawUnsafe(SQL)) || []; @@ -140,11 +140,11 @@ export abstract class RowModelService { private buildFilterConditions(filterModel: any): LogicalCondition[] { return filterModel ? Object.entries(filterModel)?.map(([key, item]) => - SQLBuilder.createFilterSql( - key === 'ag-Grid-AutoColumn' ? 'name' : key, - item, - ), - ) + SQLBuilder.createFilterSql( + key === 'ag-Grid-AutoColumn' ? 'name' : key, + item, + ), + ) : []; } 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/post.service.ts b/apps/server/src/models/post/post.service.ts index c8cd7ba..df99714 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -15,7 +15,7 @@ import { import { MessageService } from '../message/message.service'; import { BaseService } from '../base/base.service'; import { DepartmentService } from '../department/department.service'; -import { setPostRelation } from './utils'; +import { setCourseInfo, setPostRelation } from './utils'; import EventBus, { CrudOperation } from '@server/utils/event-bus'; import { BaseTreeService } from '../base/base.tree.service'; import { z } from 'zod'; @@ -180,6 +180,7 @@ export class PostService extends BaseTreeService { if (result) { await setPostRelation({ data: result, staff }); await this.setPerms(result, staff); + await setCourseInfo({ data: result }); } return result; diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 91af13e..9185a0f 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -3,6 +3,7 @@ import { EnrollmentStatus, Post, PostType, + SectionDto, UserProfile, VisitType, } from '@nice/common'; @@ -123,3 +124,43 @@ export async function updateCourseEnrollmentStats(courseId: string) { }, }); } + +export async function setCourseInfo({ data }: { data: Post }) { + // await db.term + + 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: SectionDto[] = descendants + .filter((descendant) => { + return ( + descendant.type === PostType.SECTION && + descendant.parentId === data.id + ); + }) + .map((section) => ({ + ...section, + lectures: [], + })); + const lectures = descendants.filter((descendant) => { + return ( + descendant.type === PostType.LECTURE && + sections.map((section) => section.id).includes(descendant.parentId) + ); + }); + sections.forEach((section) => { + section.lectures = lectures.filter( + (lecture) => lecture.parentId === section.id, + ); + }); + Object.assign(data, { sections }); + } +} diff --git a/apps/server/src/models/term/term.service.ts b/apps/server/src/models/term/term.service.ts index 7f21041..cc119e6 100755 --- a/apps/server/src/models/term/term.service.ts +++ b/apps/server/src/models/term/term.service.ts @@ -298,12 +298,12 @@ export class TermService extends BaseTreeService { ...(hasAnyPerms ? {} // 当有全局权限时,不添加任何额外条件 : { - // 当无全局权限时,添加域ID过滤 - OR: [ - { domainId: null }, // 通用记录 - { domainId: domainId }, // 特定域记录 - ], - }), + // 当无全局权限时,添加域ID过滤 + OR: [ + { domainId: null }, // 通用记录 + { domainId: domainId }, // 特定域记录 + ], + }), }, ancestorId: parentId, relDepth: 1, @@ -315,29 +315,29 @@ export class TermService extends BaseTreeService { }), termIds ? db.term.findMany({ - where: { - ...(termIds && { + where: { + ...(termIds && { + OR: [ + ...(validTermIds.length + ? [{ id: { in: validTermIds } }] + : []), + ], + }), + taxonomyId: taxonomyId, + // 动态权限控制条件 + ...(hasAnyPerms + ? {} // 当有全局权限时,不添加任何额外条件 + : { + // 当无全局权限时,添加域ID过滤 OR: [ - ...(validTermIds.length - ? [{ id: { in: validTermIds } }] - : []), + { domainId: null }, // 通用记录 + { domainId: domainId }, // 特定域记录 ], }), - taxonomyId: taxonomyId, - // 动态权限控制条件 - ...(hasAnyPerms - ? {} // 当有全局权限时,不添加任何额外条件 - : { - // 当无全局权限时,添加域ID过滤 - OR: [ - { domainId: null }, // 通用记录 - { domainId: domainId }, // 特定域记录 - ], - }), - }, - include: { children: true }, - orderBy: { order: 'asc' }, - }) + }, + include: { children: true }, + orderBy: { order: 'asc' }, + }) : [], ]); const children = childrenData @@ -371,12 +371,12 @@ export class TermService extends BaseTreeService { ...(hasAnyPerms ? {} // 当有全局权限时,不添加任何额外条件 : { - // 当无全局权限时,添加域ID过滤 - OR: [ - { domainId: null }, // 通用记录 - { domainId: domainId }, // 特定域记录 - ], - }), + // 当无全局权限时,添加域ID过滤 + OR: [ + { domainId: null }, // 通用记录 + { domainId: domainId }, // 特定域记录 + ], + }), }, }, include: { @@ -398,12 +398,12 @@ export class TermService extends BaseTreeService { ...(hasAnyPerms ? {} // 当有全局权限时,不添加任何额外条件 : { - // 当无全局权限时,添加域ID过滤 - OR: [ - { domainId: null }, // 通用记录 - { domainId: domainId }, // 特定域记录 - ], - }), + // 当无全局权限时,添加域ID过滤 + OR: [ + { domainId: null }, // 通用记录 + { domainId: domainId }, // 特定域记录 + ], + }), }, include: { children: true }, // 包含子节点信息 orderBy: { order: 'asc' }, // 按顺序升序排序 diff --git a/apps/server/src/tasks/init/gendev.service.ts b/apps/server/src/tasks/init/gendev.service.ts index fca5ca3..addb264 100755 --- a/apps/server/src/tasks/init/gendev.service.ts +++ b/apps/server/src/tasks/init/gendev.service.ts @@ -21,7 +21,6 @@ export class GenDevService { deptStaffRecord: Record = {}; terms: Record = { [TaxonomySlug.CATEGORY]: [], - [TaxonomySlug.UNIT]: [], [TaxonomySlug.TAG]: [], [TaxonomySlug.LEVEL]: [], }; @@ -36,7 +35,7 @@ export class GenDevService { private readonly departmentService: DepartmentService, private readonly staffService: StaffService, private readonly termService: TermService, - ) {} + ) { } async genDataEvent() { EventBus.emit('genDataEvent', { type: 'start' }); try { @@ -62,7 +61,7 @@ export class GenDevService { const domains = this.depts.filter((item) => item.isDomain); for (const domain of domains) { await this.createTerms(domain, TaxonomySlug.CATEGORY, depth, count); - await this.createTerms(domain, TaxonomySlug.UNIT, depth, count); + // await this.createTerms(domain, TaxonomySlug.UNIT, depth, count); } } const termCount = await db.term.count(); diff --git a/apps/web/.env.example b/apps/web/.env.example index dfdad5c..4d30872 100755 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,2 +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_FILE_PORT=80 +VITE_APP_VERSION=0.3.0 +VITE_APP_APP_NAME=MOOC diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 7f00070..9e6304c 100755 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -14,7 +14,6 @@ import { Toaster } from 'react-hot-toast'; dayjs.locale("zh-cn"); function App() { - return ( <> 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/course/detail/page.tsx b/apps/web/src/app/main/course/detail/page.tsx index d1bf172..16f7173 100755 --- a/apps/web/src/app/main/course/detail/page.tsx +++ b/apps/web/src/app/main/course/detail/page.tsx @@ -2,7 +2,7 @@ import CourseDetail from "@web/src/components/models/course/detail/CourseDetail" import { useParams } from "react-router-dom"; export function CourseDetailPage() { - const { id } = useParams(); + const { id, lectureId } = useParams(); console.log("Course ID:", id); - return ; + return ; } diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index 2be6bd6..5cc1574 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -5,17 +5,17 @@ import { GetTaxonomyProps, useGetTaxonomy } from '@web/src/hooks/useGetTaxonomy' import { useMemo } from 'react'; 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) { const gateGory : GetTaxonomyProps = useGetTaxonomy({ type: TaxonomySlug.CATEGORY, @@ -50,7 +50,7 @@ export default function FilterSection({
- +

难度等级

diff --git a/apps/web/src/app/main/courses/page.tsx b/apps/web/src/app/main/courses/page.tsx index f852e00..30cd309 100755 --- a/apps/web/src/app/main/courses/page.tsx +++ b/apps/web/src/app/main/courses/page.tsx @@ -1,73 +1,101 @@ -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/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index 0225772..62f9d64 100755 --- 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 (
+ -
-
-
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 ? ( - } - 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/app/main/paths/page.tsx b/apps/web/src/app/main/paths/page.tsx index 7b711c6..e1283a0 100755 --- a/apps/web/src/app/main/paths/page.tsx +++ b/apps/web/src/app/main/paths/page.tsx @@ -1,6 +1,5 @@ import MindEditor from "@web/src/components/common/editor/MindEditor"; -import MindElixir, { MindElixirInstance } from "mind-elixir"; -import { useEffect, useRef } from "react"; + export default function PathsPage() { - return -} \ No newline at end of file + return ; +} diff --git a/apps/web/src/components/common/editor/MindEditor.tsx b/apps/web/src/components/common/editor/MindEditor.tsx index 3f94c77..732cfae 100755 --- a/apps/web/src/components/common/editor/MindEditor.tsx +++ b/apps/web/src/components/common/editor/MindEditor.tsx @@ -1,28 +1,26 @@ -import { MindElixirInstance } from "packages/mind-elixir-core/dist/types"; +import { MindElixirInstance } from "mind-elixir"; import { useRef, useEffect } from "react"; -import MindElixir from 'mind-elixir'; +import MindElixir from "mind-elixir"; export default function MindEditor() { - const me = useRef(); - useEffect(() => { - const instance = new MindElixir({ - el: "#map", - direction: MindElixir.SIDE, - draggable: true, // default true - contextMenu: true, // default true - toolBar: true, // default true - nodeMenu: true, // default true - keypress: true // default true - - }); - // instance.install(NodeMenu); - instance.init(MindElixir.new("新主题")); - me.current = instance; - }, []); - return
-
- 1 -
-
-
-} \ No newline at end of file + const me = useRef(); + useEffect(() => { + const instance = new MindElixir({ + el: "#map", + direction: MindElixir.SIDE, + draggable: true, // default true + contextMenu: true, // default true + toolBar: true, // default true + nodeMenu: true, // default true + keypress: true, // default true + }); + // instance.install(NodeMenu); + instance.init(MindElixir.new("新主题")); + me.current = instance; + }, []); + return ( +
+
+
+ ); +} diff --git a/apps/web/src/components/common/form/FormArrayField.tsx b/apps/web/src/components/common/form/FormArrayField.tsx index 19852f7..4979c49 100755 --- a/apps/web/src/components/common/form/FormArrayField.tsx +++ b/apps/web/src/components/common/form/FormArrayField.tsx @@ -72,22 +72,7 @@ export function FormArrayField({ - // {inputProps.maxLength - - // ( - // Form.useWatch( - // [ - // name, - // field.name, - // ] - // ) || "" - // ).length} - // - // ) - // } + style={{ width: "100%" }} onChange={(e) => { // 更新 items 状态 const newItems = [...items]; diff --git a/apps/web/src/components/common/input/InputList.tsx b/apps/web/src/components/common/input/InputList.tsx new file mode 100644 index 0000000..f6247d5 --- /dev/null +++ b/apps/web/src/components/common/input/InputList.tsx @@ -0,0 +1,102 @@ +import React, { useState } from "react"; +import { Input, Button } from "antd"; +import { DeleteOutlined } from "@ant-design/icons"; + +interface InputListProps { + initialValue?: string[]; + onChange?: (value: string[]) => void; + placeholder?: string; +} + +const InputList: React.FC = ({ + initialValue, + onChange, + placeholder = "请输入内容", +}) => { + // Internal state management with fallback to initial value or empty array + const [inputValues, setInputValues] = useState( + initialValue && initialValue.length > 0 ? initialValue : [""] + ); + + // Handle individual input value change + const handleInputChange = (index: number, newValue: string) => { + const newValues = [...inputValues]; + newValues[index] = newValue; + + // Update internal state + setInputValues(newValues); + + // Call external onChange if provided + onChange?.(newValues); + }; + + // Handle delete operation + const handleDelete = (index: number) => { + const newValues = inputValues.filter((_, i) => i !== index); + + // Ensure at least one input remains + const finalValues = newValues.length === 0 ? [""] : newValues; + + // Update internal state + setInputValues(finalValues); + + // Call external onChange if provided + onChange?.(finalValues); + }; + + // Add new input field + const handleAdd = () => { + const newValues = [...inputValues, ""]; + + // Update internal state + setInputValues(newValues); + + // Call external onChange if provided + onChange?.(newValues); + }; + + return ( +
+ {inputValues.map((item, index) => ( +
+ + handleInputChange(index, e.target.value) + } + placeholder={placeholder} + className="flex-grow" + /> + {inputValues.length > 1 && ( +
+ ))} +
+ + {inputValues.length > 1 && ( +
+
+ ); +}; + +export default InputList; diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx index 27b120c..e9fa45b 100755 --- 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/detail/CourseDetail.tsx b/apps/web/src/components/models/course/detail/CourseDetail.tsx index 402fbd7..353ec4f 100755 --- 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 3458a1f..c67a896 100755 --- 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 React, { createContext, ReactNode, useState } from "react"; - +import { api } from "@nice/client"; +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; @@ -22,12 +23,13 @@ export function CourseDetailProvider({ children, editId, }: CourseFormProviderProps) { + const navigate = useNavigate(); const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = - api.course.findFirst.useQuery( + (api.post as any).findFirst.useQuery( { where: { id: editId }, include: { - sections: { include: { lectures: true } }, + // sections: { include: { lectures: true } }, enrollments: true, }, }, @@ -36,12 +38,24 @@ 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(`/course/${editId}/detail/${selectedLectureId}`); + }, [selectedLectureId, editId]); const [isHeaderVisible, setIsHeaderVisible] = useState(true); // 新增 return ( = ({ 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 100755 --- 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}

+
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..efa4962 100755 --- 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.id]; + }); 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..333e94d 100755 --- 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; @@ -14,6 +15,7 @@ export function CourseBasicForm() { value: key as CourseLevel, }) ); + const { form, taxonomies } = useCourseEditor(); return (
@@ -26,14 +28,12 @@ export function CourseBasicForm() { ]}> - -