add
This commit is contained in:
parent
10ffd12ebe
commit
712202deef
|
@ -1,10 +0,0 @@
|
|||
// import { Module } from '@nestjs/common';
|
||||
// import { CourseRouter } from './course.router';
|
||||
// import { CourseService } from './course.service';
|
||||
// import { TrpcService } from '@server/trpc/trpc.service';
|
||||
|
||||
// @Module({
|
||||
// providers: [CourseRouter, CourseService, TrpcService],
|
||||
// exports: [CourseRouter, CourseService]
|
||||
// })
|
||||
// export class CourseModule { }
|
|
@ -1,92 +0,0 @@
|
|||
// import { Injectable } from '@nestjs/common';
|
||||
// import { TrpcService } from '@server/trpc/trpc.service';
|
||||
// import { Prisma, UpdateOrderSchema } from '@nice/common';
|
||||
// import { CourseService } from './course.service';
|
||||
// import { z, ZodType } from 'zod';
|
||||
// // const CourseCreateArgsSchema: ZodType<Prisma.CourseCreateArgs> = z.any();
|
||||
// // const CourseUpdateArgsSchema: ZodType<Prisma.CourseUpdateArgs> = z.any();
|
||||
// // const CourseCreateManyInputSchema: ZodType<Prisma.CourseCreateManyInput> =
|
||||
// // z.any();
|
||||
// // const CourseDeleteManyArgsSchema: ZodType<Prisma.CourseDeleteManyArgs> =
|
||||
// // z.any();
|
||||
// // const CourseFindManyArgsSchema: ZodType<Prisma.CourseFindManyArgs> = z.any();
|
||||
// // const CourseFindFirstArgsSchema: ZodType<Prisma.CourseFindFirstArgs> = z.any();
|
||||
// // const CourseWhereInputSchema: ZodType<Prisma.CourseWhereInput> = z.any();
|
||||
// // const CourseSelectSchema: ZodType<Prisma.CourseSelect> = z.any();
|
||||
|
||||
// @Injectable()
|
||||
// export class CourseRouter {
|
||||
// constructor(
|
||||
// private readonly trpc: TrpcService,
|
||||
// private readonly courseService: CourseService,
|
||||
// ) {}
|
||||
// router = this.trpc.router({
|
||||
// // create: this.trpc.protectProcedure
|
||||
// // .input(CourseCreateArgsSchema)
|
||||
// // .mutation(async ({ ctx, input }) => {
|
||||
// // const { staff } = ctx;
|
||||
// // return await this.courseService.create(input, { staff });
|
||||
// // }),
|
||||
// // update: this.trpc.protectProcedure
|
||||
// // .input(CourseUpdateArgsSchema)
|
||||
// // .mutation(async ({ ctx, input }) => {
|
||||
// // const { staff } = ctx;
|
||||
// // return await this.courseService.update(input, { staff });
|
||||
// // }),
|
||||
// // createMany: this.trpc.protectProcedure
|
||||
// // .input(z.array(CourseCreateManyInputSchema))
|
||||
// // .mutation(async ({ ctx, input }) => {
|
||||
// // const { staff } = ctx;
|
||||
// // return await this.courseService.createMany({ data: input }, staff);
|
||||
// // }),
|
||||
// // deleteMany: this.trpc.procedure
|
||||
// // .input(CourseDeleteManyArgsSchema)
|
||||
// // .mutation(async ({ input }) => {
|
||||
// // return await this.courseService.deleteMany(input);
|
||||
// // }),
|
||||
// // findFirst: this.trpc.procedure
|
||||
// // .input(CourseFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
// // .query(async ({ input }) => {
|
||||
// // return await this.courseService.findFirst(input);
|
||||
// // }),
|
||||
// // softDeleteByIds: this.trpc.protectProcedure
|
||||
// // .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema
|
||||
// // .mutation(async ({ input }) => {
|
||||
// // return this.courseService.softDeleteByIds(input.ids);
|
||||
// // }),
|
||||
// // updateOrder: this.trpc.protectProcedure
|
||||
// // .input(UpdateOrderSchema)
|
||||
// // .mutation(async ({ input }) => {
|
||||
// // return this.courseService.updateOrder(input);
|
||||
// // }),
|
||||
// // findMany: this.trpc.procedure
|
||||
// // .input(CourseFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
// // .query(async ({ input }) => {
|
||||
// // return await this.courseService.findMany(input);
|
||||
// // }),
|
||||
// // findManyWithCursor: this.trpc.protectProcedure
|
||||
// // .input(
|
||||
// // z.object({
|
||||
// // cursor: z.any().nullish(),
|
||||
// // take: z.number().optional(),
|
||||
// // where: CourseWhereInputSchema.optional(),
|
||||
// // select: CourseSelectSchema.optional(),
|
||||
// // }),
|
||||
// // )
|
||||
// // .query(async ({ ctx, input }) => {
|
||||
// // return await this.courseService.findManyWithCursor(input);
|
||||
// // }),
|
||||
// // findManyWithPagination: this.trpc.procedure
|
||||
// // .input(
|
||||
// // z.object({
|
||||
// // page: z.number().optional(),
|
||||
// // pageSize: z.number().optional(),
|
||||
// // where: CourseWhereInputSchema.optional(),
|
||||
// // select: CourseSelectSchema.optional(),
|
||||
// // }),
|
||||
// // ) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
// // .query(async ({ input }) => {
|
||||
// // return await this.courseService.findManyWithPagination(input);
|
||||
// // }),
|
||||
// });
|
||||
// }
|
|
@ -1,78 +0,0 @@
|
|||
// import { Injectable } from '@nestjs/common';
|
||||
// import { BaseService } from '../base/base.service';
|
||||
// import {
|
||||
// UserProfile,
|
||||
// db,
|
||||
// ObjectType,
|
||||
// Prisma,
|
||||
// InstructorRole,
|
||||
// } from '@nice/common';
|
||||
// @Injectable()
|
||||
// export class CourseService extends BaseService<Prisma.CourseDelegate> {
|
||||
// constructor() {
|
||||
// super(db, ObjectType.COURSE);
|
||||
// }
|
||||
// async create(
|
||||
// args: Prisma.CourseCreateArgs,
|
||||
// params?: { staff?: UserProfile }
|
||||
// ) {
|
||||
// return await db.$transaction(async tx => {
|
||||
// const result = await super.create(args, { tx });
|
||||
// if (params?.staff?.id) {
|
||||
// await tx.courseInstructor.create({
|
||||
// data: {
|
||||
// instructorId: params.staff.id,
|
||||
// courseId: result.id,
|
||||
// role: InstructorRole.MAIN,
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// return result;
|
||||
// }, {
|
||||
// timeout: 10000 // 10 seconds
|
||||
// });
|
||||
// }
|
||||
// async update(
|
||||
// args: Prisma.CourseUpdateArgs,
|
||||
// params?: { staff?: UserProfile }
|
||||
// ) {
|
||||
// return await db.$transaction(async tx => {
|
||||
// const result = await super.update(args, { tx });
|
||||
// return result;
|
||||
// }, {
|
||||
// timeout: 10000 // 10 seconds
|
||||
// });
|
||||
// }
|
||||
// async removeInstructor(courseId: string, instructorId: string) {
|
||||
// return await db.courseInstructor.delete({
|
||||
// where: {
|
||||
// courseId_instructorId: {
|
||||
// courseId,
|
||||
// instructorId,
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// async addInstructor(params: {
|
||||
// courseId: string;
|
||||
// instructorId: string;
|
||||
// role?: string;
|
||||
// order?: number;
|
||||
// }) {
|
||||
// return await db.courseInstructor.create({
|
||||
// data: {
|
||||
// courseId: params.courseId,
|
||||
// instructorId: params.instructorId,
|
||||
// role: params.role || InstructorRole.ASSISTANT,
|
||||
// order: params.order,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// async getInstructors(courseId: string) {
|
||||
// return await db.courseInstructor.findMany({
|
||||
// where: { courseId },
|
||||
// include: { instructor: true },
|
||||
// orderBy: { order: 'asc' },
|
||||
// });
|
||||
// }
|
||||
// }
|
|
@ -1,49 +0,0 @@
|
|||
// import { db, EnrollmentStatus, PostType } from '@nice/common';
|
||||
|
||||
// // 更新课程评价统计
|
||||
// export async function updateCourseReviewStats(courseId: string) {
|
||||
// const reviews = await db.post.findMany({
|
||||
// where: {
|
||||
// courseId,
|
||||
// type: PostType.COURSE_REVIEW,
|
||||
// deletedAt: null,
|
||||
// },
|
||||
// select: { rating: true },
|
||||
// });
|
||||
// const numberOfReviews = reviews.length;
|
||||
// const averageRating =
|
||||
// numberOfReviews > 0
|
||||
// ? reviews.reduce((sum, review) => sum + review.rating, 0) /
|
||||
// numberOfReviews
|
||||
// : 0;
|
||||
|
||||
// return db.course.update({
|
||||
// where: { id: courseId },
|
||||
// data: {
|
||||
// // numberOfReviews,
|
||||
// //averageRating,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// // 更新课程注册统计
|
||||
// export async function updateCourseEnrollmentStats(courseId: string) {
|
||||
// const completedEnrollments = await db.enrollment.count({
|
||||
// where: {
|
||||
// courseId,
|
||||
// status: EnrollmentStatus.COMPLETED,
|
||||
// },
|
||||
// });
|
||||
// const totalEnrollments = await db.enrollment.count({
|
||||
// where: { courseId },
|
||||
// });
|
||||
// const completionRate =
|
||||
// totalEnrollments > 0 ? (completedEnrollments / totalEnrollments) * 100 : 0;
|
||||
// return db.course.update({
|
||||
// where: { id: courseId },
|
||||
// data: {
|
||||
// // numberOfStudents: totalEnrollments,
|
||||
// // completionRate,
|
||||
// },
|
||||
// });
|
||||
// }
|
|
@ -1,10 +0,0 @@
|
|||
// import { Module } from '@nestjs/common';
|
||||
// import { LectureRouter } from './lecture.router';
|
||||
// import { LectureService } from './lecture.service';
|
||||
// import { TrpcService } from '@server/trpc/trpc.service';
|
||||
|
||||
// @Module({
|
||||
// providers: [LectureRouter, LectureService, TrpcService],
|
||||
// exports: [LectureRouter, LectureService]
|
||||
// })
|
||||
// export class LectureModule { }
|
|
@ -1,70 +0,0 @@
|
|||
// import { Injectable } from '@nestjs/common';
|
||||
// import { TrpcService } from '@server/trpc/trpc.service';
|
||||
// import { Prisma, UpdateOrderSchema } from '@nice/common';
|
||||
// import { LectureService } from './lecture.service';
|
||||
// import { z, ZodType } from 'zod';
|
||||
// const LectureCreateArgsSchema: ZodType<Prisma.LectureCreateArgs> = z.any()
|
||||
// const LectureCreateManyInputSchema: ZodType<Prisma.LectureCreateManyInput> = z.any()
|
||||
// const LectureDeleteManyArgsSchema: ZodType<Prisma.LectureDeleteManyArgs> = z.any()
|
||||
// const LectureFindManyArgsSchema: ZodType<Prisma.LectureFindManyArgs> = z.any()
|
||||
// const LectureFindFirstArgsSchema: ZodType<Prisma.LectureFindFirstArgs> = z.any()
|
||||
// const LectureWhereInputSchema: ZodType<Prisma.LectureWhereInput> = z.any()
|
||||
// const LectureSelectSchema: ZodType<Prisma.LectureSelect> = z.any()
|
||||
|
||||
// @Injectable()
|
||||
// export class LectureRouter {
|
||||
// constructor(
|
||||
// private readonly trpc: TrpcService,
|
||||
// private readonly lectureService: LectureService,
|
||||
// ) { }
|
||||
// router = this.trpc.router({
|
||||
// create: this.trpc.protectProcedure
|
||||
// .input(LectureCreateArgsSchema)
|
||||
// .mutation(async ({ ctx, input }) => {
|
||||
// const { staff } = ctx;
|
||||
// return await this.lectureService.create(input, {staff});
|
||||
// }),
|
||||
// createMany: this.trpc.protectProcedure.input(z.array(LectureCreateManyInputSchema))
|
||||
// .mutation(async ({ ctx, input }) => {
|
||||
// const { staff } = ctx;
|
||||
|
||||
// return await this.lectureService.createMany({ data: input }, staff);
|
||||
// }),
|
||||
// deleteMany: this.trpc.procedure
|
||||
// .input(LectureDeleteManyArgsSchema)
|
||||
// .mutation(async ({ input }) => {
|
||||
// return await this.lectureService.deleteMany(input);
|
||||
// }),
|
||||
// findFirst: this.trpc.procedure
|
||||
// .input(LectureFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
// .query(async ({ input }) => {
|
||||
// return await this.lectureService.findFirst(input);
|
||||
// }),
|
||||
// softDeleteByIds: this.trpc.protectProcedure
|
||||
// .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema
|
||||
// .mutation(async ({ input }) => {
|
||||
// return this.lectureService.softDeleteByIds(input.ids);
|
||||
// }),
|
||||
// updateOrder: this.trpc.protectProcedure
|
||||
// .input(UpdateOrderSchema)
|
||||
// .mutation(async ({ input }) => {
|
||||
// return this.lectureService.updateOrder(input);
|
||||
// }),
|
||||
// findMany: this.trpc.procedure
|
||||
// .input(LectureFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
// .query(async ({ input }) => {
|
||||
// return await this.lectureService.findMany(input);
|
||||
// }),
|
||||
// findManyWithCursor: this.trpc.protectProcedure
|
||||
// .input(z.object({
|
||||
// cursor: z.any().nullish(),
|
||||
// take: z.number().nullish(),
|
||||
// where: LectureWhereInputSchema.nullish(),
|
||||
// select: LectureSelectSchema.nullish()
|
||||
// }))
|
||||
// .query(async ({ ctx, input }) => {
|
||||
// const { staff } = ctx;
|
||||
// return await this.lectureService.findManyWithCursor(input);
|
||||
// }),
|
||||
// });
|
||||
// }
|
|
@ -1,35 +0,0 @@
|
|||
// import { Injectable } from '@nestjs/common';
|
||||
// import { BaseService } from '../base/base.service';
|
||||
// import {
|
||||
// UserProfile,
|
||||
// db,
|
||||
// ObjectType,
|
||||
// Prisma
|
||||
// } from '@nice/common';
|
||||
// import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||
|
||||
// @Injectable()
|
||||
// export class LectureService extends BaseService<Prisma.LectureDelegate> {
|
||||
// constructor() {
|
||||
// super(db, ObjectType.COURSE);
|
||||
// }
|
||||
// async create(args: Prisma.LectureCreateArgs, params?: { staff?: UserProfile }) {
|
||||
// const result = await super.create(args)
|
||||
// EventBus.emit('dataChanged', {
|
||||
// type: ObjectType.LECTURE,
|
||||
// operation: CrudOperation.CREATED,
|
||||
// data: result,
|
||||
// });
|
||||
// return result;
|
||||
// }
|
||||
// async update(args: Prisma.LectureUpdateArgs) {
|
||||
// const result = await super.update(args);
|
||||
// EventBus.emit('dataChanged', {
|
||||
// type: ObjectType.LECTURE,
|
||||
// operation: CrudOperation.UPDATED,
|
||||
// data: result,
|
||||
// });
|
||||
// return result;
|
||||
// }
|
||||
|
||||
// }
|
|
@ -1,48 +0,0 @@
|
|||
import { db, PostType } from '@nice/common';
|
||||
|
||||
// export async function updateSectionLectureStats(sectionId: string) {
|
||||
// const sectionStats = await db.post.aggregate({
|
||||
// where: {
|
||||
// parentId: sectionId,
|
||||
// deletedAt: null,
|
||||
// type: PostType.LECTURE,
|
||||
// },
|
||||
// _count: { _all: true },
|
||||
// _sum: { duration: true },
|
||||
// });
|
||||
|
||||
// await db.post.update({
|
||||
// where: { id: sectionId },
|
||||
// data: {
|
||||
// // totalLectures: sectionStats._count._all,
|
||||
// // totalDuration: sectionStats._sum.duration || 0,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// export async function updateParentLectureStats(parentId: string) {
|
||||
// const ParentStats = await db.post.aggregate({
|
||||
// where: {
|
||||
// ancestors: {
|
||||
// some: {
|
||||
// ancestorId: parentId,
|
||||
// descendant: {
|
||||
// type: PostType.LECTURE,
|
||||
// deletedAt: null,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// _count: { _all: true },
|
||||
// _sum: {
|
||||
// duration: true,
|
||||
// },
|
||||
// });
|
||||
// await db.post.update({
|
||||
// where: { id: parentId },
|
||||
// data: {
|
||||
// //totalLectures: courseStats._count._all,
|
||||
// //totalDuration: courseStats._sum.duration || 0,
|
||||
// },
|
||||
// });
|
||||
// }
|
|
@ -1,10 +0,0 @@
|
|||
// import { Module } from '@nestjs/common';
|
||||
// import { SectionRouter } from './section.router';
|
||||
// import { SectionService } from './section.service';
|
||||
// import { TrpcService } from '@server/trpc/trpc.service';
|
||||
|
||||
// @Module({
|
||||
// exports: [SectionRouter, SectionService],
|
||||
// providers: [SectionRouter, SectionService, TrpcService]
|
||||
// })
|
||||
// export class SectionModule { }
|
|
@ -1,70 +0,0 @@
|
|||
// import { Injectable } from '@nestjs/common';
|
||||
// import { TrpcService } from '@server/trpc/trpc.service';
|
||||
// import { Prisma, UpdateOrderSchema } from '@nice/common';
|
||||
// import { SectionService } from './section.service';
|
||||
// import { z, ZodType } from 'zod';
|
||||
// const SectionCreateArgsSchema: ZodType<Prisma.SectionCreateArgs> = z.any()
|
||||
// const SectionCreateManyInputSchema: ZodType<Prisma.SectionCreateManyInput> = z.any()
|
||||
// const SectionDeleteManyArgsSchema: ZodType<Prisma.SectionDeleteManyArgs> = z.any()
|
||||
// const SectionFindManyArgsSchema: ZodType<Prisma.SectionFindManyArgs> = z.any()
|
||||
// const SectionFindFirstArgsSchema: ZodType<Prisma.SectionFindFirstArgs> = z.any()
|
||||
// const SectionWhereInputSchema: ZodType<Prisma.SectionWhereInput> = z.any()
|
||||
// const SectionSelectSchema: ZodType<Prisma.SectionSelect> = z.any()
|
||||
|
||||
// @Injectable()
|
||||
// export class SectionRouter {
|
||||
// constructor(
|
||||
// private readonly trpc: TrpcService,
|
||||
// private readonly sectionService: SectionService,
|
||||
// ) { }
|
||||
// router = this.trpc.router({
|
||||
// create: this.trpc.protectProcedure
|
||||
// .input(SectionCreateArgsSchema)
|
||||
// .mutation(async ({ ctx, input }) => {
|
||||
// const { staff } = ctx;
|
||||
// return await this.sectionService.create(input, { staff });
|
||||
// }),
|
||||
// createMany: this.trpc.protectProcedure.input(z.array(SectionCreateManyInputSchema))
|
||||
// .mutation(async ({ ctx, input }) => {
|
||||
// const { staff } = ctx;
|
||||
|
||||
// return await this.sectionService.createMany({ data: input }, staff);
|
||||
// }),
|
||||
// deleteMany: this.trpc.procedure
|
||||
// .input(SectionDeleteManyArgsSchema)
|
||||
// .mutation(async ({ input }) => {
|
||||
// return await this.sectionService.deleteMany(input);
|
||||
// }),
|
||||
// findFirst: this.trpc.procedure
|
||||
// .input(SectionFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
// .query(async ({ input }) => {
|
||||
// return await this.sectionService.findFirst(input);
|
||||
// }),
|
||||
// softDeleteByIds: this.trpc.protectProcedure
|
||||
// .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema
|
||||
// .mutation(async ({ input }) => {
|
||||
// return this.sectionService.softDeleteByIds(input.ids);
|
||||
// }),
|
||||
// updateOrder: this.trpc.protectProcedure
|
||||
// .input(UpdateOrderSchema)
|
||||
// .mutation(async ({ input }) => {
|
||||
// return this.sectionService.updateOrder(input);
|
||||
// }),
|
||||
// findMany: this.trpc.procedure
|
||||
// .input(SectionFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
// .query(async ({ input }) => {
|
||||
// return await this.sectionService.findMany(input);
|
||||
// }),
|
||||
// findManyWithCursor: this.trpc.protectProcedure
|
||||
// .input(z.object({
|
||||
// cursor: z.any().nullish(),
|
||||
// take: z.number().nullish(),
|
||||
// where: SectionWhereInputSchema.nullish(),
|
||||
// select: SectionSelectSchema.nullish()
|
||||
// }))
|
||||
// .query(async ({ ctx, input }) => {
|
||||
// const { staff } = ctx;
|
||||
// return await this.sectionService.findManyWithCursor(input);
|
||||
// }),
|
||||
// });
|
||||
// }
|
|
@ -1,23 +0,0 @@
|
|||
// import { Injectable } from '@nestjs/common';
|
||||
// import { BaseService } from '../base/base.service';
|
||||
// import {
|
||||
// UserProfile,
|
||||
// db,
|
||||
// ObjectType,
|
||||
// Prisma,
|
||||
|
||||
// } from '@nice/common';
|
||||
// @Injectable()
|
||||
// export class SectionService extends BaseService<Prisma.SectionDelegate> {
|
||||
// constructor() {
|
||||
// super(db, ObjectType.SECTION);
|
||||
// }
|
||||
|
||||
// create(args: Prisma.SectionCreateArgs, params?: { staff?: UserProfile }) {
|
||||
// return super.create(args)
|
||||
// }
|
||||
// async update(args: Prisma.SectionUpdateArgs) {
|
||||
// return super.update(args);
|
||||
// }
|
||||
|
||||
// }
|
|
@ -4,9 +4,10 @@ import { Prisma } from '@nice/common';
|
|||
|
||||
import { VisitService } from './visit.service';
|
||||
import { z, ZodType } from 'zod';
|
||||
const VisitCreateArgsSchema: ZodType<Prisma.VisitCreateArgs> = z.any()
|
||||
const VisitCreateManyInputSchema: ZodType<Prisma.VisitCreateManyInput> = z.any()
|
||||
const VisitDeleteManyArgsSchema: ZodType<Prisma.VisitDeleteManyArgs> = z.any()
|
||||
const VisitCreateArgsSchema: ZodType<Prisma.VisitCreateArgs> = z.any();
|
||||
const VisitCreateManyInputSchema: ZodType<Prisma.VisitCreateManyInput> =
|
||||
z.any();
|
||||
const VisitDeleteManyArgsSchema: ZodType<Prisma.VisitDeleteManyArgs> = z.any();
|
||||
@Injectable()
|
||||
export class VisitRouter {
|
||||
constructor(
|
||||
|
@ -20,7 +21,8 @@ export class VisitRouter {
|
|||
const { staff } = ctx;
|
||||
return await this.visitService.create(input, staff);
|
||||
}),
|
||||
createMany: this.trpc.protectProcedure.input(z.array(VisitCreateManyInputSchema))
|
||||
createMany: this.trpc.protectProcedure
|
||||
.input(z.array(VisitCreateManyInputSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
|
||||
|
@ -31,7 +33,5 @@ export class VisitRouter {
|
|||
.mutation(async ({ input }) => {
|
||||
return await this.visitService.deleteMany(input);
|
||||
}),
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
|
|
@ -30,12 +30,17 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
|||
});
|
||||
}
|
||||
|
||||
// if (troubleId && args.data.type === VisitType.READED) {
|
||||
// EventBus.emit('updateViewCount', {
|
||||
// objectType: ObjectType.TROUBLE,
|
||||
// id: troubleId,
|
||||
// });
|
||||
// }
|
||||
if (
|
||||
[VisitType.READED, VisitType.LIKE, VisitType.HATE].includes(
|
||||
args.data.type as VisitType,
|
||||
)
|
||||
) {
|
||||
EventBus.emit('updateVisitCount', {
|
||||
objectType: ObjectType.POST,
|
||||
id: postId,
|
||||
visitType: args.data.type, // 直接复用传入的类型
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async createMany(args: Prisma.VisitCreateManyArgs, staff?: UserProfile) {
|
||||
|
@ -80,4 +85,61 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
|||
|
||||
return { count: updatePromises.length }; // Return the number of updates if no new creates
|
||||
}
|
||||
async deleteMany(args: Prisma.VisitDeleteManyArgs, staff?: UserProfile) {
|
||||
// const where = Array.isArray(args.where) ? args.where : [args.where];
|
||||
// const updatePromises: any[] = [];
|
||||
// const createData: Prisma.VisitCreateManyInput[] = [];
|
||||
// super
|
||||
// await Promise.all(
|
||||
// data.map(async (item) => {
|
||||
// if (staff && !item.visitorId) item.visitorId = staff.id;
|
||||
// const { postId, messageId, visitorId } = item;
|
||||
// const existingVisit = await db.visit.findFirst({
|
||||
// where: {
|
||||
// visitorId,
|
||||
// OR: [{ postId }, { messageId }],
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (existingVisit) {
|
||||
// updatePromises.push(
|
||||
// super.update({
|
||||
// where: { id: existingVisit.id },
|
||||
// data: {
|
||||
// ...item,
|
||||
// views: existingVisit.views + 1,
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
// } else {
|
||||
// createData.push(item);
|
||||
// }
|
||||
// }),
|
||||
// );
|
||||
// // Execute all updates in parallel
|
||||
// await Promise.all(updatePromises);
|
||||
// // Create new visits for those not existing
|
||||
// if (createData.length > 0) {
|
||||
// return super.createMany({
|
||||
// ...args,
|
||||
// data: createData,
|
||||
// });
|
||||
// }
|
||||
// return { count: updatePromises.length }; // Return the number of updates if no new creates
|
||||
const superDetele = super.deleteMany(args, staff);
|
||||
if (args?.where?.postId) {
|
||||
if (
|
||||
[VisitType.READED, VisitType.LIKE, VisitType.HATE].includes(
|
||||
args.where.type as any,
|
||||
)
|
||||
) {
|
||||
EventBus.emit('updateVisitCount', {
|
||||
objectType: ObjectType.POST,
|
||||
id: args?.where?.postId as string,
|
||||
visitType: args.where.type as any, // 直接复用传入的类型
|
||||
});
|
||||
}
|
||||
}
|
||||
return superDetele;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { Queue } from 'bullmq';
|
||||
import EventBus from '@server/utils/event-bus';
|
||||
import { ObjectType, VisitType } from '@nice/common';
|
||||
import {
|
||||
QueueJobType,
|
||||
updatePostStateJobData,
|
||||
updateVisitCountJobData,
|
||||
} from '@server/queue/types';
|
||||
|
||||
@Injectable()
|
||||
export class PostQueueService implements OnModuleInit {
|
||||
private readonly logger = new Logger(PostQueueService.name);
|
||||
constructor(@InjectQueue('general') private generalQueue: Queue) {}
|
||||
onModuleInit() {
|
||||
EventBus.on('updateVisitCount', ({ id, objectType, visitType }) => {
|
||||
if (objectType === ObjectType.POST) {
|
||||
this.addUpdateVisitCountJob({ id, type: visitType });
|
||||
}
|
||||
});
|
||||
EventBus.on('updatePostState', ({ id }) => {
|
||||
this.addUpdatePostState({ id });
|
||||
});
|
||||
}
|
||||
async addUpdateVisitCountJob(data: updateVisitCountJobData) {
|
||||
this.logger.log(`update post view count ${data.id}`);
|
||||
await this.generalQueue.add(QueueJobType.UPDATE_POST_VISIT_COUNT, data, {
|
||||
debounce: {
|
||||
id: `${QueueJobType.UPDATE_POST_VISIT_COUNT}_${data.type}_${data.id}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
async addUpdatePostState(data: updatePostStateJobData) {
|
||||
this.logger.log(`update post state ${data.id}`);
|
||||
await this.generalQueue.add(QueueJobType.UPDATE_POST_STATE, data, {
|
||||
debounce: { id: `${QueueJobType.UPDATE_POST_STATE}_${data.id}` },
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { db, VisitType } from '@nice/common';
|
||||
export async function updatePostViewCount(id: string, type: VisitType) {
|
||||
const totalViews = await db.visit.aggregate({
|
||||
_sum: {
|
||||
views: true,
|
||||
},
|
||||
where: {
|
||||
postId: id,
|
||||
type: type,
|
||||
},
|
||||
});
|
||||
if (type === VisitType.READED) {
|
||||
await db.post.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
views: totalViews._sum.views || 0,
|
||||
}, // Use 0 if no visits exist
|
||||
},
|
||||
});
|
||||
} else if (type === VisitType.LIKE) {
|
||||
await db.post.update({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
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: {
|
||||
hates: totalViews._sum.views || 0, // Use 0 if no visits exist
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import { BullModule } from '@nestjs/bullmq';
|
|||
import { Logger, Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { join } from 'path';
|
||||
import { PostQueueService } from './models/post/post.queue.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -28,7 +29,7 @@ import { join } from 'path';
|
|||
},
|
||||
),
|
||||
],
|
||||
providers: [Logger],
|
||||
providers: [Logger, PostQueueService],
|
||||
exports: [],
|
||||
})
|
||||
export class QueueModule {}
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
import { VisitType } from '@nice/common';
|
||||
export enum QueueJobType {
|
||||
UPDATE_STATS = 'update_stats',
|
||||
FILE_PROCESS = 'file_process',
|
||||
UPDATE_POST_VISIT_COUNT = 'updatePostVisitCount',
|
||||
UPDATE_POST_STATE = 'updatePostState',
|
||||
}
|
||||
export type updateVisitCountJobData = {
|
||||
id: string;
|
||||
type: VisitType | string;
|
||||
};
|
||||
export type updatePostStateJobData = {
|
||||
id: string;
|
||||
};
|
||||
|
|
|
@ -1,16 +1,28 @@
|
|||
import mitt from 'mitt';
|
||||
import { ObjectType, UserProfile, MessageDto } from '@nice/common';
|
||||
import { ObjectType, UserProfile, MessageDto, VisitType } from '@nice/common';
|
||||
export enum CrudOperation {
|
||||
CREATED,
|
||||
UPDATED,
|
||||
DELETED
|
||||
DELETED,
|
||||
}
|
||||
type Events = {
|
||||
genDataEvent: { type: "start" | "end" },
|
||||
markDirty: { objectType: string, id: string, staff?: UserProfile, subscribers?: string[] }
|
||||
updateViewCount: { id: string, objectType: ObjectType },
|
||||
onMessageCreated: { data: Partial<MessageDto> },
|
||||
dataChanged: { type: string, operation: CrudOperation, data: any }
|
||||
genDataEvent: { type: 'start' | 'end' };
|
||||
markDirty: {
|
||||
objectType: string;
|
||||
id: string;
|
||||
staff?: UserProfile;
|
||||
subscribers?: string[];
|
||||
};
|
||||
updateVisitCount: {
|
||||
id: string;
|
||||
objectType: ObjectType;
|
||||
visitType: VisitType | string;
|
||||
};
|
||||
updatePostState: {
|
||||
id: string;
|
||||
};
|
||||
onMessageCreated: { data: Partial<MessageDto> };
|
||||
dataChanged: { type: string; operation: CrudOperation; data: any };
|
||||
};
|
||||
const EventBus = mitt<Events>();
|
||||
export default EventBus;
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { api } from "@nice/client";
|
||||
import { courseDetailSelect, CourseDto, Lecture } from "@nice/common";
|
||||
import { api, useVisitor } from "@nice/client";
|
||||
import {
|
||||
courseDetailSelect,
|
||||
CourseDto,
|
||||
Lecture,
|
||||
VisitType,
|
||||
} from "@nice/common";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import React, { createContext, ReactNode, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
|
@ -25,6 +31,8 @@ export function CourseDetailProvider({
|
|||
editId,
|
||||
}: CourseFormProviderProps) {
|
||||
const navigate = useNavigate();
|
||||
const { read } = useVisitor();
|
||||
const { user } = useAuth();
|
||||
const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } =
|
||||
(api.post as any).findFirst.useQuery(
|
||||
{
|
||||
|
@ -36,6 +44,7 @@ export function CourseDetailProvider({
|
|||
},
|
||||
{ enabled: Boolean(editId) }
|
||||
);
|
||||
|
||||
const [selectedLectureId, setSelectedLectureId] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
|
@ -47,6 +56,17 @@ export function CourseDetailProvider({
|
|||
},
|
||||
{ enabled: Boolean(editId) }
|
||||
);
|
||||
useEffect(() => {
|
||||
if (course) {
|
||||
read.mutateAsync({
|
||||
data: {
|
||||
visitorId: user?.id || null,
|
||||
postId: course.id,
|
||||
type: VisitType.READED,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [course]);
|
||||
useEffect(() => {
|
||||
navigate(`/course/${editId}/detail/${selectedLectureId}`);
|
||||
}, [selectedLectureId, editId]);
|
||||
|
|
|
@ -1,58 +1,86 @@
|
|||
// components/Header.tsx
|
||||
import { motion, useScroll, useTransform } from "framer-motion";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { Input, Layout, Avatar, Button, Dropdown } from "antd";
|
||||
import {
|
||||
EditFilled,
|
||||
HomeOutlined,
|
||||
SearchOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { UserMenu } from "@web/src/components/layout/element/usermenu/usermenu";
|
||||
import { CourseDetailContext } from "../CourseDetailContext";
|
||||
import { Button } from "antd";
|
||||
|
||||
export const CourseDetailHeader = () => {
|
||||
const { scrollY } = useScroll();
|
||||
const { Header } = Layout;
|
||||
|
||||
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]);
|
||||
export function CourseDetailHeader() {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { course } = useContext(CourseDetailContext);
|
||||
|
||||
return (
|
||||
<motion.header
|
||||
initial={{ y: 0 }}
|
||||
animate={{ y: isHeaderVisible ? 0 : -100 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
className="fixed top-0 left-0 w-full h-16 bg-slate-900 backdrop-blur-sm z-50 shadow-sm">
|
||||
<div className="w-full mx-auto px-4 h-full flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h1 className="text-white text-xl ">{course?.title}</h1>
|
||||
</div>
|
||||
<Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
|
||||
<div className="w-full max-w-screen-2xl px-4 md:px-6 mx-auto flex items-center justify-between h-full">
|
||||
<div className="flex items-center space-x-2">
|
||||
<HomeOutlined
|
||||
onClick={() => {
|
||||
navigate("/");
|
||||
}}
|
||||
className="text-2xl text-primary-500 hover:scale-105 cursor-pointer"
|
||||
/>
|
||||
|
||||
<nav className="flex items-center space-x-4">
|
||||
<button className="px-4 py-2 rounded-full bg-blue-500 text-white hover:bg-blue-600 transition-colors">
|
||||
开始学习
|
||||
</button>
|
||||
</nav>
|
||||
<div className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent transition-transform ">
|
||||
{course?.title}
|
||||
</div>
|
||||
</motion.header>
|
||||
{/* <NavigationMenu /> */}
|
||||
</div>
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="group relative">
|
||||
<Input
|
||||
size="large"
|
||||
prefix={
|
||||
<SearchOutlined className="text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||
}
|
||||
placeholder="搜索课程"
|
||||
className="w-72 rounded-full"
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => navigate("/course/editor")}
|
||||
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
|
||||
icon={<EditFilled />}>
|
||||
创建课程
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{isAuthenticated ? (
|
||||
<Dropdown
|
||||
overlay={<UserMenu />}
|
||||
trigger={["click"]}
|
||||
placement="bottomRight">
|
||||
<Avatar
|
||||
size="large"
|
||||
className="cursor-pointer hover:scale-105 transition-all bg-gradient-to-r from-blue-500 to-blue-600 text-white font-semibold">
|
||||
{(user?.showname ||
|
||||
user?.username ||
|
||||
"")[0]?.toUpperCase()}
|
||||
</Avatar>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => navigate("/login")}
|
||||
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
|
||||
icon={<UserOutlined />}>
|
||||
登录
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default CourseDetailHeader;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
// // 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 (
|
||||
// <motion.header
|
||||
// initial={{ y: 0 }}
|
||||
// animate={{ y: isHeaderVisible ? 0 : -100 }}
|
||||
// transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
||||
// className="fixed top-0 left-0 w-full h-16 bg-slate-900 backdrop-blur-sm z-50 shadow-sm">
|
||||
// <div className="w-full mx-auto px-4 h-full flex items-center justify-between">
|
||||
// <div className="flex items-center space-x-4">
|
||||
// <h1 className="text-white text-xl ">{course?.title}</h1>
|
||||
// </div>
|
||||
|
||||
// {isAuthenticated ? (
|
||||
// <Dropdown
|
||||
// overlay={<UserMenu />}
|
||||
// trigger={["click"]}
|
||||
// placement="bottomRight">
|
||||
// <Avatar
|
||||
// size="large"
|
||||
// className="cursor-pointer hover:scale-105 transition-all bg-gradient-to-r from-blue-500 to-blue-600 text-white font-semibold">
|
||||
// {(user?.showname ||
|
||||
// user?.username ||
|
||||
// "")[0]?.toUpperCase()}
|
||||
// </Avatar>
|
||||
// </Dropdown>
|
||||
// ) : (
|
||||
// <Button
|
||||
// onClick={() => navigator("/login")}
|
||||
// className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
|
||||
// icon={<UserOutlined />}>
|
||||
// 登录
|
||||
// </Button>
|
||||
// )}
|
||||
// </div>
|
||||
// </motion.header>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default CourseDetailHeader;
|
|
@ -4,8 +4,7 @@ import { CourseDetailContext } from "./CourseDetailContext";
|
|||
|
||||
import CourseDetailDisplayArea from "./CourseDetailDisplayArea";
|
||||
import { CourseSyllabus } from "./CourseSyllabus/CourseSyllabus";
|
||||
import CourseDetailHeader from "./CourseDetailHeader/CourseDetailHeader";
|
||||
import { Button } from "antd";
|
||||
import { CourseDetailHeader } from "./CourseDetailHeader/CourseDetailHeader";
|
||||
|
||||
export default function CourseDetailLayout() {
|
||||
const {
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
import { Lecture, LectureType } from "@nice/common";
|
||||
import React from "react";
|
||||
import { ClockCircleOutlined, FileTextOutlined, PlayCircleOutlined } from "@ant-design/icons"; // 使用 Ant Design 图标
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
FileTextOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from "@ant-design/icons"; // 使用 Ant Design 图标
|
||||
|
||||
interface LectureItemProps {
|
||||
lecture: Lecture;
|
||||
|
@ -28,9 +32,9 @@ export const LectureItem: React.FC<LectureItemProps> = ({
|
|||
<p className="text-sm text-gray-500 mt-1">{lecture.subTitle}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm text-gray-500">
|
||||
{/* <div className="flex items-center gap-1 text-sm text-gray-500">
|
||||
<ClockCircleOutlined className="w-4 h-4" />
|
||||
<span>{lecture.duration}分钟</span>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -66,7 +66,9 @@ export function CourseFormProvider({
|
|||
title: course.title,
|
||||
subTitle: course.subTitle,
|
||||
content: course.content,
|
||||
meta: {
|
||||
thumbnail: course?.meta?.thumbnail,
|
||||
},
|
||||
};
|
||||
course.terms?.forEach((term) => {
|
||||
formData[term.taxonomyId] = term.id; // 假设 taxonomyName 是您在 Form.Item 中使用的 name
|
||||
|
@ -85,8 +87,7 @@ export function CourseFormProvider({
|
|||
const formattedValues = {
|
||||
...values,
|
||||
meta: {
|
||||
requirements: values.requirements,
|
||||
objectives: values.objectives,
|
||||
thumbnail: values.thumbnail,
|
||||
},
|
||||
terms: {
|
||||
connect: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
||||
|
@ -96,8 +97,6 @@ export function CourseFormProvider({
|
|||
taxonomies.forEach((tax) => {
|
||||
delete formattedValues[tax.id];
|
||||
});
|
||||
delete formattedValues.requirements;
|
||||
delete formattedValues.objectives;
|
||||
delete formattedValues.sections;
|
||||
try {
|
||||
if (editId) {
|
||||
|
@ -111,7 +110,7 @@ export function CourseFormProvider({
|
|||
courseDetail: {
|
||||
data: {
|
||||
title: formattedValues.title || "12345",
|
||||
state: CourseStatus.DRAFT,
|
||||
// state: CourseStatus.DRAFT,
|
||||
type: PostType.COURSE,
|
||||
...formattedValues,
|
||||
},
|
||||
|
|
|
@ -34,6 +34,14 @@ export function CourseBasicForm() {
|
|||
rules={[{ max: 10, message: "副标题最多10个字符" }]}>
|
||||
<Input placeholder="请输入课程副标题" />
|
||||
</Form.Item>
|
||||
<Form.Item name={["meta", "thumbnail"]} label="课程封面">
|
||||
<AvatarUploader
|
||||
style={{
|
||||
width: "192px",
|
||||
height: "108px",
|
||||
margin: " 0 10px",
|
||||
}}></AvatarUploader>
|
||||
</Form.Item>
|
||||
<Form.Item name="content" label="课程描述">
|
||||
<TextArea
|
||||
placeholder="请输入课程描述"
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { FormArrayField } from "@web/src/components/common/form/FormArrayField";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { CourseFormData } from "../context/CourseEditorContext";
|
||||
import InputList from "@web/src/components/common/input/InputList";
|
||||
import { useState } from "react";
|
||||
import { Form } from "antd";
|
||||
// import { FormArrayField } from "@web/src/components/common/form/FormArrayField";
|
||||
// import { useFormContext } from "react-hook-form";
|
||||
// import { CourseFormData } from "../context/CourseEditorContext";
|
||||
// import InputList from "@web/src/components/common/input/InputList";
|
||||
// import { useState } from "react";
|
||||
// import { Form } from "antd";
|
||||
|
||||
export function CourseGoalForm() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-6 p-6">
|
||||
<Form.Item name="requirements" label="前置要求">
|
||||
<InputList placeholder="请输入前置要求"></InputList>
|
||||
</Form.Item>
|
||||
<Form.Item name="objectives" label="学习目标">
|
||||
<InputList placeholder="请输入学习目标"></InputList>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// export function CourseGoalForm() {
|
||||
// return (
|
||||
// <div className="max-w-2xl mx-auto space-y-6 p-6">
|
||||
// <Form.Item name="requirements" label="前置要求">
|
||||
// <InputList placeholder="请输入前置要求"></InputList>
|
||||
// </Form.Item>
|
||||
// <Form.Item name="objectives" label="学习目标">
|
||||
// <InputList placeholder="请输入学习目标"></InputList>
|
||||
// </Form.Item>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
|
||||
import { Form, Input } from "antd";
|
||||
// import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
|
||||
// import { Form, Input } from "antd";
|
||||
|
||||
export default function CourseSettingForm() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-6 p-6">
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="课程预览图"
|
||||
>
|
||||
<AvatarUploader
|
||||
style={
|
||||
{
|
||||
width: "120px",
|
||||
height: "120px",
|
||||
margin:" 0 10px"
|
||||
}
|
||||
}
|
||||
onChange={(value) => {
|
||||
console.log(value);
|
||||
}}
|
||||
></AvatarUploader>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// export default function CourseSettingForm() {
|
||||
// return (
|
||||
// <div className="max-w-2xl mx-auto space-y-6 p-6">
|
||||
// <Form.Item
|
||||
// name="title"
|
||||
// label="课程预览图"
|
||||
// >
|
||||
// <AvatarUploader
|
||||
// style={
|
||||
// {
|
||||
// width: "120px",
|
||||
// height: "120px",
|
||||
// margin:" 0 10px"
|
||||
// }
|
||||
// }
|
||||
// onChange={(value) => {
|
||||
// console.log(value);
|
||||
// }}
|
||||
// ></AvatarUploader>
|
||||
// </Form.Item>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
|
@ -41,7 +41,7 @@ export default function CourseEditorHeader() {
|
|||
<Title level={4} style={{ margin: 0 }}>
|
||||
{course?.title || "新建课程"}
|
||||
</Title>
|
||||
<Tag
|
||||
{/* <Tag
|
||||
color={
|
||||
courseStatusVariant[
|
||||
course?.state || CourseStatus.DRAFT
|
||||
|
@ -50,20 +50,20 @@ export default function CourseEditorHeader() {
|
|||
{course?.state
|
||||
? CourseStatusLabel[course.state]
|
||||
: CourseStatusLabel[CourseStatus.DRAFT]}
|
||||
</Tag>
|
||||
{course?.duration && (
|
||||
</Tag> */}
|
||||
{/* {course?.duration && (
|
||||
<span className="hidden md:flex items-center text-gray-500 text-sm">
|
||||
<ClockCircleOutlined
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
总时长 {course.duration}
|
||||
</span>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
// size="small"
|
||||
onClick={handleSave}
|
||||
// disabled={form
|
||||
// .getFieldsError()
|
||||
|
|
|
@ -2,23 +2,22 @@ import { ReactNode, useEffect, useState } from "react";
|
|||
import { Outlet, useNavigate, useParams } from "react-router-dom";
|
||||
import CourseEditorHeader from "./CourseEditorHeader";
|
||||
import { motion } from "framer-motion";
|
||||
import { NavItem } from "@nice/client"
|
||||
import { NavItem } from "@nice/client";
|
||||
import CourseEditorSidebar from "./CourseEditorSidebar";
|
||||
import { CourseFormProvider } from "../context/CourseEditorContext";
|
||||
import { getNavItems } from "../navItems";
|
||||
|
||||
|
||||
export default function CourseEditorLayout() {
|
||||
const { id } = useParams();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [selectedSection, setSelectedSection] = useState<number>(0);
|
||||
const [navItems, setNavItems] = useState<NavItem[]>(getNavItems(id));
|
||||
useEffect(() => {
|
||||
setNavItems(getNavItems(id))
|
||||
}, [id])
|
||||
setNavItems(getNavItems(id));
|
||||
}, [id]);
|
||||
useEffect(() => {
|
||||
const currentPath = location.pathname;
|
||||
const index = navItems.findIndex(item => item.path === currentPath);
|
||||
const index = navItems.findIndex((item) => item.path === currentPath);
|
||||
if (index !== -1) {
|
||||
setSelectedSection(index);
|
||||
}
|
||||
|
@ -41,10 +40,14 @@ export default function CourseEditorLayout() {
|
|||
onNavigate={handleNavigation}
|
||||
/>
|
||||
<motion.main
|
||||
animate={{ marginLeft: isHovered ? "16rem" : "5rem" }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 25, mass: 1 }}
|
||||
className="flex-1 p-8"
|
||||
>
|
||||
animate={{ marginLeft: "16rem" }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 200,
|
||||
damping: 25,
|
||||
mass: 1,
|
||||
}}
|
||||
className="flex-1 p-8">
|
||||
<div className="max-w-6xl mx-auto bg-white rounded-xl shadow-lg">
|
||||
<header className="p-6 border-b border-gray-100">
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
|
@ -59,6 +62,5 @@ export default function CourseEditorLayout() {
|
|||
</div>
|
||||
</div>
|
||||
</CourseFormProvider>
|
||||
|
||||
);
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { motion } from "framer-motion";
|
||||
import { NavItem } from "@nice/client";
|
||||
import { useCourseEditor } from "../context/CourseEditorContext";
|
||||
import toast from "react-hot-toast";
|
||||
interface CourseSidebarProps {
|
||||
id?: string | undefined;
|
||||
isHovered: boolean;
|
||||
|
@ -23,21 +24,22 @@ export default function CourseEditorSidebar({
|
|||
const { editId } = useCourseEditor();
|
||||
return (
|
||||
<motion.nav
|
||||
initial={{ width: "5rem" }}
|
||||
animate={{ width: isHovered ? "16rem" : "5rem" }}
|
||||
animate={{ width: "16rem" }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 40 }}
|
||||
onHoverStart={() => setIsHovered(true)}
|
||||
onHoverEnd={() => setIsHovered(false)}
|
||||
className="fixed left-0 top-16 h-[calc(100vh-4rem)] bg-white border-r border-gray-200 shadow-lg overflow-x-hidden">
|
||||
<div className="p-4">
|
||||
{navItems.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
disabled={!editId && !item.isInitialized}
|
||||
onClick={(e) => {
|
||||
if (!editId && !item.isInitialized) {
|
||||
e.preventDefault();
|
||||
toast.error("请先完成课程概述填写并保存"); // 提示信息
|
||||
} else {
|
||||
e.preventDefault();
|
||||
onNavigate(item, index);
|
||||
}
|
||||
}}
|
||||
className={`w-full flex ${!isHovered ? "justify-center" : "items-center"} px-5 py-2.5 mb-1 rounded-lg transition-all duration-200 ${
|
||||
selectedSection === index
|
||||
|
@ -45,16 +47,13 @@ export default function CourseEditorSidebar({
|
|||
: "text-gray-600 hover:bg-gray-50"
|
||||
}`}>
|
||||
<span className="flex-shrink-0">{item.icon}</span>
|
||||
{isHovered && (
|
||||
{
|
||||
<>
|
||||
<motion.span
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="ml-3 font-medium flex-1 truncate">
|
||||
<span className="ml-3 font-medium flex-1 truncate">
|
||||
{item.label}
|
||||
</motion.span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -15,22 +15,22 @@ export const getNavItems = (
|
|||
path: `/course/${courseId ? `${courseId}/` : ""}editor`,
|
||||
isInitialized: true,
|
||||
},
|
||||
{
|
||||
label: "目标学员",
|
||||
icon: <AcademicCapIcon className="w-5 h-5" />,
|
||||
path: `/course/${courseId ? `${courseId}/` : ""}editor/goal`,
|
||||
isInitialized: false,
|
||||
},
|
||||
// {
|
||||
// label: "目标学员",
|
||||
// icon: <AcademicCapIcon className="w-5 h-5" />,
|
||||
// path: `/course/${courseId ? `${courseId}/` : ""}editor/goal`,
|
||||
// isInitialized: false,
|
||||
// },
|
||||
{
|
||||
label: "课程内容",
|
||||
icon: <VideoCameraIcon className="w-5 h-5" />,
|
||||
path: `/course/${courseId ? `${courseId}/` : ""}editor/content`,
|
||||
isInitialized: false,
|
||||
},
|
||||
{
|
||||
label: "课程设置",
|
||||
icon: <Cog6ToothIcon className="w-5 h-5" />,
|
||||
path: `/course/${courseId ? `${courseId}/` : ""}editor/setting`,
|
||||
isInitialized: false,
|
||||
},
|
||||
// {
|
||||
// label: "课程设置",
|
||||
// icon: <Cog6ToothIcon className="w-5 h-5" />,
|
||||
// path: `/course/${courseId ? `${courseId}/` : ""}editor/setting`,
|
||||
// isInitialized: false,
|
||||
// },
|
||||
];
|
||||
|
|
|
@ -13,8 +13,6 @@ import HomePage from "../app/main/home/page";
|
|||
import { CourseDetailPage } from "../app/main/course/detail/page";
|
||||
import { CourseBasicForm } from "../components/models/course/editor/form/CourseBasicForm";
|
||||
import CourseContentForm from "../components/models/course/editor/form/CourseContentForm/CourseContentForm";
|
||||
import { CourseGoalForm } from "../components/models/course/editor/form/CourseGoalForm";
|
||||
import CourseSettingForm from "../components/models/course/editor/form/CourseSettingForm";
|
||||
import CourseEditorLayout from "../components/models/course/editor/layout/CourseEditorLayout";
|
||||
import { MainLayout } from "../app/main/layout/MainLayout";
|
||||
import CoursesPage from "../app/main/courses/page";
|
||||
|
@ -96,9 +94,8 @@ export const routes: CustomRouteObject[] = [
|
|||
// 课程预览页面
|
||||
{
|
||||
path: "coursePreview",
|
||||
element:<CoursePreview></CoursePreview>
|
||||
}
|
||||
|
||||
element: <CoursePreview></CoursePreview>,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -112,22 +109,22 @@ export const routes: CustomRouteObject[] = [
|
|||
index: true,
|
||||
element: <CourseBasicForm></CourseBasicForm>,
|
||||
},
|
||||
{
|
||||
path: "goal",
|
||||
element: <CourseGoalForm></CourseGoalForm>,
|
||||
},
|
||||
// {
|
||||
// path: "goal",
|
||||
// element: <CourseGoalForm></CourseGoalForm>,
|
||||
// },
|
||||
{
|
||||
path: "content",
|
||||
element: (
|
||||
<CourseContentForm></CourseContentForm>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "setting",
|
||||
element: (
|
||||
<CourseSettingForm></CourseSettingForm>
|
||||
),
|
||||
},
|
||||
// {
|
||||
// path: "setting",
|
||||
// element: (
|
||||
// <CourseSettingForm></CourseSettingForm>
|
||||
// ),
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -100,7 +100,7 @@ server {
|
|||
# 仅供内部使用
|
||||
internal;
|
||||
# 代理到认证服务
|
||||
proxy_pass http://${SERVER_IP}:3000/auth/file;
|
||||
proxy_pass http://${SERVER_IP}:${SERVER_PORT}/auth/file;
|
||||
# 请求优化:不传递请求体
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
|
|
|
@ -5,7 +5,7 @@ for template in /etc/nginx/conf.d/*.template; do
|
|||
# 将输出文件名改为 .conf 结尾
|
||||
conf="${template%.template}.conf"
|
||||
echo "Processing $template"
|
||||
if envsubst '$SERVER_IP' < "$template" > "$conf"; then
|
||||
if envsubst '$SERVER_IP $SERVER_PORT' < "$template" > "$conf"; then
|
||||
echo "Replaced $conf successfully"
|
||||
else
|
||||
echo "Failed to replace $conf"
|
||||
|
|
Loading…
Reference in New Issue