From 5872f4b7280ccc91ad47067d2cd3239d8f6ac11e Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 22:03:07 +0800 Subject: [PATCH 1/5] add --- apps/server/src/models/post/post.service.ts | 15 ++------------- apps/server/src/models/post/utils.ts | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 8f90a85..947ecfc 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -166,19 +166,7 @@ export class PostService extends BaseTreeService { ); return transDto; } - // async findMany(args: Prisma.PostFindManyArgs, staff?: UserProfile) { - // if (!args.where) args.where = {}; - // args.where.OR = await this.preFilter(args.where.OR, staff); - // return this.wrapResult(super.findMany(args), async (result) => { - // await Promise.all( - // result.map(async (item) => { - // await setPostRelation({ data: item, staff }); - // await this.setPerms(item, staff); - // }), - // ); - // return { ...result }; - // }); - // } + async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) { if (!args.where) args.where = {}; args.where.OR = await this.preFilter(args.where.OR, staff); @@ -255,6 +243,7 @@ export class PostService extends BaseTreeService { // 批量执行更新 return updates.length > 0 ? await db.$transaction(updates) : []; } + protected async setPerms(data: Post, staff?: UserProfile) { if (!staff) return; const perms: ResPerm = { diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 0e1f835..57b0fd0 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -168,6 +168,21 @@ export async function setCourseInfo({ data }: { data: Post }) { (lecture) => lecture.parentId === section.id, ) as any as Lecture[]; }); - Object.assign(data, { sections, lectureCount }); + + const students = await db.staff.findMany({ + where: { + learningPosts: { + some: { + id: data.id, + }, + }, + }, + select: { + id: true, + }, + }); + + const studentIds = (students || []).map((student) => student?.id); + Object.assign(data, { sections, lectureCount, studentIds }); } } From 4ec92966ab09bc3f32c6665ce39df763fa8d0d69 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 22:03:18 +0800 Subject: [PATCH 2/5] add --- packages/common/prisma/schema.prisma | 2 +- packages/common/src/models/post.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 75e8be6..db764d2 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -93,7 +93,7 @@ model Staff { posts Post[] - learningPost Post[] @relation("post_student") + learningPosts Post[] @relation("post_student") sentMsgs Message[] @relation("message_sender") receivedMsgs Message[] @relation("message_receiver") registerToken String? diff --git a/packages/common/src/models/post.ts b/packages/common/src/models/post.ts index a10fcf5..63a38b0 100755 --- a/packages/common/src/models/post.ts +++ b/packages/common/src/models/post.ts @@ -83,4 +83,5 @@ export type CourseDto = Course & { terms: TermDto[]; lectureCount?: number; depts: Department[]; + studentIds: string[]; }; From 4c89a43197605f4c2324a130ba1337a2cc57d495 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 22:03:25 +0800 Subject: [PATCH 3/5] add --- packages/client/src/api/hooks/useStaff.ts | 67 +++++++++++++---------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/packages/client/src/api/hooks/useStaff.ts b/packages/client/src/api/hooks/useStaff.ts index fbdf571..aed3aa7 100755 --- a/packages/client/src/api/hooks/useStaff.ts +++ b/packages/client/src/api/hooks/useStaff.ts @@ -5,39 +5,48 @@ import { ObjectType, Staff } from "@nice/common"; import { findQueryData } from "../utils"; import { CrudOperation, emitDataChange } from "../../event"; export function useStaff() { - const queryClient = useQueryClient(); - const queryKey = getQueryKey(api.staff); + const queryClient = useQueryClient(); + const queryKey = getQueryKey(api.staff); - const create = api.staff.create.useMutation({ - onSuccess: (result) => { - queryClient.invalidateQueries({ queryKey }); - emitDataChange(ObjectType.STAFF, result as any, CrudOperation.CREATED) - }, - }); - const updateUserDomain = api.staff.updateUserDomain.useMutation({ - onSuccess: async (result) => { - queryClient.invalidateQueries({ queryKey }); - }, - }); - const update = api.staff.update.useMutation({ - onSuccess: (result) => { - queryClient.invalidateQueries({ queryKey }); - emitDataChange(ObjectType.STAFF, result as any, CrudOperation.UPDATED) - }, - }); + const create = api.staff.create.useMutation({ + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey }); + emitDataChange( + ObjectType.STAFF, + result as any, + CrudOperation.CREATED + ); + }, + }); + const updateUserDomain = api.staff.updateUserDomain.useMutation({ + onSuccess: async (result) => { + queryClient.invalidateQueries({ queryKey }); + }, + }); + const update = api.staff.update.useMutation({ + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey }); + queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) }); + emitDataChange( + ObjectType.STAFF, + result as any, + CrudOperation.UPDATED + ); + }, + }); const softDeleteByIds = api.staff.softDeleteByIds.useMutation({ onSuccess: (result, variables) => { queryClient.invalidateQueries({ queryKey }); }, }); - const getStaff = (key: string) => { - return findQueryData(queryClient, api.staff, key); - }; - return { - create, - update, - softDeleteByIds, - getStaff, - updateUserDomain - }; + const getStaff = (key: string) => { + return findQueryData(queryClient, api.staff, key); + }; + return { + create, + update, + softDeleteByIds, + getStaff, + updateUserDomain, + }; } From a7bbc88e994c515c67ecb8d60bb3903472129df0 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 22:03:29 +0800 Subject: [PATCH 4/5] add --- apps/web/src/app/main/my-learning/page.tsx | 7 +- .../course/detail/CourseDetailContext.tsx | 23 +++--- .../CourseDetailHeader/CourseDetailHeader.tsx | 61 +++++++++++---- .../CourseDetailHeader_BACKUP.tsx | 77 ------------------- .../editor/context/CourseEditorContext.tsx | 4 +- .../CourseContentForm/SortableLecture.tsx | 4 +- 6 files changed, 64 insertions(+), 112 deletions(-) delete mode 100755 apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx index 0216515..8807119 100644 --- a/apps/web/src/app/main/my-learning/page.tsx +++ b/apps/web/src/app/main/my-learning/page.tsx @@ -7,11 +7,14 @@ export default function MyLearningPage() { <>
diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index b722f3c..0da1702 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -28,6 +28,7 @@ interface CourseDetailContextType { isHeaderVisible: boolean; // 新增 setIsHeaderVisible: (visible: boolean) => void; // 新增 canEdit?: boolean; + userIsLearning?: boolean; } interface CourseFormProviderProps { @@ -43,30 +44,25 @@ export function CourseDetailProvider({ }: CourseFormProviderProps) { const navigate = useNavigate(); const { read } = useVisitor(); - const { user, hasSomePermissions } = useAuth(); + const { user, hasSomePermissions, isAuthenticated } = useAuth(); const { lectureId } = useParams(); const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = (api.post as any).findFirst.useQuery( { where: { id: editId }, - // include: { - // // sections: { include: { lectures: true } }, - // enrollments: true, - // terms:true - // }, - - select:courseDetailSelect + select: courseDetailSelect, }, { enabled: Boolean(editId) } ); + + const userIsLearning = useMemo(() => { + return (course?.studentIds || []).includes(user?.id); + }, [user, course, isLoading]); const canEdit = useMemo(() => { - const isAuthor = user?.id === course?.authorId; - const isDept = course?.depts - ?.map((dept) => dept.id) - .includes(user?.deptId); + const isAuthor = isAuthenticated && user?.id === course?.authorId; const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST); - return isAuthor || isDept || isRoot; + return isAuthor || isRoot; }, [user, course]); const [selectedLectureId, setSelectedLectureId] = useState< string | undefined @@ -109,6 +105,7 @@ export function CourseDetailProvider({ isHeaderVisible, setIsHeaderVisible, canEdit, + userIsLearning, }}> {children} 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 401484a..e07ad48 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -10,17 +10,18 @@ import { useAuth } from "@web/src/providers/auth-provider"; import { useNavigate, useParams } from "react-router-dom"; import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu"; import { CourseDetailContext } from "../CourseDetailContext"; - +import { usePost, useStaff } from "@nice/client"; +import toast from "react-hot-toast"; const { Header } = Layout; export function CourseDetailHeader() { - const [searchValue, setSearchValue] = useState(""); const { id } = useParams(); const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } = useAuth(); const navigate = useNavigate(); - const { course, canEdit } = useContext(CourseDetailContext); + const { course, canEdit, userIsLearning } = useContext(CourseDetailContext); + const { update } = useStaff(); return (
@@ -39,20 +40,48 @@ export function CourseDetailHeader() { {/* */}
+ {isAuthenticated && ( + + )} {canEdit && ( - <> - - + )} {isAuthenticated ? ( diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx deleted file mode 100755 index 0fc9815..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx +++ /dev/null @@ -1,77 +0,0 @@ -// // components/Header.tsx -// import { motion, useScroll, useTransform } from "framer-motion"; -// import { useContext, useEffect, useState } from "react"; -// import { CourseDetailContext } from "../CourseDetailContext"; -// import { Avatar, Button, Dropdown } from "antd"; -// import { UserOutlined } from "@ant-design/icons"; -// import { UserMenu } from "@web/src/app/main/layout/UserMenu"; -// import { useAuth } from "@web/src/providers/auth-provider"; - -// export const CourseDetailHeader = () => { -// const { scrollY } = useScroll(); -// const { user, isAuthenticated } = useAuth(); -// const [lastScrollY, setLastScrollY] = useState(0); -// const { course, isHeaderVisible, setIsHeaderVisible, lecture } = -// useContext(CourseDetailContext); -// useEffect(() => { -// const updateHeader = () => { -// const current = scrollY.get(); -// const direction = current > lastScrollY ? "down" : "up"; - -// if (direction === "down" && current > 100) { -// setIsHeaderVisible(false); -// } else if (direction === "up") { -// setIsHeaderVisible(true); -// } - -// setLastScrollY(current); -// }; - -// // 使用 requestAnimationFrame 来优化性能 -// const unsubscribe = scrollY.on("change", () => { -// requestAnimationFrame(updateHeader); -// }); - -// return () => { -// unsubscribe(); -// }; -// }, [lastScrollY, scrollY, setIsHeaderVisible]); - -// return ( -// -//
-//
-//

{course?.title}

-//
- -// {isAuthenticated ? ( -// } -// trigger={["click"]} -// placement="bottomRight"> -// -// {(user?.showname || -// user?.username || -// "")[0]?.toUpperCase()} -// -// -// ) : ( -// -// )} -//
-//
-// ); -// }; - -// export default CourseDetailHeader; 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 0fd8040..88571a3 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -99,10 +99,10 @@ export function CourseFormProvider({ }), }, terms: { - connect: termIds.map((id) => ({ id })), // 转换成 connect 格式 + set: termIds.map((id) => ({ id })), // 转换成 connect 格式 }, depts: { - connect: deptIds.map((id) => ({ id })), + set: deptIds.map((id) => ({ id })), }, }; // 删除原始的 taxonomy 字段 diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx index 26282d2..a153137 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx @@ -83,7 +83,7 @@ export const SortableLecture: React.FC = ({ : undefined, }, resources: { - connect: [videoUrlId, ...fileIds] + set: [videoUrlId, ...fileIds] .filter(Boolean) .map((fileId) => ({ fileId, @@ -109,7 +109,7 @@ export const SortableLecture: React.FC = ({ : undefined, }, resources: { - connect: [videoUrlId, ...fileIds] + set: [videoUrlId, ...fileIds] .filter(Boolean) .map((fileId) => ({ fileId, From ed85a700a4d5213c040dec39116b1d418c2a98b6 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 22:38:28 +0800 Subject: [PATCH 5/5] add --- .../main/courses/components/CourseCard.tsx | 13 +++- .../course/detail/CourseDetailDescription.tsx | 74 +++++++++++-------- .../course/editor/form/CourseBasicForm.tsx | 2 +- .../CourseContentForm/SortableLecture.tsx | 4 +- 4 files changed, 55 insertions(+), 38 deletions(-) diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 3dd5032..acb4e9d 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -1,5 +1,6 @@ import { Card, Tag, Typography, Button } from "antd"; import { + BookOutlined, EyeOutlined, PlayCircleOutlined, TeamOutlined, @@ -73,10 +74,10 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) { -
+
- + {course?.depts?.length > 1 ? `${course.depts[0].name}等` : course?.depts?.[0]?.name} @@ -84,10 +85,16 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) { {/* {course?.depts?.map((dept)=>{return dept.name})} */}
+
+
- + {`观看次数 ${course?.meta?.views || 0}`} + + + {`学习人数 ${course?.studentIds?.length || 0}`} +