diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts index f7f679c..49d25c1 100755 --- a/apps/server/src/auth/auth.service.ts +++ b/apps/server/src/auth/auth.service.ts @@ -32,7 +32,7 @@ export class AuthService { return { isValid: false, error: FileValidationErrorType.INVALID_URI }; } const fileId = extractFileIdFromNginxUrl(params.originalUri); - console.log(params.originalUri, fileId); + // console.log(params.originalUri, fileId); const resource = await db.resource.findFirst({ where: { fileId } }); // 资源验证 diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index b9d4a21..0df249e 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -108,15 +108,12 @@ export class PostService extends BaseTreeService { return await db.$transaction(async (tx) => { const courseParams = { ...params, tx }; // Create the course first - console.log(courseParams?.staff?.id); - console.log('courseDetail', courseDetail); const createdCourse = await this.create(courseDetail, courseParams); // If sections are provided, create them return createdCourse; }); } // If transaction is provided, use it directly - console.log('courseDetail', courseDetail); const createdCourse = await this.create(courseDetail, params); // If sections are provided, create them return createdCourse; @@ -160,25 +157,13 @@ export class PostService extends BaseTreeService { await this.setPerms(result, staff); await setCourseInfo({ data: result }); } - + // console.log(result); return result; }, ); 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 +240,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 = { @@ -306,37 +292,37 @@ export class PostService extends BaseTreeService { staff?.id && { authorId: staff.id, }, - staff?.id && { - watchableStaffs: { - some: { - id: staff.id, - }, - }, - }, - deptId && { - watchableDepts: { - some: { - id: { - in: parentDeptIds, - }, - }, - }, - }, + // staff?.id && { + // watchableStaffs: { + // some: { + // id: staff.id, + // }, + // }, + // }, + // deptId && { + // watchableDepts: { + // some: { + // id: { + // in: parentDeptIds, + // }, + // }, + // }, + // }, - { - AND: [ - { - watchableStaffs: { - none: {}, // 匹配 watchableStaffs 为空 - }, - }, - { - watchableDepts: { - none: {}, // 匹配 watchableDepts 为空 - }, - }, - ], - }, + // { + // AND: [ + // { + // watchableStaffs: { + // none: {}, // 匹配 watchableStaffs 为空 + // }, + // }, + // { + // watchableDepts: { + // none: {}, // 匹配 watchableDepts 为空 + // }, + // }, + // ], + // }, ].filter(Boolean); if (orCondition?.length > 0) return orCondition; diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 6ca6139..57b0fd0 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -1,6 +1,7 @@ import { db, EnrollmentStatus, + Lecture, Post, PostType, SectionDto, @@ -127,7 +128,6 @@ 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: { @@ -144,29 +144,45 @@ export async function setCourseInfo({ data }: { data: Post }) { }, }); const descendants = ancestries.map((ancestry) => ancestry.descendant); - const sections: SectionDto[] = descendants - .filter((descendant) => { + const sections: SectionDto[] = ( + descendants.filter((descendant) => { return ( descendant.type === PostType.SECTION && descendant.parentId === data.id ); - }) - .map((section) => ({ - ...section, - lectures: [], - })); + }) as any + ).map((section) => ({ + ...section, + lectures: [], + })); const lectures = descendants.filter((descendant) => { return ( descendant.type === PostType.LECTURE && sections.map((section) => section.id).includes(descendant.parentId) ); }); + const lectureCount = lectures?.length || 0; sections.forEach((section) => { section.lectures = lectures.filter( (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 }); } } diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 0b25170..51bffca 100644 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -3,7 +3,6 @@ import { BaseSetting, db, PostType, - TaxonomySlug, VisitType, } from '@nice/common'; export async function updateTotalCourseViewCount(type: VisitType) { @@ -24,7 +23,7 @@ export async function updateTotalCourseViewCount(type: VisitType) { views: true, }, where: { - postId: { in: courseIds }, + postId: { in: posts.map((post) => post.id) }, type: type, }, }); @@ -65,8 +64,50 @@ export async function updateTotalCourseViewCount(type: VisitType) { export async function updatePostViewCount(id: string, type: VisitType) { const post = await db.post.findFirst({ where: { id }, - select: { id: true, meta: true }, + select: { id: true, meta: true, type: true }, }); + const metaFieldMap = { + [VisitType.READED]: 'views', + [VisitType.LIKE]: 'likes', + [VisitType.HATE]: 'hates', + }; + if (post?.type === PostType.LECTURE) { + const course = await db.postAncestry.findFirst({ + where: { + descendantId: post?.id, + ancestor: { + type: PostType.COURSE, + }, + }, + select: { id: true }, + }); + const lectures = await db.postAncestry.findMany({ + where: { ancestorId: course.id, descendant: { type: PostType.LECTURE } }, + select: { + id: true, + }, + }); + const courseViews = await db.visit.aggregate({ + _sum: { + views: true, + }, + where: { + postId: { + in: [course.id, ...lectures.map((lecture) => lecture.id)], + }, + type: type, + }, + }); + await db.post.update({ + where: { id: course.id }, + data: { + meta: { + ...((post?.meta as any) || {}), + [metaFieldMap[type]]: courseViews._sum.views || 0, + }, + }, + }); + } const totalViews = await db.visit.aggregate({ _sum: { views: true, @@ -76,42 +117,13 @@ export async function updatePostViewCount(id: string, type: VisitType) { type: type, }, }); - if (type === VisitType.READED) { - await db.post.update({ - where: { - id: id, + await db.post.update({ + where: { id }, + data: { + meta: { + ...((post?.meta as any) || {}), + [metaFieldMap[type]]: totalViews._sum.views || 0, }, - data: { - meta: { - ...((post?.meta as any) || {}), - views: totalViews._sum.views || 0, - }, // Use 0 if no visits exist - }, - }); - console.log('readed'); - } else if (type === VisitType.LIKE) { - await db.post.update({ - where: { - id: id, - }, - data: { - meta: { - ...((post?.meta as any) || {}), - likes: totalViews._sum.views || 0, // Use 0 if no visits exist - }, - }, - }); - } else if (type === VisitType.HATE) { - await db.post.update({ - where: { - id: id, - }, - data: { - meta: { - ...((post?.meta as any) || {}), - hates: totalViews._sum.views || 0, // Use 0 if no visits exist - }, - }, - }); - } + }, + }); } diff --git a/apps/server/src/queue/worker/file.processor.ts b/apps/server/src/queue/worker/file.processor.ts index b416af4..1012a8c 100755 --- a/apps/server/src/queue/worker/file.processor.ts +++ b/apps/server/src/queue/worker/file.processor.ts @@ -10,7 +10,7 @@ const pipeline = new ResourceProcessingPipeline() .addProcessor(new VideoProcessor()); export default async function processJob(job: Job) { if (job.name === QueueJobType.FILE_PROCESS) { - console.log('job', job); + // console.log('job', job); const { resource } = job.data; if (!resource) { throw new Error('No resource provided in job data'); diff --git a/apps/server/src/upload/tus.service.ts b/apps/server/src/upload/tus.service.ts index 8844b97..6de4e53 100755 --- a/apps/server/src/upload/tus.service.ts +++ b/apps/server/src/upload/tus.service.ts @@ -89,8 +89,8 @@ export class TusService implements OnModuleInit { upload: Upload, ) { try { - console.log('upload.id', upload.id); - console.log('fileId', this.getFileId(upload.id)); + // console.log('upload.id', upload.id); + // console.log('fileId', this.getFileId(upload.id)); const resource = await this.resourceService.update({ where: { fileId: this.getFileId(upload.id) }, data: { status: ResourceStatus.UPLOADED }, diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index d830c4d..597caf0 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -1,7 +1,7 @@ -import { Card, Rate, Tag, Typography, Button } from "antd"; +import { Card, Tag, Typography, Button } from "antd"; import { - UserOutlined, - ClockCircleOutlined, + BookOutlined, + EyeOutlined, PlayCircleOutlined, TeamOutlined, } from "@ant-design/icons"; @@ -10,22 +10,25 @@ import { useNavigate } from "react-router-dom"; interface CourseCardProps { course: CourseDto; + edit?: boolean; } const { Title, Text } = Typography; -export default function CourseCard({ course }: CourseCardProps) { +export default function CourseCard({ course, edit = false }: CourseCardProps) { const navigate = useNavigate(); const handleClick = (course: CourseDto) => { - navigate(`/course/${course.id}/detail`); - window.scrollTo({top: 0,behavior: "smooth",}) + if (!edit) { + navigate(`/course/${course.id}/detail`); + } else { + navigate(`/course/${course.id}/editor`); + } + window.scrollTo({ top: 0, behavior: "smooth" }); }; - return ( handleClick(course)} key={course.id} hoverable - className="group overflow-hidden rounded-2xl border border-gray-200 bg-white - shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" + className="group overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" cover={
}> -
-
- {course?.terms?.map((term) => { - return ( - - {term.name} - - ); - })} +
+
+
+ {course?.terms?.map((term) => { + return ( + <> + + {term.name} + + + ); + })} +
+ className="mb-4 mt-4 line-clamp-2 font-bold leading-snug text-gray-800 hover:text-blue-600 transition-colors duration-300 group-hover:scale-[1.02] transform origin-left"> <button> {course.title}</button> -
+
- + {course?.depts?.length > 1 ? `${course.depts[0].name}等` : course?.depts?.[0]?.name} @@ -79,10 +85,15 @@ export default function CourseCard({ course }: CourseCardProps) { {/* {course?.depts?.map((dept)=>{return dept.name})} */}
- - {course?.meta?.views - ? `观看次数 ${course?.meta?.views}` - : null} +
+
+ + + {`观看次数 ${course?.meta?.views || 0}`} + + + + {`学习人数 ${course?.studentIds?.length || 0}`}
@@ -91,7 +102,7 @@ export default function CourseCard({ course }: CourseCardProps) { size="large" className="w-full shadow-[0_8px_20px_-6px_rgba(59,130,246,0.5)] hover:shadow-[0_12px_24px_-6px_rgba(59,130,246,0.6)] transform hover:translate-y-[-2px] transition-all duration-500 ease-out"> - 立即学习 + {edit ? "进行编辑" : "立即学习"}
diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index 01ffba6..b437058 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -19,7 +19,7 @@ export default function FilterSection() { }); }; return ( -
+
{taxonomies?.map((tax, index) => { const items = Object.entries(selectedTerms).find( ([key, items]) => key === tax.slug @@ -32,7 +32,7 @@ export default function FilterSection() { handleTermChange( tax?.slug, diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index 40043a3..d589d35 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -43,7 +43,7 @@ const CategorySection = () => { return (
-
+
diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index 3aaec9e..bd2f45b 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -3,8 +3,9 @@ import { Typography, Skeleton } from "antd"; import { TaxonomySlug, TermDto } from "@nice/common"; import { api } from "@nice/client"; import { CoursesSectionTag } from "./CoursesSectionTag"; -import PostList from "@web/src/components/models/course/list/PostList"; import LookForMore from "./LookForMore"; +import PostList from "@web/src/components/models/course/list/PostList"; +import CourseCard from "../../courses/components/CourseCard"; interface GetTaxonomyProps { categories: string[]; isLoading: boolean; @@ -16,8 +17,9 @@ function useGetTaxonomy({ type }): GetTaxonomyProps { taxonomy: { slug: type, }, + parentId: null }, - take: 10, // 只取前10个 + take: 11, // 只取前10个 }); const categories = useMemo(() => { const allCategories = isLoading @@ -43,16 +45,15 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ type: TaxonomySlug.CATEGORY, }); return ( - <section className="relative py-20 overflow-hidden bg-gradient-to-b from-gray-50 to-white"> - <div className="max-w-screen-2xl mx-auto px-6 relative"> - <div className="flex justify-between items-end mb-16"> + <section className="relative py-16 overflow-hidden "> + <div className="max-w-screen-2xl mx-auto px-4 relative"> + <div className="flex justify-between items-end mb-12 "> <div> <Title level={2} className="font-bold text-5xl mb-6 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> {title} - @@ -81,6 +82,7 @@ const CoursesSection: React.FC = ({ )}
} params={{ page: 1, pageSize: initialVisibleCoursesCount, diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx index 17aa228..4073109 100755 --- a/apps/web/src/app/main/home/components/HeroSection.tsx +++ b/apps/web/src/app/main/home/components/HeroSection.tsx @@ -1,4 +1,10 @@ -import React, { useRef, useCallback, useEffect, useMemo, useState } from "react"; +import React, { + useRef, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { Carousel, Typography } from "antd"; import { TeamOutlined, @@ -30,13 +36,29 @@ interface PlatformStat { const HeroSection = () => { const carouselRef = useRef(null); const { statistics, slides } = useAppConfig(); - const [countStatistics, setCountStatistics] = useState(0) + const [countStatistics, setCountStatistics] = useState(4); const platformStats: PlatformStat[] = useMemo(() => { return [ - { icon: , value: statistics.staffs, label: "注册学员" }, - { icon: , value: statistics.courses, label: "精品课程" }, - { icon: , value: statistics.lectures, label: '课程章节' }, - { icon: , value: statistics.reads, label: "观看次数" }, + { + icon: , + value: statistics.staffs, + label: "注册学员", + }, + { + icon: , + value: statistics.courses, + label: "精品课程", + }, + { + icon: , + value: statistics.lectures, + label: "课程章节", + }, + { + icon: , + value: statistics.reads, + label: "观看次数", + }, ]; }, [statistics]); const handlePrev = useCallback(() => { @@ -48,7 +70,7 @@ const HeroSection = () => { }, []); const countNonZeroValues = (statistics: Record): number => { - return Object.values(statistics).filter(value => value !== 0).length; + return Object.values(statistics).filter((value) => value !== 0).length; }; useEffect(() => { @@ -67,8 +89,8 @@ const HeroSection = () => { dots={{ className: "carousel-dots !bottom-32 !z-20", }}> - {Array.isArray(slides) ? - (slides.map((item, index) => ( + {Array.isArray(slides) ? ( + slides.map((item, index) => (
{
)) - ) : ( -
- )} + ) : ( +
+ )} {/* Navigation Buttons */} @@ -108,31 +130,30 @@ const HeroSection = () => {
{/* Stats Container */} - { - countStatistics > 1 && ( -
-
- {platformStats.map((stat, index) => { - return stat.value - ? (
-
- {stat.icon} -
-
- {stat.value} -
-
- {stat.label} -
+ {countStatistics > 1 && ( +
+
+ {platformStats.map((stat, index) => { + return stat.value ? ( +
+
+ {stat.icon}
- ) : null - })} -
+
+ {stat.value} +
+
+ {stat.label} +
+
+ ) : null; + })}
- ) - } +
+ )}
); }; diff --git a/apps/web/src/app/main/layout/MainFooter.tsx b/apps/web/src/app/main/layout/MainFooter.tsx index b08449a..9d060dc 100755 --- a/apps/web/src/app/main/layout/MainFooter.tsx +++ b/apps/web/src/app/main/layout/MainFooter.tsx @@ -2,7 +2,7 @@ import { CloudOutlined, FileSearchOutlined, HomeOutlined, MailOutlined, PhoneOut export function MainFooter() { return ( -