diff --git a/apps/server/src/models/base/base.service.ts b/apps/server/src/models/base/base.service.ts index d1b8b16..ae45e06 100755 --- a/apps/server/src/models/base/base.service.ts +++ b/apps/server/src/models/base/base.service.ts @@ -8,6 +8,7 @@ import { DelegateFuncs, UpdateOrderArgs, TransactionType, + OrderByArgs, SelectArgs, } from './base.type'; import { @@ -450,9 +451,10 @@ export class BaseService< page?: number; pageSize?: number; where?: WhereArgs; + orderBy?: OrderByArgs; select?: SelectArgs; }): Promise<{ items: R['findMany']; totalPages: number }> { - const { page = 1, pageSize = 10, where, select } = args; + const { page = 1, pageSize = 10, where, select, orderBy } = args; try { // 获取总记录数 @@ -461,6 +463,7 @@ export class BaseService< const items = (await this.getModel().findMany({ where, select, + orderBy, skip: (page - 1) * pageSize, take: pageSize, } as any)) as R['findMany']; diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 03d89de..cbcf7bc 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -21,6 +21,7 @@ import { BaseTreeService } from '../base/base.tree.service'; import { z } from 'zod'; import { DefaultArgs } from '@prisma/client/runtime/library'; import dayjs from 'dayjs'; +import { OrderByArgs } from '../base/base.type'; @Injectable() export class PostService extends BaseTreeService { @@ -181,6 +182,7 @@ export class PostService extends BaseTreeService { page?: number; pageSize?: number; where?: Prisma.PostWhereInput; + orderBy?: OrderByArgs<(typeof db.post)['findMany']>; select?: Prisma.PostSelect; }): Promise<{ items: { @@ -197,6 +199,9 @@ export class PostService extends BaseTreeService { duration: number | null; rating: number | null; createdAt: Date; + views: number; + hates: number; + likes: number; publishedAt: Date | null; updatedAt: Date; deletedAt: Date | null; diff --git a/apps/server/src/models/visit/visit.router.ts b/apps/server/src/models/visit/visit.router.ts index ad6d632..e600ff2 100755 --- a/apps/server/src/models/visit/visit.router.ts +++ b/apps/server/src/models/visit/visit.router.ts @@ -15,13 +15,13 @@ export class VisitRouter { private readonly visitService: VisitService, ) {} router = this.trpc.router({ - create: this.trpc.protectProcedure + create: this.trpc.procedure .input(VisitCreateArgsSchema) .mutation(async ({ ctx, input }) => { const { staff } = ctx; return await this.visitService.create(input, staff); }), - createMany: this.trpc.protectProcedure + createMany: this.trpc.procedure .input(z.array(VisitCreateManyInputSchema)) .mutation(async ({ ctx, input }) => { const { staff } = ctx; diff --git a/apps/server/src/models/visit/visit.service.ts b/apps/server/src/models/visit/visit.service.ts index 20607e3..e050fa5 100755 --- a/apps/server/src/models/visit/visit.service.ts +++ b/apps/server/src/models/visit/visit.service.ts @@ -9,17 +9,22 @@ export class VisitService extends BaseService { } async create(args: Prisma.VisitCreateArgs, staff?: UserProfile) { const { postId, lectureId, messageId } = args.data; - const visitorId = args.data.visitorId || staff?.id; + const visitorId = args.data?.visitorId || staff?.id; let result; + console.log(args.data.type); + console.log(visitorId); + console.log(postId); const existingVisit = await db.visit.findFirst({ where: { type: args.data.type, - visitorId, - OR: [{ postId }, { lectureId }, { messageId }], + // visitorId: visitorId ? visitorId : null, + OR: [{ postId }, { messageId }], }, }); + console.log('result', existingVisit); if (!existingVisit) { result = await super.create(args); + console.log('createdResult', result); } else if (args.data.type === VisitType.READED) { result = await super.update({ where: { id: existingVisit.id }, diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 51bffca..57129a3 100755 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -8,7 +8,7 @@ import { export async function updateTotalCourseViewCount(type: VisitType) { const posts = await db.post.findMany({ where: { - type: { in: [PostType.COURSE, PostType.LECTURE] }, + // type: { in: [PostType.COURSE, PostType.LECTURE,] }, deletedAt: null, }, select: { id: true, type: true }, @@ -66,27 +66,34 @@ export async function updatePostViewCount(id: string, type: VisitType) { where: { id }, select: { id: true, meta: true, type: true }, }); + console.log(post?.type); + console.log('updatePostViewCount'); const metaFieldMap = { [VisitType.READED]: 'views', [VisitType.LIKE]: 'likes', [VisitType.HATE]: 'hates', }; if (post?.type === PostType.LECTURE) { - const course = await db.postAncestry.findFirst({ + const courseAncestry = await db.postAncestry.findFirst({ where: { descendantId: post?.id, ancestor: { type: PostType.COURSE, }, }, - select: { id: true }, + select: { id: true, ancestorId: true }, }); - const lectures = await db.postAncestry.findMany({ + const course = { id: courseAncestry.ancestorId }; + const lecturesAncestry = await db.postAncestry.findMany({ where: { ancestorId: course.id, descendant: { type: PostType.LECTURE } }, select: { id: true, + descendantId: true, }, }); + const lectures = lecturesAncestry.map((ancestry) => ({ + id: ancestry.descendantId, + })); const courseViews = await db.visit.aggregate({ _sum: { views: true, @@ -98,9 +105,11 @@ export async function updatePostViewCount(id: string, type: VisitType) { type: type, }, }); + console.log(courseViews); await db.post.update({ where: { id: course.id }, data: { + [metaFieldMap[type]]: courseViews._sum.views || 0, meta: { ...((post?.meta as any) || {}), [metaFieldMap[type]]: courseViews._sum.views || 0, @@ -117,9 +126,11 @@ export async function updatePostViewCount(id: string, type: VisitType) { type: type, }, }); + console.log('totalViews', totalViews); await db.post.update({ where: { id }, data: { + [metaFieldMap[type]]: totalViews._sum.views || 0, meta: { ...((post?.meta as any) || {}), [metaFieldMap[type]]: totalViews._sum.views || 0, diff --git a/apps/server/src/tasks/init/gendev.service.ts b/apps/server/src/tasks/init/gendev.service.ts index e2c31c9..e5c3f9f 100755 --- a/apps/server/src/tasks/init/gendev.service.ts +++ b/apps/server/src/tasks/init/gendev.service.ts @@ -31,6 +31,7 @@ export class GenDevService { domainDepts: Record = {}; staffs: Staff[] = []; deptGeneratedCount = 0; + courseGeneratedCount = 1; constructor( private readonly appConfigService: AppConfigService, @@ -194,8 +195,9 @@ export class GenDevService { cate.id, randomLevelId, ); + this.courseGeneratedCount++; this.logger.log( - `Generated ${this.deptGeneratedCount}/${total} departments`, + `Generated ${this.courseGeneratedCount}/${total} course`, ); } } diff --git a/apps/web/src/app/main/course/detail/page.tsx b/apps/web/src/app/main/course/detail/page.tsx index 16f7173..82dd7ae 100755 --- a/apps/web/src/app/main/course/detail/page.tsx +++ b/apps/web/src/app/main/course/detail/page.tsx @@ -3,6 +3,5 @@ import { useParams } from "react-router-dom"; export function CourseDetailPage() { const { id, lectureId } = useParams(); - console.log("Course ID:", id); return ; } diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index 4c8141e..7d23030 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -35,6 +35,7 @@ const CategorySection = () => { const handleMouseClick = useCallback((categoryId: string) => { setSelectedTerms({ + ...selectedTerms, [TaxonomySlug.CATEGORY]: [categoryId], }); navigate("/courses"); diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index 3280945..cc68bd0 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -1,12 +1,10 @@ -import React, { useState, useMemo } from "react"; +import React, { useState, useMemo, ReactNode } from "react"; import { Typography, Skeleton } from "antd"; import { TaxonomySlug, TermDto } from "@nice/common"; import { api } from "@nice/client"; import { CoursesSectionTag } from "./CoursesSectionTag"; import LookForMore from "./LookForMore"; import PostList from "@web/src/components/models/course/list/PostList"; -import PostCard from "@web/src/components/models/post/PostCard"; -import CourseCard from "@web/src/components/models/post/SubPost/CourseCard"; interface GetTaxonomyProps { categories: string[]; isLoading: boolean; @@ -35,11 +33,17 @@ interface CoursesSectionProps { title: string; description: string; initialVisibleCoursesCount?: number; + postType:string; + render?:(post)=>ReactNode; + to:string } const CoursesSection: React.FC = ({ title, description, initialVisibleCoursesCount = 8, + postType, + render, + to }) => { const [selectedCategory, setSelectedCategory] = useState("全部"); const gateGory: GetTaxonomyProps = useGetTaxonomy({ @@ -83,7 +87,7 @@ const CoursesSection: React.FC = ({ )} } + renderItem={(post) => render(post)} params={{ page: 1, pageSize: initialVisibleCoursesCount, @@ -95,11 +99,12 @@ const CoursesSection: React.FC = ({ }, } : {}, + type: postType }, }} showPagination={false} cols={4}> - + ); diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx index 4073109..8f6b7a8 100755 --- a/apps/web/src/app/main/home/components/HeroSection.tsx +++ b/apps/web/src/app/main/home/components/HeroSection.tsx @@ -57,7 +57,7 @@ const HeroSection = () => { { icon: , value: statistics.reads, - label: "观看次数", + label: "播放次数", }, ]; }, [statistics]); diff --git a/apps/web/src/app/main/home/components/LookForMore.tsx b/apps/web/src/app/main/home/components/LookForMore.tsx index 88e1b41..2bd74cc 100755 --- a/apps/web/src/app/main/home/components/LookForMore.tsx +++ b/apps/web/src/app/main/home/components/LookForMore.tsx @@ -11,7 +11,10 @@ export default function LookForMore({to}:{to:string}) {
)} {isAuthenticated && ( )} {isAuthenticated ? ( ) : ( @@ -104,4 +111,3 @@ export function MainHeader() {
); } - diff --git a/apps/web/src/app/main/layout/MainLayout.tsx b/apps/web/src/app/main/layout/MainLayout.tsx index 739bcaa..fa3bad7 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/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx index 971dfe3..1f50140 100755 --- a/apps/web/src/app/main/layout/NavigationMenu.tsx +++ b/apps/web/src/app/main/layout/NavigationMenu.tsx @@ -11,8 +11,8 @@ export const NavigationMenu = () => { const menuItems = useMemo(() => { const baseItems = [ { key: "home", path: "/", label: "首页" }, - { key: "path", path: "/path", label: "学习路径" }, - { key: "courses", path: "/courses", label: "全部课程" }, + { key: "path", path: "/path", label: "全部思维导图" }, + { key: "courses", path: "/courses", label: "所有课程" }, ]; if (!isAuthenticated) { @@ -20,9 +20,10 @@ export const NavigationMenu = () => { } else { return [ ...baseItems, - { key: "my-duty", path: "/my-duty", label: "我的授课" }, - { key: "my-learning", path: "/my-learning", label: "我的课程" }, - { key: "my-path", path: "/my-path", label: "我的路径" }, + { key: "my-duty", path: "/my-duty", label: "我创建的课程" }, + { key: "my-learning", path: "/my-learning", label: "我学习的课程" }, + { key: "my-duty-path", path: "/my-duty-path", label: "我创建的思维导图" }, + { key: "my-path", path: "/my-path", label: "我学习的思维导图" }, ]; } }, [isAuthenticated]); diff --git a/apps/web/src/app/main/layout/UserMenu/UserForm.tsx b/apps/web/src/app/main/layout/UserMenu/UserForm.tsx index a496ed0..edef377 100755 --- a/apps/web/src/app/main/layout/UserMenu/UserForm.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserForm.tsx @@ -12,7 +12,7 @@ import toast from "react-hot-toast"; export default function StaffForm() { const { user } = useAuth(); const { create, update } = useStaff(); // Ensure you have these methods in your hooks - const {formLoading,modalOpen,setModalOpen,domainId,setDomainId,form,setFormLoading,} = useContext(UserEditorContext); + const { formLoading, modalOpen, setModalOpen, domainId, setDomainId, form, setFormLoading, } = useContext(UserEditorContext); const { data, isLoading, @@ -68,7 +68,7 @@ export default function StaffForm() { } useEffect(() => { form.resetFields(); - console.log('cc',data); + console.log('cc', data); if (data) { form.setFieldValue("username", data.username); @@ -121,7 +121,7 @@ export default function StaffForm() { name={"showname"} label="名称"> + } + params={{ + pageSize: 12, + where: { + type: PostType.PATH, + students: { + some: { + id: user?.id, + }, + }, + ...termsCondition, + ...searchCondition, + }, + }} + cols={4}> + + ); +} diff --git a/apps/web/src/app/main/my-duty-path/page.tsx b/apps/web/src/app/main/my-duty-path/page.tsx new file mode 100755 index 0000000..24d7931 --- /dev/null +++ b/apps/web/src/app/main/my-duty-path/page.tsx @@ -0,0 +1,17 @@ +import { useEffect } from "react"; +import BasePostLayout from "../layout/BasePost/BasePostLayout"; +import { useMainContext } from "../layout/MainProvider"; +import { PostType } from "@nice/common"; +import MyDutyPathContainer from "./components/MyDutyPathContainer"; + +export default function MyDutyPathPage() { + const { setSearchMode } = useMainContext(); + useEffect(() => { + setSearchMode(PostType.PATH); + }, [setSearchMode]); + return ( + + + + ); +} diff --git a/apps/web/src/app/main/path/components/DeptInfo.tsx b/apps/web/src/app/main/path/components/DeptInfo.tsx index fe82bbd..ed6510b 100644 --- a/apps/web/src/app/main/path/components/DeptInfo.tsx +++ b/apps/web/src/app/main/path/components/DeptInfo.tsx @@ -23,8 +23,9 @@ const DeptInfo = ({ post }: { post: PostDto }) => { {post && (
+ 浏览量 - {`${post?.meta?.views || 0}`} + {`${post?.views || 0}`} {post?.studentIds && post?.studentIds?.length > 0 && ( diff --git a/apps/web/src/app/main/path/components/TermInfo.tsx b/apps/web/src/app/main/path/components/TermInfo.tsx index 718096f..b26f721 100644 --- a/apps/web/src/app/main/path/components/TermInfo.tsx +++ b/apps/web/src/app/main/path/components/TermInfo.tsx @@ -1,21 +1,21 @@ import { Tag } from "antd"; -import { PostDto, TaxonomySlug } from "@nice/common"; +import { PostDto, TaxonomySlug, TermDto } from "@nice/common"; -const TermInfo = ({ post }: { post: PostDto }) => { +const TermInfo = ({ terms = [] }: { terms?: TermDto[] }) => { return (
- {post?.terms && post?.terms?.length > 0 ? ( + {terms && terms?.length > 0 ? (
- {post?.terms?.map((term: any) => { + {terms?.map((term: any) => { return ( - -
- ); + return
+ +
} diff --git a/apps/web/src/components/common/editor/MindEditor.tsx b/apps/web/src/components/common/editor/MindEditor.tsx index 4de929c..6a4e540 100755 --- a/apps/web/src/components/common/editor/MindEditor.tsx +++ b/apps/web/src/components/common/editor/MindEditor.tsx @@ -1,6 +1,6 @@ -import { Button, Card, Empty, Form, Space, Spin, message, theme } from "antd"; +import { Button, Empty, Form, Spin } from "antd"; import NodeMenu from "./NodeMenu"; -import { api, usePost } from "@nice/client"; +import { api, usePost, useVisitor } from "@nice/client"; import { ObjectType, PathDto, @@ -8,6 +8,7 @@ import { PostType, Prisma, RolePerms, + VisitType, } from "@nice/common"; import TermSelect from "../../models/term/term-select"; import DepartmentSelect from "../../models/department/department-select"; @@ -19,19 +20,20 @@ import { useTusUpload } from "@web/src/hooks/useTusUpload"; import { useNavigate } from "react-router-dom"; import { useAuth } from "@web/src/providers/auth-provider"; import { MIND_OPTIONS } from "./constant"; +import { SaveOutlined } from "@ant-design/icons"; export default function MindEditor({ id }: { id?: string }) { const containerRef = useRef(null); const [instance, setInstance] = useState(null); const { isAuthenticated, user, hasSomePermissions } = useAuth(); + const { read } = useVisitor() const { data: post, isLoading }: { data: PathDto; isLoading: boolean } = api.post.findFirst.useQuery({ where: { id, }, select: postDetailSelect, - }); + }, { enabled: Boolean(id) }); const canEdit: boolean = useMemo(() => { - //登录了且是作者、超管、无id新建模式 const isAuth = isAuthenticated && user?.id === post?.author?.id; return !!id || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST); }, [user]); @@ -42,9 +44,19 @@ export default function MindEditor({ id }: { id?: string }) { }); const { handleFileUpload } = useTusUpload(); const [form] = Form.useForm(); + useEffect(() => { + if (post?.id && id) { + read.mutateAsync({ + data: { + visitorId: user?.id || null, + postId: post?.id, + type: VisitType.READED, + }, + }); + } + }, [post]); useEffect(() => { if (post && form && instance && id) { - console.log(post); instance.refresh((post as any).meta); const deptIds = (post?.depts || [])?.map((dept) => dept.id); const formData = { @@ -52,8 +64,8 @@ export default function MindEditor({ id }: { id?: string }) { deptIds: deptIds, }; post.terms?.forEach((term) => { - formData[term.taxonomyId] = term.id; // 假设 taxonomyName 是您在 Form.Item 中使用的 name - }); + formData[term.taxonomyId] = term.id // 假设 taxonomyName是您在 Form.Item 中使用的name + }) form.setFieldsValue(formData); } }, [post, form, instance, id]); @@ -73,8 +85,9 @@ export default function MindEditor({ id }: { id?: string }) { nodeMenu: canEdit, // 禁用节点右键菜单 keypress: canEdit, // 禁用键盘快捷键 }); - mind.init(MindElixir.new("新学习路径")); + mind.init(MindElixir.new("新思维导图")); containerRef.current.hidden = true; + //挂载实例 setInstance(mind); }, [canEdit]); useEffect(() => { @@ -86,6 +99,7 @@ export default function MindEditor({ id }: { id?: string }) { } } }, [id, post, instance]); + //保存 按钮 函数 const handleSave = async () => { if (!instance) return; const values = form.getFieldsValue(); @@ -145,19 +159,23 @@ export default function MindEditor({ id }: { id?: string }) { } console.log(result); }, - (error) => {}, + (error) => { }, `mind-thumb-${new Date().toString()}` ); }; useEffect(() => { - containerRef.current.style.height = `${Math.floor(window.innerHeight / 1.25)}px`; + + containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`; }, []); return ( -
+ +
{canEdit && taxonomies && ( -
-
+ +
{taxonomies.map((tax, index) => (
- + }
)} @@ -199,20 +220,24 @@ export default function MindEditor({ id }: { id?: string }) { onContextMenu={(e) => e.preventDefault()} /> {canEdit && instance && } - {isLoading && ( -
- -
- )} - {!post && id && !isLoading && ( -
- -
- )} -
+ { + isLoading && ( +
+ +
+ ) + } + { + !post && id && !isLoading && ( +
+ +
+ ) + } +
); } diff --git a/apps/web/src/components/common/editor/NodeMenu.tsx b/apps/web/src/components/common/editor/NodeMenu.tsx index a73603f..f762967 100755 --- a/apps/web/src/components/common/editor/NodeMenu.tsx +++ b/apps/web/src/components/common/editor/NodeMenu.tsx @@ -35,23 +35,18 @@ const NodeMenu: React.FC = ({ mind }) => { 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(() => { diff --git a/apps/web/src/components/common/editor/i18n.ts b/apps/web/src/components/common/editor/i18n.ts deleted file mode 100755 index ad60d5f..0000000 --- a/apps/web/src/components/common/editor/i18n.ts +++ /dev/null @@ -1,152 +0,0 @@ -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/uploader/ResourceShower.tsx b/apps/web/src/components/common/uploader/ResourceShower.tsx index 29e7a93..e1e1e01 100755 --- a/apps/web/src/components/common/uploader/ResourceShower.tsx +++ b/apps/web/src/components/common/uploader/ResourceShower.tsx @@ -108,15 +108,15 @@ export default function ResourcesShower({ {resource.title?.slice(0, 12) || "未命名"}

-
- +
+ {resource.url .split(".") .pop() ?.slice(0, 4) .toUpperCase()} - + {resource.meta.size && formatFileSize( resource.meta.size diff --git a/apps/web/src/components/models/course/detail/CourseDetail.tsx b/apps/web/src/components/models/course/detail/CourseDetail.tsx index 353ec4f..c33c268 100755 --- a/apps/web/src/components/models/course/detail/CourseDetail.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetail.tsx @@ -8,11 +8,7 @@ export default function CourseDetail({ id?: string; lectureId?: string; }) { - const iframeStyle = { - width: "50%", - height: "100vh", - border: "none", - }; + return ( <> diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 1e29d87..c833dc5 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -29,6 +29,7 @@ interface CourseDetailContextType { setIsHeaderVisible: (visible: boolean) => void; // 新增 canEdit?: boolean; userIsLearning?: boolean; + setUserIsLearning:(learning: boolean) => void; } interface CourseFormProviderProps { @@ -56,9 +57,14 @@ export function CourseDetailProvider({ { enabled: Boolean(editId) } ); - const userIsLearning = useMemo(() => { - return (course?.studentIds || []).includes(user?.id); - }, [user, course, isLoading]); + // const userIsLearning = useMemo(() => { + // return (course?.studentIds || []).includes(user?.id); + // }, [user, course, isLoading]); + const [userIsLearning, setUserIsLearning] = useState(false); + useEffect(()=>{ + console.log(course?.studentIds,user?.id) + setUserIsLearning((course?.studentIds || []).includes(user?.id)); + },[user, course, isLoading]) const canEdit = useMemo(() => { const isAuthor = isAuthenticated && user?.id === course?.authorId; const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST); @@ -79,16 +85,28 @@ export function CourseDetailProvider({ ); useEffect(() => { - if (lecture?.id) { + if (lectureId) { + console.log(123); + console.log(lectureId); read.mutateAsync({ data: { visitorId: user?.id || null, - postId: lecture?.id, + postId: lectureId, + type: VisitType.READED, + }, + }); + } else { + console.log(321); + console.log(editId); + read.mutateAsync({ + data: { + visitorId: user?.id || null, + postId: editId, type: VisitType.READED, }, }); } - }, [course]); + }, [editId, lectureId]); useEffect(() => { if (lectureId !== selectedLectureId) { navigate(`/course/${editId}/detail/${selectedLectureId}`); @@ -109,6 +127,7 @@ export function CourseDetailProvider({ setIsHeaderVisible, canEdit, userIsLearning, + setUserIsLearning }}> {children} diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx index 463dc8b..019dcbc 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx @@ -1,5 +1,5 @@ import { Course, TaxonomySlug } from "@nice/common"; -import React, { useContext, useMemo } from "react"; +import React, { useContext, useEffect, useMemo } from "react"; import { Image, Typography, Skeleton, Tag } from "antd"; // 引入 antd 组件 import { CourseDetailContext } from "./CourseDetailContext"; import { useNavigate, useParams } from "react-router-dom"; @@ -36,11 +36,11 @@ export const CourseDetailDescription: React.FC = () => { {!selectedLectureId && (
{ - }
{ }); } }} - className="w-full h-full absolute top-0 z-10 bg-[rgba(0,0,0,0.3)] transition-all duration-300 ease-in-out hover:bg-[rgba(0,0,0,0.7)] cursor-pointer group"> + className="absolute rounded-xl top-0 left-0 right-0 bottom-0 z-10 bg-[rgba(0,0,0,0.3)] transition-all duration-300 ease-in-out hover:bg-[rgba(0,0,0,0.7)] cursor-pointer group">
点击进入学习
@@ -70,7 +70,7 @@ export const CourseDetailDescription: React.FC = () => {
{course?.subTitle &&
{course?.subTitle}
} - +
{ // 创建滚动动画效果 diff --git a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx index 56831e4..4ac0d73 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx @@ -19,11 +19,7 @@ export default function CourseDetailLayout() { const [isSyllabusOpen, setIsSyllabusOpen] = useState(true); return (
- {/* */} - {/* 添加 Header 组件 */} - {/* 主内容区域 */} - {/* 为了防止 Header 覆盖内容,添加上边距 */}
{" "} {/* 添加这个包装 div */} diff --git a/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx b/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx index cc984e1..de577eb 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx @@ -12,11 +12,12 @@ import dayjs from "dayjs"; import CourseOperationBtns from "./JoinLearingButton"; export default function CourseDetailTitle() { - const { course } = useContext(CourseDetailContext); + const { course, lecture, selectedLectureId } = + useContext(CourseDetailContext); return (
- {course?.title} + {!selectedLectureId ? course?.title : lecture?.title}
{course?.author?.showname && ( @@ -36,15 +37,27 @@ export default function CourseDetailTitle() {
{"发布于:"} - {dayjs(course?.createdAt).format("YYYY年M月D日")} + {dayjs( + !selectedLectureId + ? course?.createdAt + : lecture?.createdAt + ).format("YYYY年M月D日")}
{"最后更新:"} - {dayjs(course?.updatedAt).format("YYYY年M月D日")} + {dayjs( + !selectedLectureId + ? course?.updatedAt + : lecture?.updatedAt + ).format("YYYY年M月D日")}
-
{`观看次数${course?.meta?.views || 0}`}
+
{`观看次数${ + !selectedLectureId + ? course?.views || 0 + : lecture?.views || 0 + }`}
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 39bce21..9f41c82 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -45,7 +45,9 @@ export const LectureItem: React.FC = ({
)}
-

{lecture.title}

+

+ {lecture.title} +

{lecture.subTitle && ( {lecture.subTitle} @@ -53,7 +55,9 @@ export const LectureItem: React.FC = ({ )}
- {lecture?.meta?.views ? lecture?.meta?.views : 0} + + {lecture?.views ? lecture?.views : 0} +
diff --git a/apps/web/src/components/models/course/detail/JoinLearingButton.tsx b/apps/web/src/components/models/course/detail/JoinLearingButton.tsx index 68c4243..942a7de 100644 --- a/apps/web/src/components/models/course/detail/JoinLearingButton.tsx +++ b/apps/web/src/components/models/course/detail/JoinLearingButton.tsx @@ -12,15 +12,17 @@ import { EditTwoTone, LoginOutlined, } from "@ant-design/icons"; +import toast from "react-hot-toast"; export default function CourseOperationBtns() { const { id } = useParams(); const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } = useAuth(); const navigate = useNavigate(); - const { course, canEdit, userIsLearning } = useContext(CourseDetailContext); + const { course, canEdit, userIsLearning, setUserIsLearning} = useContext(CourseDetailContext); const { update } = useStaff(); const [isHovered, setIsHovered] = useState(false); + const toggleLearning = async () => { if (!userIsLearning) { await update.mutateAsync({ @@ -31,7 +33,10 @@ export default function CourseOperationBtns() { }, }, }); + setUserIsLearning(true) + toast.success("加入学习成功"); } else { + await update.mutateAsync({ where: { id: user?.id }, data: { @@ -42,6 +47,8 @@ export default function CourseOperationBtns() { }, }, }); + toast.success("退出学习成功"); + setUserIsLearning(false) } }; return ( diff --git a/apps/web/src/components/models/course/detail/course-objectives.tsx b/apps/web/src/components/models/course/detail/course-objectives.tsx deleted file mode 100755 index b849eac..0000000 --- a/apps/web/src/components/models/course/detail/course-objectives.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { CheckOutlined } from '@ant-design/icons'; -import React from 'react'; -interface CourseObjectivesProps { - objectives: string[]; - title?: string; -} -const CourseObjectives: React.FC = ({ - objectives, - title = "您将会学到" -}) => { - return ( -
-

{title}

-
- {objectives.map((objective, index) => ( -
- - {objective} -
- ))} -
-
- ); -}; - -export default CourseObjectives; \ No newline at end of file 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 bc84b18..6cbfd00 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -82,7 +82,7 @@ export function CourseFormProvider({ }, [course, form]); const onSubmit = async (values: any) => { - console.log(values); + const sections = values?.sections || []; const deptIds = values?.deptIds || []; const termIds = taxonomies @@ -101,13 +101,17 @@ export function CourseFormProvider({ terms: termIds?.length > 0 ? { - set: termIds.map((id) => ({ id })), // 转换成 connect 格式 + [editId ? "set" : "connect"]: termIds.map((id) => ({ + id, + })), // 转换成 connect 格式 } : undefined, depts: deptIds?.length > 0 ? { - set: deptIds.map((id) => ({ id })), + [editId ? "set" : "connect"]: deptIds.map((id) => ({ + id, + })), } : undefined, }; @@ -149,6 +153,7 @@ export function CourseFormProvider({ } }; + return ( ({ label: value, 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 3c9f279..2f74c61 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 @@ -115,7 +115,7 @@ export const SortableLecture: React.FC = ({ resources: [videoUrlId, ...fileIds].filter(Boolean)?.length > 0 ? { - connect: [videoUrlId, ...fileIds] + set: [videoUrlId, ...fileIds] .filter(Boolean) .map((fileId) => ({ fileId, diff --git a/apps/web/src/components/models/post/PostCard.tsx b/apps/web/src/components/models/post/PostCard.tsx index aa0e2c5..eb9f51e 100644 --- a/apps/web/src/components/models/post/PostCard.tsx +++ b/apps/web/src/components/models/post/PostCard.tsx @@ -1,4 +1,4 @@ -import { Card, Typography, Button, Empty } from "antd"; +import { Typography, Button, Empty, Card } from "antd"; import { PostDto } from "@nice/common"; import DeptInfo from "@web/src/app/main/path/components/DeptInfo"; @@ -19,9 +19,9 @@ export default function PostCard({ post, onClick }: PostCardProps) { onClick={() => handleClick(post)} key={post?.id} hoverable - className="group overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" + className="group overflow-hidden rounded-2xl border border-gray-200 shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" cover={ -
+
{post?.meta?.thumbnail ? (
- +
<Button + shape="round" type="primary" 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)] diff --git a/apps/web/src/components/models/term/taxonomy-form.tsx b/apps/web/src/components/models/term/taxonomy-form.tsx index 3bdeee2..d612976 100755 --- a/apps/web/src/components/models/term/taxonomy-form.tsx +++ b/apps/web/src/components/models/term/taxonomy-form.tsx @@ -31,7 +31,7 @@ export default function TaxonomyForm() { setTaxonomyModalOpen(false) }}> <Form.Item - rules={[{ required: true, message: "请输入名称" }]} + rules={[{ required: true, message: "请输入姓名" }]} name={"name"} label="名称"> <Input></Input> diff --git a/apps/web/src/components/models/term/term-parent-selector.tsx b/apps/web/src/components/models/term/term-parent-selector.tsx index b604390..58c1765 100755 --- a/apps/web/src/components/models/term/term-parent-selector.tsx +++ b/apps/web/src/components/models/term/term-parent-selector.tsx @@ -1,50 +1,56 @@ import { api } from "@nice/client/"; -import { Checkbox, Form } from "antd"; +import { Checkbox, Skeleton } from "antd"; import { TermDto } from "@nice/common"; -import { useCallback, useEffect, useState } from "react"; +import React from "react"; export default function TermParentSelector({ - value, - onChange, - className, - placeholder = "选择分类", - multiple = true, - taxonomyId, - domainId, - style, -}: any) { - const [selectedValues, setSelectedValues] = useState<string[]>([]); // 用于存储选中的值 - const { - data, - isLoading, - }: { data: TermDto[]; isLoading: boolean } = api.term.findMany.useQuery({ - where: { - taxonomy: { - id: taxonomyId, - }, - parentId: null - }, - }); - const handleCheckboxChange = (checkedValues: string[]) => { - setSelectedValues(checkedValues); // 更新选中的值 - if (onChange) { - onChange(checkedValues); // 调用外部传入的 onChange 回调 - } - }; - return ( - <div className={className} style={style}> - <Form onFinish={null}> - <Form.Item name="categories"> - <Checkbox.Group onChange={handleCheckboxChange}> - {data?.map((category) => ( - <div className="w-full h-9 p-2 my-1"> - <Checkbox className="text-base text-slate-700" key={category.id} value={category.id}> - {category.name} - </Checkbox> - </div> - ))} - </Checkbox.Group> - </Form.Item> - </Form> - </div> - ) -} \ No newline at end of file + value, + onChange, + className, + taxonomyId, + domainId = undefined, + style, +}: { + value?: string[]; + onChange?: (value: string[]) => void; + className?: string; + taxonomyId: string; + domainId?: string; + style?: React.CSSProperties; +}) { + const { data, isLoading }: { data: TermDto[]; isLoading: boolean } = + api.term.findMany.useQuery({ + where: { + taxonomyId: taxonomyId, + parentId: null, + domainId, + }, + }); + const handleCheckboxChange = (checkedValues: string[]) => { + // setSelectedValues(checkedValues); // 更新选中的值 + if (onChange) { + onChange(checkedValues); // 调用外部传入的 onChange 回调 + } + }; + return ( + <div className={className} style={style}> + {isLoading ? ( + <Skeleton + paragraph={{ + rows: 4, + }}></Skeleton> + ) : ( + <Checkbox.Group value={value} onChange={handleCheckboxChange}> + {data?.map((category) => ( + <div className="w-full h-9 p-2 my-1" key={category.id}> + <Checkbox + className="text-base text-slate-700" + value={category.id}> + {category.name} + </Checkbox> + </div> + ))} + </Checkbox.Group> + )} + </div> + ); +} diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 0899fd2..b12f419 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -70,7 +70,9 @@ export const routes: CustomRouteObject[] = [ }, { path: "editor/:id?", - element: <PathEditorPage></PathEditorPage>, + element: <WithAuth> + <PathEditorPage></PathEditorPage> + </WithAuth>, }, ], }, @@ -86,6 +88,14 @@ export const routes: CustomRouteObject[] = [ </WithAuth> ), }, + { + path: "my-duty-path", + element: ( + <WithAuth> + <MyPathPage></MyPathPage> + </WithAuth> + ), + }, { path: "my-duty", element: ( diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 2715faf..ed02c77 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -206,6 +206,9 @@ model Post { rating Int? @default(0) students Staff[] @relation("post_student") depts Department[] @relation("post_dept") + views Int @default(0) @map("views") + hates Int @default(0) @map("hates") + likes Int @default(0) @map("likes") // 索引 // 日期时间类型字段 createdAt DateTime @default(now()) @map("created_at") @@ -238,6 +241,7 @@ model Post { @@index([type, publishedAt]) @@index([state]) @@index([level]) + @@index([views]) @@index([important]) @@map("post") } @@ -282,8 +286,8 @@ model Visit { views Int @default(1) @map("views") // sourceIP String? @map("source_ip") // 关联关系 - visitorId String @map("visitor_id") - visitor Staff @relation(fields: [visitorId], references: [id]) + visitorId String? @map("visitor_id") + visitor Staff? @relation(fields: [visitorId], references: [id]) postId String? @map("post_id") post Post? @relation(fields: [postId], references: [id]) message Message? @relation(fields: [messageId], references: [id]) diff --git a/packages/common/src/models/select.ts b/packages/common/src/models/select.ts index 8245ffe..f21f1a6 100755 --- a/packages/common/src/models/select.ts +++ b/packages/common/src/models/select.ts @@ -45,11 +45,13 @@ export const postDetailSelect: Prisma.PostSelect = { }, }, meta: true, + views: true, }; export const postUnDetailSelect: Prisma.PostSelect = { id: true, type: true, title: true, + views: true, parent: true, parentId: true, content: true, @@ -79,6 +81,7 @@ export const messageDetailSelect: Prisma.MessageSelect = { id: true, sender: true, content: true, + title: true, url: true, option: true, @@ -88,6 +91,7 @@ export const courseDetailSelect: Prisma.PostSelect = { id: true, title: true, subTitle: true, + views: true, type: true, author: true, authorId: true, @@ -124,6 +128,7 @@ export const lectureDetailSelect: Prisma.PostSelect = { subTitle: true, content: true, resources: true, + views: true, createdAt: true, updatedAt: true, // 关联表选择