From 90c3795eccf85a14312ef78e89675a15284368c2 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 11:53:14 +0800 Subject: [PATCH 01/17] add --- packages/common/prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 01bb99e..c65a392 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -204,7 +204,7 @@ model Post { // 日期时间类型字段 createdAt DateTime @default(now()) @map("created_at") publishedAt DateTime? @map("published_at") // 发布时间 - updatedAt DateTime @updatedAt @map("updated_at") + updatedAt DateTime @map("updated_at") deletedAt DateTime? @map("deleted_at") // 删除时间,可为空 instructors PostInstructor[] // 关系类型字段 From 8cb8de8fa9404c04d72f7d5aa39104c659c3ead9 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 12:19:09 +0800 Subject: [PATCH 02/17] add --- apps/server/src/models/post/post.service.ts | 45 ++++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index eeb6e98..30d56e8 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -49,7 +49,7 @@ export class PostService extends BaseTreeService { meta: { type: type, }, - }, + } as any, }, { tx }, ); @@ -71,7 +71,7 @@ export class PostService extends BaseTreeService { parentId: courseId, title: title, authorId: staff?.id, - }, + } as any, }, { tx }, ); @@ -216,15 +216,38 @@ export class PostService extends BaseTreeService { return { ...result, items }; }); } - async findManyWithPagination(args: - { page?: number; - pageSize?: number; - where?: Prisma.PostWhereInput; - select?: Prisma.PostSelect; - }): Promise<{ items: { id: string; type: string | null; level: string | null; state: string | null; title: string | null; subTitle: string | null; content: string | null; important: boolean | null; domainId: string | null; order: number | null; duration: number | null; rating: number | null; createdAt: Date; publishedAt: Date | null; updatedAt: Date; deletedAt: Date | null; authorId: string | null; parentId: string | null; hasChildren: boolean | null; meta: Prisma.JsonValue | null; }[]; totalPages: number; }> - { - return super.findManyWithPagination(args); - } + async findManyWithPagination(args: { + page?: number; + pageSize?: number; + where?: Prisma.PostWhereInput; + select?: Prisma.PostSelect; + }): Promise<{ + items: { + id: string; + type: string | null; + level: string | null; + state: string | null; + title: string | null; + subTitle: string | null; + content: string | null; + important: boolean | null; + domainId: string | null; + order: number | null; + duration: number | null; + rating: number | null; + createdAt: Date; + publishedAt: Date | null; + updatedAt: Date; + deletedAt: Date | null; + authorId: string | null; + parentId: string | null; + hasChildren: boolean | null; + meta: Prisma.JsonValue | null; + }[]; + totalPages: number; + }> { + return super.findManyWithPagination(args); + } protected async setPerms(data: Post, staff?: UserProfile) { if (!staff) return; const perms: ResPerm = { From 5a5d4ec3ff6fe1b147fbb03e00203077e1d21bce Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 12:57:26 +0800 Subject: [PATCH 03/17] add --- apps/server/src/models/post/utils.ts | 5 +++++ apps/server/src/queue/models/post/post.queue.service.ts | 1 + .../editor/form/CourseContentForm/CourseContentForm.tsx | 3 +++ .../course/editor/form/CourseContentForm/LectureList.tsx | 3 +++ 4 files changed, 12 insertions(+) diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 20bbe59..c257663 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -137,6 +137,11 @@ export async function setCourseInfo({ data }: { data: Post }) { id: true, descendant: true, }, + orderBy: { + descendant: { + order: 'asc', + }, + }, }); const descendants = ancestries.map((ancestry) => ancestry.descendant); const sections: SectionDto[] = descendants diff --git a/apps/server/src/queue/models/post/post.queue.service.ts b/apps/server/src/queue/models/post/post.queue.service.ts index f7370df..452076c 100644 --- a/apps/server/src/queue/models/post/post.queue.service.ts +++ b/apps/server/src/queue/models/post/post.queue.service.ts @@ -37,4 +37,5 @@ export class PostQueueService implements OnModuleInit { debounce: { id: `${QueueJobType.UPDATE_POST_STATE}_${data.id}` }, }); } + } diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx index ee0916a..4e4d641 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx @@ -41,6 +41,9 @@ const CourseContentForm: React.FC = () => { type: PostType.SECTION, deletedAt: null, }, + orderBy: { + order: "asc", + }, }, { enabled: !!editId, diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx index 1bbc610..dac0143 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx @@ -58,6 +58,9 @@ export const LectureList: React.FC = ({ type: PostType.LECTURE, deletedAt: null, }, + orderBy: { + order: "asc", + }, }, { enabled: !!sectionId, From 7ee38383863555c2b4991f1b59fa224ea97baeff Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Mon, 24 Feb 2025 19:10:38 +0800 Subject: [PATCH 04/17] 2.23 --- .../main/home/components/CategorySection.tsx | 1 - .../main/home/components/CoursesSection.tsx | 201 +++++++++--------- apps/web/src/app/main/home/page.tsx | 10 +- .../models/course/card/CourseStats.tsx | 1 - apps/web/src/hooks/useLocalSetting.ts | 2 +- apps/web/src/routes/index.tsx | 3 + apps/web/src/utils/axios-client.ts | 2 +- config/nginx/conf.d/web.conf | 1 + packages/common/prisma/schema.prisma | 1 - 9 files changed, 111 insertions(+), 111 deletions(-) diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index a64b551..02fa591 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -1,7 +1,6 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { Typography, Button } from 'antd'; import { stringToColor, TaxonomySlug, TermDto } from '@nice/common'; -import { api } from '@nice/client'; const { Title, Text } = Typography; diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index a1f0cfe..ce0d5e0 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Button, Card, Typography, Tag, Progress,Spin } from 'antd'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { Button, Card, Typography, Tag, Progress, Spin } from 'antd'; import { PlayCircleOutlined, UserOutlined, @@ -8,36 +8,37 @@ import { TeamOutlined, StarOutlined, ArrowRightOutlined, + EyeOutlined, } from '@ant-design/icons'; import { TaxonomySlug, TermDto } from '@nice/common'; import { api } from '@nice/client'; - +// const {courseId} = useParams(); interface GetTaxonomyProps { categories: string[]; isLoading: boolean; } -function useGetTaxonomy({type}) : GetTaxonomyProps { - const {data,isLoading} :{data:TermDto[],isLoading:boolean}= api.term.findMany.useQuery({ - where:{ - taxonomy: { - //TaxonomySlug.CATEGORY - slug:type - } - }, - include:{ - children :true - }, - take:10, // 只取前10个 - orderBy: { - createdAt: 'desc', // 按创建时间降序排列 - }, +function useGetTaxonomy({ type }): GetTaxonomyProps { + const { data, isLoading }: { data: TermDto[], isLoading: boolean } = api.term.findMany.useQuery({ + where: { + taxonomy: { + //TaxonomySlug.CATEGORY + slug: type + } + }, + include: { + children: true + }, + take: 10, // 只取前10个 + orderBy: { + createdAt: 'desc', // 按创建时间降序排列 + }, }) const categories = useMemo(() => { - const allCategories = isLoading ? [] : data?.map((course) => course.name); - return [...Array.from(new Set(allCategories))]; + const allCategories = isLoading ? [] : data?.map((course) => course.name); + return [...Array.from(new Set(allCategories))]; }, [data]); - return {categories,isLoading} + return { categories, isLoading } } @@ -63,7 +64,6 @@ interface CoursesSectionProps { initialVisibleCoursesCount?: number; } - const CoursesSection: React.FC = ({ title, description, @@ -73,12 +73,20 @@ const CoursesSection: React.FC = ({ const navigate = useNavigate(); const [selectedCategory, setSelectedCategory] = useState('全部'); const [visibleCourses, setVisibleCourses] = useState(initialVisibleCoursesCount); - const gateGory : GetTaxonomyProps = useGetTaxonomy({ + const gateGory: GetTaxonomyProps = useGetTaxonomy({ type: TaxonomySlug.CATEGORY, }) + const { data } = api.post.findMany.useQuery({ + take: 10, + } + ) useEffect(() => { - - }) + console.log(data) + }, [data]) + const handleClick = (course: Course) => { + navigate(`/courses?courseId=${course.id}/detail`); + } + const filteredCourses = useMemo(() => { return selectedCategory === '全部' ? courses @@ -86,76 +94,74 @@ const CoursesSection: React.FC = ({ }, [selectedCategory, courses]); const displayedCourses = filteredCourses.slice(0, visibleCourses); - return ( -
-
-
+
+
+
{title} - + {description}
-
- {gateGory.isLoading ? : +
+ {gateGory.isLoading ? : ( <> - setSelectedCategory("全部")} - className={`px-4 py-2 text-base cursor-pointer hover:scale-105 transform transition-all duration-300 ${selectedCategory === "全部" - ? 'shadow-[0_2px_8px_-4px_rgba(59,130,246,0.5)]' - : 'hover:shadow-md' - }`} - >全部 - { - gateGory.categories.map((category) => ( - setSelectedCategory(category)} - className={`px-4 py-2 text-base cursor-pointer hover:scale-105 transform transition-all duration-300 ${selectedCategory === category - ? 'shadow-[0_2px_8px_-4px_rgba(59,130,246,0.5)]' - : 'hover:shadow-md' - }`} - > - {category} - - )) - } + setSelectedCategory("全部")} + className={`px-6 py-2 text-base cursor-pointer rounded-full transition-all duration-300 ${selectedCategory === "全部" + ? 'bg-blue-600 text-white shadow-lg' + : 'bg-white text-gray-600 hover:bg-gray-100' + }`} + >全部 + { + gateGory.categories.map((category) => ( + setSelectedCategory(category)} + className={`px-6 py-2 text-base cursor-pointer rounded-full transition-all duration-300 ${selectedCategory === category + ? 'bg-blue-600 text-white shadow-lg' + : 'bg-white text-gray-600 hover:bg-gray-100' + }`} + > + {category} + + )) + } - ) - } + }
-
+
{displayedCourses.map((course) => ( handleClick(course)} key={course.id} hoverable - className="group overflow-hidden rounded-2xl border-0 bg-white/70 backdrop-blur-sm - shadow-[0_10px_40px_-15px_rgba(0,0,0,0.1)] hover:shadow-[0_20px_50px_-15px_rgba(0,0,0,0.15)] - transition-all duration-700 ease-out transform hover:-translate-y-1 will-change-transform" + 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.progress > 0 && ( -
- + {/* = ({ to: '#60a5fa', }} className="m-0" - /> + /> */}
)}
} > -
+
{course.category} @@ -185,35 +191,28 @@ const CoursesSection: React.FC = ({ ? 'blue' : 'purple' } - className="px-3 py-1 rounded-full border-0 shadow-sm transition-all duration-300 hover:shadow-md" + className="px-3 py-1 rounded-full border-0" > {course.level}
- {course.title} + <button > {course.title}</button> -
- - - {course.instructor} - -
-
- - - {course.duration} - - - - {course.students.toLocaleString()} - - - - {course.rating} + +
+ +
+ + {course.instructor} + +
+ + + 观看次数{course.progress}%
@@ -226,25 +225,25 @@ const CoursesSection: React.FC = ({ 立即学习
+
))}
{filteredCourses.length >= visibleCourses && ( -
-
-
-
+
+
+
- + +
-
)}
diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index 4d4be9e..453fa17 100755 --- a/apps/web/src/app/main/home/page.tsx +++ b/apps/web/src/app/main/home/page.tsx @@ -2,6 +2,8 @@ import HeroSection from './components/HeroSection'; import CategorySection from './components/CategorySection'; import CoursesSection from './components/CoursesSection'; import FeaturedTeachersSection from './components/FeaturedTeachersSection'; +import { useEffect } from 'react'; +import { api } from '@nice/client' const HomePage = () => { const mockCourses = [ { @@ -13,7 +15,7 @@ const HomePage = () => { level: '入门', duration: '36小时', category: '编程语言', - progress: 0, + progress: 16, thumbnail: '/images/course1.jpg', }, { @@ -49,7 +51,7 @@ const HomePage = () => { level: '高级', duration: '56小时', category: '编程语言', - progress: 0, + progress: 15, thumbnail: '/images/course4.jpg', }, { @@ -97,15 +99,13 @@ const HomePage = () => { level: '中级', duration: '40小时', category: '移动开发', - progress: 0, + progress: 70, thumbnail: '/images/course8.jpg', }, ]; - return (
- getBaseUrl("http", parseInt(env.SERVER_PORT)), [getBaseUrl] ); - const websocketUrl = useMemo(() => getBaseUrl("ws", 3000), [getBaseUrl]); + const websocketUrl = useMemo(() => parseInt(env.SERVER_PORT), [getBaseUrl]); const checkIsTusUrl = useCallback( (url: string) => { return url.startsWith(tusUrl); diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 09aeaba..70f60a8 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -3,6 +3,7 @@ import { IndexRouteObject, Link, NonIndexRouteObject, + useParams, } from "react-router-dom"; import ErrorPage from "../app/error"; import WithAuth from "../components/utils/with-auth"; @@ -40,6 +41,7 @@ export type CustomRouteObject = | CustomIndexRouteObject | CustomNonIndexRouteObject; export const routes: CustomRouteObject[] = [ + { path: "/", errorElement: , @@ -144,4 +146,5 @@ export const routes: CustomRouteObject[] = [ }, ]; + export const router = createBrowserRouter(routes); diff --git a/apps/web/src/utils/axios-client.ts b/apps/web/src/utils/axios-client.ts index 23877df..f75302f 100755 --- a/apps/web/src/utils/axios-client.ts +++ b/apps/web/src/utils/axios-client.ts @@ -1,6 +1,6 @@ import axios from 'axios'; import { env } from '../env'; -const BASE_URL = `http://${env.SERVER_IP}:3000` +const BASE_URL = `http://${env.SERVER_IP}:${env.SERVER_PORT}` const apiClient = axios.create({ baseURL: BASE_URL, // withCredentials: true, diff --git a/config/nginx/conf.d/web.conf b/config/nginx/conf.d/web.conf index bf90259..67302b8 100755 --- a/config/nginx/conf.d/web.conf +++ b/config/nginx/conf.d/web.conf @@ -101,6 +101,7 @@ server { internal; # 代理到认证服务 proxy_pass http://host.docker.internal:3000/auth/file; + # 请求优化:不传递请求体 proxy_pass_request_body off; proxy_set_header Content-Length ""; diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 01bb99e..89768ea 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -268,7 +268,6 @@ model Message { visits Visit[] createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime? @updatedAt @map("updated_at") - @@index([type, createdAt]) @@map("message") } From 7e482f8c53c4417a60854283f60f4ab95f5bd411 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 19:33:18 +0800 Subject: [PATCH 05/17] add --- apps/server/src/models/base/base.type.ts | 56 ++++++++++--------- apps/server/src/models/post/post.router.ts | 18 ++++++ apps/server/src/models/post/post.service.ts | 35 ++++++++++++ .../queue/models/post/post.queue.service.ts | 1 - apps/server/src/tasks/init/gendev.service.ts | 54 +++++++++++++++++- .../editor/context/CourseEditorContext.tsx | 2 +- .../form/CourseContentForm/LectureList.tsx | 14 ++++- .../course/editor/form/CourseGoalForm.tsx | 19 ------- .../course/editor/form/CourseSettingForm.tsx | 26 --------- packages/client/src/api/hooks/useEntity.ts | 3 + packages/common/prisma/schema.prisma | 3 + 11 files changed, 152 insertions(+), 79 deletions(-) delete mode 100755 apps/web/src/components/models/course/editor/form/CourseGoalForm.tsx delete mode 100755 apps/web/src/components/models/course/editor/form/CourseSettingForm.tsx diff --git a/apps/server/src/models/base/base.type.ts b/apps/server/src/models/base/base.type.ts index 878dffe..01254df 100755 --- a/apps/server/src/models/base/base.type.ts +++ b/apps/server/src/models/base/base.type.ts @@ -1,25 +1,27 @@ -import { db, Prisma, PrismaClient } from "@nice/common"; +import { db, Prisma, PrismaClient } from '@nice/common'; export type Operations = - | 'aggregate' - | 'count' - | 'create' - | 'createMany' - | 'delete' - | 'deleteMany' - | 'findFirst' - | 'findMany' - | 'findUnique' - | 'update' - | 'updateMany' - | 'upsert'; -export type DelegateFuncs = { [K in Operations]: (args: any) => Promise } + | 'aggregate' + | 'count' + | 'create' + | 'createMany' + | 'delete' + | 'deleteMany' + | 'findFirst' + | 'findMany' + | 'findUnique' + | 'update' + | 'updateMany' + | 'upsert'; +export type DelegateFuncs = { + [K in Operations]: (args: any) => Promise; +}; export type DelegateArgs = { - [K in keyof T]: T[K] extends (args: infer A) => Promise ? A : never; + [K in keyof T]: T[K] extends (args: infer A) => Promise ? A : never; }; export type DelegateReturnTypes = { - [K in keyof T]: T[K] extends (args: any) => Promise ? R : never; + [K in keyof T]: T[K] extends (args: any) => Promise ? R : never; }; export type WhereArgs = T extends { where?: infer W } ? W : never; @@ -28,17 +30,17 @@ export type DataArgs = T extends { data: infer D } ? D : never; export type IncludeArgs = T extends { include: infer I } ? I : never; export type OrderByArgs = T extends { orderBy: infer O } ? O : never; export type UpdateOrderArgs = { - id: string - overId: string -} + id: string; + overId: string; +}; export interface FindManyWithCursorType { - cursor?: string; - limit?: number; - where?: WhereArgs['findUnique']>; - select?: SelectArgs['findUnique']>; - orderBy?: OrderByArgs['findMany']> + cursor?: string; + limit?: number; + where?: WhereArgs['findUnique']>; + select?: SelectArgs['findUnique']>; + orderBy?: OrderByArgs['findMany']>; } export type TransactionType = Omit< - PrismaClient, - '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends' ->; \ No newline at end of file + PrismaClient, + '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends' +>; diff --git a/apps/server/src/models/post/post.router.ts b/apps/server/src/models/post/post.router.ts index 458564d..2c7ae56 100755 --- a/apps/server/src/models/post/post.router.ts +++ b/apps/server/src/models/post/post.router.ts @@ -3,8 +3,10 @@ import { TrpcService } from '@server/trpc/trpc.service'; import { CourseMethodSchema, Prisma } from '@nice/common'; import { PostService } from './post.service'; import { z, ZodType } from 'zod'; +import { UpdateOrderArgs } from '../base/base.type'; const PostCreateArgsSchema: ZodType = z.any(); const PostUpdateArgsSchema: ZodType = z.any(); +const PostUpdateOrderArgsSchema: ZodType = z.any(); const PostFindFirstArgsSchema: ZodType = z.any(); const PostFindManyArgsSchema: ZodType = z.any(); const PostDeleteManyArgsSchema: ZodType = z.any(); @@ -107,5 +109,21 @@ export class PostRouter { .query(async ({ input }) => { return await this.postService.findManyWithPagination(input); }), + updateOrder: this.trpc.protectProcedure + .input(PostUpdateOrderArgsSchema) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.postService.updateOrder(input); + }), + updateOrderByIds: this.trpc.protectProcedure + .input( + z.object({ + ids: z.array(z.string()), + }), + ) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.postService.updateOrderByIds(input.ids); + }), }); } diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 30d56e8..e260698 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -20,6 +20,7 @@ import EventBus, { CrudOperation } from '@server/utils/event-bus'; import { BaseTreeService } from '../base/base.tree.service'; import { z } from 'zod'; import { DefaultArgs } from '@prisma/client/runtime/library'; +import dayjs from 'dayjs'; @Injectable() export class PostService extends BaseTreeService { @@ -43,6 +44,7 @@ export class PostService extends BaseTreeService { content: content, title: title, authorId: params?.staff?.id, + updatedAt: dayjs().toDate(), resources: { connect: resourceIds.map((fileId) => ({ fileId })), }, @@ -71,6 +73,7 @@ export class PostService extends BaseTreeService { parentId: courseId, title: title, authorId: staff?.id, + updatedAt: dayjs().toDate(), } as any, }, { tx }, @@ -152,6 +155,7 @@ export class PostService extends BaseTreeService { params?: { staff?: UserProfile; tx?: Prisma.TransactionClient }, ) { args.data.authorId = params?.staff?.id; + args.data.updatedAt = dayjs().toDate(); const result = await super.create(args); EventBus.emit('dataChanged', { type: ObjectType.POST, @@ -162,6 +166,7 @@ export class PostService extends BaseTreeService { } async update(args: Prisma.PostUpdateArgs, staff?: UserProfile) { args.data.authorId = staff?.id; + args.data.updatedAt = dayjs().toDate(); const result = await super.update(args); EventBus.emit('dataChanged', { type: ObjectType.POST, @@ -246,8 +251,38 @@ export class PostService extends BaseTreeService { }[]; totalPages: number; }> { + // super.updateOrder; return super.findManyWithPagination(args); } + + async updateOrderByIds(ids: string[]) { + const posts = await db.post.findMany({ + where: { id: { in: ids } }, + select: { id: true, order: true }, + }); + const postMap = new Map(posts.map((post) => [post.id, post])); + const orderedPosts = ids + .map((id) => postMap.get(id)) + .filter((post): post is { id: string; order: number } => !!post); + + // 生成仅需更新的操作 + const updates = orderedPosts + .map((post, index) => ({ + id: post.id, + newOrder: index, // 按数组索引设置新顺序 + currentOrder: post.order, + })) + .filter(({ newOrder, currentOrder }) => newOrder !== currentOrder) + .map(({ id, newOrder }) => + db.post.update({ + where: { id }, + data: { order: newOrder }, + }), + ); + + // 批量执行更新 + 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/queue/models/post/post.queue.service.ts b/apps/server/src/queue/models/post/post.queue.service.ts index 452076c..f7370df 100644 --- a/apps/server/src/queue/models/post/post.queue.service.ts +++ b/apps/server/src/queue/models/post/post.queue.service.ts @@ -37,5 +37,4 @@ export class PostQueueService implements OnModuleInit { debounce: { id: `${QueueJobType.UPDATE_POST_STATE}_${data.id}` }, }); } - } diff --git a/apps/server/src/tasks/init/gendev.service.ts b/apps/server/src/tasks/init/gendev.service.ts index d7c3e01..a2f7756 100755 --- a/apps/server/src/tasks/init/gendev.service.ts +++ b/apps/server/src/tasks/init/gendev.service.ts @@ -7,6 +7,7 @@ import { Department, getRandomElement, getRandomElements, + PostType, Staff, TaxonomySlug, Term, @@ -14,6 +15,7 @@ import { import EventBus from '@server/utils/event-bus'; import { capitalizeFirstLetter, DevDataCounts, getCounts } from './utils'; import { StaffService } from '@server/models/staff/staff.service'; +import dayjs from 'dayjs'; @Injectable() export class GenDevService { private readonly logger = new Logger(GenDevService.name); @@ -22,7 +24,7 @@ export class GenDevService { terms: Record = { [TaxonomySlug.CATEGORY]: [], [TaxonomySlug.TAG]: [], - [TaxonomySlug.LEVEL]: [] + [TaxonomySlug.LEVEL]: [], }; depts: Department[] = []; domains: Department[] = []; @@ -35,7 +37,7 @@ export class GenDevService { private readonly departmentService: DepartmentService, private readonly staffService: StaffService, private readonly termService: TermService, - ) { } + ) {} async genDataEvent() { EventBus.emit('genDataEvent', { type: 'start' }); try { @@ -58,6 +60,7 @@ export class GenDevService { if (this.counts.termCount === 0) { this.logger.log('Generate terms'); await this.createTerms(null, TaxonomySlug.CATEGORY, depth, count); + await this.createLevelTerm(); const domains = this.depts.filter((item) => item.isDomain); for (const domain of domains) { await this.createTerms(domain, TaxonomySlug.CATEGORY, depth, count); @@ -174,7 +177,54 @@ export class GenDevService { } } } + private async createLevelTerm() { + try { + // 1. 获取分类时添加异常处理 + const taxLevel = await db.taxonomy.findFirst({ + where: { slug: TaxonomySlug.LEVEL }, + }); + if (!taxLevel) { + throw new Error('LEVEL taxonomy not found'); + } + + // 2. 使用数组定义初始化数据 + 名称去重 + const termsToCreate = [ + { name: '初级', taxonomyId: taxLevel.id }, + { name: '中级', taxonomyId: taxLevel.id }, + { name: '高级', taxonomyId: taxLevel.id }, // 改为高级更合理 + ]; + await this.termService.createMany({ + data: termsToCreate, + }); + console.log('created level terms'); + } catch (error) { + console.error('Failed to create level terms:', error); + throw error; // 向上抛出错误供上层处理 + } + } + private async createCourse( + title: string, + deptId: string, + cateId: string, + levelId: string, + ) { + const course = await db.post.create({ + data: { + type: PostType.COURSE, + title: title, + updatedAt: dayjs().toDate(), + depts: { + connect: { + id: deptId, + }, + }, + terms:{ + connect:[cateId,levelId].map + } + }, + }); + } private async createDepartment( name: string, parentId?: string | null, 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 913bf34..82f4d4d 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -115,7 +115,7 @@ export function CourseFormProvider({ }); message.success("课程更新成功!"); } else { - const result = await createCourse.mutateAsync({ + const result = await create.mutateAsync({ courseDetail: { data: { title: formattedValues.title || "12345", diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx index dac0143..7bad0ce 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx @@ -48,7 +48,7 @@ export const LectureList: React.FC = ({ field, sectionId, }) => { - const { softDeleteByIds } = usePost(); + const { softDeleteByIds, updateOrderByIds } = usePost(); const { data: lectures = [], isLoading } = ( api.post.findMany as any ).useQuery( @@ -87,11 +87,19 @@ export const LectureList: React.FC = ({ const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; - + // updateOrder.mutateAsync({ + // id: active.id, + // overId: over.id, + // }); + let newItems = []; setItems((items) => { const oldIndex = items.findIndex((item) => item.id === active.id); const newIndex = items.findIndex((item) => item.id === over.id); - return arrayMove(items, oldIndex, newIndex); + newItems = arrayMove(items, oldIndex, newIndex); + return newItems; + }); + updateOrderByIds.mutateAsync({ + ids: newItems.map((item) => item.id), }); }; diff --git a/apps/web/src/components/models/course/editor/form/CourseGoalForm.tsx b/apps/web/src/components/models/course/editor/form/CourseGoalForm.tsx deleted file mode 100755 index 28fbe6b..0000000 --- a/apps/web/src/components/models/course/editor/form/CourseGoalForm.tsx +++ /dev/null @@ -1,19 +0,0 @@ -// 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 ( -//
-// -// -// -// -// -// -//
-// ); -// } diff --git a/apps/web/src/components/models/course/editor/form/CourseSettingForm.tsx b/apps/web/src/components/models/course/editor/form/CourseSettingForm.tsx deleted file mode 100755 index 6a1b1a9..0000000 --- a/apps/web/src/components/models/course/editor/form/CourseSettingForm.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader"; -// import { Form, Input } from "antd"; - -// export default function CourseSettingForm() { -// return ( -//
-// -// { -// console.log(value); -// }} -// > -// -//
-// ) -// } \ No newline at end of file diff --git a/packages/client/src/api/hooks/useEntity.ts b/packages/client/src/api/hooks/useEntity.ts index 2b1dbfa..175b342 100755 --- a/packages/client/src/api/hooks/useEntity.ts +++ b/packages/client/src/api/hooks/useEntity.ts @@ -113,5 +113,8 @@ export function useEntity( T, "updateOrder" >, // 更新实体顺序的 mutation 函数 + updateOrderByIds: createMutationHandler( + "updateOrderByIds" + ) as MutationResult, // 更新实体顺序的 mutation 函数 }; } diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index c65a392..759afa5 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -110,6 +110,7 @@ model Department { id String @id @default(cuid()) name String order Float? + posts Post[] @relation("post_dept") ancestors DeptAncestry[] @relation("DescendantToAncestor") descendants DeptAncestry[] @relation("AncestorToDescendant") parentId String? @map("parent_id") @@ -200,6 +201,8 @@ model Post { order Float? @default(0) @map("order") duration Int? rating Int? @default(0) + + depts Department[] @relation("post_dept") // 索引 // 日期时间类型字段 createdAt DateTime @default(now()) @map("created_at") From 01db8808c4fb24ade840859e523d457ec5607f1e Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 19:34:58 +0800 Subject: [PATCH 06/17] add --- .../models/course/editor/context/CourseEditorContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 82f4d4d..913bf34 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -115,7 +115,7 @@ export function CourseFormProvider({ }); message.success("课程更新成功!"); } else { - const result = await create.mutateAsync({ + const result = await createCourse.mutateAsync({ courseDetail: { data: { title: formattedValues.title || "12345", From c8bfbc081a0aaf552584c5b421c4f534b600bd3a Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Mon, 24 Feb 2025 19:35:10 +0800 Subject: [PATCH 07/17] add --- apps/server/src/tasks/init/gendev.service.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/server/src/tasks/init/gendev.service.ts b/apps/server/src/tasks/init/gendev.service.ts index a2f7756..ab8be57 100755 --- a/apps/server/src/tasks/init/gendev.service.ts +++ b/apps/server/src/tasks/init/gendev.service.ts @@ -214,14 +214,14 @@ export class GenDevService { type: PostType.COURSE, title: title, updatedAt: dayjs().toDate(), - depts: { - connect: { - id: deptId, - }, - }, - terms:{ - connect:[cateId,levelId].map - } + // depts: { + // connect: { + // id: deptId, + // }, + // }, + // terms:{ + // connect:[cateId,levelId].map + // } }, }); } From 4a6d7e7701653832b18f457979356f70faeaae70 Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Mon, 24 Feb 2025 19:40:41 +0800 Subject: [PATCH 08/17] add . --- packages/common/prisma/schema.prisma | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 0d188f0..acb4ee0 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -407,6 +407,7 @@ model NodeEdge { @@map("node_edge") } + model Animal { id String @id @default(cuid()) name String From 71b1dd591021c36908ebcb99195a7580fae50f6f Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Mon, 24 Feb 2025 19:43:46 +0800 Subject: [PATCH 09/17] add --- packages/common/prisma/schema.prisma | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index acb4ee0..1c245e2 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -406,8 +406,6 @@ model NodeEdge { @@index([targetId]) @@map("node_edge") } - - model Animal { id String @id @default(cuid()) name String From 7047445c524f842c9af32e623f0d92de3e0bea4a Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 19:56:43 +0800 Subject: [PATCH 10/17] add --- apps/server/src/models/post/post.service.ts | 32 +------- apps/server/src/models/post/utils.ts | 4 +- apps/server/src/tasks/init/gendev.service.ts | 78 +++++++++++++++++-- apps/server/src/tasks/init/utils.ts | 52 ++++++++----- .../CourseContentForm/CourseContentForm.tsx | 10 ++- .../form/CourseContentForm/LectureList.tsx | 25 +++--- packages/common/src/models/post.ts | 1 + 7 files changed, 126 insertions(+), 76 deletions(-) diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index e260698..5048fff 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -98,11 +98,10 @@ export class PostService extends BaseTreeService { async createCourse( args: { courseDetail: Prisma.PostCreateArgs; - sections?: z.infer[]; }, params: { staff?: UserProfile; tx?: Prisma.TransactionClient }, ) { - const { courseDetail, sections } = args; + const { courseDetail } = args; // If no transaction is provided, create a new one if (!params.tx) { return await db.$transaction(async (tx) => { @@ -112,20 +111,6 @@ export class PostService extends BaseTreeService { console.log('courseDetail', courseDetail); const createdCourse = await this.create(courseDetail, courseParams); // If sections are provided, create them - if (sections && sections.length > 0) { - const sectionPromises = sections.map((section) => - this.createSection( - { - courseId: createdCourse.id, - title: section.title, - lectures: section.lectures, - }, - courseParams, - ), - ); - // Create all sections (and their lectures) in parallel - await Promise.all(sectionPromises); - } return createdCourse; }); } @@ -133,21 +118,6 @@ export class PostService extends BaseTreeService { console.log('courseDetail', courseDetail); const createdCourse = await this.create(courseDetail, params); // If sections are provided, create them - if (sections && sections.length > 0) { - const sectionPromises = sections.map((section) => - this.createSection( - { - courseId: createdCourse.id, - title: section.title, - lectures: section.lectures, - }, - params, - ), - ); - // Create all sections (and their lectures) in parallel - await Promise.all(sectionPromises); - } - return createdCourse; } async create( diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index c257663..6ca6139 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -161,12 +161,12 @@ export async function setCourseInfo({ data }: { data: Post }) { 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, ); }); - Object.assign(data, { sections }); + Object.assign(data, { sections, lectureCount }); } } diff --git a/apps/server/src/tasks/init/gendev.service.ts b/apps/server/src/tasks/init/gendev.service.ts index ab8be57..e8c3dfa 100755 --- a/apps/server/src/tasks/init/gendev.service.ts +++ b/apps/server/src/tasks/init/gendev.service.ts @@ -45,6 +45,7 @@ export class GenDevService { await this.generateDepartments(3, 6); await this.generateTerms(2, 6); await this.generateStaffs(4); + await this.generateCourses(); } catch (err) { this.logger.error(err); } @@ -142,6 +143,64 @@ export class GenDevService { collectChildren(domainId); return children; } + private async generateCourses(countPerCate: number = 3) { + const titleList = [ + '计算机科学导论', + '数据结构与算法', + '网络安全', + '机器学习', + '数据库管理系统', + 'Web开发', + '移动应用开发', + '人工智能', + '计算机网络', + '操作系统', + '数字信号处理', + '无线通信', + '信息论', + '密码学', + '计算机图形学', + ]; + + if (!this.counts.courseCount) { + this.logger.log('Generating courses...'); + const depts = await db.department.findMany({ + select: { id: true, name: true }, + }); + const cates = await db.term.findMany({ + where: { + taxonomy: { slug: TaxonomySlug.CATEGORY }, + }, + select: { id: true, name: true }, + }); + const total = cates.length * countPerCate; + const levels = await db.term.findMany({ + where: { + taxonomy: { slug: TaxonomySlug.LEVEL }, + }, + select: { id: true, name: true }, + }); + for (const cate of cates) { + for (let i = 0; i < countPerCate; i++) { + const randomTitle = `${titleList[Math.floor(Math.random() * titleList.length)]} ${Math.random().toString(36).substring(7)}`; + const randomLevelId = + levels[Math.floor(Math.random() * levels.length)].id; + const randomDeptId = + depts[Math.floor(Math.random() * depts.length)].id; + + await this.createCourse( + randomTitle, + randomDeptId, + cate.id, + randomLevelId, + ); + this.logger.log( + `Generated ${this.deptGeneratedCount}/${total} departments`, + ); + } + } + } + } private async generateStaffs(countPerDept: number = 3) { if (this.counts.staffCount === 1) { this.logger.log('Generating staffs...'); @@ -214,16 +273,19 @@ export class GenDevService { type: PostType.COURSE, title: title, updatedAt: dayjs().toDate(), - // depts: { - // connect: { - // id: deptId, - // }, - // }, - // terms:{ - // connect:[cateId,levelId].map - // } + depts: { + connect: { + id: deptId, + }, + }, + terms: { + connect: [cateId, levelId].map((id) => ({ + id: id, + })), + }, }, }); + return course; } private async createDepartment( name: string, diff --git a/apps/server/src/tasks/init/utils.ts b/apps/server/src/tasks/init/utils.ts index 292eb9e..d55dcec 100755 --- a/apps/server/src/tasks/init/utils.ts +++ b/apps/server/src/tasks/init/utils.ts @@ -1,34 +1,44 @@ -import { db, getRandomElement, getRandomIntInRange, getRandomTimeInterval, } from '@nice/common'; +import { + db, + getRandomElement, + getRandomIntInRange, + getRandomTimeInterval, + PostType, +} from '@nice/common'; import dayjs from 'dayjs'; export interface DevDataCounts { - deptCount: number; + deptCount: number; - staffCount: number - termCount: number + staffCount: number; + termCount: number; + courseCount: number; } export async function getCounts(): Promise { - const counts = { - deptCount: await db.department.count(), + const counts = { + deptCount: await db.department.count(), - staffCount: await db.staff.count(), - termCount: await db.term.count(), - }; - return counts; + staffCount: await db.staff.count(), + termCount: await db.term.count(), + courseCount: await db.post.count({ + where: { + type: PostType.COURSE, + }, + }), + }; + return counts; } export function capitalizeFirstLetter(string: string) { - return string.charAt(0).toUpperCase() + string.slice(1); + return string.charAt(0).toUpperCase() + string.slice(1); } export function getRandomImageLinks(count: number = 5): string[] { - const baseUrl = 'https://picsum.photos/200/300?random='; - const imageLinks: string[] = []; + const baseUrl = 'https://picsum.photos/200/300?random='; + const imageLinks: string[] = []; - for (let i = 0; i < count; i++) { - // 生成随机数以确保每个链接都是唯一的 - const randomId = Math.floor(Math.random() * 1000); - imageLinks.push(`${baseUrl}${randomId}`); - } + for (let i = 0; i < count; i++) { + // 生成随机数以确保每个链接都是唯一的 + const randomId = Math.floor(Math.random() * 1000); + imageLinks.push(`${baseUrl}${randomId}`); + } - return imageLinks; + return imageLinks; } - - diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx index 4e4d641..dcf9a32 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx @@ -24,6 +24,7 @@ import { CourseContentFormHeader } from "./CourseContentFormHeader"; import { CourseSectionEmpty } from "./CourseSectionEmpty"; import { SortableSection } from "./SortableSection"; import { LectureList } from "./LectureList"; +import toast from "react-hot-toast"; const CourseContentForm: React.FC = () => { const { editId } = useCourseEditor(); @@ -110,10 +111,11 @@ const CourseContentForm: React.FC = () => { icon={} className="mt-4" onClick={() => { - setItems([ - ...items.filter((item) => !!item.id), - { id: null, title: "" }, - ]); + if (items.some((item) => item.id === null)) { + toast.error("请先保存当前编辑章节"); + } else { + setItems([...items, { id: null, title: "" }]); + } }}> 添加章节 diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx index 7bad0ce..a9cf666 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx @@ -38,6 +38,7 @@ import { useCourseEditor } from "../../context/CourseEditorContext"; import { usePost } from "@nice/client"; import { LectureData, SectionData } from "./interface"; import { SortableLecture } from "./SortableLecture"; +import toast from "react-hot-toast"; interface LectureListProps { field: SectionData; @@ -135,16 +136,20 @@ export const LectureList: React.FC = ({ icon={} className="mt-4" onClick={() => { - setItems((prevItems) => [ - ...prevItems.filter((item) => !!item.id), - { - id: null, - title: "", - meta: { - type: LectureType.ARTICLE, - }, - } as Lecture, - ]); + if (items.some((item) => item.id === null)) { + toast.error("请先保存当前编辑章节"); + } else { + setItems((prevItems) => [ + ...prevItems.filter((item) => !!item.id), + { + id: null, + title: "", + meta: { + type: LectureType.ARTICLE, + }, + } as Lecture, + ]); + } }}> 添加课时 diff --git a/packages/common/src/models/post.ts b/packages/common/src/models/post.ts index fa030b6..de740d5 100755 --- a/packages/common/src/models/post.ts +++ b/packages/common/src/models/post.ts @@ -78,4 +78,5 @@ export type CourseDto = Course & { enrollments?: Enrollment[]; sections?: SectionDto[]; terms: Term[]; + lectureCount?: number; }; From e1cc05fec13bcda87450b165d3b59100ab5082e9 Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Mon, 24 Feb 2025 19:59:43 +0800 Subject: [PATCH 11/17] add --- apps/web/src/app/main/home/components/CoursesSection.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index ce0d5e0..fe2653c 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -77,11 +77,11 @@ const CoursesSection: React.FC = ({ type: TaxonomySlug.CATEGORY, }) const { data } = api.post.findMany.useQuery({ - take: 10, + take: 8, } ) useEffect(() => { - console.log(data) + console.log(data,'成功') }, [data]) const handleClick = (course: Course) => { navigate(`/courses?courseId=${course.id}/detail`); @@ -212,7 +212,7 @@ const CoursesSection: React.FC = ({
- 观看次数{course.progress}% + 观看次数{course.progress}次
From e44795ebeef5df8c1f686a564927880bbb3af5a3 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 20:24:15 +0800 Subject: [PATCH 12/17] add --- apps/server/src/tasks/init/gendev.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/server/src/tasks/init/gendev.service.ts b/apps/server/src/tasks/init/gendev.service.ts index e8c3dfa..8f42c9a 100755 --- a/apps/server/src/tasks/init/gendev.service.ts +++ b/apps/server/src/tasks/init/gendev.service.ts @@ -253,9 +253,11 @@ export class GenDevService { { name: '中级', taxonomyId: taxLevel.id }, { name: '高级', taxonomyId: taxLevel.id }, // 改为高级更合理 ]; - await this.termService.createMany({ - data: termsToCreate, - }); + for (const termData of termsToCreate) { + await this.termService.create({ + data: termData, + }); + } console.log('created level terms'); } catch (error) { console.error('Failed to create level terms:', error); From cf3e242462017286cfd0665f4c5c80097719f0b8 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 20:24:17 +0800 Subject: [PATCH 13/17] add --- .../app/main/home/components/HeroSection.tsx | 245 ++++++++---------- 1 file changed, 110 insertions(+), 135 deletions(-) diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx index 3dac945..9dae829 100755 --- a/apps/web/src/app/main/home/components/HeroSection.tsx +++ b/apps/web/src/app/main/home/components/HeroSection.tsx @@ -1,160 +1,135 @@ -import React, { useRef, useCallback } from 'react'; -import { Button, Carousel, Typography } from 'antd'; +import React, { useRef, useCallback } from "react"; +import { Button, Carousel, Typography } from "antd"; import { - TeamOutlined, - BookOutlined, - StarOutlined, - ClockCircleOutlined, - LeftOutlined, - RightOutlined, - EyeOutlined -} from '@ant-design/icons'; -import type { CarouselRef } from 'antd/es/carousel'; + TeamOutlined, + BookOutlined, + StarOutlined, + ClockCircleOutlined, + LeftOutlined, + RightOutlined, + EyeOutlined, +} from "@ant-design/icons"; +import type { CarouselRef } from "antd/es/carousel"; const { Title, Text } = Typography; interface CarouselItem { - title: string; - desc: string; - image: string; - action: string; - color: string; + title: string; + desc: string; + image: string; + action: string; + color: string; } interface PlatformStat { - icon: React.ReactNode; - value: string; - label: string; + icon: React.ReactNode; + value: string; + label: string; } const carouselItems: CarouselItem[] = [ - { - title: '探索编程世界', - desc: '从零开始学习编程,开启你的技术之旅', - image: '/images/banner1.jpg', - action: '立即开始', - color: 'from-blue-600/90' - }, - { - title: '人工智能课程', - desc: '掌握AI技术,引领未来发展', - image: '/images/banner2.jpg', - action: '了解更多', - color: 'from-purple-600/90' - } + { + title: "探索编程世界", + desc: "从零开始学习编程,开启你的技术之旅", + image: "/images/banner1.jpg", + action: "立即开始", + color: "from-blue-600/90", + }, + { + title: "人工智能课程", + desc: "掌握AI技术,引领未来发展", + image: "/images/banner2.jpg", + action: "了解更多", + color: "from-purple-600/90", + }, ]; const platformStats: PlatformStat[] = [ - { icon: , value: '50,000+', label: '注册学员' }, - { icon: , value: '1,000+', label: '精品课程' }, - // { icon: , value: '98%', label: '好评度' }, - { icon: , value: '100万+', label: '观看次数' } + { icon: , value: "50,000+", label: "注册学员" }, + { icon: , value: "1,000+", label: "精品课程" }, + // { icon: , value: '98%', label: '好评度' }, + { icon: , value: "100万+", label: "观看次数" }, ]; const HeroSection = () => { - const carouselRef = useRef(null); + const carouselRef = useRef(null); - const handlePrev = useCallback(() => { - carouselRef.current?.prev(); - }, []); + const handlePrev = useCallback(() => { + carouselRef.current?.prev(); + }, []); - const handleNext = useCallback(() => { - carouselRef.current?.next(); - }, []); + const handleNext = useCallback(() => { + carouselRef.current?.next(); + }, []); - return ( -
-
- - {carouselItems.map((item, index) => ( -
-
-
-
+ return ( +
+
+ + {carouselItems.map((item, index) => ( +
+
+
+
- {/* Content Container */} -
-
- - {item.title} - - - {item.desc} - - -
-
-
- ))} - + {/* Content Container */} +
+
+ ))} + - {/* Navigation Buttons */} - - -
+ {/* Navigation Buttons */} + + +
- {/* Stats Container */} -
-
- {platformStats.map((stat, index) => ( -
-
- {stat.icon} -
-
- {stat.value} -
-
- {stat.label} -
-
- ))} -
-
-
- ); + {/* Stats Container */} +
+
+ {platformStats.map((stat, index) => ( +
+
+ {stat.icon} +
+
+ {stat.value} +
+
+ {stat.label} +
+
+ ))} +
+
+
+ ); }; -export default HeroSection; \ No newline at end of file +export default HeroSection; From 3e160b49e70f1c338af878bef0f8641d1abb6d47 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 20:24:20 +0800 Subject: [PATCH 14/17] add --- .../form/CourseContentForm/CourseContentForm.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx index dcf9a32..6b0e103 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx @@ -34,7 +34,7 @@ const CourseContentForm: React.FC = () => { coordinateGetter: sortableKeyboardCoordinates, }) ); - const { softDeleteByIds } = usePost(); + const { softDeleteByIds, updateOrderByIds } = usePost(); const { data: sections = [], isLoading } = api.post.findMany.useQuery( { where: { @@ -60,11 +60,15 @@ const CourseContentForm: React.FC = () => { const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; - + let newItems = []; setItems((items) => { const oldIndex = items.findIndex((item) => item.id === active.id); const newIndex = items.findIndex((item) => item.id === over.id); - return arrayMove(items, oldIndex, newIndex); + newItems = arrayMove(items, oldIndex, newIndex); + return newItems; + }); + updateOrderByIds.mutateAsync({ + ids: newItems.map((item) => item.id), }); }; @@ -112,7 +116,7 @@ const CourseContentForm: React.FC = () => { className="mt-4" onClick={() => { if (items.some((item) => item.id === null)) { - toast.error("请先保存当前编辑章节"); + toast.error("请先保存当前编辑的章节"); } else { setItems([...items, { id: null, title: "" }]); } From 8ddcd6993e80b0ee7f9a30c9de50b4a46eab4146 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 20:24:22 +0800 Subject: [PATCH 15/17] add --- .../models/course/editor/form/CourseContentForm/LectureList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx index a9cf666..b495edd 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx @@ -137,7 +137,7 @@ export const LectureList: React.FC = ({ className="mt-4" onClick={() => { if (items.some((item) => item.id === null)) { - toast.error("请先保存当前编辑章节"); + toast.error("请先保存当前编辑中的课时!"); } else { setItems((prevItems) => [ ...prevItems.filter((item) => !!item.id), From 726d2ff766adb3b083a701ce104ecf15d7e03c88 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 20:24:24 +0800 Subject: [PATCH 16/17] add --- packages/common/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index fbc63cd..7e9174f 100755 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -56,7 +56,7 @@ export const InitTaxonomies: { objectType?: string[]; }[] = [ { - name: "分类", + name: "课程分类", slug: TaxonomySlug.CATEGORY, objectType: [ObjectType.COURSE], }, From 7929654f34b4cf70e5a7e58ed50ff8aae5fb1a8c Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 20:53:22 +0800 Subject: [PATCH 17/17] add --- .../models/app-config/app-config.router.ts | 80 ++++++++++--------- .../models/app-config/app-config.service.ts | 13 +-- apps/server/src/models/post/post.service.ts | 5 ++ apps/server/src/tasks/init/gendev.service.ts | 2 +- apps/web/src/app/admin/base-setting/page.tsx | 67 +++++++--------- .../common/uploader/AvatarUploader.tsx | 14 +++- .../editor/context/CourseEditorContext.tsx | 5 +- packages/common/src/enum.ts | 1 + 8 files changed, 97 insertions(+), 90 deletions(-) diff --git a/apps/server/src/models/app-config/app-config.router.ts b/apps/server/src/models/app-config/app-config.router.ts index ece1b35..882fa16 100755 --- a/apps/server/src/models/app-config/app-config.router.ts +++ b/apps/server/src/models/app-config/app-config.router.ts @@ -4,44 +4,48 @@ import { AppConfigService } from './app-config.service'; import { z, ZodType } from 'zod'; import { Prisma } from '@nice/common'; import { RealtimeServer } from '@server/socket/realtime/realtime.server'; -const AppConfigUncheckedCreateInputSchema: ZodType = z.any() -const AppConfigUpdateArgsSchema: ZodType = z.any() -const AppConfigDeleteManyArgsSchema: ZodType = z.any() -const AppConfigFindFirstArgsSchema: ZodType = z.any() +const AppConfigUncheckedCreateInputSchema: ZodType = + z.any(); +const AppConfigUpdateArgsSchema: ZodType = z.any(); +const AppConfigDeleteManyArgsSchema: ZodType = + z.any(); +const AppConfigFindFirstArgsSchema: ZodType = + z.any(); @Injectable() export class AppConfigRouter { - constructor( - private readonly trpc: TrpcService, - private readonly appConfigService: AppConfigService, - private readonly realtimeServer: RealtimeServer - ) { } - router = this.trpc.router({ - create: this.trpc.protectProcedure - .input(AppConfigUncheckedCreateInputSchema) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.appConfigService.create({ data: input }); - }), - update: this.trpc.protectProcedure - .input(AppConfigUpdateArgsSchema) - .mutation(async ({ ctx, input }) => { - - const { staff } = ctx; - return await this.appConfigService.update(input); - }), - deleteMany: this.trpc.protectProcedure.input(AppConfigDeleteManyArgsSchema).mutation(async ({ input }) => { - return await this.appConfigService.deleteMany(input) - }), - findFirst: this.trpc.protectProcedure.input(AppConfigFindFirstArgsSchema). - query(async ({ input }) => { - - return await this.appConfigService.findFirst(input) - }), - clearRowCache: this.trpc.protectProcedure.mutation(async () => { - return await this.appConfigService.clearRowCache() - }), - getClientCount: this.trpc.protectProcedure.query(() => { - return this.realtimeServer.getClientCount() - }) - }); + constructor( + private readonly trpc: TrpcService, + private readonly appConfigService: AppConfigService, + private readonly realtimeServer: RealtimeServer, + ) {} + router = this.trpc.router({ + create: this.trpc.protectProcedure + .input(AppConfigUncheckedCreateInputSchema) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.appConfigService.create({ data: input }); + }), + update: this.trpc.protectProcedure + .input(AppConfigUpdateArgsSchema) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.appConfigService.update(input); + }), + deleteMany: this.trpc.protectProcedure + .input(AppConfigDeleteManyArgsSchema) + .mutation(async ({ input }) => { + return await this.appConfigService.deleteMany(input); + }), + findFirst: this.trpc.protectProcedure + .input(AppConfigFindFirstArgsSchema) + .query(async ({ input }) => { + return await this.appConfigService.findFirst(input); + }), + clearRowCache: this.trpc.protectProcedure.mutation(async () => { + return await this.appConfigService.clearRowCache(); + }), + getClientCount: this.trpc.protectProcedure.query(() => { + return this.realtimeServer.getClientCount(); + }), + }); } diff --git a/apps/server/src/models/app-config/app-config.service.ts b/apps/server/src/models/app-config/app-config.service.ts index 733e620..bd003d7 100755 --- a/apps/server/src/models/app-config/app-config.service.ts +++ b/apps/server/src/models/app-config/app-config.service.ts @@ -1,10 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { - db, - ObjectType, - Prisma, -} from '@nice/common'; - +import { db, ObjectType, Prisma } from '@nice/common'; import { BaseService } from '../base/base.service'; import { deleteByPattern } from '@server/utils/redis/utils'; @@ -12,10 +7,10 @@ import { deleteByPattern } from '@server/utils/redis/utils'; @Injectable() export class AppConfigService extends BaseService { constructor() { - super(db, "appConfig"); + super(db, 'appConfig'); } async clearRowCache() { - await deleteByPattern("row-*") - return true + await deleteByPattern('row-*'); + return true; } } diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 5048fff..675dda4 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -101,6 +101,11 @@ export class PostService extends BaseTreeService { }, params: { staff?: UserProfile; tx?: Prisma.TransactionClient }, ) { + // const await db.post.findMany({ + // where: { + // type: PostType.COURSE, + // }, + // }); const { courseDetail } = args; // If no transaction is provided, create a new one if (!params.tx) { diff --git a/apps/server/src/tasks/init/gendev.service.ts b/apps/server/src/tasks/init/gendev.service.ts index 8f42c9a..e2c31c9 100755 --- a/apps/server/src/tasks/init/gendev.service.ts +++ b/apps/server/src/tasks/init/gendev.service.ts @@ -45,7 +45,7 @@ export class GenDevService { await this.generateDepartments(3, 6); await this.generateTerms(2, 6); await this.generateStaffs(4); - await this.generateCourses(); + await this.generateCourses(8); } catch (err) { this.logger.error(err); } diff --git a/apps/web/src/app/admin/base-setting/page.tsx b/apps/web/src/app/admin/base-setting/page.tsx index ae1060d..82601ee 100755 --- a/apps/web/src/app/admin/base-setting/page.tsx +++ b/apps/web/src/app/admin/base-setting/page.tsx @@ -1,33 +1,26 @@ -import { - AppConfigSlug, - BaseSetting, - RolePerms, -} from "@nice/common"; +import { AppConfigSlug, BaseSetting, RolePerms } from "@nice/common"; import { useContext, useEffect, useState } from "react"; -import { - Button, - Form, - Input, - message, - theme, -} from "antd"; +import { Button, Form, Input, message, theme } from "antd"; import { useAppConfig } from "@nice/client"; import { useAuth } from "@web/src/providers/auth-provider"; import FixedHeader from "@web/src/components/layout/fix-header"; import { useForm } from "antd/es/form/Form"; -import { api } from "@nice/client" +import { api } from "@nice/client"; import { MainLayoutContext } from "../layout"; export default function BaseSettingPage() { const { update, baseSetting } = useAppConfig(); - const utils = api.useUtils() - const [form] = useForm() + const utils = api.useUtils(); + const [form] = useForm(); const { token } = theme.useToken(); - const { data: clientCount } = api.app_config.getClientCount.useQuery(undefined, { - refetchInterval: 3000, - refetchIntervalInBackground: true - }) + const { data: clientCount } = api.app_config.getClientCount.useQuery( + undefined, + { + refetchInterval: 3000, + refetchIntervalInBackground: true, + } + ); const [isFormChanged, setIsFormChanged] = useState(false); const [loading, setLoading] = useState(false); const { user, hasSomePermissions } = useAuth(); @@ -36,31 +29,27 @@ export default function BaseSettingPage() { setIsFormChanged(true); } function onResetClick() { - if (!form) - return + if (!form) return; if (!baseSetting) { form.resetFields(); } else { form.resetFields(); form.setFieldsValue(baseSetting); - } setIsFormChanged(false); } function onSaveClick() { - if (form) - form.submit(); + if (form) form.submit(); } async function onSubmit(values: BaseSetting) { setLoading(true); try { - await update.mutateAsync({ where: { slug: AppConfigSlug.BASE_SETTING, }, - data: { meta: JSON.stringify(values) } + data: { meta: { ...baseSetting, ...values } }, }); setIsFormChanged(false); message.success("已保存"); @@ -72,7 +61,6 @@ export default function BaseSettingPage() { } useEffect(() => { if (baseSetting && form) { - form.setFieldsValue(baseSetting); } }, [baseSetting, form]); @@ -103,7 +91,6 @@ export default function BaseSettingPage() { !hasSomePermissions(RolePerms.MANAGE_BASE_SETTING) } onFinish={onSubmit} - onFieldsChange={handleFieldsChange} layout="vertical"> {/*
- {
- app在线人数 -
- {clientCount && clientCount > 0 ? `${clientCount}人在线` : '无人在线'} + { +
+ app在线人数 +
+ {clientCount && clientCount > 0 + ? `${clientCount}人在线` + : "无人在线"} +
-
} + }
); diff --git a/apps/web/src/components/common/uploader/AvatarUploader.tsx b/apps/web/src/components/common/uploader/AvatarUploader.tsx index 1ca2e20..abbb355 100755 --- a/apps/web/src/components/common/uploader/AvatarUploader.tsx +++ b/apps/web/src/components/common/uploader/AvatarUploader.tsx @@ -36,7 +36,7 @@ const AvatarUploader: React.FC = ({ const [file, setFile] = useState(null); const avatarRef = useRef(null); const [previewUrl, setPreviewUrl] = useState(value || ""); - + const [imageSrc, setImageSrc] = useState(value); const [compressedUrl, setCompressedUrl] = useState(value || ""); const [url, setUrl] = useState(value || ""); const [uploading, setUploading] = useState(false); @@ -45,7 +45,9 @@ const AvatarUploader: React.FC = ({ const [avatarKey, setAvatarKey] = useState(0); const { token } = theme.useToken(); useEffect(() => { - setPreviewUrl(value || ""); + if (!previewUrl || previewUrl?.length < 1) { + setPreviewUrl(value || ""); + } }, [value]); const handleChange = async (event: React.ChangeEvent) => { const selectedFile = event.target.files?.[0]; @@ -128,6 +130,14 @@ const AvatarUploader: React.FC = ({ ref={avatarRef} src={previewUrl} shape="square" + onError={() => { + if (value && previewUrl && imageSrc === value) { + // 当原始图片(value)加载失败时,切换到 previewUrl + setImageSrc(previewUrl); + return true; // 阻止默认的 fallback 行为,让它尝试新设置的 src + } + return false; // 如果 previewUrl 也失败了,显示默认头像 + }} className="w-full h-full object-cover" /> ) : ( 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 913bf34..6a12e2c 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -109,11 +109,12 @@ export function CourseFormProvider({ } try { if (editId) { - await update.mutateAsync({ + const result = await update.mutateAsync({ where: { id: editId }, data: formattedValues, }); message.success("课程更新成功!"); + navigate(`/course/${result.id}/editor/content`); } else { const result = await createCourse.mutateAsync({ courseDetail: { @@ -127,8 +128,8 @@ export function CourseFormProvider({ }, sections, }); - navigate(`/course/${result.id}/editor`, { replace: true }); message.success("课程创建成功!"); + navigate(`/course/${result.id}/editor/content`); } form.resetFields(); } catch (error) { diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 32903b2..6264c9b 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -100,6 +100,7 @@ export enum RolePerms { } export enum AppConfigSlug { BASE_SETTING = "base_setting", + } // 资源类型的枚举,定义了不同类型的资源,以字符串值表示 export enum ResourceType {