+ 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 && (
<>
diff --git a/apps/web/src/app/main/layout/MainLayout.tsx b/apps/web/src/app/main/layout/MainLayout.tsx
index fa6627a..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/MainProvider.tsx b/apps/web/src/app/main/layout/MainProvider.tsx
index d928e9d..73db96a 100755
--- a/apps/web/src/app/main/layout/MainProvider.tsx
+++ b/apps/web/src/app/main/layout/MainProvider.tsx
@@ -6,6 +6,7 @@ import React, {
useMemo,
useState,
} from "react";
+import { useDebounce } from "use-debounce";
interface SelectedTerms {
[key: string]: string[]; // 每个 slug 对应一个 string 数组
}
@@ -35,7 +36,8 @@ export function MainProvider({ children }: MainProviderProps) {
PostType.COURSE | PostType.PATH | "both"
>("both");
const [showSearchMode, setShowSearchMode] = useState(false);
- const [searchValue, setSearchValue] = useState("");
+ const [searchValue, setSearchValue] = useState("");
+ const [debouncedValue] = useDebounce(searchValue, 500);
const [selectedTerms, setSelectedTerms] = useState({}); // 初始化状态
const termFilters = useMemo(() => {
return Object.entries(selectedTerms)
@@ -60,10 +62,10 @@ export function MainProvider({ children }: MainProviderProps) {
}, [termFilters]);
const searchCondition: Prisma.PostWhereInput = useMemo(() => {
const containTextCondition: Prisma.StringNullableFilter = {
- contains: searchValue,
+ contains: debouncedValue,
mode: "insensitive" as Prisma.QueryMode, // 使用类型断言
};
- return searchValue
+ return debouncedValue
? {
OR: [
{ title: containTextCondition },
@@ -79,7 +81,7 @@ export function MainProvider({ children }: MainProviderProps) {
],
}
: {};
- }, [searchValue]);
+ }, [searchValue, debouncedValue]);
return (
{
const menuItems = useMemo(() => {
const baseItems = [
{ key: "home", path: "/", label: "首页" },
- { key: "courses", path: "/courses", label: "全部课程" },
- { key: "path", path: "/path", label: "学习路径" },
+ { key: "path", path: "/path", label: "全部思维导图" },
+ { key: "courses", path: "/courses", label: "所有课程" },
];
if (!isAuthenticated) {
@@ -20,15 +20,19 @@ 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]);
- const selectedKey =
- menuItems.find((item) => item.path === pathname)?.key || "";
+ const selectedKey = useMemo(() => {
+ const normalizePath = (path: string): string => path.replace(/\/$/, "");
+ return pathname === '/' ? "home" : menuItems.find((item) => normalizePath(pathname) === item.path)?.key || "";
+ }, [pathname]);
+
return (
-
- {/* 用户信息,显示在 Avatar 右侧 */}
-
-
- {user?.showname || user?.username}
-
-
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/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 d034888..b26f721 100644
--- a/apps/web/src/app/main/path/components/TermInfo.tsx
+++ b/apps/web/src/app/main/path/components/TermInfo.tsx
@@ -1,24 +1,22 @@
import { Tag } from "antd";
-import { PostDto, TaxonomySlug } from "@nice/common";
-
-const TermInfo = ({ post }: { post: PostDto }) => {
- console.log("xx", post?.terms);
+import { PostDto, TaxonomySlug, TermDto } from "@nice/common";
+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 (
@@ -36,7 +34,7 @@ const TermInfo = ({ post }: { post: PostDto }) => {
)}
- >
+
);
};
diff --git a/apps/web/src/app/main/path/editor/page.tsx b/apps/web/src/app/main/path/editor/page.tsx
index 5ce3a20..eaed95d 100755
--- a/apps/web/src/app/main/path/editor/page.tsx
+++ b/apps/web/src/app/main/path/editor/page.tsx
@@ -2,11 +2,9 @@ import MindEditor from "@web/src/components/common/editor/MindEditor";
import { useParams } from "react-router-dom";
export default function PathEditorPage() {
- const { id } = useParams();
+ const { id } = useParams();
- return (
-
-
-
- );
+ return
+
+
}
diff --git a/apps/web/src/components/common/container/CollapsibleContent.tsx b/apps/web/src/components/common/container/CollapsibleContent.tsx
index 80b9756..7480439 100755
--- a/apps/web/src/components/common/container/CollapsibleContent.tsx
+++ b/apps/web/src/components/common/container/CollapsibleContent.tsx
@@ -10,7 +10,7 @@ const CollapsibleContent: React.FC = ({ content }) => {
const contentWrapperRef = useRef(null);
return (
-
+
{/* 包装整个内容区域的容器 */}
{/* 内容区域 */}
diff --git a/apps/web/src/components/common/editor/MindEditor.tsx b/apps/web/src/components/common/editor/MindEditor.tsx
index 032bb2f..dfcff70 100755
--- a/apps/web/src/components/common/editor/MindEditor.tsx
+++ b/apps/web/src/components/common/editor/MindEditor.tsx
@@ -1,67 +1,45 @@
-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,
postDetailSelect,
- PostDto,
PostType,
Prisma,
- Taxonomy,
+ RolePerms,
+ VisitType,
} from "@nice/common";
import TermSelect from "../../models/term/term-select";
import DepartmentSelect from "../../models/department/department-select";
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useMemo, 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",
- },
- },
-};
+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);
-
- //根据id 查询post,以获取相关信息。第一条信息?
- const { data: post, isLoading }: { data: PostDto; isLoading: boolean } =
- api.post.findFirst.useQuery({
- where: {
- id,
+ const { isAuthenticated, user, hasSomePermissions } = useAuth();
+ const { read } = useVisitor();
+ const { data: post, isLoading }: { data: PathDto; isLoading: boolean } =
+ api.post.findFirst.useQuery(
+ {
+ where: {
+ id,
+ },
+ select: postDetailSelect,
},
- select: postDetailSelect,
- });
+ { enabled: Boolean(id) }
+ );
+ const canEdit: boolean = useMemo(() => {
+ 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({
@@ -69,9 +47,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 = {
@@ -79,30 +67,42 @@ 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;
+ },
+ },
+ draggable: canEdit, // 禁用拖拽
+ contextMenu: canEdit, // 禁用右键菜单
+ toolBar: canEdit, // 禁用工具栏
+ 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();
@@ -167,16 +167,15 @@ export default function MindEditor({ id }: { id?: string }) {
`mind-thumb-${new Date().toString()}`
);
};
+ useEffect(() => {
+ containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`;
+ }, []);
+
return (
-