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..5e9a116 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,32 +182,9 @@ export class PostService extends BaseTreeService { page?: number; pageSize?: number; where?: Prisma.PostWhereInput; + orderBy?: OrderByArgs<(typeof db.post)['findMany']>; select?: Prisma.PostSelect; - }): Promise<{ - items: { - id: string; - type: string | null; - level: string | null; - state: string | null; - title: string | null; - subTitle: string | null; - content: string | null; - important: boolean | null; - domainId: string | null; - order: number | null; - duration: number | null; - rating: number | null; - createdAt: Date; - publishedAt: Date | null; - updatedAt: Date; - deletedAt: Date | null; - authorId: string | null; - parentId: string | null; - hasChildren: boolean | null; - meta: Prisma.JsonValue | null; - }[]; - totalPages: number; - }> { + }) { // super.updateOrder; return super.findManyWithPagination(args); } diff --git a/apps/web/package.json b/apps/web/package.json index 1456e66..f10e338 100755 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -36,7 +36,6 @@ "@nice/iconer": "workspace:^", "@nice/utils": "workspace:^", "mind-elixir": "workspace:^", - "@mind-elixir/node-menu": "workspace:*", "@nice/ui": "workspace:^", "@tanstack/query-async-storage-persister": "^5.51.9", "@tanstack/react-query": "^5.51.21", 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/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/layout/BasePost/FilterSection.tsx b/apps/web/src/app/main/layout/BasePost/FilterSection.tsx index 25575d1..9742e47 100755 --- a/apps/web/src/app/main/layout/BasePost/FilterSection.tsx +++ b/apps/web/src/app/main/layout/BasePost/FilterSection.tsx @@ -14,7 +14,7 @@ export default function FilterSection() { }); }; return ( -
+
{showSearchMode && } {taxonomies?.map((tax, index) => { const items = Object.entries(selectedTerms).find( diff --git a/apps/web/src/app/main/layout/BasePost/SearchModeRadio.tsx b/apps/web/src/app/main/layout/BasePost/SearchModeRadio.tsx index 692bfe2..507e94f 100644 --- a/apps/web/src/app/main/layout/BasePost/SearchModeRadio.tsx +++ b/apps/web/src/app/main/layout/BasePost/SearchModeRadio.tsx @@ -11,14 +11,14 @@ export default function SearchModeRadio() { return ( -

搜索模式

+

只搜索

- 课程 - 路径 - 全部 + 视频课程 + 思维导图 + 所有资源
); diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx index 1735bbd..deb6b62 100755 --- a/apps/web/src/app/main/layout/MainHeader.tsx +++ b/apps/web/src/app/main/layout/MainHeader.tsx @@ -30,44 +30,44 @@ export function MainHeader() {
- {/* 中间搜索区域 - 允许适当收缩但保持可用性 */} -
- - } - placeholder="搜索课程" - className="w-full md:w-96 rounded-full" - value={searchValue} - onClick={(e) => { - if (!window.location.pathname.startsWith("/search")) { - navigate(`/search`); - window.scrollTo({ - top: 0, - behavior: "smooth", - }); - } - }} - onChange={(e) => setSearchValue(e.target.value)} - onPressEnter={(e) => { - if (!window.location.pathname.startsWith("/search")) { - navigate(`/search`); - window.scrollTo({ - top: 0, - behavior: "smooth", - }); - } - }} - /> -
{/* 右侧区域 - 可以灵活收缩 */}
+ + } + placeholder="搜索课程" + className="w-full md:w-96 rounded-full" + value={searchValue} + onClick={(e) => { + if (!window.location.pathname.startsWith("/search")) { + navigate(`/search`); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + } + }} + onChange={(e) => setSearchValue(e.target.value)} + onPressEnter={(e) => { + if (!window.location.pathname.startsWith("/search")) { + navigate(`/search`); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + } + }} + /> {isAuthenticated && ( <> )} {isAuthenticated ? ( ) : ( diff --git a/apps/web/src/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx index d7aa53f..3b159e2 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/my-duty-path/components/MyDutyPathContainer.tsx b/apps/web/src/app/main/my-duty-path/components/MyDutyPathContainer.tsx new file mode 100644 index 0000000..148706e --- /dev/null +++ b/apps/web/src/app/main/my-duty-path/components/MyDutyPathContainer.tsx @@ -0,0 +1,30 @@ +import PostList from "@web/src/components/models/course/list/PostList"; +import { useAuth } from "@web/src/providers/auth-provider"; +import { useMainContext } from "../../layout/MainProvider"; +import { PostType } from "@nice/common"; +import PathCard from "@web/src/components/models/post/SubPost/PathCard"; + +export default function MyLearningListContainer() { + const { user } = useAuth(); + const { searchCondition, termsCondition } = useMainContext(); + return ( + <> + } + 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/components/common/editor/MindEditor.tsx b/apps/web/src/components/common/editor/MindEditor.tsx index 087830d..7b95363 100755 --- a/apps/web/src/components/common/editor/MindEditor.tsx +++ b/apps/web/src/components/common/editor/MindEditor.tsx @@ -1,14 +1,14 @@ import { Button, Card, Empty, Form, Space, Spin, message, theme } from "antd"; import NodeMenu from "./NodeMenu"; -import { api, usePost } from "@nice/client"; +import { api, usePost, useVisitor } from "@nice/client"; import { ObjectType, + PathDto, postDetailSelect, - PostDto, PostType, Prisma, RolePerms, - Taxonomy, + VisitType, } from "@nice/common"; import TermSelect from "../../models/term/term-select"; import DepartmentSelect from "../../models/department/department-select"; @@ -19,55 +19,26 @@ import MindElixir from "mind-elixir"; import { useTusUpload } from "@web/src/hooks/useTusUpload"; import { useNavigate } from "react-router-dom"; import { useAuth } from "@web/src/providers/auth-provider"; -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", - }, - }, -}; +import { MIND_OPTIONS } from "./constant"; +import { SaveOutlined } from "@ant-design/icons"; export default function MindEditor({ id }: { id?: string }) { + //containerRef 容器ref instance 实例 const containerRef = useRef(null); const [instance, setInstance] = useState(null); const { isAuthenticated, user, hasSomePermissions } = useAuth(); - const { data: post, isLoading }: { data: PostDto; isLoading: boolean } = + 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 !Boolean(id) || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST); - }, [user]) + const isAuth = isAuthenticated && user?.id === post?.author?.id; + return !!id || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST); + }, [user]); const navigate = useNavigate(); const { create, update } = usePost(); const { data: taxonomies } = api.taxonomy.getAll.useQuery({ @@ -75,9 +46,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 = { @@ -85,21 +66,20 @@ 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]); - useEffect(() => { if (!containerRef.current) return; const mind = new MindElixir({ ...MIND_OPTIONS, el: containerRef.current, - before:{ - beginEdit(){ - return canEdit - } + before: { + beginEdit() { + return canEdit; + }, }, draggable: canEdit, // 禁用拖拽 contextMenu: canEdit, // 禁用右键菜单 @@ -107,17 +87,21 @@ 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(() => { if ((!id || post) && instance) { containerRef.current.hidden = false; instance.toCenter(); - instance.refresh((post as any)?.meta); + if (post?.meta?.nodeData) { + instance.refresh(post?.meta); + } } }, [id, post, instance]); + //保存 按钮 函数 const handleSave = async () => { if (!instance) return; const values = form.getFieldsValue(); @@ -181,10 +165,10 @@ export default function MindEditor({ id }: { id?: string }) { `mind-thumb-${new Date().toString()}` ); }; - useEffect(()=>{ - containerRef.current.style.height = `${Math.floor(window.innerHeight/1.25)}px` - },[]) - + useEffect(() => { + containerRef.current.style.height = `${Math.floor(window.innerHeight / 1.25)}px`; + }, []); + return (
@@ -202,6 +186,7 @@ export default function MindEditor({ id }: { id?: string }) { // rules={[{ required: true }]} noStyle>
- + }
)} -
e.preventDefault()}/> +
e.preventDefault()} + /> {canEdit && instance && } - {isLoading && ( -
- -
- )} - {!post && id && !isLoading && ( -
- -
- )} -
+ { + isLoading && ( +
+ +
+ ) + } + { + !post && id && !isLoading && ( +
+ +
+ ) + } +
); } diff --git a/apps/web/src/components/common/editor/constant.ts b/apps/web/src/components/common/editor/constant.ts new file mode 100644 index 0000000..29e6890 --- /dev/null +++ b/apps/web/src/components/common/editor/constant.ts @@ -0,0 +1,34 @@ +import MindElixir from "mind-elixir"; +export 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", + }, + }, +}; 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/CourseDetailDisplayArea.tsx b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx index cfcea17..7d89f1a 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx @@ -7,25 +7,10 @@ import { Course, LectureType, PostType } from "@nice/common"; import { CourseDetailContext } from "./CourseDetailContext"; import CollapsibleContent from "@web/src/components/common/container/CollapsibleContent"; import { Skeleton } from "antd"; -import { CoursePreview } from "./CoursePreview/CoursePreview"; import ResourcesShower from "@web/src/components/common/uploader/ResourceShower"; -import { - BookOutlined, - CalendarOutlined, - EditTwoTone, - EyeOutlined, - ReloadOutlined, -} from "@ant-design/icons"; -import dayjs from "dayjs"; import { useNavigate } from "react-router-dom"; import CourseDetailTitle from "./CourseDetailTitle"; -// interface CourseDetailDisplayAreaProps { -// // course: Course; -// // videoSrc?: string; -// // videoPoster?: string; -// // isLoading?: boolean; -// } export const CourseDetailDisplayArea: React.FC = () => { // 创建滚动动画效果 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..90228ad 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx @@ -44,7 +44,7 @@ export default function CourseDetailTitle() {
-
{`观看次数${course?.meta?.views || 0}`}
+
{`播放次数${course?.meta?.views || 0}`}
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..90bcd04 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 @@ -149,6 +149,7 @@ export function CourseFormProvider({ } }; + return ( ({ label: value, diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 7740621..b12f419 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -88,6 +88,14 @@ export const routes: CustomRouteObject[] = [ ), }, + { + path: "my-duty-path", + element: ( + + + + ), + }, { path: "my-duty", element: ( diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index db764d2..6bcd44b 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -206,6 +206,7 @@ model Post { rating Int? @default(0) students Staff[] @relation("post_student") depts Department[] @relation("post_dept") + views Int @default(0) @map("views") // 索引 // 日期时间类型字段 createdAt DateTime @default(now()) @map("created_at") @@ -226,8 +227,6 @@ model Post { ancestors PostAncestry[] @relation("DescendantPosts") descendants PostAncestry[] @relation("AncestorPosts") resources Resource[] // 附件列表 - // watchableStaffs Staff[] @relation("post_watch_staff") - // watchableDepts Department[] @relation("post_watch_dept") // 可观看的部门列表,关联 Department 模型 meta Json? // 封面url 视频url objectives具体的学习目标 rating评分Int // 索引 @@ -240,6 +239,7 @@ model Post { @@index([type, publishedAt]) @@index([state]) @@index([level]) + @@index([views]) @@index([important]) @@map("post") }