From 57f486ca6eee676e68d6c94352c203591cbfd7fb Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 19:49:50 +0800 Subject: [PATCH 01/28] add --- .../app/main/home/components/HeroSection.tsx | 91 ++++++++++++------- .../src/app/main/layout/UserMenu/UserMenu.tsx | 28 ++++-- apps/web/src/app/main/my-duty/page.tsx | 7 ++ apps/web/src/app/main/my-learning/page.tsx | 3 + packages/common/prisma/schema.prisma | 8 -- 5 files changed, 88 insertions(+), 49 deletions(-) create mode 100644 apps/web/src/app/main/my-duty/page.tsx create mode 100644 apps/web/src/app/main/my-learning/page.tsx diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx index eca460d..4073109 100755 --- a/apps/web/src/app/main/home/components/HeroSection.tsx +++ b/apps/web/src/app/main/home/components/HeroSection.tsx @@ -1,4 +1,10 @@ -import React, { useRef, useCallback, useEffect, useMemo, useState } from "react"; +import React, { + useRef, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { Carousel, Typography } from "antd"; import { TeamOutlined, @@ -30,13 +36,29 @@ interface PlatformStat { const HeroSection = () => { const carouselRef = useRef(null); const { statistics, slides } = useAppConfig(); - const [countStatistics, setCountStatistics] = useState(4) + const [countStatistics, setCountStatistics] = useState(4); const platformStats: PlatformStat[] = useMemo(() => { return [ - { icon: , value: statistics.staffs, label: "注册学员" }, - { icon: , value: statistics.courses, label: "精品课程" }, - { icon: , value: statistics.lectures, label: '课程章节' }, - { icon: , value: statistics.reads, label: "观看次数" }, + { + icon: , + value: statistics.staffs, + label: "注册学员", + }, + { + icon: , + value: statistics.courses, + label: "精品课程", + }, + { + icon: , + value: statistics.lectures, + label: "课程章节", + }, + { + icon: , + value: statistics.reads, + label: "观看次数", + }, ]; }, [statistics]); const handlePrev = useCallback(() => { @@ -48,7 +70,7 @@ const HeroSection = () => { }, []); const countNonZeroValues = (statistics: Record): number => { - return Object.values(statistics).filter(value => value !== 0).length; + return Object.values(statistics).filter((value) => value !== 0).length; }; useEffect(() => { @@ -67,8 +89,8 @@ const HeroSection = () => { dots={{ className: "carousel-dots !bottom-32 !z-20", }}> - {Array.isArray(slides) ? - (slides.map((item, index) => ( + {Array.isArray(slides) ? ( + slides.map((item, index) => (
{
)) - ) : ( -
- )} + ) : ( +
+ )} {/* Navigation Buttons */} @@ -108,31 +130,30 @@ const HeroSection = () => {
{/* Stats Container */} - { - countStatistics > 1 && ( -
-
- {platformStats.map((stat, index) => { - return stat.value - ? (
-
- {stat.icon} -
-
- {stat.value} -
-
- {stat.label} -
+ {countStatistics > 1 && ( +
+
+ {platformStats.map((stat, index) => { + return stat.value ? ( +
+
+ {stat.icon}
- ) : null - })} -
+
+ {stat.value} +
+
+ {stat.label} +
+
+ ) : null; + })}
- ) - } +
+ )} ); }; diff --git a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx index ea23902..41da474 100755 --- a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx @@ -86,6 +86,20 @@ export function UserMenu() { setModalOpen(true); }, }, + { + icon: , + label: "我创建的课程", + action: () => { + setModalOpen(true); + }, + }, + { + icon: , + label: "我学习的课程", + action: () => { + setModalOpen(true); + }, + }, canManageAnyStaff && { icon: , label: "设置", @@ -222,18 +236,20 @@ export function UserMenu() { focus:ring-2 focus:ring-[#00538E]/20 group relative overflow-hidden active:scale-[0.99] - ${item.label === "注销" + ${ + item.label === "注销" ? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700" : "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]" - }`}> + }`}> + group-hover:translate-x-0.5 ${ + item.label === "注销" + ? "group-hover:text-red-600" + : "group-hover:text-[#003F6A]" + }`}> {item.icon} {item.label} diff --git a/apps/web/src/app/main/my-duty/page.tsx b/apps/web/src/app/main/my-duty/page.tsx new file mode 100644 index 0000000..7871969 --- /dev/null +++ b/apps/web/src/app/main/my-duty/page.tsx @@ -0,0 +1,7 @@ +export default function MyDutyPage() { + + + + return <> + +} diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx new file mode 100644 index 0000000..503600c --- /dev/null +++ b/apps/web/src/app/main/my-learning/page.tsx @@ -0,0 +1,3 @@ +export default function MyLearningPage() { + return <>; +} diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 1c245e2..236c4ab 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -288,14 +288,6 @@ model Visit { message Message? @relation(fields: [messageId], references: [id]) messageId String? @map("message_id") lectureId String? @map("lecture_id") // 课时ID - - // 学习数据 - // progress Float? @default(0) @map("progress") // 完成进度(0-100%) - // isCompleted Boolean? @default(false) @map("is_completed") // 是否完成 - // lastPosition Int? @default(0) @map("last_position") // 视频播放位置(秒) - // totalWatchTime Int? @default(0) @map("total_watch_time") // 总观看时长(秒) - // // 时间记录 - // lastWatchedAt DateTime? @map("last_watched_at") // 最后观看时间 createdAt DateTime @default(now()) @map("created_at") // 创建时间 updatedAt DateTime @updatedAt @map("updated_at") // 更新时间 deletedAt DateTime? @map("deleted_at") // 删除时间,可为空 From 54f2ed407fc406c32850ac592f67be72c820b85d Mon Sep 17 00:00:00 2001 From: Rao <1227431568@qq.com> Date: Wed, 26 Feb 2025 20:00:15 +0800 Subject: [PATCH 02/28] rht02262000 --- .../course/detail/CourseSyllabus/CourseSyllabus.tsx | 1 - .../course/detail/CourseSyllabus/LectureItem.tsx | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx index 9e6cf84..10b756a 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx @@ -45,7 +45,6 @@ export const CourseSyllabus: React.FC = ({ block: "start", }); }; - return ( <> {/* 收起按钮直接显示 */} diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx index ffc0a49..6bdc9b3 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -4,6 +4,7 @@ import { Lecture, LectureType, LessonTypeLabel } from "@nice/common"; import React, { useMemo } from "react"; import { ClockCircleOutlined, + EyeOutlined, FileTextOutlined, PlayCircleOutlined, } from "@ant-design/icons"; // 使用 Ant Design 图标 @@ -43,13 +44,17 @@ export const LectureItem: React.FC = ({ {LessonTypeLabel[lecture?.meta?.type]}
)} -
+

{lecture.title}

{lecture.subTitle && ( -

+ {lecture.subTitle} -

+ )} +
+ + {lecture?.meta?.views} +
); From 4a6957f1814f4125800baf6b6bfed4f508b40da3 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 21:08:38 +0800 Subject: [PATCH 03/28] addd --- apps/server/src/models/post/post.service.ts | 60 +++++++++---------- apps/server/src/queue/models/post/utils.ts | 2 +- .../main/courses/components/CourseCard.tsx | 22 ++++--- .../src/app/main/layout/NavigationMenu.tsx | 27 +++++++-- .../src/app/main/layout/UserMenu/UserMenu.tsx | 4 +- apps/web/src/app/main/my-duty/page.tsx | 24 ++++++-- apps/web/src/app/main/my-learning/page.tsx | 20 ++++++- .../models/course/list/CourseList.tsx | 14 ++++- apps/web/src/routes/index.tsx | 21 ++++++- packages/common/prisma/schema.prisma | 13 ++-- packages/common/src/models/select.ts | 4 +- 11 files changed, 147 insertions(+), 64 deletions(-) diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 6964696..8f90a85 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -306,37 +306,37 @@ export class PostService extends BaseTreeService { staff?.id && { authorId: staff.id, }, - staff?.id && { - watchableStaffs: { - some: { - id: staff.id, - }, - }, - }, - deptId && { - watchableDepts: { - some: { - id: { - in: parentDeptIds, - }, - }, - }, - }, + // staff?.id && { + // watchableStaffs: { + // some: { + // id: staff.id, + // }, + // }, + // }, + // deptId && { + // watchableDepts: { + // some: { + // id: { + // in: parentDeptIds, + // }, + // }, + // }, + // }, - { - AND: [ - { - watchableStaffs: { - none: {}, // 匹配 watchableStaffs 为空 - }, - }, - { - watchableDepts: { - none: {}, // 匹配 watchableDepts 为空 - }, - }, - ], - }, + // { + // AND: [ + // { + // watchableStaffs: { + // none: {}, // 匹配 watchableStaffs 为空 + // }, + // }, + // { + // watchableDepts: { + // none: {}, // 匹配 watchableDepts 为空 + // }, + // }, + // ], + // }, ].filter(Boolean); if (orCondition?.length > 0) return orCondition; diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 51d2b04..51bffca 100644 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -23,7 +23,7 @@ export async function updateTotalCourseViewCount(type: VisitType) { views: true, }, where: { - postId: { in: lectures.map((lecture) => lecture.id) }, + postId: { in: posts.map((post) => post.id) }, type: type, }, }); diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 5de3a9d..3dd5032 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -9,13 +9,18 @@ import { useNavigate } from "react-router-dom"; interface CourseCardProps { course: CourseDto; + edit?: boolean; } const { Title, Text } = Typography; -export default function CourseCard({ course }: CourseCardProps) { +export default function CourseCard({ course, edit = false }: CourseCardProps) { const navigate = useNavigate(); const handleClick = (course: CourseDto) => { - navigate(`/course/${course.id}/detail`); - window.scrollTo({ top: 0, behavior: "smooth", }) + if (!edit) { + navigate(`/course/${course.id}/detail`); + } else { + navigate(`/course/${course.id}/editor`); + } + window.scrollTo({ top: 0, behavior: "smooth" }); }; return (
- {course?.meta?.views - ? `观看次数 ${course?.meta?.views}` - : null} + + {`观看次数 ${course?.meta?.views || 0}`}
@@ -91,7 +95,7 @@ export default function CourseCard({ course }: CourseCardProps) { size="large" className="w-full shadow-[0_8px_20px_-6px_rgba(59,130,246,0.5)] hover:shadow-[0_12px_24px_-6px_rgba(59,130,246,0.6)] transform hover:translate-y-[-2px] transition-all duration-500 ease-out"> - 立即学习 + {edit ? "进行编辑" : "立即学习"}
diff --git a/apps/web/src/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx index 53efdb3..6f0f5de 100755 --- a/apps/web/src/app/main/layout/NavigationMenu.tsx +++ b/apps/web/src/app/main/layout/NavigationMenu.tsx @@ -1,15 +1,30 @@ +import { useAuth } from "@web/src/providers/auth-provider"; import { Menu } from "antd"; +import { useMemo } from "react"; import { useNavigate, useLocation } from "react-router-dom"; -const menuItems = [ - { key: "home", path: "/", label: "首页" }, - { key: "courses", path: "/courses", label: "全部课程" }, - { key: "paths", path: "/paths", label: "学习路径" }, -]; - export const NavigationMenu = () => { const navigate = useNavigate(); + const { isAuthenticated } = useAuth(); const { pathname } = useLocation(); + + const menuItems = useMemo(() => { + const baseItems = [ + { key: "home", path: "/", label: "首页" }, + { key: "courses", path: "/courses", label: "全部课程" }, + { key: "paths", path: "/paths", label: "学习路径" }, + ]; + if (!isAuthenticated) { + return baseItems; + } else { + return [ + ...baseItems, + { key: "my-duty", path: "/my-duty", label: "我创建的" }, + { key: "my-learning", path: "/my-learning", label: "我学习的" }, + ]; + } + }, [isAuthenticated]); + const selectedKey = menuItems.find((item) => item.path === pathname)?.key || ""; return ( diff --git a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx index 41da474..e3ef21f 100755 --- a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx @@ -90,14 +90,14 @@ export function UserMenu() { icon: , label: "我创建的课程", action: () => { - setModalOpen(true); + navigate("/my/duty"); }, }, { icon: , label: "我学习的课程", action: () => { - setModalOpen(true); + navigate("/my/learning"); }, }, canManageAnyStaff && { diff --git a/apps/web/src/app/main/my-duty/page.tsx b/apps/web/src/app/main/my-duty/page.tsx index 7871969..fd33761 100644 --- a/apps/web/src/app/main/my-duty/page.tsx +++ b/apps/web/src/app/main/my-duty/page.tsx @@ -1,7 +1,21 @@ +import CourseList from "@web/src/components/models/course/list/CourseList"; +import { useAuth } from "@web/src/providers/auth-provider"; + export default function MyDutyPage() { - - - - return <> - + const { user } = useAuth(); + return ( + <> +
+ +
+ + ); } diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx index 503600c..0216515 100644 --- a/apps/web/src/app/main/my-learning/page.tsx +++ b/apps/web/src/app/main/my-learning/page.tsx @@ -1,3 +1,21 @@ +import CourseList from "@web/src/components/models/course/list/CourseList"; +import { useAuth } from "@web/src/providers/auth-provider"; + export default function MyLearningPage() { - return <>; + const { user } = useAuth(); + return ( + <> +
+ +
+ + ); } diff --git a/apps/web/src/components/models/course/list/CourseList.tsx b/apps/web/src/components/models/course/list/CourseList.tsx index 3ba3eaf..2111a3a 100755 --- a/apps/web/src/components/models/course/list/CourseList.tsx +++ b/apps/web/src/components/models/course/list/CourseList.tsx @@ -13,6 +13,7 @@ interface CourseListProps { }; cols?: number; showPagination?: boolean; + edit?: boolean; } interface CoursesPagnationProps { data: { @@ -25,6 +26,7 @@ export default function CourseList({ params, cols = 3, showPagination = true, + edit = false, }: CourseListProps) { const [currentPage, setCurrentPage] = useState(params?.page || 1); const { data, isLoading }: CoursesPagnationProps = @@ -55,7 +57,11 @@ export default function CourseList({ window.scrollTo({ top: 0, behavior: "smooth" }); } if (isLoading) { - return ; + return ( +
+ +
+ ); } return (
@@ -66,7 +72,11 @@ export default function CourseList({ ) : ( courses.map((course) => ( - + )) )}
diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index e1ae965..1ba39aa 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -18,6 +18,8 @@ import CoursesPage from "../app/main/courses/page"; import PathsPage from "../app/main/paths/page"; import { adminRoute } from "./admin-route"; import { CoursePreview } from "../app/main/course/preview/page"; +import MyLearningPage from "../app/main/my-learning/page"; +import MyDutyPage from "../app/main/my-duty/page"; interface CustomIndexRouteObject extends IndexRouteObject { name?: string; breadcrumb?: string; @@ -63,15 +65,32 @@ export const routes: CustomRouteObject[] = [ path: "courses", element: , }, + { + path: "my-duty", + element: ( + + + + ), + }, + { + path: "my-learning", + element: ( + + + + ), + }, ], }, + { path: "course", children: [ { path: ":id?/editor", element: ( - + ), diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 236c4ab..75e8be6 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -88,9 +88,12 @@ model Staff { deletedAt DateTime? @map("deleted_at") officerId String? @map("officer_id") - watchedPost Post[] @relation("post_watch_staff") + // watchedPost Post[] @relation("post_watch_staff") visits Visit[] posts Post[] + + + learningPost Post[] @relation("post_student") sentMsgs Message[] @relation("message_sender") receivedMsgs Message[] @relation("message_receiver") registerToken String? @@ -124,7 +127,7 @@ model Department { deptStaffs Staff[] @relation("DeptStaff") terms Term[] @relation("department_term") - watchedPost Post[] @relation("post_watch_dept") + // watchedPost Post[] @relation("post_watch_dept") hasChildren Boolean? @default(false) @map("has_children") @@index([parentId]) @@ -201,7 +204,7 @@ model Post { order Float? @default(0) @map("order") duration Int? rating Int? @default(0) - + students Staff[] @relation("post_student") depts Department[] @relation("post_dept") // 索引 // 日期时间类型字段 @@ -223,8 +226,8 @@ model Post { ancestors PostAncestry[] @relation("DescendantPosts") descendants PostAncestry[] @relation("AncestorPosts") resources Resource[] // 附件列表 - watchableStaffs Staff[] @relation("post_watch_staff") // 可观看的员工列表,关联 Staff 模型 - watchableDepts Department[] @relation("post_watch_dept") // 可观看的部门列表,关联 Department 模型 + // watchableStaffs Staff[] @relation("post_watch_staff") + // watchableDepts Department[] @relation("post_watch_dept") // 可观看的部门列表,关联 Department 模型 meta Json? // 封面url 视频url objectives具体的学习目标 rating评分Int // 索引 diff --git a/packages/common/src/models/select.ts b/packages/common/src/models/select.ts index 2e96315..38fd3bf 100755 --- a/packages/common/src/models/select.ts +++ b/packages/common/src/models/select.ts @@ -6,8 +6,8 @@ export const postDetailSelect: Prisma.PostSelect = { title: true, content: true, resources: true, - watchableDepts: true, - watchableStaffs: true, + // watchableDepts: true, + // watchableStaffs: true, updatedAt: true, author: { select: { From 5f140e7e33d6af7347e2ec77504a9ca749ab22bc Mon Sep 17 00:00:00 2001 From: Rao <1227431568@qq.com> Date: Wed, 26 Feb 2025 21:08:57 +0800 Subject: [PATCH 04/28] rht --- apps/web/src/app/main/courses/components/FilterSection.tsx | 2 +- .../models/course/detail/CourseSyllabus/LectureItem.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index 01ffba6..d594d4c 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -19,7 +19,7 @@ export default function FilterSection() { }); }; return ( -
+
{taxonomies?.map((tax, index) => { const items = Object.entries(selectedTerms).find( ([key, items]) => key === tax.slug diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx index 6bdc9b3..72393db 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -53,7 +53,7 @@ export const LectureItem: React.FC = ({ )}
- {lecture?.meta?.views} + {lecture?.meta?.views ? lecture?.meta?.views : 0}
From 5872f4b7280ccc91ad47067d2cd3239d8f6ac11e Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 22:03:07 +0800 Subject: [PATCH 05/28] add --- apps/server/src/models/post/post.service.ts | 15 ++------------- apps/server/src/models/post/utils.ts | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 8f90a85..947ecfc 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -166,19 +166,7 @@ export class PostService extends BaseTreeService { ); return transDto; } - // async findMany(args: Prisma.PostFindManyArgs, staff?: UserProfile) { - // if (!args.where) args.where = {}; - // args.where.OR = await this.preFilter(args.where.OR, staff); - // return this.wrapResult(super.findMany(args), async (result) => { - // await Promise.all( - // result.map(async (item) => { - // await setPostRelation({ data: item, staff }); - // await this.setPerms(item, staff); - // }), - // ); - // return { ...result }; - // }); - // } + async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) { if (!args.where) args.where = {}; args.where.OR = await this.preFilter(args.where.OR, staff); @@ -255,6 +243,7 @@ export class PostService extends BaseTreeService { // 批量执行更新 return updates.length > 0 ? await db.$transaction(updates) : []; } + protected async setPerms(data: Post, staff?: UserProfile) { if (!staff) return; const perms: ResPerm = { diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 0e1f835..57b0fd0 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -168,6 +168,21 @@ export async function setCourseInfo({ data }: { data: Post }) { (lecture) => lecture.parentId === section.id, ) as any as Lecture[]; }); - Object.assign(data, { sections, lectureCount }); + + const students = await db.staff.findMany({ + where: { + learningPosts: { + some: { + id: data.id, + }, + }, + }, + select: { + id: true, + }, + }); + + const studentIds = (students || []).map((student) => student?.id); + Object.assign(data, { sections, lectureCount, studentIds }); } } From 4ec92966ab09bc3f32c6665ce39df763fa8d0d69 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 22:03:18 +0800 Subject: [PATCH 06/28] add --- packages/common/prisma/schema.prisma | 2 +- packages/common/src/models/post.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 75e8be6..db764d2 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -93,7 +93,7 @@ model Staff { posts Post[] - learningPost Post[] @relation("post_student") + learningPosts Post[] @relation("post_student") sentMsgs Message[] @relation("message_sender") receivedMsgs Message[] @relation("message_receiver") registerToken String? diff --git a/packages/common/src/models/post.ts b/packages/common/src/models/post.ts index a10fcf5..63a38b0 100755 --- a/packages/common/src/models/post.ts +++ b/packages/common/src/models/post.ts @@ -83,4 +83,5 @@ export type CourseDto = Course & { terms: TermDto[]; lectureCount?: number; depts: Department[]; + studentIds: string[]; }; From 4c89a43197605f4c2324a130ba1337a2cc57d495 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 22:03:25 +0800 Subject: [PATCH 07/28] add --- packages/client/src/api/hooks/useStaff.ts | 67 +++++++++++++---------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/packages/client/src/api/hooks/useStaff.ts b/packages/client/src/api/hooks/useStaff.ts index fbdf571..aed3aa7 100755 --- a/packages/client/src/api/hooks/useStaff.ts +++ b/packages/client/src/api/hooks/useStaff.ts @@ -5,39 +5,48 @@ import { ObjectType, Staff } from "@nice/common"; import { findQueryData } from "../utils"; import { CrudOperation, emitDataChange } from "../../event"; export function useStaff() { - const queryClient = useQueryClient(); - const queryKey = getQueryKey(api.staff); + const queryClient = useQueryClient(); + const queryKey = getQueryKey(api.staff); - const create = api.staff.create.useMutation({ - onSuccess: (result) => { - queryClient.invalidateQueries({ queryKey }); - emitDataChange(ObjectType.STAFF, result as any, CrudOperation.CREATED) - }, - }); - const updateUserDomain = api.staff.updateUserDomain.useMutation({ - onSuccess: async (result) => { - queryClient.invalidateQueries({ queryKey }); - }, - }); - const update = api.staff.update.useMutation({ - onSuccess: (result) => { - queryClient.invalidateQueries({ queryKey }); - emitDataChange(ObjectType.STAFF, result as any, CrudOperation.UPDATED) - }, - }); + const create = api.staff.create.useMutation({ + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey }); + emitDataChange( + ObjectType.STAFF, + result as any, + CrudOperation.CREATED + ); + }, + }); + const updateUserDomain = api.staff.updateUserDomain.useMutation({ + onSuccess: async (result) => { + queryClient.invalidateQueries({ queryKey }); + }, + }); + const update = api.staff.update.useMutation({ + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey }); + queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) }); + emitDataChange( + ObjectType.STAFF, + result as any, + CrudOperation.UPDATED + ); + }, + }); const softDeleteByIds = api.staff.softDeleteByIds.useMutation({ onSuccess: (result, variables) => { queryClient.invalidateQueries({ queryKey }); }, }); - const getStaff = (key: string) => { - return findQueryData(queryClient, api.staff, key); - }; - return { - create, - update, - softDeleteByIds, - getStaff, - updateUserDomain - }; + const getStaff = (key: string) => { + return findQueryData(queryClient, api.staff, key); + }; + return { + create, + update, + softDeleteByIds, + getStaff, + updateUserDomain, + }; } From a7bbc88e994c515c67ecb8d60bb3903472129df0 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 22:03:29 +0800 Subject: [PATCH 08/28] add --- apps/web/src/app/main/my-learning/page.tsx | 7 +- .../course/detail/CourseDetailContext.tsx | 23 +++--- .../CourseDetailHeader/CourseDetailHeader.tsx | 61 +++++++++++---- .../CourseDetailHeader_BACKUP.tsx | 77 ------------------- .../editor/context/CourseEditorContext.tsx | 4 +- .../CourseContentForm/SortableLecture.tsx | 4 +- 6 files changed, 64 insertions(+), 112 deletions(-) delete mode 100755 apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx index 0216515..8807119 100644 --- a/apps/web/src/app/main/my-learning/page.tsx +++ b/apps/web/src/app/main/my-learning/page.tsx @@ -7,11 +7,14 @@ export default function MyLearningPage() { <>
diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index b722f3c..0da1702 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -28,6 +28,7 @@ interface CourseDetailContextType { isHeaderVisible: boolean; // 新增 setIsHeaderVisible: (visible: boolean) => void; // 新增 canEdit?: boolean; + userIsLearning?: boolean; } interface CourseFormProviderProps { @@ -43,30 +44,25 @@ export function CourseDetailProvider({ }: CourseFormProviderProps) { const navigate = useNavigate(); const { read } = useVisitor(); - const { user, hasSomePermissions } = useAuth(); + const { user, hasSomePermissions, isAuthenticated } = useAuth(); const { lectureId } = useParams(); const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = (api.post as any).findFirst.useQuery( { where: { id: editId }, - // include: { - // // sections: { include: { lectures: true } }, - // enrollments: true, - // terms:true - // }, - - select:courseDetailSelect + select: courseDetailSelect, }, { enabled: Boolean(editId) } ); + + const userIsLearning = useMemo(() => { + return (course?.studentIds || []).includes(user?.id); + }, [user, course, isLoading]); const canEdit = useMemo(() => { - const isAuthor = user?.id === course?.authorId; - const isDept = course?.depts - ?.map((dept) => dept.id) - .includes(user?.deptId); + const isAuthor = isAuthenticated && user?.id === course?.authorId; const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST); - return isAuthor || isDept || isRoot; + return isAuthor || isRoot; }, [user, course]); const [selectedLectureId, setSelectedLectureId] = useState< string | undefined @@ -109,6 +105,7 @@ export function CourseDetailProvider({ isHeaderVisible, setIsHeaderVisible, canEdit, + userIsLearning, }}> {children} diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx index 401484a..e07ad48 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -10,17 +10,18 @@ import { useAuth } from "@web/src/providers/auth-provider"; import { useNavigate, useParams } from "react-router-dom"; import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu"; import { CourseDetailContext } from "../CourseDetailContext"; - +import { usePost, useStaff } from "@nice/client"; +import toast from "react-hot-toast"; const { Header } = Layout; export function CourseDetailHeader() { - const [searchValue, setSearchValue] = useState(""); const { id } = useParams(); const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } = useAuth(); const navigate = useNavigate(); - const { course, canEdit } = useContext(CourseDetailContext); + const { course, canEdit, userIsLearning } = useContext(CourseDetailContext); + const { update } = useStaff(); return (
@@ -39,20 +40,48 @@ export function CourseDetailHeader() { {/* */}
+ {isAuthenticated && ( + + )} {canEdit && ( - <> - - + )} {isAuthenticated ? ( diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx deleted file mode 100755 index 0fc9815..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx +++ /dev/null @@ -1,77 +0,0 @@ -// // components/Header.tsx -// import { motion, useScroll, useTransform } from "framer-motion"; -// import { useContext, useEffect, useState } from "react"; -// import { CourseDetailContext } from "../CourseDetailContext"; -// import { Avatar, Button, Dropdown } from "antd"; -// import { UserOutlined } from "@ant-design/icons"; -// import { UserMenu } from "@web/src/app/main/layout/UserMenu"; -// import { useAuth } from "@web/src/providers/auth-provider"; - -// export const CourseDetailHeader = () => { -// const { scrollY } = useScroll(); -// const { user, isAuthenticated } = useAuth(); -// const [lastScrollY, setLastScrollY] = useState(0); -// const { course, isHeaderVisible, setIsHeaderVisible, lecture } = -// useContext(CourseDetailContext); -// useEffect(() => { -// const updateHeader = () => { -// const current = scrollY.get(); -// const direction = current > lastScrollY ? "down" : "up"; - -// if (direction === "down" && current > 100) { -// setIsHeaderVisible(false); -// } else if (direction === "up") { -// setIsHeaderVisible(true); -// } - -// setLastScrollY(current); -// }; - -// // 使用 requestAnimationFrame 来优化性能 -// const unsubscribe = scrollY.on("change", () => { -// requestAnimationFrame(updateHeader); -// }); - -// return () => { -// unsubscribe(); -// }; -// }, [lastScrollY, scrollY, setIsHeaderVisible]); - -// return ( -// -//
-//
-//

{course?.title}

-//
- -// {isAuthenticated ? ( -// } -// trigger={["click"]} -// placement="bottomRight"> -// -// {(user?.showname || -// user?.username || -// "")[0]?.toUpperCase()} -// -// -// ) : ( -// -// )} -//
-//
-// ); -// }; - -// export default CourseDetailHeader; diff --git a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx index 0fd8040..88571a3 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -99,10 +99,10 @@ export function CourseFormProvider({ }), }, terms: { - connect: termIds.map((id) => ({ id })), // 转换成 connect 格式 + set: termIds.map((id) => ({ id })), // 转换成 connect 格式 }, depts: { - connect: deptIds.map((id) => ({ id })), + set: deptIds.map((id) => ({ id })), }, }; // 删除原始的 taxonomy 字段 diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx index 26282d2..a153137 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx @@ -83,7 +83,7 @@ export const SortableLecture: React.FC = ({ : undefined, }, resources: { - connect: [videoUrlId, ...fileIds] + set: [videoUrlId, ...fileIds] .filter(Boolean) .map((fileId) => ({ fileId, @@ -109,7 +109,7 @@ export const SortableLecture: React.FC = ({ : undefined, }, resources: { - connect: [videoUrlId, ...fileIds] + set: [videoUrlId, ...fileIds] .filter(Boolean) .map((fileId) => ({ fileId, From ed85a700a4d5213c040dec39116b1d418c2a98b6 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 22:38:28 +0800 Subject: [PATCH 09/28] add --- .../main/courses/components/CourseCard.tsx | 13 +++- .../course/detail/CourseDetailDescription.tsx | 74 +++++++++++-------- .../course/editor/form/CourseBasicForm.tsx | 2 +- .../CourseContentForm/SortableLecture.tsx | 4 +- 4 files changed, 55 insertions(+), 38 deletions(-) diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 3dd5032..acb4e9d 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -1,5 +1,6 @@ import { Card, Tag, Typography, Button } from "antd"; import { + BookOutlined, EyeOutlined, PlayCircleOutlined, TeamOutlined, @@ -73,10 +74,10 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) { -
+
- + {course?.depts?.length > 1 ? `${course.depts[0].name}等` : course?.depts?.[0]?.name} @@ -84,10 +85,16 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) { {/* {course?.depts?.map((dept)=>{return dept.name})} */}
+
+
- + {`观看次数 ${course?.meta?.views || 0}`} + + + {`学习人数 ${course?.studentIds?.length || 0}`} +
- - )} - {isAuthenticated ? ( - - ) : ( - - )} +
+
+
navigate("/")} + className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent hover:scale-105 transition-transform cursor-pointer"> + 烽火慕课
+
- +
+
+ + } + placeholder="搜索课程" + className="w-72 rounded-full" + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} + onPressEnter={(e) => { + if ( + !window.location.pathname.startsWith( + "/courses/" + ) + ) { + navigate(`/courses/`); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + } + }} + /> +
+ {isAuthenticated && ( + <> + + + )} + { + isAuthenticated && + } + {isAuthenticated ? ( + + ) : ( + + )} +
+
); } diff --git a/apps/web/src/app/main/layout/MainLayout.tsx b/apps/web/src/app/main/layout/MainLayout.tsx index b573e2f..f5b462b 100755 --- a/apps/web/src/app/main/layout/MainLayout.tsx +++ b/apps/web/src/app/main/layout/MainLayout.tsx @@ -9,13 +9,13 @@ const { Content } = Layout; export function MainLayout() { return ( - +
- + - +
); } diff --git a/apps/web/src/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx index 53efdb3..66dc35b 100755 --- a/apps/web/src/app/main/layout/NavigationMenu.tsx +++ b/apps/web/src/app/main/layout/NavigationMenu.tsx @@ -4,7 +4,7 @@ import { useNavigate, useLocation } from "react-router-dom"; const menuItems = [ { key: "home", path: "/", label: "首页" }, { key: "courses", path: "/courses", label: "全部课程" }, - { key: "paths", path: "/paths", label: "学习路径" }, + { key: "path", path: "/path", label: "学习路径" }, ]; export const NavigationMenu = () => { diff --git a/apps/web/src/app/main/path/components/PathCard.tsx b/apps/web/src/app/main/path/components/PathCard.tsx new file mode 100755 index 0000000..8ccb01f --- /dev/null +++ b/apps/web/src/app/main/path/components/PathCard.tsx @@ -0,0 +1,94 @@ +import { Card, Rate, Tag, Typography, Button } from "antd"; +import { + PlayCircleOutlined, + TeamOutlined, +} from "@ant-design/icons"; +import { PostDto, TaxonomySlug } from "@nice/common"; +import { useNavigate } from "react-router-dom"; +interface pathCardProps { + path: PostDto; +} +const { Title, Text } = Typography; +export default function PathCard({ path }: pathCardProps) { + const navigate = useNavigate(); + const handleClick = (path: PostDto) => { + navigate(`/path/editor/${path.id}`); + window.scrollTo({ top: 0, behavior: "smooth", }) + }; + return ( + handleClick(path)} + key={path.id} + hoverable + className="group overflow-hidden rounded-xl border border-gray-200 bg-white + shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" + cover={ +
+
+ {/*
*/} +
+ }> +
+
+ {path?.terms?.map((term) => { + return ( + + {term.name} + + ); + })} +
+ + + <button> {path.title}</button> + + +
+ +
+ + {path?.depts?.length > 1 + ? `${path.depts[0].name}等` + : path?.depts?.[0]?.name} + {/* {path?.depts?.map((dept) => {return dept.name.length > 1 ?`${dept.name.slice}等`: dept.name})} */} + {/* {path?.depts?.map((dept)=>{return dept.name})} */} + +
+ + {path?.meta?.views + ? `观看次数 ${path?.meta?.views}` + : null} + +
+
+ +
+
+ + ); +} diff --git a/apps/web/src/app/main/path/components/PathFilter.tsx b/apps/web/src/app/main/path/components/PathFilter.tsx new file mode 100755 index 0000000..01442c8 --- /dev/null +++ b/apps/web/src/app/main/path/components/PathFilter.tsx @@ -0,0 +1,44 @@ + +import { api } from "@nice/client"; +import { useMainContext } from "../../layout/MainProvider"; +import TermParentSelector from "@web/src/components/models/term/term-parent-selector"; + +export default function PathFilter() { + const { data: taxonomies } = api.taxonomy.getAll.useQuery({}); + const { selectedTerms, setSelectedTerms } = useMainContext(); + const handleTermChange = (slug: string, selected: string[]) => { + setSelectedTerms({ + ...selectedTerms, + [slug]: selected, // 更新对应 slug 的选择 + }); + }; + return ( +
+ {taxonomies?.map((tax, index) => { + const items = Object.entries(selectedTerms).find( + ([key, items]) => key === tax.slug + )?.[1]; + return ( +
+

+ {tax?.name} +

+ + handleTermChange( + tax?.slug, + selected as string[] + ) + } + taxonomyId={tax?.id} + > + +
+ ); + })} +
+ ); +} diff --git a/apps/web/src/app/main/path/components/PathListContainer.tsx b/apps/web/src/app/main/path/components/PathListContainer.tsx new file mode 100644 index 0000000..125d9e8 --- /dev/null +++ b/apps/web/src/app/main/path/components/PathListContainer.tsx @@ -0,0 +1,54 @@ +import PostList from "@web/src/components/models/course/list/PostList"; +import { useMainContext } from "../../layout/MainProvider"; +import { PostType, Prisma } from "@nice/common"; +import { useMemo } from "react"; +import PathCard from "./PathCard"; + +export function PathListContainer() { + const { searchValue, selectedTerms } = useMainContext(); + const termFilters = useMemo(() => { + return Object.entries(selectedTerms) + .filter(([, terms]) => terms.length > 0) + .map(([, terms]) => terms); + }, [selectedTerms]); + const searchCondition: Prisma.StringNullableFilter = { + contains: searchValue, + mode: "insensitive" as Prisma.QueryMode, // 使用类型断言 + }; + return ( + <> + } + params={{ + pageSize: 12, + where: { + type: PostType.PATH, + AND: termFilters.map((termFilter) => ({ + terms: { + some: { + id: { + in: termFilter, // 确保至少有一个 term.id 在当前 termFilter 中 + }, + }, + }, + })), + OR: [ + { title: searchCondition }, + { subTitle: searchCondition }, + { content: searchCondition }, + { + terms: { + some: { + name: searchCondition, + }, + }, + }, + ], + }, + }} + cols={4}> + + ); +} + +export default PathListContainer; diff --git a/apps/web/src/app/main/path/editor/page.tsx b/apps/web/src/app/main/path/editor/page.tsx new file mode 100644 index 0000000..bffdec2 --- /dev/null +++ b/apps/web/src/app/main/path/editor/page.tsx @@ -0,0 +1,10 @@ +import MindEditor from "@web/src/components/common/editor/MindEditor"; +import { useParams } from "react-router-dom"; + +export default function PathEditorPage() { + const { id } = useParams(); + + return
+ +
+} diff --git a/apps/web/src/app/main/path/layout/PathListLayout.tsx b/apps/web/src/app/main/path/layout/PathListLayout.tsx new file mode 100644 index 0000000..19ebe94 --- /dev/null +++ b/apps/web/src/app/main/path/layout/PathListLayout.tsx @@ -0,0 +1,20 @@ +import PathFilter from "../components/PathFilter"; +import PathListContainer from "../components/PathListContainer"; + +export function PathListLayout() { + return ( + <> +
+
+
+ +
+
+ +
+
+
+ + ); +} +export default PathListLayout; diff --git a/apps/web/src/app/main/path/page.tsx b/apps/web/src/app/main/path/page.tsx new file mode 100755 index 0000000..825f3f7 --- /dev/null +++ b/apps/web/src/app/main/path/page.tsx @@ -0,0 +1,5 @@ +import PathListLayout from "./layout/PathListLayout"; + +export default function PathPage() { + return +} diff --git a/apps/web/src/app/main/paths/page.tsx b/apps/web/src/app/main/paths/page.tsx deleted file mode 100755 index 9624011..0000000 --- a/apps/web/src/app/main/paths/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import MindEditor from "@web/src/components/common/editor/MindEditor"; - -export default function PathsPage() { - // return ; - return <>123 -} diff --git a/apps/web/src/components/common/editor/MindEditor.tsx b/apps/web/src/components/common/editor/MindEditor.tsx index cd06b8e..dd6306e 100755 --- a/apps/web/src/components/common/editor/MindEditor.tsx +++ b/apps/web/src/components/common/editor/MindEditor.tsx @@ -1,27 +1,197 @@ -import { MindElixirInstance } from "mind-elixir"; -import { useRef, useEffect } from "react"; -import MindElixir from "mind-elixir"; +import { Button, Card, Empty, Form, Space, Spin, message, theme } from 'antd'; +import NodeMenu from './NodeMenu'; +import { useEntity, api, usePost } from '@nice/client'; +import { ObjectType, postDetailSelect, PostDto, PostType, Prisma, Taxonomy } from '@nice/common'; +import TermSelect from '../../models/term/term-select'; +import DepartmentSelect from '../../models/department/department-select'; +import { useEffect, useRef, useState } from 'react'; +import toast from 'react-hot-toast'; +import { MindElixirInstance } from 'mind-elixir'; +import MindElixir from 'mind-elixir'; +import { useTusUpload } from '@web/src/hooks/useTusUpload'; +import { useNavigate } from 'react-router-dom'; +const MIND_OPTIONS = { + direction: MindElixir.SIDE, + draggable: true, + contextMenu: true, + toolBar: true, + nodeMenu: true, + keypress: true, + locale: 'zh_CN' as const, + theme: { + name: 'Latte', + palette: [ + '#dd7878', + '#ea76cb', + '#8839ef', + '#e64553', + '#fe640b', + '#df8e1d', + '#40a02b', + '#209fb5', + '#1e66f5', + '#7287fd', + ], + cssVar: { + '--main-color': '#444446', + '--main-bgcolor': '#ffffff', + '--color': '#777777', + '--bgcolor': '#f6f6f6', + '--panel-color': '#444446', + '--panel-bgcolor': '#ffffff', + '--panel-border-color': '#eaeaea', + }, + } +}; +export default function MindEditor({ id }: { id?: string }) { + const containerRef = useRef(null); + const [instance, setInstance] = useState(null); -export default function MindEditor() { - const me = useRef(); + const { data: post, isLoading }: { data: PostDto, isLoading: boolean } = api.post.findFirst.useQuery({ + where: { + id + }, + select: postDetailSelect + }) + const navigate = useNavigate() + const { create, update } = usePost(); + const { data: taxonomies } = api.taxonomy.getAll.useQuery({ + type: ObjectType.COURSE, + }); + const { handleFileUpload } = useTusUpload() + const [form] = Form.useForm() useEffect(() => { - const instance = new MindElixir({ - el: "#map", - direction: MindElixir.SIDE, - draggable: true, // default true - contextMenu: true, // default true - toolBar: true, // default true - nodeMenu: true, // default true - keypress: true, // default true - locale: "zh_CN", + if (post && form && instance && id) { + console.log(post) + instance.refresh((post as any).meta) + const deptIds = (post?.depts || [])?.map((dept) => dept.id); + const formData = { + title: post.title, + deptIds: deptIds, + }; + post.terms?.forEach((term) => { + formData[term.taxonomyId] = term.id; // 假设 taxonomyName 是您在 Form.Item 中使用的 name + }); + form.setFieldsValue(formData); + } + }, [post, form, instance, id]); + + useEffect(() => { + if (!containerRef.current) return; + const mind = new MindElixir({ + ...MIND_OPTIONS, + el: containerRef.current, }); - // instance.install(NodeMenu); - instance.init(MindElixir.new("新主题")); - me.current = instance; + mind.init(MindElixir.new('新学习路径')); + containerRef.current.hidden = true; + setInstance(mind); }, []); + useEffect(() => { + if ((!id || post) && instance) { + containerRef.current.hidden = false + instance.toCenter() + instance.refresh((post as any)?.meta) + } + }, [id, post, instance]) + const handleSave = async () => { + if (!instance) return; + const values = form.getFieldsValue() + const imgBlob = await instance?.exportPng() + handleFileUpload(imgBlob, async (result) => { + const termIds = taxonomies.map((tax) => values[tax.id]).filter((id) => id); + const deptIds = (values?.deptIds || []) as string[]; + const { theme, ...data } = instance.getData(); + try { + if (post && id) { + const params: Prisma.PostUpdateArgs = { + where: { + id + }, + data: { + title: data.nodeData.topic, + meta: { ...data, thumbnail: result.compressedUrl }, + terms: { + set: termIds.map((id) => ({ id })) + }, + depts: { + set: deptIds.map((id) => ({ id })), + }, + updatedAt: new Date() + } + } + await update.mutateAsync(params); + toast.success('更新成功'); + } else { + const params: Prisma.PostCreateInput = { + type: PostType.PATH, + title: data.nodeData.topic, + meta: { ...data, thumbnail: result.compressedUrl }, + terms: { + connect: termIds.map((id) => ({ id })) + }, + depts: { + connect: deptIds.map((id) => ({ id })), + }, + updatedAt: new Date() + }; + + const res = await create.mutateAsync({ data: params }); + navigate(`/path/editor/${res.id}`, { replace: true }) + toast.success('创建成功'); + } + } catch (error) { + toast.error('保存失败'); + throw error; + } + console.log(result) + }, (error) => { }, `mind-thumb-${new Date().toString()}`) + + + }; return ( -
-
+
+ {taxonomies && ( +
{ + console.log(values) + }} form={form} className=' bg-white p-2 '> +
+
+ {taxonomies.map((tax, index) => ( + + + + + ))} + + + +
+ +
+
+ ) + } +
+ {instance && ( + )} + {isLoading &&
+ +
} + {!post && id && !isLoading &&
+ +
}
); } diff --git a/apps/web/src/components/common/editor/NodeMenu.tsx b/apps/web/src/components/common/editor/NodeMenu.tsx new file mode 100644 index 0000000..a73603f --- /dev/null +++ b/apps/web/src/components/common/editor/NodeMenu.tsx @@ -0,0 +1,195 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Input, Button, ColorPicker, Select } from 'antd'; +import { + FontSizeOutlined, + BoldOutlined, + LinkOutlined, +} from '@ant-design/icons'; +import type { MindElixirInstance, NodeObj } from 'mind-elixir'; + +const xmindColorPresets = [ + // 经典16色 + '#FFFFFF', '#F5F5F5', // 白色系 + '#2196F3', '#1976D2', // 蓝色系 + '#4CAF50', '#388E3C', // 绿色系 + '#FF9800', '#F57C00', // 橙色系 + '#F44336', '#D32F2F', // 红色系 + '#9C27B0', '#7B1FA2', // 紫色系 + '#424242', '#757575', // 灰色系 + '#FFEB3B', '#FBC02D' // 黄色系 +]; + +interface NodeMenuProps { + mind: MindElixirInstance; +} + +const NodeMenu: React.FC = ({ mind }) => { + const [isOpen, setIsOpen] = useState(false); + const [selectedFontColor, setSelectedFontColor] = useState(''); + const [selectedBgColor, setSelectedBgColor] = useState(''); + const [selectedSize, setSelectedSize] = useState(''); + const [isBold, setIsBold] = useState(false); + const [url, setUrl] = useState(''); + const containerRef = useRef(null); + + useEffect(() => { + const handleSelectNode = (nodeObj: NodeObj) => { + setIsOpen(true); + + const style = nodeObj.style || {}; + setSelectedFontColor(style.color || ''); + setSelectedBgColor(style.background || ''); + + setSelectedSize(style.fontSize || '24'); + setIsBold(style.fontWeight === 'bold'); + setUrl(nodeObj.hyperLink || ''); + }; + + const handleUnselectNode = () => { + setIsOpen(false); + }; + + mind.bus.addListener('selectNode', handleSelectNode); + mind.bus.addListener('unselectNode', handleUnselectNode); + + }, [mind]); + + useEffect(() => { + if (containerRef.current && mind.container) { + mind.container.appendChild(containerRef.current); + } + + }, [mind.container]); + + const handleColorChange = (type: "font" | "background", color: string) => { + if (type === 'font') { + setSelectedFontColor(color); + } else { + setSelectedBgColor(color); + } + const patch = { style: {} as any }; + if (type === 'font') { + patch.style.color = color; + } else { + patch.style.background = color; + } + mind.reshapeNode(mind.currentNode, patch); + }; + + const handleSizeChange = (size: string) => { + setSelectedSize(size); + mind.reshapeNode(mind.currentNode, { style: { fontSize: size } }); + }; + + const handleBoldToggle = () => { + const fontWeight = isBold ? '' : 'bold'; + setIsBold(!isBold); + mind.reshapeNode(mind.currentNode, { style: { fontWeight } }); + }; + + const handleUrlChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setUrl(value); + mind.reshapeNode(mind.currentNode, { hyperLink: value }); + }; + + return ( +
+
+ {/* Font Size Selector */} +
+

文字样式

+
+ } + /> + {url && !/^https?:\/\/\S+$/.test(url) && ( +

请输入有效的URL地址

+ )} +
+
+
+ ); +}; + +export default NodeMenu; diff --git a/apps/web/src/components/common/editor/i18n.ts b/apps/web/src/components/common/editor/i18n.ts new file mode 100644 index 0000000..ad60d5f --- /dev/null +++ b/apps/web/src/components/common/editor/i18n.ts @@ -0,0 +1,152 @@ +interface I18n { + addChild: string + addParent: string + addSibling: string + removeNode: string + focus: string + cancelFocus: string + moveUp: string + moveDown: string + link: string + clickTips: string + font: string + background: string + tag: string + icon: string + tagsSeparate: string + iconsSeparate: string + url: string + memo?: string +} + +const cn: I18n = { + addChild: '插入子节点', + addParent: '插入父节点', + addSibling: '插入同级节点', + removeNode: '删除节点', + focus: '专注', + cancelFocus: '取消专注', + moveUp: '上移', + moveDown: '下移', + link: '连接', + clickTips: '请点击目标节点', + font: '文字', + background: '背景', + tag: '标签', + icon: '图标', + tagsSeparate: '多个标签半角逗号分隔', + iconsSeparate: '多个图标半角逗号分隔', + url: 'URL', +} + +interface I18nCollection { + cn: I18n + zh_CN: I18n + zh_TW: I18n + en: I18n + ru: I18n + ja: I18n + pt: I18n +} + +const i18n: I18nCollection = { + cn, + zh_CN: cn, + zh_TW: { + addChild: '插入子節點', + addParent: '插入父節點', + addSibling: '插入同級節點', + removeNode: '刪除節點', + focus: '專注', + cancelFocus: '取消專注', + moveUp: '上移', + moveDown: '下移', + link: '連接', + clickTips: '請點擊目標節點', + font: '文字', + background: '背景', + tag: '標簽', + icon: '圖標', + tagsSeparate: '多個標簽半角逗號分隔', + iconsSeparate: '多個圖標半角逗號分隔', + url: 'URL', + }, + en: { + addChild: 'Add child', + addParent: 'Add parent', + addSibling: 'Add sibling', + removeNode: 'Remove node', + focus: 'Focus Mode', + cancelFocus: 'Cancel Focus Mode', + moveUp: 'Move up', + moveDown: 'Move down', + link: 'Link', + clickTips: 'Please click the target node', + font: 'Font', + background: 'Background', + tag: 'Tag', + icon: 'Icon', + tagsSeparate: 'Separate tags by comma', + iconsSeparate: 'Separate icons by comma', + url: 'URL', + }, + ru: { + addChild: 'Добавить дочерний элемент', + addParent: 'Добавить родительский элемент', + addSibling: 'Добавить на этом уровне', + removeNode: 'Удалить узел', + focus: 'Режим фокусировки', + cancelFocus: 'Отменить режим фокусировки', + moveUp: 'Поднять выше', + moveDown: 'Опустить ниже', + link: 'Ссылка', + clickTips: 'Пожалуйста, нажмите на целевой узел', + font: 'Цвет шрифта', + background: 'Цвет фона', + tag: 'Тег', + icon: 'Иконка', + tagsSeparate: 'Разделяйте теги запятой', + iconsSeparate: 'Разделяйте иконки запятой', + url: 'URL', + }, + ja: { + addChild: '子ノードを追加する', + addParent: '親ノードを追加します', + addSibling: '兄弟ノードを追加する', + removeNode: 'ノードを削除', + focus: '集中', + cancelFocus: '集中解除', + moveUp: '上へ移動', + moveDown: '下へ移動', + link: 'コネクト', + clickTips: 'ターゲットノードをクリックしてください', + font: 'フォント', + background: 'バックグラウンド', + tag: 'タグ', + icon: 'アイコン', + tagsSeparate: '複数タグはカンマ区切り', + iconsSeparate: '複数アイコンはカンマ区切り', + url: 'URL', + }, + pt: { + addChild: 'Adicionar item filho', + addParent: 'Adicionar item pai', + addSibling: 'Adicionar item irmao', + removeNode: 'Remover item', + focus: 'Modo Foco', + cancelFocus: 'Cancelar Modo Foco', + moveUp: 'Mover para cima', + moveDown: 'Mover para baixo', + link: 'Link', + clickTips: 'Favor clicar no item alvo', + font: 'Fonte', + background: 'Cor de fundo', + tag: 'Tag', + icon: 'Icone', + tagsSeparate: 'Separe tags por virgula', + iconsSeparate: 'Separe icones por virgula', + url: 'URL', + }, +} + +export default i18n \ No newline at end of file diff --git a/apps/web/src/components/common/editor/types.ts b/apps/web/src/components/common/editor/types.ts new file mode 100644 index 0000000..3810ba7 --- /dev/null +++ b/apps/web/src/components/common/editor/types.ts @@ -0,0 +1,14 @@ +import { MindElixirInstance, MindElixirData } from 'mind-elixir'; +import { PostType, ObjectType } from '@nice/common'; + +export interface MindEditorProps { + initialData?: MindElixirData; + onSave?: (data: MindElixirData) => Promise; + taxonomyType?: ObjectType; +} + +export interface MindEditorState { + instance: MindElixirInstance | null; + isSaving: boolean; + error: Error | null; +} diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx index 1811135..17e2685 100755 --- a/apps/web/src/components/common/uploader/TusUploader.tsx +++ b/apps/web/src/components/common/uploader/TusUploader.tsx @@ -40,7 +40,6 @@ export const TusUploader = ({ })) || [] ); const [uploadResults, setUploadResults] = useState(value || []); - const handleRemoveFile = useCallback( (fileId: string) => { setCompletedFiles((prev) => diff --git a/apps/web/src/components/models/course/list/CourseList.tsx b/apps/web/src/components/models/course/list/PostList.tsx similarity index 80% rename from apps/web/src/components/models/course/list/CourseList.tsx rename to apps/web/src/components/models/course/list/PostList.tsx index 3ba3eaf..4944e60 100755 --- a/apps/web/src/components/models/course/list/CourseList.tsx +++ b/apps/web/src/components/models/course/list/PostList.tsx @@ -1,10 +1,9 @@ import { Pagination, Empty, Skeleton } from "antd"; -import CourseCard from "../../../../app/main/courses/components/CourseCard"; import { courseDetailSelect, CourseDto, Prisma } from "@nice/common"; import { api } from "@nice/client"; import { DefaultArgs } from "@prisma/client/runtime/library"; import { useEffect, useMemo, useState } from "react"; -interface CourseListProps { +interface PostListProps { params?: { page?: number; pageSize?: number; @@ -13,21 +12,24 @@ interface CourseListProps { }; cols?: number; showPagination?: boolean; + renderItem: (post: any) => React.ReactNode } -interface CoursesPagnationProps { +interface PostPagnationProps { data: { items: CourseDto[]; totalPages: number; }; isLoading: boolean; + } -export default function CourseList({ +export default function PostList({ params, cols = 3, showPagination = true, -}: CourseListProps) { + renderItem +}: PostListProps) { const [currentPage, setCurrentPage] = useState(params?.page || 1); - const { data, isLoading }: CoursesPagnationProps = + const { data, isLoading }: PostPagnationProps = api.post.findManyWithPagination.useQuery({ select: courseDetailSelect, ...params, @@ -40,7 +42,7 @@ export default function CourseList({ return 1; }, [data, isLoading]); - const courses = useMemo(() => { + const posts = useMemo(() => { if (data && !isLoading) { return data?.items; } @@ -59,15 +61,15 @@ export default function CourseList({ } return (
- {courses.length > 0 ? ( + {posts.length > 0 ? ( <>
{isLoading ? ( ) : ( - courses.map((course) => ( - - )) + posts.map((post) =>
+ {renderItem(post)} +
) )}
{showPagination && ( @@ -83,7 +85,10 @@ export default function CourseList({ )} ) : ( - +
+ + +
)}
); diff --git a/apps/web/src/hooks/useTusUpload.ts b/apps/web/src/hooks/useTusUpload.ts index 3f6d038..0337110 100755 --- a/apps/web/src/hooks/useTusUpload.ts +++ b/apps/web/src/hooks/useTusUpload.ts @@ -15,7 +15,7 @@ export function useTusUpload() { >({}); const [isUploading, setIsUploading] = useState(false); const [uploadError, setUploadError] = useState(null); - + const getFileId = (url: string) => { const parts = url.split("/"); const uploadIndex = parts.findIndex((part) => part === "upload"); @@ -34,7 +34,7 @@ export function useTusUpload() { return resUrl; }; const handleFileUpload = async ( - file: File, + file: File | Blob, onSuccess: (result: UploadResult) => void, onError: (error: Error) => void, fileKey: string // 添加文件唯一标识 @@ -45,14 +45,24 @@ export function useTusUpload() { setUploadError(null); try { + // 如果是Blob,需要转换为File + let fileName = "uploaded-file"; + if (file instanceof Blob && !(file instanceof File)) { + // 根据MIME类型设置文件扩展名 + const extension = file.type.split('/')[1]; + fileName = `uploaded-file.${extension}`; + } + const uploadFile = file instanceof Blob && !(file instanceof File) + ? new File([file], fileName, { type: file.type }) + : file as File; console.log(`http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`); - const upload = new tus.Upload(file, { + const upload = new tus.Upload(uploadFile, { endpoint: `http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`, retryDelays: [0, 1000, 3000, 5000], metadata: { - filename: file.name, - filetype: file.type, - size: file.size as any, + filename: uploadFile.name, + filetype: uploadFile.type, + size: uploadFile.size as any, }, onProgress: (bytesUploaded, bytesTotal) => { const progress = Number( diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 9ad7f65..f65bd8b 100755 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -14,6 +14,7 @@ border-bottom-right-radius: 8px; border: none; } + .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before { content: "标题 1"; @@ -23,6 +24,7 @@ .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before { content: "标题 2"; } + .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before { content: "标题 3"; @@ -32,6 +34,7 @@ .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before { content: "标题 4"; } + .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before, .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before { content: "标题 5"; @@ -41,11 +44,13 @@ .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before { content: "标题 6"; } + /* 针对下拉菜单中的选项 */ .ql-snow .ql-picker.ql-header .ql-picker-item:not([data-value])::before, .ql-snow .ql-picker.ql-header .ql-picker-label:not([data-value])::before { content: "正文" !important; } + .ag-custom-dragging-class { @apply border-b-2 border-blue-200; } @@ -76,11 +81,11 @@ background-color: transparent !important; } -.ant-table-thead > tr > th { +.ant-table-thead>tr>th { background-color: transparent !important; } -.ant-table-tbody > tr > td { +.ant-table-tbody>tr>td { background-color: transparent !important; border-bottom-color: transparent !important; } @@ -117,9 +122,7 @@ } /* 覆盖 Ant Design 的默认样式 raido button左侧的按钮*/ -.ant-radio-button-wrapper-checked:not( - .ant-radio-button-wrapper-disabled - )::before { +.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)::before { background-color: unset !important; } @@ -132,7 +135,7 @@ display: none !important; } -.no-wrap-header .ant-table-thead > tr > th { +.no-wrap-header .ant-table-thead>tr>th { white-space: nowrap; } @@ -148,17 +151,17 @@ /* 设置单元格边框 */ } -.custom-table .ant-table-tbody > tr > td { +.custom-table .ant-table-tbody>tr>td { border-bottom: 1px solid #ddd; /* 设置表格行底部边框 */ } -.custom-table .ant-table-tbody > tr:last-child > td { +.custom-table .ant-table-tbody>tr:last-child>td { border-bottom: none; /* 去除最后一行的底部边框 */ } -#map { - height: 600px; +.mind-editor { + height: calc(100vh - 285px); width: 100%; -} +} \ No newline at end of file diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 7592489..ab52f2c 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -15,9 +15,10 @@ import CourseContentForm from "../components/models/course/editor/form/CourseCon import CourseEditorLayout from "../components/models/course/editor/layout/CourseEditorLayout"; import { MainLayout } from "../app/main/layout/MainLayout"; import CoursesPage from "../app/main/courses/page"; -import PathsPage from "../app/main/paths/page"; +import PathPage from "../app/main/path/page"; import { adminRoute } from "./admin-route"; -import { CoursePreview } from "../app/main/course/preview/page"; +import PathEditorPage from "../app/main/path/editor/page"; + interface CustomIndexRouteObject extends IndexRouteObject { name?: string; breadcrumb?: string; @@ -56,8 +57,17 @@ export const routes: CustomRouteObject[] = [ element: , }, { - path: "paths", - element: , + path: "path", + children: [ + { + index: true, + element: , + }, + { + path: "editor/:id?", + element: + } + ] }, { path: "courses", @@ -103,12 +113,7 @@ export const routes: CustomRouteObject[] = [ ), }, - // { - // path: "setting", - // element: ( - // - // ), - // }, + ], }, { diff --git a/config/nginx/conf.d/web.conf b/config/nginx/conf.d/web.conf index e0ac769..67302b8 100755 --- a/config/nginx/conf.d/web.conf +++ b/config/nginx/conf.d/web.conf @@ -100,7 +100,7 @@ server { # 仅供内部使用 internal; # 代理到认证服务 - proxy_pass http://host.docker.internal:/auth/file; + proxy_pass http://host.docker.internal:3000/auth/file; # 请求优化:不传递请求体 proxy_pass_request_body off; diff --git a/package.json b/package.json index 8a85a11..8156253 100755 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "pnpm run --parallel dev", - "db:clear": "pnpm --filter common run db:clear" + "db:clear": "pnpm --filter common run db:clear", + "studio": "pnpm --filter common run studio" }, "keywords": [], "author": "insiinc", diff --git a/packages/client/src/api/hooks/index.ts b/packages/client/src/api/hooks/index.ts index 896a883..73b516d 100755 --- a/packages/client/src/api/hooks/index.ts +++ b/packages/client/src/api/hooks/index.ts @@ -9,4 +9,4 @@ export * from "./useTaxonomy" export * from "./useVisitor" export * from "./useMessage" export * from "./usePost" -// export * from "./useCourse" \ No newline at end of file +export * from "./useEntity" diff --git a/packages/client/src/api/hooks/usePost.ts b/packages/client/src/api/hooks/usePost.ts index 41e0063..88c6326 100755 --- a/packages/client/src/api/hooks/usePost.ts +++ b/packages/client/src/api/hooks/usePost.ts @@ -1,12 +1,5 @@ -import { useQueryClient } from "@tanstack/react-query"; -import { getQueryKey } from "@trpc/react-query"; -import { MutationResult, useEntity } from "./useEntity"; -import { ObjectType } from "@nice/common"; -import { api } from "../trpc"; -import { CrudOperation, emitDataChange } from "../../event"; -export function usePost() { - const queryClient = useQueryClient(); - const queryKey = getQueryKey(api.post); +import { MutationResult, useEntity } from "./useEntity"; +export function usePost() { return useEntity("post"); } diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 1c245e2..de09e6a 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -201,7 +201,6 @@ model Post { order Float? @default(0) @map("order") duration Int? rating Int? @default(0) - depts Department[] @relation("post_dept") // 索引 // 日期时间类型字段 diff --git a/packages/common/src/models/post.ts b/packages/common/src/models/post.ts index 0d2bf07..0db6919 100755 --- a/packages/common/src/models/post.ts +++ b/packages/common/src/models/post.ts @@ -8,6 +8,7 @@ import { } from "@prisma/client"; import { StaffDto } from "./staff"; import { TermDto } from "./term"; +import { DepartmentDto } from "./department"; export type PostComment = { id: string; @@ -40,6 +41,8 @@ export type PostDto = Post & { }; watchableDepts: Department[]; watchableStaffs: Staff[]; + terms:TermDto[] + depts:DepartmentDto[] }; export type LectureMeta = { diff --git a/packages/common/src/models/select.ts b/packages/common/src/models/select.ts index a591134..31646dc 100755 --- a/packages/common/src/models/select.ts +++ b/packages/common/src/models/select.ts @@ -9,6 +9,20 @@ export const postDetailSelect: Prisma.PostSelect = { watchableDepts: true, watchableStaffs: true, updatedAt: true, + terms: { + select: { + id: true, + name: true, + taxonomyId: true, + taxonomy: { + select: { + id: true, + slug: true, + } + } + } + }, + depts: true, author: { select: { id: true, @@ -28,6 +42,7 @@ export const postDetailSelect: Prisma.PostSelect = { }, }, }, + meta: true }; export const postUnDetailSelect: Prisma.PostSelect = { id: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73c6a31..87760d6 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,6 +284,9 @@ importers: '@hookform/resolvers': specifier: ^3.9.1 version: 3.10.0(react-hook-form@7.54.2(react@18.2.0)) + '@mind-elixir/node-menu': + specifier: workspace:* + version: link:../../packages/mind-node-menu '@nice/client': specifier: workspace:^ version: link:../../packages/client @@ -706,6 +709,18 @@ importers: specifier: ^3.5.1 version: 3.5.2(vite@4.5.9(@types/node@20.17.12)(less@4.2.2)(terser@5.37.0)) + packages/mind-node-menu: + devDependencies: + less: + specifier: ^4.1.3 + version: 4.2.2 + mind-elixir: + specifier: workspace:^ + version: link:../mind-elixir-core + vite: + specifier: ^3.0.0 + version: 3.2.11(@types/node@20.17.12)(less@4.2.2)(terser@5.37.0) + packages/template: devDependencies: '@types/node': @@ -1460,6 +1475,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm@0.15.18': + resolution: {integrity: sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -1622,6 +1643,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.15.18': + resolution: {integrity: sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.18.20': resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -4547,6 +4574,131 @@ packages: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} + esbuild-android-64@0.15.18: + resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + esbuild-android-arm64@0.15.18: + resolution: {integrity: sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + esbuild-darwin-64@0.15.18: + resolution: {integrity: sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + esbuild-darwin-arm64@0.15.18: + resolution: {integrity: sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + esbuild-freebsd-64@0.15.18: + resolution: {integrity: sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + esbuild-freebsd-arm64@0.15.18: + resolution: {integrity: sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + esbuild-linux-32@0.15.18: + resolution: {integrity: sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + esbuild-linux-64@0.15.18: + resolution: {integrity: sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + esbuild-linux-arm64@0.15.18: + resolution: {integrity: sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + esbuild-linux-arm@0.15.18: + resolution: {integrity: sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + esbuild-linux-mips64le@0.15.18: + resolution: {integrity: sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + esbuild-linux-ppc64le@0.15.18: + resolution: {integrity: sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + esbuild-linux-riscv64@0.15.18: + resolution: {integrity: sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + esbuild-linux-s390x@0.15.18: + resolution: {integrity: sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + esbuild-netbsd-64@0.15.18: + resolution: {integrity: sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + esbuild-openbsd-64@0.15.18: + resolution: {integrity: sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + esbuild-sunos-64@0.15.18: + resolution: {integrity: sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + esbuild-windows-32@0.15.18: + resolution: {integrity: sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + esbuild-windows-64@0.15.18: + resolution: {integrity: sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + esbuild-windows-arm64@0.15.18: + resolution: {integrity: sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + esbuild@0.15.18: + resolution: {integrity: sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -6889,6 +7041,11 @@ packages: engines: {node: 20 || >=22} hasBin: true + rollup@2.79.2: + resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==} + engines: {node: '>=10.0.0'} + hasBin: true + rollup@3.29.5: resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -7634,6 +7791,31 @@ packages: peerDependencies: vite: '>=2.6.0' + vite@3.2.11: + resolution: {integrity: sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@4.5.9: resolution: {integrity: sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -8978,6 +9160,9 @@ snapshots: '@esbuild/android-arm64@0.24.2': optional: true + '@esbuild/android-arm@0.15.18': + optional: true + '@esbuild/android-arm@0.18.20': optional: true @@ -9059,6 +9244,9 @@ snapshots: '@esbuild/linux-ia32@0.24.2': optional: true + '@esbuild/linux-loong64@0.15.18': + optional: true + '@esbuild/linux-loong64@0.18.20': optional: true @@ -12330,6 +12518,91 @@ snapshots: dependencies: es-errors: 1.3.0 + esbuild-android-64@0.15.18: + optional: true + + esbuild-android-arm64@0.15.18: + optional: true + + esbuild-darwin-64@0.15.18: + optional: true + + esbuild-darwin-arm64@0.15.18: + optional: true + + esbuild-freebsd-64@0.15.18: + optional: true + + esbuild-freebsd-arm64@0.15.18: + optional: true + + esbuild-linux-32@0.15.18: + optional: true + + esbuild-linux-64@0.15.18: + optional: true + + esbuild-linux-arm64@0.15.18: + optional: true + + esbuild-linux-arm@0.15.18: + optional: true + + esbuild-linux-mips64le@0.15.18: + optional: true + + esbuild-linux-ppc64le@0.15.18: + optional: true + + esbuild-linux-riscv64@0.15.18: + optional: true + + esbuild-linux-s390x@0.15.18: + optional: true + + esbuild-netbsd-64@0.15.18: + optional: true + + esbuild-openbsd-64@0.15.18: + optional: true + + esbuild-sunos-64@0.15.18: + optional: true + + esbuild-windows-32@0.15.18: + optional: true + + esbuild-windows-64@0.15.18: + optional: true + + esbuild-windows-arm64@0.15.18: + optional: true + + esbuild@0.15.18: + optionalDependencies: + '@esbuild/android-arm': 0.15.18 + '@esbuild/linux-loong64': 0.15.18 + esbuild-android-64: 0.15.18 + esbuild-android-arm64: 0.15.18 + esbuild-darwin-64: 0.15.18 + esbuild-darwin-arm64: 0.15.18 + esbuild-freebsd-64: 0.15.18 + esbuild-freebsd-arm64: 0.15.18 + esbuild-linux-32: 0.15.18 + esbuild-linux-64: 0.15.18 + esbuild-linux-arm: 0.15.18 + esbuild-linux-arm64: 0.15.18 + esbuild-linux-mips64le: 0.15.18 + esbuild-linux-ppc64le: 0.15.18 + esbuild-linux-riscv64: 0.15.18 + esbuild-linux-s390x: 0.15.18 + esbuild-netbsd-64: 0.15.18 + esbuild-openbsd-64: 0.15.18 + esbuild-sunos-64: 0.15.18 + esbuild-windows-32: 0.15.18 + esbuild-windows-64: 0.15.18 + esbuild-windows-arm64: 0.15.18 + esbuild@0.18.20: optionalDependencies: '@esbuild/android-arm': 0.18.20 @@ -15038,6 +15311,10 @@ snapshots: glob: 11.0.0 package-json-from-dist: 1.0.1 + rollup@2.79.2: + optionalDependencies: + fsevents: 2.3.3 + rollup@3.29.5: optionalDependencies: fsevents: 2.3.3 @@ -15885,6 +16162,18 @@ snapshots: - supports-color - typescript + vite@3.2.11(@types/node@20.17.12)(less@4.2.2)(terser@5.37.0): + dependencies: + esbuild: 0.15.18 + postcss: 8.4.49 + resolve: 1.22.10 + rollup: 2.79.2 + optionalDependencies: + '@types/node': 20.17.12 + fsevents: 2.3.3 + less: 4.2.2 + terser: 5.37.0 + vite@4.5.9(@types/node@20.17.12)(less@4.2.2)(terser@5.37.0): dependencies: esbuild: 0.18.20 From f22e55a85e755fc5ada7e011d40b31592569ce12 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 23:55:44 +0800 Subject: [PATCH 14/28] add --- .../main/home/components/CategorySection.tsx | 18 +++++++++--------- .../src/app/main/layout/UserMenu/UserMenu.tsx | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index d589d35..4c8141e 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -19,11 +19,11 @@ const CategorySection = () => { taxonomy: { slug: TaxonomySlug.CATEGORY, }, - parentId : null + parentId: null, }, take: 8, }); - const navigate = useNavigate() + const navigate = useNavigate(); const handleMouseEnter = useCallback((index: number) => { setHoveredIndex(index); @@ -33,13 +33,13 @@ const CategorySection = () => { setHoveredIndex(null); }, []); - const handleMouseClick = useCallback((categoryId:string) => { + const handleMouseClick = useCallback((categoryId: string) => { setSelectedTerms({ - [TaxonomySlug.CATEGORY] : [categoryId] - }) - navigate('/courses') - window.scrollTo({top: 0,behavior: "smooth",}) - },[]); + [TaxonomySlug.CATEGORY]: [categoryId], + }); + navigate("/courses"); + window.scrollTo({ top: 0, behavior: "smooth" }); + }, []); return (
@@ -57,7 +57,7 @@ const CategorySection = () => { {isLoading ? ( ) : ( - courseCategoriesData.map((category, index) => { + courseCategoriesData?.map((category, index) => { const categoryColor = stringToColor(category.name); const isHovered = hoveredIndex === index; diff --git a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx index e3ef21f..c25fc53 100755 --- a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx @@ -90,14 +90,14 @@ export function UserMenu() { icon: , label: "我创建的课程", action: () => { - navigate("/my/duty"); + navigate("/my-duty"); }, }, { icon: , label: "我学习的课程", action: () => { - navigate("/my/learning"); + navigate("/my-learning"); }, }, canManageAnyStaff && { From 4c13d70a28820f7d1a25a118738bef89733b164d Mon Sep 17 00:00:00 2001 From: longdayi <13477510+longdayilongdayi@user.noreply.gitee.com> Date: Thu, 27 Feb 2025 08:00:11 +0800 Subject: [PATCH 15/28] 02270800 --- .../src/app/main/courses/components/CoursesContainer.tsx | 2 +- packages/common/src/models/post.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/main/courses/components/CoursesContainer.tsx b/apps/web/src/app/main/courses/components/CoursesContainer.tsx index 47cf21a..237e8b3 100644 --- a/apps/web/src/app/main/courses/components/CoursesContainer.tsx +++ b/apps/web/src/app/main/courses/components/CoursesContainer.tsx @@ -18,7 +18,7 @@ export function CoursesContainer() { return ( <> } + renderItem={(post) => } params={{ pageSize: 12, where: { diff --git a/packages/common/src/models/post.ts b/packages/common/src/models/post.ts index bde39c9..a12c727 100755 --- a/packages/common/src/models/post.ts +++ b/packages/common/src/models/post.ts @@ -42,8 +42,12 @@ export type PostDto = Post & { }; watchableDepts: Department[]; watchableStaffs: Staff[]; - terms:TermDto[] - depts:DepartmentDto[] + terms: TermDto[] + depts: DepartmentDto[] + meta?: { + thumbnail?: string + views?: number + } }; export type LectureMeta = { From 51b37068d039ef62df92e9563d4a5d6664af14b0 Mon Sep 17 00:00:00 2001 From: longdayi <13477510+longdayilongdayi@user.noreply.gitee.com> Date: Thu, 27 Feb 2025 08:03:31 +0800 Subject: [PATCH 16/28] 02270803 --- apps/web/src/app/main/my-duty/page.tsx | 10 ++++++---- apps/web/src/app/main/my-learning/page.tsx | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/main/my-duty/page.tsx b/apps/web/src/app/main/my-duty/page.tsx index fd33761..64a529f 100644 --- a/apps/web/src/app/main/my-duty/page.tsx +++ b/apps/web/src/app/main/my-duty/page.tsx @@ -1,20 +1,22 @@ -import CourseList from "@web/src/components/models/course/list/CourseList"; +import PostList from "@web/src/components/models/course/list/PostList"; import { useAuth } from "@web/src/providers/auth-provider"; +import CourseCard from "../courses/components/CourseCard"; export default function MyDutyPage() { const { user } = useAuth(); return ( <>
- } + params={{ pageSize: 12, where: { authorId: user.id, }, }} - cols={4}> + cols={4}>
); diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx index 8807119..14278e8 100644 --- a/apps/web/src/app/main/my-learning/page.tsx +++ b/apps/web/src/app/main/my-learning/page.tsx @@ -1,12 +1,14 @@ -import CourseList from "@web/src/components/models/course/list/CourseList"; +import PostList from "@web/src/components/models/course/list/PostList"; import { useAuth } from "@web/src/providers/auth-provider"; +import CourseCard from "../courses/components/CourseCard"; export default function MyLearningPage() { const { user } = useAuth(); return ( <>
- } params={{ pageSize: 12, where: { @@ -17,7 +19,7 @@ export default function MyLearningPage() { }, }, }} - cols={4}> + cols={4}>
); From defbdbe3b5e310b0bf73a1fb80b6e2129572441f Mon Sep 17 00:00:00 2001 From: longdayi <13477510+longdayilongdayi@user.noreply.gitee.com> Date: Thu, 27 Feb 2025 08:11:52 +0800 Subject: [PATCH 17/28] 02270811 --- apps/web/src/app/main/layout/MainHeader.tsx | 118 +++++--------------- 1 file changed, 31 insertions(+), 87 deletions(-) diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx index dec3ca5..dfbf5e2 100755 --- a/apps/web/src/app/main/layout/MainHeader.tsx +++ b/apps/web/src/app/main/layout/MainHeader.tsx @@ -14,96 +14,40 @@ export function MainHeader() { return ( -
-
-
-
navigate("/")} - className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent hover:scale-105 transition-transform cursor-pointer"> - 烽火慕课 -
- -
-
-
- - } - placeholder="搜索课程" - className="w-72 rounded-full" - value={searchValue} - onChange={(e) => setSearchValue(e.target.value)} - onPressEnter={(e) => { - if ( - !window.location.pathname.startsWith( - "/courses/" - ) - ) { - navigate(`/courses/`); - window.scrollTo({ - top: 0, - behavior: "smooth", - }); - } - }} - /> -
- {isAuthenticated && ( - <> - - - )} - {isAuthenticated ? ( - - ) : ( - - )} +
+ +
+
navigate("/")} + className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent hover:scale-105 transition-transform cursor-pointer"> + 烽火慕课
-
-
- - } - placeholder="搜索课程" - className="w-72 rounded-full" - value={searchValue} - onChange={(e) => setSearchValue(e.target.value)} - onPressEnter={(e) => { - if ( - !window.location.pathname.startsWith( - "/courses/" - ) - ) { - navigate(`/courses/`); - window.scrollTo({ - top: 0, - behavior: "smooth", - }); - } - }} - /> -
+ + } + placeholder="搜索课程" + className="w-96 rounded-full" + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} + onPressEnter={(e) => { + if ( + !window.location.pathname.startsWith( + "/courses/" + ) + ) { + navigate(`/courses/`); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + } + }} + /> +
{isAuthenticated && ( <> + )} {isAuthenticated ? ( ) : ( @@ -82,62 +89,6 @@ export function MainHeader() { )}
- -
- - } - placeholder="搜索课程" - className="w-96 rounded-full" - value={searchValue} - onChange={(e) => setSearchValue(e.target.value)} - onPressEnter={(e) => { - if (!window.location.pathname.startsWith("/courses/")) { - navigate(`/courses/`); - window.scrollTo({ - top: 0, - behavior: "smooth", - }); - } - }} - /> -
- {isAuthenticated && ( - <> - - - )} - {isAuthenticated && ( - - )} - {isAuthenticated ? ( - - ) : ( - - )}
); From 3a9a7076f38802ccad2cfc162c89ff94cb32853a Mon Sep 17 00:00:00 2001 From: ditiqi Date: Thu, 27 Feb 2025 09:05:29 +0800 Subject: [PATCH 25/28] add --- .../course/detail/CourseDetailTitle.tsx | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx b/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx index 3ebc083..0b8da3b 100644 --- a/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx @@ -1,8 +1,15 @@ import { useContext } from "react"; import { CourseDetailContext } from "./CourseDetailContext"; import { useNavigate } from "react-router-dom"; -import { BookOutlined, CalendarOutlined, EditTwoTone, EyeOutlined, ReloadOutlined } from "@ant-design/icons"; +import { + BookOutlined, + CalendarOutlined, + EditTwoTone, + EyeOutlined, + ReloadOutlined, +} from "@ant-design/icons"; import dayjs from "dayjs"; +import CourseOperationBtns from "./JoinLearingButton"; export default function CourseDetailTitle() { const { @@ -19,14 +26,14 @@ export default function CourseDetailTitle() {
{course?.title}
-
+
{"创建于:"} {dayjs(course?.createdAt).format("YYYY年M月D日")}
- + {"更新于:"} {dayjs(course?.updatedAt).format("YYYY年M月D日")}
@@ -38,19 +45,7 @@ export default function CourseDetailTitle() {
{`学习人数${course?.studentIds?.length || 0}`}
- {canEdit && ( -
{ - const url = course?.id - ? `/course/${course?.id}/editor` - : "/course/editor"; - navigate(url); - }}> - - {"点击编辑课程"} -
- )} +
); From f5dce3210c720c66474d6b800cb56d7006d1a9dc Mon Sep 17 00:00:00 2001 From: ditiqi Date: Thu, 27 Feb 2025 09:05:31 +0800 Subject: [PATCH 26/28] add --- .../course/detail/JoinLearingButton.tsx | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 apps/web/src/components/models/course/detail/JoinLearingButton.tsx diff --git a/apps/web/src/components/models/course/detail/JoinLearingButton.tsx b/apps/web/src/components/models/course/detail/JoinLearingButton.tsx new file mode 100644 index 0000000..68c4243 --- /dev/null +++ b/apps/web/src/components/models/course/detail/JoinLearingButton.tsx @@ -0,0 +1,94 @@ +import { useAuth } from "@web/src/providers/auth-provider"; +import { useContext, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { CourseDetailContext } from "./CourseDetailContext"; +import { useStaff } from "@nice/client"; +import { + CheckCircleFilled, + CheckCircleOutlined, + CloseCircleFilled, + CloseCircleOutlined, + EditFilled, + EditTwoTone, + LoginOutlined, +} from "@ant-design/icons"; + +export default function CourseOperationBtns() { + const { id } = useParams(); + const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } = + useAuth(); + const navigate = useNavigate(); + const { course, canEdit, userIsLearning } = useContext(CourseDetailContext); + const { update } = useStaff(); + const [isHovered, setIsHovered] = useState(false); + const toggleLearning = async () => { + if (!userIsLearning) { + await update.mutateAsync({ + where: { id: user?.id }, + data: { + learningPosts: { + connect: { id: course.id }, + }, + }, + }); + } else { + await update.mutateAsync({ + where: { id: user?.id }, + data: { + learningPosts: { + disconnect: { + id: course.id, + }, + }, + }, + }); + } + }; + return ( + <> + {isAuthenticated && ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + className={`flex px-1 py-0.5 gap-1 hover:cursor-pointer transition-all ${ + userIsLearning + ? isHovered + ? "text-red-500 border-red-500 rounded-md " + : "text-green-500 " + : "text-primary " + }`}> + {userIsLearning ? ( + isHovered ? ( + + ) : ( + + ) + ) : ( + + )} + + {userIsLearning + ? isHovered + ? "退出学习" + : "正在学习" + : "加入学习"} + +
+ )} + {canEdit && ( +
{ + const url = course?.id + ? `/course/${course?.id}/editor` + : "/course/editor"; + navigate(url); + }}> + + {"编辑课程"} +
+ )} + + ); +} From aff206cbe6225a0c9b0933100783ee0eedf2b7b8 Mon Sep 17 00:00:00 2001 From: longdayi <13477510+longdayilongdayi@user.noreply.gitee.com> Date: Thu, 27 Feb 2025 09:27:53 +0800 Subject: [PATCH 27/28] 02270927 --- .../queue/models/post/post.queue.service.ts | 0 apps/server/src/queue/models/post/utils.ts | 0 .../components/couresPreviewTabmsg.tsx | 0 .../preview/components/courseCatalog.tsx | 0 .../components/coursePreviewAllmsg.tsx | 0 apps/web/src/app/main/course/preview/page.tsx | 0 apps/web/src/app/main/course/preview/type.ts | 0 .../courses/components/CoursesContainer.tsx | 0 .../main/courses/layout/AllCoursesLayout.tsx | 0 .../home/components/CategorySectionCard.tsx | 0 .../home/components/CoursesSectionTag.tsx | 0 .../app/main/home/components/LookForMore.tsx | 0 apps/web/src/app/main/layout/MainProvider.tsx | 0 .../main/layout/UserMenu/UserEditModal.tsx | 0 .../src/app/main/layout/UserMenu/UserForm.tsx | 0 .../web/src/app/main/layout/UserMenu/types.ts | 0 apps/web/src/app/main/my-duty/page.tsx | 0 apps/web/src/app/main/my-learning/page.tsx | 0 .../path/components/PathListContainer.tsx | 0 apps/web/src/app/main/path/editor/page.tsx | 0 .../app/main/path/layout/PathListLayout.tsx | 0 .../common/container/CollapsibleContent.tsx | 0 .../src/components/common/editor/NodeMenu.tsx | 0 apps/web/src/components/common/editor/i18n.ts | 0 .../web/src/components/common/editor/types.ts | 0 .../src/components/common/input/InputList.tsx | 0 .../common/uploader/MultiAvatarUploader.tsx | 0 .../common/uploader/ResourceShower.tsx | 0 .../src/components/common/uploader/utils.tsx | 0 .../course/detail/CourseDetailTitle.tsx | 0 .../detail/CoursePreview/CoursePreview.tsx | 0 .../CoursePreview/couresPreviewTabmsg.tsx | 0 .../detail/CoursePreview/courseCatalog.tsx | 0 .../models/term/term-parent-selector.tsx | 0 packages/common/src/models/resource.ts | 0 scripts/git_stats.py | 503 ++++++++++++++++++ web-dist/index.html | 0 37 files changed, 503 insertions(+) mode change 100644 => 100755 apps/server/src/queue/models/post/post.queue.service.ts mode change 100644 => 100755 apps/server/src/queue/models/post/utils.ts mode change 100644 => 100755 apps/web/src/app/main/course/preview/components/couresPreviewTabmsg.tsx mode change 100644 => 100755 apps/web/src/app/main/course/preview/components/courseCatalog.tsx mode change 100644 => 100755 apps/web/src/app/main/course/preview/components/coursePreviewAllmsg.tsx mode change 100644 => 100755 apps/web/src/app/main/course/preview/page.tsx mode change 100644 => 100755 apps/web/src/app/main/course/preview/type.ts mode change 100644 => 100755 apps/web/src/app/main/courses/components/CoursesContainer.tsx mode change 100644 => 100755 apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx mode change 100644 => 100755 apps/web/src/app/main/home/components/CategorySectionCard.tsx mode change 100644 => 100755 apps/web/src/app/main/home/components/CoursesSectionTag.tsx mode change 100644 => 100755 apps/web/src/app/main/home/components/LookForMore.tsx mode change 100644 => 100755 apps/web/src/app/main/layout/MainProvider.tsx mode change 100644 => 100755 apps/web/src/app/main/layout/UserMenu/UserEditModal.tsx mode change 100644 => 100755 apps/web/src/app/main/layout/UserMenu/UserForm.tsx mode change 100644 => 100755 apps/web/src/app/main/layout/UserMenu/types.ts mode change 100644 => 100755 apps/web/src/app/main/my-duty/page.tsx mode change 100644 => 100755 apps/web/src/app/main/my-learning/page.tsx mode change 100644 => 100755 apps/web/src/app/main/path/components/PathListContainer.tsx mode change 100644 => 100755 apps/web/src/app/main/path/editor/page.tsx mode change 100644 => 100755 apps/web/src/app/main/path/layout/PathListLayout.tsx mode change 100644 => 100755 apps/web/src/components/common/container/CollapsibleContent.tsx mode change 100644 => 100755 apps/web/src/components/common/editor/NodeMenu.tsx mode change 100644 => 100755 apps/web/src/components/common/editor/i18n.ts mode change 100644 => 100755 apps/web/src/components/common/editor/types.ts mode change 100644 => 100755 apps/web/src/components/common/input/InputList.tsx mode change 100644 => 100755 apps/web/src/components/common/uploader/MultiAvatarUploader.tsx mode change 100644 => 100755 apps/web/src/components/common/uploader/ResourceShower.tsx mode change 100644 => 100755 apps/web/src/components/common/uploader/utils.tsx mode change 100644 => 100755 apps/web/src/components/models/course/detail/CourseDetailTitle.tsx mode change 100644 => 100755 apps/web/src/components/models/course/detail/CoursePreview/CoursePreview.tsx mode change 100644 => 100755 apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx mode change 100644 => 100755 apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx mode change 100644 => 100755 apps/web/src/components/models/term/term-parent-selector.tsx mode change 100644 => 100755 packages/common/src/models/resource.ts create mode 100755 scripts/git_stats.py mode change 100644 => 100755 web-dist/index.html diff --git a/apps/server/src/queue/models/post/post.queue.service.ts b/apps/server/src/queue/models/post/post.queue.service.ts old mode 100644 new mode 100755 diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/course/preview/components/couresPreviewTabmsg.tsx b/apps/web/src/app/main/course/preview/components/couresPreviewTabmsg.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/course/preview/components/courseCatalog.tsx b/apps/web/src/app/main/course/preview/components/courseCatalog.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/course/preview/components/coursePreviewAllmsg.tsx b/apps/web/src/app/main/course/preview/components/coursePreviewAllmsg.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/course/preview/page.tsx b/apps/web/src/app/main/course/preview/page.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/course/preview/type.ts b/apps/web/src/app/main/course/preview/type.ts old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/courses/components/CoursesContainer.tsx b/apps/web/src/app/main/courses/components/CoursesContainer.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx b/apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/home/components/CategorySectionCard.tsx b/apps/web/src/app/main/home/components/CategorySectionCard.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/home/components/CoursesSectionTag.tsx b/apps/web/src/app/main/home/components/CoursesSectionTag.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/home/components/LookForMore.tsx b/apps/web/src/app/main/home/components/LookForMore.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/layout/MainProvider.tsx b/apps/web/src/app/main/layout/MainProvider.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/layout/UserMenu/UserEditModal.tsx b/apps/web/src/app/main/layout/UserMenu/UserEditModal.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/layout/UserMenu/UserForm.tsx b/apps/web/src/app/main/layout/UserMenu/UserForm.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/layout/UserMenu/types.ts b/apps/web/src/app/main/layout/UserMenu/types.ts old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/my-duty/page.tsx b/apps/web/src/app/main/my-duty/page.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/path/components/PathListContainer.tsx b/apps/web/src/app/main/path/components/PathListContainer.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/path/editor/page.tsx b/apps/web/src/app/main/path/editor/page.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/app/main/path/layout/PathListLayout.tsx b/apps/web/src/app/main/path/layout/PathListLayout.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/container/CollapsibleContent.tsx b/apps/web/src/components/common/container/CollapsibleContent.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/editor/NodeMenu.tsx b/apps/web/src/components/common/editor/NodeMenu.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/editor/i18n.ts b/apps/web/src/components/common/editor/i18n.ts old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/editor/types.ts b/apps/web/src/components/common/editor/types.ts old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/input/InputList.tsx b/apps/web/src/components/common/input/InputList.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx b/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/uploader/ResourceShower.tsx b/apps/web/src/components/common/uploader/ResourceShower.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/common/uploader/utils.tsx b/apps/web/src/components/common/uploader/utils.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx b/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/models/course/detail/CoursePreview/CoursePreview.tsx b/apps/web/src/components/models/course/detail/CoursePreview/CoursePreview.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx b/apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx b/apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx old mode 100644 new mode 100755 diff --git a/apps/web/src/components/models/term/term-parent-selector.tsx b/apps/web/src/components/models/term/term-parent-selector.tsx old mode 100644 new mode 100755 diff --git a/packages/common/src/models/resource.ts b/packages/common/src/models/resource.ts old mode 100644 new mode 100755 diff --git a/scripts/git_stats.py b/scripts/git_stats.py new file mode 100755 index 0000000..374834c --- /dev/null +++ b/scripts/git_stats.py @@ -0,0 +1,503 @@ +import statistics +from git import Repo +from collections import defaultdict, Counter +from datetime import datetime, timedelta +import os + + +def get_contributor_stats(repo_path, start_date=None, end_date=None, branch='HEAD'): + """ + 获取仓库贡献者的详细统计信息 + + Args: + repo_path: Git仓库路径 + start_date: 开始日期(可选) + end_date: 结束日期(可选) + branch: 要分析的分支(默认为HEAD) + + Returns: + 包含每个贡献者详细统计信息的字典 + """ + # 初始化仓库对象 + repo = Repo(repo_path) + + # 存储统计结果 + stats = defaultdict(lambda: { + 'additions': 0, # 添加的行数 + 'deletions': 0, # 删除的行数 + 'commits': 0, # 提交次数 + 'files_modified': set(), # 修改过的文件集合 + 'file_types': defaultdict(int),# 各类型文件的修改次数 + 'commit_dates': set(), # 提交日期集合 + 'commit_hours': defaultdict(int), # 提交小时分布 + 'commit_weekdays': defaultdict(int), # 提交工作日分布 + 'largest_commit': 0, # 最大单次提交修改量 + 'first_commit': None, # 首次提交时间 + 'last_commit': None, # 最近提交时间 + 'commit_sizes': [], # 每次提交的大小,用于计算平均值和中位数 + 'commit_messages': [], # 提交消息列表 + 'commit_message_lengths': [], # 提交消息长度列表 + 'directories_modified': set(), # 修改过的目录集合 + 'co_authors': set(), # 合作者集合 + 'impact_score': 0, # 影响力得分 + 'complexity_score': 0, # 复杂度得分 + 'commit_by_month': defaultdict(int), # 按月份统计的提交次数 + 'commit_by_quarter': defaultdict(int), # 按季度统计的提交次数 + 'commit_by_year': defaultdict(int), # 按年份统计的提交次数 + 'commit_by_week': defaultdict(int), # 按周统计的提交次数 + 'file_operations': { # 文件操作统计 + 'created': set(), # 创建的文件 + 'deleted': set(), # 删除的文件 + 'modified': set(), # 修改的文件 + }, + 'review_comments': 0, # 代码审查评论数(如果可用) + 'merge_commits': 0, # 合并提交数 + 'commit_streak': 0, # 最长连续提交天数 + 'current_streak': 0, # 当前连续提交天数 + 'contribution_days': [], # 所有贡献的日期列表(用于热图) + 'code_churn': 0, # 代码周转率(添加后又删除的代码) + 'file_ownership': {}, # 文件所有权百分比 + 'key_files_modified': set(), # 修改过的关键文件 + 'refactoring_commits': 0, # 重构提交数(基于提交消息分析) + 'bug_fix_commits': 0, # 修复bug的提交数 + 'feature_commits': 0, # 新功能提交数 + 'documentation_commits': 0, # 文档相关提交数 + 'commit_size_distribution': defaultdict(int), # 提交大小分布 + 'collaboration_score': 0, # 协作得分 + 'consistency_score': 0, # 一致性得分 + 'expertise_areas': defaultdict(float), # 专业领域(目录/语言) + }) + + # 存储所有文件的修改者,用于计算协作指标 + file_authors = defaultdict(set) + + # 存储项目文件的重要性权重 (基于修改频率) + file_importance = Counter() + + # 存储用于检测关键词的正则表达式 + import re + refactor_pattern = re.compile(r'refactor|重构', re.IGNORECASE) + bugfix_pattern = re.compile(r'fix|修复|bug|问题|issue|错误', re.IGNORECASE) + feature_pattern = re.compile(r'feature|功能|新增|add|实现', re.IGNORECASE) + docs_pattern = re.compile(r'doc|文档|注释|comment', re.IGNORECASE) + + # 记录每位贡献者的提交日期,用于计算连续贡献天数 + author_commit_days = defaultdict(set) + + # 定义关键文件路径模式 (可以根据项目自定义) + key_file_patterns = [ + re.compile(r'package\.json$'), + re.compile(r'docker-compose\.yml$'), + re.compile(r'Dockerfile$'), + re.compile(r'tsconfig\..*\.json$'), + re.compile(r'/src/index\.[jt]s$'), + re.compile(r'README\.md$'), + re.compile(r'\.env'), + re.compile(r'/main\.[jt]s$'), + re.compile(r'/app\.[jt]s$'), + ] + + # 遍历所有提交 + for commit in repo.iter_commits(branch): + # 过滤日期 + commit_date = datetime.fromtimestamp(commit.committed_date) + if start_date and commit_date < start_date: + continue + if end_date and commit_date > end_date: + continue + + author = commit.author.name + stats[author]['commits'] += 1 + + # 记录提交日期和时间 + commit_day = commit_date.date() + stats[author]['commit_dates'].add(commit_day) + stats[author]['contribution_days'].append(commit_day) # 用于热图 + stats[author]['commit_hours'][commit_date.hour] += 1 + stats[author]['commit_weekdays'][commit_date.weekday()] += 1 + + # 添加到作者的提交日集合 + author_commit_days[author].add(commit_day) + + # 按时间段统计 + year = commit_date.year + month = commit_date.month + quarter = (month - 1) // 3 + 1 + week_num = commit_date.isocalendar()[1] + stats[author]['commit_by_year'][year] += 1 + stats[author]['commit_by_month'][f"{year}-{month:02d}"] += 1 + stats[author]['commit_by_quarter'][f"{year}-Q{quarter}"] += 1 + stats[author]['commit_by_week'][f"{year}-W{week_num:02d}"] += 1 + + # 分析提交消息,对提交进行分类 + commit_message = commit.message.strip() + if refactor_pattern.search(commit_message): + stats[author]['refactoring_commits'] += 1 + if bugfix_pattern.search(commit_message): + stats[author]['bug_fix_commits'] += 1 + if feature_pattern.search(commit_message): + stats[author]['feature_commits'] += 1 + if docs_pattern.search(commit_message): + stats[author]['documentation_commits'] += 1 + + # 记录首次和最近提交 + if stats[author]['first_commit'] is None or commit_date < stats[author]['first_commit']: + stats[author]['first_commit'] = commit_date + if stats[author]['last_commit'] is None or commit_date > stats[author]['last_commit']: + stats[author]['last_commit'] = commit_date + + # 记录提交消息 + commit_message = commit.message.strip() + stats[author]['commit_messages'].append(commit_message) + stats[author]['commit_message_lengths'].append(len(commit_message)) + + # 检测是否为合并提交 + if len(commit.parents) > 1: + stats[author]['merge_commits'] += 1 + + # 统计添加和删除的行数 + total_changes = 0 + modified_files = set() + created_files = set() + deleted_files = set() + directories = set() + + # 尝试获取提交前后的差异,以确定文件操作类型 + try: + if commit.parents: + parent = commit.parents[0] + diffs = parent.diff(commit) + for diff_item in diffs: + if diff_item.new_file: + if diff_item.b_path: + created_files.add(diff_item.b_path) + elif diff_item.deleted_file: + if diff_item.a_path: + deleted_files.add(diff_item.a_path) + else: + if diff_item.a_path: + modified_files.add(diff_item.a_path) + else: + # 对于首次提交,所有文件都是新创建的 + for file_path in commit.stats.files: + created_files.add(file_path) + except Exception as e: + # 如果获取差异失败,退回到简单的文件修改统计 + modified_files = set(commit.stats.files.keys()) + + for file_path, item in commit.stats.files.items(): + # 统计文件类型 + _, ext = os.path.splitext(file_path) + if ext: # 确保扩展名不为空 + stats[author]['file_types'][ext] += 1 + else: + stats[author]['file_types']['no_extension'] += 1 + + # 记录目录 + directory = os.path.dirname(file_path) + if directory: + directories.add(directory) + + # 记录修改的文件 + modified_files.add(file_path) + + # 记录文件的修改者,用于计算协作指标 + file_authors[file_path].add(author) + + # 统计添加和删除的行数 + stats[author]['additions'] += item['insertions'] + stats[author]['deletions'] += item['deletions'] + total_changes += item['insertions'] + item['deletions'] + + # 更新修改过的文件和目录集合 + stats[author]['files_modified'].update(modified_files) + stats[author]['directories_modified'].update(directories) + stats[author]['file_operations']['created'].update(created_files) + stats[author]['file_operations']['deleted'].update(deleted_files) + stats[author]['file_operations']['modified'].update(modified_files - created_files - deleted_files) + + # 记录本次提交的修改量 + stats[author]['commit_sizes'].append(total_changes) + + # 记录提交大小分布 + commit_size_category = "小型(1-10行)" if total_changes <= 10 else \ + "中型(11-100行)" if total_changes <= 100 else \ + "大型(101-500行)" if total_changes <= 500 else \ + "超大型(500+行)" + stats[author]['commit_size_distribution'][commit_size_category] += 1 + + # 更新最大单次提交修改量 + if total_changes > stats[author]['largest_commit']: + stats[author]['largest_commit'] = total_changes + + # 检查修改的文件是否为关键文件 + for file_path in modified_files: + for pattern in key_file_patterns: + if pattern.search(file_path): + stats[author]['key_files_modified'].add(file_path) + break + + # 更新文件重要性权重 + for file_path in modified_files: + file_importance[file_path] += 1 + + # 计算影响力得分 (基于修改的文件数和总修改行数) + impact = total_changes * len(modified_files) / 100 if modified_files else 0 + stats[author]['impact_score'] += impact + + # 计算文件协作度和文件所有权 + for file_path, authors in file_authors.items(): + # 如果只有一个作者修改了文件,则该作者100%拥有此文件 + if len(authors) == 1: + author = next(iter(authors)) + if 'file_ownership' not in stats[author]: + stats[author]['file_ownership'] = {} + stats[author]['file_ownership'][file_path] = 100.0 + else: + # 如果多个作者修改了文件,则按照每个作者的修改比例计算所有权 + for author in authors: + # 简化处理:平均分配所有权 + ownership_percent = 100.0 / len(authors) + if 'file_ownership' not in stats[author]: + stats[author]['file_ownership'] = {} + stats[author]['file_ownership'][file_path] = ownership_percent + + # 计算每个作者的连续提交天数 + for author, commit_days in author_commit_days.items(): + if not commit_days: + continue + + # 按日期排序 + sorted_days = sorted(commit_days) + + # 计算最长提交连续天数 + current_streak = 1 + max_streak = 1 + + for i in range(1, len(sorted_days)): + # 如果当前日期与前一天相差正好一天,则增加连续计数 + if (sorted_days[i] - sorted_days[i-1]).days == 1: + current_streak += 1 + else: + # 重置当前连续计数 + current_streak = 1 + + max_streak = max(max_streak, current_streak) + + # 记录最长连续提交天数 + stats[author]['commit_streak'] = max_streak + + # 计算当前连续提交天数 (到最后一个日期) + if sorted_days: + today = datetime.now().date() + days_since_last = (today - sorted_days[-1]).days + + if days_since_last <= 1: # 如果最后提交是今天或昨天 + current_streak = 1 + for i in range(len(sorted_days) - 1, 0, -1): + if (sorted_days[i] - sorted_days[i-1]).days == 1: + current_streak += 1 + else: + break + stats[author]['current_streak'] = current_streak + + # 后处理:计算派生指标并转换集合为计数 + for author, data in stats.items(): + # 将文件集合转换为数量 + data['files_count'] = len(data['files_modified']) + data['active_days'] = len(data['commit_dates']) + data['key_files_count'] = len(data['key_files_modified']) + + # 计算平均每次提交的修改量 + if data['commits'] > 0: + data['avg_commit_size'] = sum(data['commit_sizes']) / data['commits'] + data['median_commit_size'] = statistics.median(data['commit_sizes']) if data['commit_sizes'] else 0 + + # 计算代码复杂度得分 (基于修改量、文件数和一致性) + variability = statistics.stdev(data['commit_sizes']) if len(data['commit_sizes']) > 1 else 0 + data['complexity_score'] = (data['avg_commit_size'] * data['files_count'] * (1 + variability / 1000)) / 100 + + # 计算一致性得分 (提交大小和频率的一致性) + if variability > 0: + data['consistency_score'] = 100 * (1 - min(1, variability / data['avg_commit_size'])) + else: + data['consistency_score'] = 100 + else: + data['avg_commit_size'] = 0 + data['median_commit_size'] = 0 + data['complexity_score'] = 0 + data['consistency_score'] = 0 + + # 计算总修改量 + data['total_changes'] = data['additions'] + data['deletions'] + + # 计算代码周转率 (code churn) - 估算值 + if data['additions'] > 0 and data['deletions'] > 0: + data['code_churn'] = min(data['additions'], data['deletions']) / max(data['additions'], data['deletions']) * 100 + + # 计算活跃时长(天) + if data['first_commit'] and data['last_commit']: + delta = data['last_commit'] - data['first_commit'] + data['active_period_days'] = delta.days + 1 + + # 计算活跃密度 (提交数/活跃天数) + if delta.days > 0: + data['activity_density'] = data['commits'] / delta.days + else: + data['activity_density'] = data['commits'] + else: + data['active_period_days'] = 0 + data['activity_density'] = 0 + + # 计算协作得分 (基于参与修改的共享文件比例) + total_files = len(data['files_modified']) + shared_files = sum(1 for f in data['files_modified'] if len(file_authors[f]) > 1) + if total_files > 0: + data['collaboration_score'] = (shared_files / total_files) * 100 + + # 计算专业领域 (基于文件类型和目录) + if data['file_types']: + primary_type = max(data['file_types'].items(), key=lambda x: x[1])[0] + data['primary_file_type'] = primary_type + data['primary_file_type_percent'] = (data['file_types'][primary_type] / sum(data['file_types'].values())) * 100 + + # 统计目录专业度 + if data['directories_modified']: + dir_counts = Counter() + for directory in data['directories_modified']: + dir_counts[directory] += 1 + + # 检查父目录 + parent = os.path.dirname(directory) + while parent: + dir_counts[parent] += 0.5 # 对父目录给予较低的权重 + parent = os.path.dirname(parent) + + # 找出专业领域(最常修改的目录) + if dir_counts: + primary_dir = max(dir_counts.items(), key=lambda x: x[1])[0] + data['primary_directory'] = primary_dir + data['expertise_areas'][primary_dir] = dir_counts[primary_dir] / sum(dir_counts.values()) + + return stats + +def print_stats(stats): + """打印贡献者统计信息的详细报告""" + # 基本信息表头 + print("\n===== 贡献者基本统计 =====") + print("{:<20} {:<10} {:<10} {:<10} {:<10} {:<15} {:<15}".format( + "作者", "提交数", "添加行数", "删除行数", "总修改行数", "修改文件数", "活跃天数")) + print("-" * 90) + + # 按总修改量排序 + for author, data in sorted(stats.items(), key=lambda x: x[1]['total_changes'], reverse=True): + print("{:<20} {:<10} {:<10} {:<10} {:<10} {:<15} {:<15}".format( + author, + data['commits'], + data['additions'], + data['deletions'], + data['total_changes'], + data['files_count'], + data['active_days'] + )) + + # 为每个贡献者打印详细信息 + for author, data in sorted(stats.items(), key=lambda x: x[1]['total_changes'], reverse=True): + print(f"\n\n===== {author} 的详细贡献统计 =====") + + # 活跃时间信息 + if data['first_commit'] and data['last_commit']: + print(f"首次提交时间: {data['first_commit'].strftime('%Y-%m-%d %H:%M:%S')}") + print(f"最近提交时间: {data['last_commit'].strftime('%Y-%m-%d %H:%M:%S')}") + print(f"活跃时长: {data['active_period_days']} 天") + + # 提交规模信息 + print(f"平均每次提交修改: {data['avg_commit_size']:.2f} 行") + print(f"最大单次提交修改: {data['largest_commit']} 行") + + # 文件类型分布 + if data['file_types']: + print("\n文件类型分布:") + for ext, count in sorted(data['file_types'].items(), key=lambda x: x[1], reverse=True): + print(f" {ext}: {count} 次修改") + + # 提交时间分布 + if data['commit_hours']: + print("\n提交时间分布:") + for hour in range(24): + count = data['commit_hours'].get(hour, 0) + if count > 0: + print(f" {hour:02d}:00-{hour+1:02d}:00: {count} 次提交") + + # 工作日分布 + if data['commit_weekdays']: + weekday_names = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] + print("\n工作日分布:") + for day in range(7): + count = data['commit_weekdays'].get(day, 0) + if count > 0: + print(f" {weekday_names[day]}: {count} 次提交") + +def get_team_summary(stats): + """生成团队整体统计摘要""" + summary = { + 'total_commits': 0, + 'total_additions': 0, + 'total_deletions': 0, + 'total_files': set(), + 'contributors': len(stats), + 'first_commit': None, + 'last_commit': None, + } + + for author, data in stats.items(): + summary['total_commits'] += data['commits'] + summary['total_additions'] += data['additions'] + summary['total_deletions'] += data['deletions'] + summary['total_files'].update(data['files_modified']) + + # 更新首次和最近提交 + if data['first_commit']: + if summary['first_commit'] is None or data['first_commit'] < summary['first_commit']: + summary['first_commit'] = data['first_commit'] + + if data['last_commit']: + if summary['last_commit'] is None or data['last_commit'] > summary['last_commit']: + summary['last_commit'] = data['last_commit'] + + return summary + +def print_team_summary(summary): + """打印团队整体统计摘要""" + print("\n===== 团队整体统计 =====") + print(f"贡献者数量: {summary['contributors']}") + print(f"总提交次数: {summary['total_commits']}") + print(f"总添加行数: {summary['total_additions']}") + print(f"总删除行数: {summary['total_deletions']}") + print(f"总修改行数: {summary['total_additions'] + summary['total_deletions']}") + print(f"修改的文件数: {len(summary['total_files'])}") + + if summary['first_commit'] and summary['last_commit']: + print(f"项目起始时间: {summary['first_commit'].strftime('%Y-%m-%d')}") + print(f"最近活动时间: {summary['last_commit'].strftime('%Y-%m-%d')}") + delta = summary['last_commit'] - summary['first_commit'] + print(f"项目活跃时长: {delta.days + 1} 天") +if __name__ == "__main__": + # 设置仓库路径(当前目录) + repo_path = '.' + + # 设置日期范围(示例) + # 注意:这里使用的是2025年的日期,可能需要根据实际情况调整 + start_date = datetime(2025, 1, 1) # 修改为更合理的日期范围 + end_date = datetime(2025, 12, 31) + + print(f"分析Git仓库: {os.path.abspath(repo_path)}") + print(f"时间范围: {start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}") + + # 获取统计信息 + stats = get_contributor_stats(repo_path, start_date, end_date) + + # 打印团队摘要 + team_summary = get_team_summary(stats) + print_team_summary(team_summary) + print(stats) \ No newline at end of file diff --git a/web-dist/index.html b/web-dist/index.html old mode 100644 new mode 100755 From f0f15024c02813477199c1c2841ea6834322f15a Mon Sep 17 00:00:00 2001 From: ditiqi Date: Thu, 27 Feb 2025 09:28:16 +0800 Subject: [PATCH 28/28] afff --- .../main/courses/components/CourseCard.tsx | 6 +- .../main/courses/components/FilterSection.tsx | 27 +--- apps/web/src/app/main/layout/MainFooter.tsx | 130 ++++++++++-------- apps/web/src/app/main/layout/MainLayout.tsx | 2 +- .../course/detail/CourseDetailLayout.tsx | 2 +- .../detail/CourseSyllabus/CourseSyllabus.tsx | 4 +- 6 files changed, 80 insertions(+), 91 deletions(-) diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 597caf0..fa4b0d5 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -87,11 +87,11 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) {
- + {`观看次数 ${course?.meta?.views || 0}`} - + {`学习人数 ${course?.studentIds?.length || 0}`} @@ -102,7 +102,7 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) { size="large" className="w-full shadow-[0_8px_20px_-6px_rgba(59,130,246,0.5)] hover:shadow-[0_12px_24px_-6px_rgba(59,130,246,0.6)] transform hover:translate-y-[-2px] transition-all duration-500 ease-out"> - {edit ? "进行编辑" : "立即学习"} + {edit ? "编辑" : "立即学习"}
diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index b437058..110ba8c 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -19,7 +19,7 @@ export default function FilterSection() { }); }; return ( -
+
{taxonomies?.map((tax, index) => { const items = Object.entries(selectedTerms).find( ([key, items]) => key === tax.slug @@ -31,7 +31,7 @@ export default function FilterSection() { handleTermChange( @@ -39,27 +39,8 @@ export default function FilterSection() { selected as string[] ) } - taxonomyId={tax?.id} - > - {/* ( -
{menu}
- )} - dropdownStyle={{ maxHeight: 400, overflow: "auto" }} - multiple - taxonomyId={tax?.id} - onChange={(selected) => - handleTermChange( - tax?.slug, - selected as string[] - ) - }>
- {index < taxonomies.length - 1 && ( - - )} */} + taxonomyId={tax?.id}> +
); })} diff --git a/apps/web/src/app/main/layout/MainFooter.tsx b/apps/web/src/app/main/layout/MainFooter.tsx index 9d060dc..5ebe46f 100755 --- a/apps/web/src/app/main/layout/MainFooter.tsx +++ b/apps/web/src/app/main/layout/MainFooter.tsx @@ -1,68 +1,76 @@ -import { CloudOutlined, FileSearchOutlined, HomeOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons'; +import { + CloudOutlined, + FileSearchOutlined, + HomeOutlined, + MailOutlined, + PhoneOutlined, +} from "@ant-design/icons"; export function MainFooter() { - return ( -
-
-
- {/* 开发组织信息 */} -
-

- 软件与数据小组 -

-

- 提供技术支持 -

-
+ return ( +
+
+
+ {/* 开发组织信息 */} +
+

+ 软件与数据小组 +

+

+ 提供技术支持 +

+
- {/* 联系方式 */} -
-
- - 628118 -
-
- - gcsjs6@tx3l.nb.kj -
-
+ {/* 联系方式 */} +
+
+ + + 628118 + +
+
+ + + gcsjs6@tx3l.nb.kj + +
+
- {/* 系统链接 */} -
-
- - - - - - + {/* 系统链接 */} +
+ -
-
+ + + +
+
+
- {/* 版权信息 */} -
-

- © {new Date().getFullYear()} 南天烽火. All rights reserved. -

-
-
-
- ); + {/* 版权信息 */} +
+

+ © {new Date().getFullYear()} 南天烽火. All rights + reserved. +

+
+
+ + ); } diff --git a/apps/web/src/app/main/layout/MainLayout.tsx b/apps/web/src/app/main/layout/MainLayout.tsx index f5b462b..fa6627a 100755 --- a/apps/web/src/app/main/layout/MainLayout.tsx +++ b/apps/web/src/app/main/layout/MainLayout.tsx @@ -11,7 +11,7 @@ export function MainLayout() {
- + diff --git a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx index cac3bf8..56831e4 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx @@ -24,7 +24,7 @@ export default function CourseDetailLayout() { {/* 添加 Header 组件 */} {/* 主内容区域 */} {/* 为了防止 Header 覆盖内容,添加上边距 */} -
+
{" "} {/* 添加这个包装 div */} = ({ style={{ width: isOpen ? "25%" : "0", right: 0, - top: isHeaderVisible ? "64px" : "0", + top: isHeaderVisible ? "56px" : "0", }} - className="fixed top-0 bottom-0 z-20 bg-white shadow-xl"> + className="fixed top-0 bottom-0 z-10 bg-white shadow-xl"> {isOpen && (