Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
711ab6290e
|
@ -8,6 +8,7 @@ import {
|
||||||
DelegateFuncs,
|
DelegateFuncs,
|
||||||
UpdateOrderArgs,
|
UpdateOrderArgs,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
|
OrderByArgs,
|
||||||
SelectArgs,
|
SelectArgs,
|
||||||
} from './base.type';
|
} from './base.type';
|
||||||
import {
|
import {
|
||||||
|
@ -450,9 +451,10 @@ export class BaseService<
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
where?: WhereArgs<A['findMany']>;
|
where?: WhereArgs<A['findMany']>;
|
||||||
|
orderBy?: OrderByArgs<A['findMany']>;
|
||||||
select?: SelectArgs<A['findMany']>;
|
select?: SelectArgs<A['findMany']>;
|
||||||
}): Promise<{ items: R['findMany']; totalPages: number }> {
|
}): Promise<{ items: R['findMany']; totalPages: number }> {
|
||||||
const { page = 1, pageSize = 10, where, select } = args;
|
const { page = 1, pageSize = 10, where, select, orderBy } = args;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取总记录数
|
// 获取总记录数
|
||||||
|
@ -461,6 +463,7 @@ export class BaseService<
|
||||||
const items = (await this.getModel().findMany({
|
const items = (await this.getModel().findMany({
|
||||||
where,
|
where,
|
||||||
select,
|
select,
|
||||||
|
orderBy,
|
||||||
skip: (page - 1) * pageSize,
|
skip: (page - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
} as any)) as R['findMany'];
|
} as any)) as R['findMany'];
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { BaseTreeService } from '../base/base.tree.service';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { DefaultArgs } from '@prisma/client/runtime/library';
|
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { OrderByArgs } from '../base/base.type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
||||||
|
@ -181,32 +182,9 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
where?: Prisma.PostWhereInput;
|
where?: Prisma.PostWhereInput;
|
||||||
|
orderBy?: OrderByArgs<(typeof db.post)['findMany']>;
|
||||||
select?: Prisma.PostSelect<DefaultArgs>;
|
select?: Prisma.PostSelect<DefaultArgs>;
|
||||||
}): 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;
|
// super.updateOrder;
|
||||||
return super.findManyWithPagination(args);
|
return super.findManyWithPagination(args);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,6 @@
|
||||||
"@nice/iconer": "workspace:^",
|
"@nice/iconer": "workspace:^",
|
||||||
"@nice/utils": "workspace:^",
|
"@nice/utils": "workspace:^",
|
||||||
"mind-elixir": "workspace:^",
|
"mind-elixir": "workspace:^",
|
||||||
"@mind-elixir/node-menu": "workspace:*",
|
|
||||||
"@nice/ui": "workspace:^",
|
"@nice/ui": "workspace:^",
|
||||||
"@tanstack/query-async-storage-persister": "^5.51.9",
|
"@tanstack/query-async-storage-persister": "^5.51.9",
|
||||||
"@tanstack/react-query": "^5.51.21",
|
"@tanstack/react-query": "^5.51.21",
|
||||||
|
|
|
@ -3,6 +3,5 @@ import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
export function CourseDetailPage() {
|
export function CourseDetailPage() {
|
||||||
const { id, lectureId } = useParams();
|
const { id, lectureId } = useParams();
|
||||||
console.log("Course ID:", id);
|
|
||||||
return <CourseDetail id={id} lectureId={lectureId}></CourseDetail>;
|
return <CourseDetail id={id} lectureId={lectureId}></CourseDetail>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ const HeroSection = () => {
|
||||||
{
|
{
|
||||||
icon: <EyeOutlined />,
|
icon: <EyeOutlined />,
|
||||||
value: statistics.reads,
|
value: statistics.reads,
|
||||||
label: "观看次数",
|
label: "播放次数",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [statistics]);
|
}, [statistics]);
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default function FilterSection() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className=" flex z-0 p-6 flex-col rounded-lg mt-4 space-y-6 h-[820px] overscroll-contain overflow-x-hidden">
|
<div className=" flex z-0 p-6 flex-col mt-4 space-y-6 overscroll-contain overflow-x-hidden">
|
||||||
{showSearchMode && <SearchModeRadio></SearchModeRadio>}
|
{showSearchMode && <SearchModeRadio></SearchModeRadio>}
|
||||||
{taxonomies?.map((tax, index) => {
|
{taxonomies?.map((tax, index) => {
|
||||||
const items = Object.entries(selectedTerms).find(
|
const items = Object.entries(selectedTerms).find(
|
||||||
|
|
|
@ -11,14 +11,14 @@ export default function SearchModeRadio() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" align="start" className="mb-2">
|
<Space direction="vertical" align="start" className="mb-2">
|
||||||
<h3 className="text-lg font-medium mb-4">搜索模式</h3>
|
<h3 className="text-lg font-medium mb-4">只搜索</h3>
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
value={searchMode}
|
value={searchMode}
|
||||||
onChange={handleModeChange}
|
onChange={handleModeChange}
|
||||||
buttonStyle="solid">
|
buttonStyle="solid">
|
||||||
<Radio.Button value={PostType.COURSE}>课程</Radio.Button>
|
<Radio.Button value={PostType.COURSE}>视频课程</Radio.Button>
|
||||||
<Radio.Button value={PostType.PATH}>路径</Radio.Button>
|
<Radio.Button value={PostType.PATH}>思维导图</Radio.Button>
|
||||||
<Radio.Button value="both">全部</Radio.Button>
|
<Radio.Button value="both">所有资源</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|
|
@ -30,8 +30,10 @@ export function MainHeader() {
|
||||||
<NavigationMenu />
|
<NavigationMenu />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 中间搜索区域 - 允许适当收缩但保持可用性 */}
|
|
||||||
<div className="mx-4 flex-shrink md:flex-shrink-0 md:w-auto w-auto">
|
{/* 右侧区域 - 可以灵活收缩 */}
|
||||||
|
<div className="flex justify-end gap-2 md:gap-4 flex-shrink">
|
||||||
|
<div className="flex items-center gap-2 md:gap-4">
|
||||||
<Input
|
<Input
|
||||||
size="large"
|
size="large"
|
||||||
prefix={
|
prefix={
|
||||||
|
@ -60,14 +62,12 @@ export function MainHeader() {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧区域 - 可以灵活收缩 */}
|
|
||||||
<div className="flex justify-end gap-2 md:gap-4 flex-shrink">
|
|
||||||
<div className="flex items-center gap-2 md:gap-4">
|
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
icon={<PlusOutlined></PlusOutlined>}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const url = id
|
const url = id
|
||||||
? `/course/${id}/editor`
|
? `/course/${id}/editor`
|
||||||
|
@ -82,19 +82,25 @@ export function MainHeader() {
|
||||||
)}
|
)}
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<Button
|
<Button
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.location.href = "/path/editor";
|
window.location.href = "/path/editor";
|
||||||
}}
|
}}
|
||||||
|
ghost type="primary"
|
||||||
icon={<PlusOutlined></PlusOutlined>}>
|
icon={<PlusOutlined></PlusOutlined>}>
|
||||||
创建学习路径
|
创建思维导图
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<UserMenu />
|
<UserMenu />
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
onClick={() => navigate("/login")}
|
onClick={() => navigate("/login")}
|
||||||
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
|
|
||||||
icon={<UserOutlined />}>
|
icon={<UserOutlined />}>
|
||||||
登录
|
登录
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -11,8 +11,8 @@ export const NavigationMenu = () => {
|
||||||
const menuItems = useMemo(() => {
|
const menuItems = useMemo(() => {
|
||||||
const baseItems = [
|
const baseItems = [
|
||||||
{ key: "home", path: "/", label: "首页" },
|
{ key: "home", path: "/", label: "首页" },
|
||||||
{ key: "path", path: "/path", label: "学习路径" },
|
{ key: "path", path: "/path", label: "全部思维导图" },
|
||||||
{ key: "courses", path: "/courses", label: "全部课程" },
|
{ key: "courses", path: "/courses", label: "所有课程" },
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
|
@ -20,9 +20,10 @@ export const NavigationMenu = () => {
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
...baseItems,
|
...baseItems,
|
||||||
{ key: "my-duty", path: "/my-duty", label: "我的授课" },
|
{ key: "my-duty", path: "/my-duty", label: "我创建的课程" },
|
||||||
{ key: "my-learning", path: "/my-learning", label: "我的课程" },
|
{ key: "my-learning", path: "/my-learning", label: "我学习的课程" },
|
||||||
{ key: "my-path", path: "/my-path", label: "我的路径" },
|
{ key: "my-duty-path", path: "/my-duty-path", label: "我创建的思维导图" },
|
||||||
|
{ key: "my-path", path: "/my-path", label: "我学习的思维导图" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<PostList
|
||||||
|
renderItem={(post) => <PathCard post={post}></PathCard>}
|
||||||
|
params={{
|
||||||
|
pageSize: 12,
|
||||||
|
where: {
|
||||||
|
type: PostType.PATH,
|
||||||
|
students: {
|
||||||
|
some: {
|
||||||
|
id: user?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...termsCondition,
|
||||||
|
...searchCondition,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
cols={4}></PostList>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<BasePostLayout>
|
||||||
|
<MyDutyPathContainer></MyDutyPathContainer>
|
||||||
|
</BasePostLayout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,14 +1,14 @@
|
||||||
import { Button, Card, Empty, Form, Space, Spin, message, theme } from "antd";
|
import { Button, Card, Empty, Form, Space, Spin, message, theme } from "antd";
|
||||||
import NodeMenu from "./NodeMenu";
|
import NodeMenu from "./NodeMenu";
|
||||||
import { api, usePost } from "@nice/client";
|
import { api, usePost, useVisitor } from "@nice/client";
|
||||||
import {
|
import {
|
||||||
ObjectType,
|
ObjectType,
|
||||||
|
PathDto,
|
||||||
postDetailSelect,
|
postDetailSelect,
|
||||||
PostDto,
|
|
||||||
PostType,
|
PostType,
|
||||||
Prisma,
|
Prisma,
|
||||||
RolePerms,
|
RolePerms,
|
||||||
Taxonomy,
|
VisitType,
|
||||||
} from "@nice/common";
|
} from "@nice/common";
|
||||||
import TermSelect from "../../models/term/term-select";
|
import TermSelect from "../../models/term/term-select";
|
||||||
import DepartmentSelect from "../../models/department/department-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 { useTusUpload } from "@web/src/hooks/useTusUpload";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
const MIND_OPTIONS = {
|
import { MIND_OPTIONS } from "./constant";
|
||||||
direction: MindElixir.SIDE,
|
import { SaveOutlined } from "@ant-design/icons";
|
||||||
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 }) {
|
export default function MindEditor({ id }: { id?: string }) {
|
||||||
|
//containerRef 容器ref instance 实例
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [instance, setInstance] = useState<MindElixirInstance | null>(null);
|
const [instance, setInstance] = useState<MindElixirInstance | null>(null);
|
||||||
const { isAuthenticated, user, hasSomePermissions } = useAuth();
|
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({
|
api.post.findFirst.useQuery({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
select: postDetailSelect,
|
select: postDetailSelect,
|
||||||
});
|
}, { enabled: Boolean(id) });
|
||||||
const canEdit: boolean = useMemo(() => {
|
const canEdit: boolean = useMemo(() => {
|
||||||
//登录了且是作者、超管、无id新建模式
|
//登录了且是作者、超管、无id新建模式
|
||||||
const isAuth = isAuthenticated && user?.id == post?.author.id
|
const isAuth = isAuthenticated && user?.id === post?.author?.id;
|
||||||
return !Boolean(id) || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST);
|
return !!id || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST);
|
||||||
}, [user])
|
}, [user]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { create, update } = usePost();
|
const { create, update } = usePost();
|
||||||
const { data: taxonomies } = api.taxonomy.getAll.useQuery({
|
const { data: taxonomies } = api.taxonomy.getAll.useQuery({
|
||||||
|
@ -75,9 +46,19 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
});
|
});
|
||||||
const { handleFileUpload } = useTusUpload();
|
const { handleFileUpload } = useTusUpload();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
useEffect(() => {
|
||||||
|
if (post?.id && id) {
|
||||||
|
read.mutateAsync({
|
||||||
|
data: {
|
||||||
|
visitorId: user?.id || null,
|
||||||
|
postId: post?.id,
|
||||||
|
type: VisitType.READED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [post]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (post && form && instance && id) {
|
if (post && form && instance && id) {
|
||||||
console.log(post);
|
|
||||||
instance.refresh((post as any).meta);
|
instance.refresh((post as any).meta);
|
||||||
const deptIds = (post?.depts || [])?.map((dept) => dept.id);
|
const deptIds = (post?.depts || [])?.map((dept) => dept.id);
|
||||||
const formData = {
|
const formData = {
|
||||||
|
@ -85,21 +66,20 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
deptIds: deptIds,
|
deptIds: deptIds,
|
||||||
};
|
};
|
||||||
post.terms?.forEach((term) => {
|
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);
|
form.setFieldsValue(formData);
|
||||||
}
|
}
|
||||||
}, [post, form, instance, id]);
|
}, [post, form, instance, id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
const mind = new MindElixir({
|
const mind = new MindElixir({
|
||||||
...MIND_OPTIONS,
|
...MIND_OPTIONS,
|
||||||
el: containerRef.current,
|
el: containerRef.current,
|
||||||
before:{
|
before: {
|
||||||
beginEdit(){
|
beginEdit() {
|
||||||
return canEdit
|
return canEdit;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
draggable: canEdit, // 禁用拖拽
|
draggable: canEdit, // 禁用拖拽
|
||||||
contextMenu: canEdit, // 禁用右键菜单
|
contextMenu: canEdit, // 禁用右键菜单
|
||||||
|
@ -107,17 +87,21 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
nodeMenu: canEdit, // 禁用节点右键菜单
|
nodeMenu: canEdit, // 禁用节点右键菜单
|
||||||
keypress: canEdit, // 禁用键盘快捷键
|
keypress: canEdit, // 禁用键盘快捷键
|
||||||
});
|
});
|
||||||
mind.init(MindElixir.new("新学习路径"));
|
mind.init(MindElixir.new("新思维导图"));
|
||||||
containerRef.current.hidden = true;
|
containerRef.current.hidden = true;
|
||||||
|
//挂载实例
|
||||||
setInstance(mind);
|
setInstance(mind);
|
||||||
}, [canEdit]);
|
}, [canEdit]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((!id || post) && instance) {
|
if ((!id || post) && instance) {
|
||||||
containerRef.current.hidden = false;
|
containerRef.current.hidden = false;
|
||||||
instance.toCenter();
|
instance.toCenter();
|
||||||
instance.refresh((post as any)?.meta);
|
if (post?.meta?.nodeData) {
|
||||||
|
instance.refresh(post?.meta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [id, post, instance]);
|
}, [id, post, instance]);
|
||||||
|
//保存 按钮 函数
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!instance) return;
|
if (!instance) return;
|
||||||
const values = form.getFieldsValue();
|
const values = form.getFieldsValue();
|
||||||
|
@ -181,9 +165,9 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
`mind-thumb-${new Date().toString()}`
|
`mind-thumb-${new Date().toString()}`
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
useEffect(()=>{
|
useEffect(() => {
|
||||||
containerRef.current.style.height = `${Math.floor(window.innerHeight/1.25)}px`
|
containerRef.current.style.height = `${Math.floor(window.innerHeight / 1.25)}px`;
|
||||||
},[])
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
|
@ -202,6 +186,7 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
// rules={[{ required: true }]}
|
// rules={[{ required: true }]}
|
||||||
noStyle>
|
noStyle>
|
||||||
<TermSelect
|
<TermSelect
|
||||||
|
disabled={!canEdit}
|
||||||
className=" w-48"
|
className=" w-48"
|
||||||
placeholder={`请选择${tax.name}`}
|
placeholder={`请选择${tax.name}`}
|
||||||
taxonomyId={tax.id}
|
taxonomyId={tax.id}
|
||||||
|
@ -213,34 +198,48 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
name="deptIds"
|
name="deptIds"
|
||||||
noStyle>
|
noStyle>
|
||||||
<DepartmentSelect
|
<DepartmentSelect
|
||||||
|
disabled={!canEdit}
|
||||||
className="w-96"
|
className="w-96"
|
||||||
placeholder="请选择制作单位"
|
placeholder="请选择制作单位"
|
||||||
multiple
|
multiple
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
<Button ghost type="primary" onSubmit={(e) => e.preventDefault()} onClick={handleSave}>
|
{canEdit && <Button
|
||||||
|
ghost
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined></SaveOutlined>}
|
||||||
|
onSubmit={(e) => e.preventDefault()}
|
||||||
|
onClick={handleSave}>
|
||||||
{id ? "更新" : "保存"}
|
{id ? "更新" : "保存"}
|
||||||
</Button>
|
</Button>}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
<div ref={containerRef} className="w-full" onContextMenu={(e)=>e.preventDefault()}/>
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="w-full"
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
/>
|
||||||
{canEdit && instance && <NodeMenu mind={instance} />}
|
{canEdit && instance && <NodeMenu mind={instance} />}
|
||||||
{isLoading && (
|
{
|
||||||
|
isLoading && (
|
||||||
<div
|
<div
|
||||||
className="py-64 justify-center flex"
|
className="py-64 justify-center flex"
|
||||||
style={{ height: "calc(100vh - 287px)" }}>
|
style={{ height: "calc(100vh - 287px)" }}>
|
||||||
<Spin size="large"></Spin>
|
<Spin size="large"></Spin>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
{!post && id && !isLoading && (
|
}
|
||||||
|
{
|
||||||
|
!post && id && !isLoading && (
|
||||||
<div
|
<div
|
||||||
className="py-64"
|
className="py-64"
|
||||||
style={{ height: "calc(100vh - 287px)" }}>
|
style={{ height: "calc(100vh - 287px)" }}>
|
||||||
<Empty></Empty>
|
<Empty></Empty>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
</div>
|
}
|
||||||
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -8,11 +8,7 @@ export default function CourseDetail({
|
||||||
id?: string;
|
id?: string;
|
||||||
lectureId?: string;
|
lectureId?: string;
|
||||||
}) {
|
}) {
|
||||||
const iframeStyle = {
|
|
||||||
width: "50%",
|
|
||||||
height: "100vh",
|
|
||||||
border: "none",
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CourseDetailProvider editId={id}>
|
<CourseDetailProvider editId={id}>
|
||||||
|
|
|
@ -7,25 +7,10 @@ import { Course, LectureType, PostType } from "@nice/common";
|
||||||
import { CourseDetailContext } from "./CourseDetailContext";
|
import { CourseDetailContext } from "./CourseDetailContext";
|
||||||
import CollapsibleContent from "@web/src/components/common/container/CollapsibleContent";
|
import CollapsibleContent from "@web/src/components/common/container/CollapsibleContent";
|
||||||
import { Skeleton } from "antd";
|
import { Skeleton } from "antd";
|
||||||
import { CoursePreview } from "./CoursePreview/CoursePreview";
|
|
||||||
import ResourcesShower from "@web/src/components/common/uploader/ResourceShower";
|
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 { useNavigate } from "react-router-dom";
|
||||||
import CourseDetailTitle from "./CourseDetailTitle";
|
import CourseDetailTitle from "./CourseDetailTitle";
|
||||||
|
|
||||||
// interface CourseDetailDisplayAreaProps {
|
|
||||||
// // course: Course;
|
|
||||||
// // videoSrc?: string;
|
|
||||||
// // videoPoster?: string;
|
|
||||||
// // isLoading?: boolean;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const CourseDetailDisplayArea: React.FC = () => {
|
export const CourseDetailDisplayArea: React.FC = () => {
|
||||||
// 创建滚动动画效果
|
// 创建滚动动画效果
|
||||||
|
|
|
@ -19,11 +19,7 @@ export default function CourseDetailLayout() {
|
||||||
const [isSyllabusOpen, setIsSyllabusOpen] = useState(true);
|
const [isSyllabusOpen, setIsSyllabusOpen] = useState(true);
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* <CourseDetailHeader /> */}
|
|
||||||
|
|
||||||
{/* 添加 Header 组件 */}
|
|
||||||
{/* 主内容区域 */}
|
|
||||||
{/* 为了防止 Header 覆盖内容,添加上边距 */}
|
|
||||||
<div className="pt-12 px-32">
|
<div className="pt-12 px-32">
|
||||||
{" "}
|
{" "}
|
||||||
{/* 添加这个包装 div */}
|
{/* 添加这个包装 div */}
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default function CourseDetailTitle() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<EyeOutlined></EyeOutlined>
|
<EyeOutlined></EyeOutlined>
|
||||||
<div>{`观看次数${course?.meta?.views || 0}`}</div>
|
<div>{`播放次数${course?.meta?.views || 0}`}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<BookOutlined />
|
<BookOutlined />
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { CheckOutlined } from '@ant-design/icons';
|
|
||||||
import React from 'react';
|
|
||||||
interface CourseObjectivesProps {
|
|
||||||
objectives: string[];
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
const CourseObjectives: React.FC<CourseObjectivesProps> = ({
|
|
||||||
objectives,
|
|
||||||
title = "您将会学到"
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
|
||||||
<h2 className="text-xl font-bold mb-4">{title}</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{objectives.map((objective, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="flex items-start space-x-3"
|
|
||||||
>
|
|
||||||
<CheckOutlined></CheckOutlined>
|
|
||||||
<span className="text-gray-700">{objective}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CourseObjectives;
|
|
|
@ -82,7 +82,7 @@ export function CourseFormProvider({
|
||||||
}, [course, form]);
|
}, [course, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: any) => {
|
const onSubmit = async (values: any) => {
|
||||||
console.log(values);
|
|
||||||
const sections = values?.sections || [];
|
const sections = values?.sections || [];
|
||||||
const deptIds = values?.deptIds || [];
|
const deptIds = values?.deptIds || [];
|
||||||
const termIds = taxonomies
|
const termIds = taxonomies
|
||||||
|
@ -149,6 +149,7 @@ export function CourseFormProvider({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CourseEditorContext.Provider
|
<CourseEditorContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|
|
@ -9,7 +9,7 @@ import DepartmentSelect from "../../../department/department-select";
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
export function CourseBasicForm() {
|
export function CourseBasicForm() {
|
||||||
// 将 CourseLevelLabel 转换为 Ant Design Select 需要的选项格式
|
// 将 CourseLevelLabel 使用 Object.entries 将 CourseLevelLabel 对象转换为键值对数组。
|
||||||
const levelOptions = Object.entries(CourseLevelLabel).map(
|
const levelOptions = Object.entries(CourseLevelLabel).map(
|
||||||
([key, value]) => ({
|
([key, value]) => ({
|
||||||
label: value,
|
label: value,
|
||||||
|
|
|
@ -88,6 +88,14 @@ export const routes: CustomRouteObject[] = [
|
||||||
</WithAuth>
|
</WithAuth>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "my-duty-path",
|
||||||
|
element: (
|
||||||
|
<WithAuth>
|
||||||
|
<MyPathPage></MyPathPage>
|
||||||
|
</WithAuth>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "my-duty",
|
path: "my-duty",
|
||||||
element: (
|
element: (
|
||||||
|
|
|
@ -206,6 +206,7 @@ model Post {
|
||||||
rating Int? @default(0)
|
rating Int? @default(0)
|
||||||
students Staff[] @relation("post_student")
|
students Staff[] @relation("post_student")
|
||||||
depts Department[] @relation("post_dept")
|
depts Department[] @relation("post_dept")
|
||||||
|
views Int @default(0) @map("views")
|
||||||
// 索引
|
// 索引
|
||||||
// 日期时间类型字段
|
// 日期时间类型字段
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
@ -226,8 +227,6 @@ model Post {
|
||||||
ancestors PostAncestry[] @relation("DescendantPosts")
|
ancestors PostAncestry[] @relation("DescendantPosts")
|
||||||
descendants PostAncestry[] @relation("AncestorPosts")
|
descendants PostAncestry[] @relation("AncestorPosts")
|
||||||
resources Resource[] // 附件列表
|
resources Resource[] // 附件列表
|
||||||
// watchableStaffs Staff[] @relation("post_watch_staff")
|
|
||||||
// watchableDepts Department[] @relation("post_watch_dept") // 可观看的部门列表,关联 Department 模型
|
|
||||||
meta Json? // 封面url 视频url objectives具体的学习目标 rating评分Int
|
meta Json? // 封面url 视频url objectives具体的学习目标 rating评分Int
|
||||||
|
|
||||||
// 索引
|
// 索引
|
||||||
|
@ -240,6 +239,7 @@ model Post {
|
||||||
@@index([type, publishedAt])
|
@@index([type, publishedAt])
|
||||||
@@index([state])
|
@@index([state])
|
||||||
@@index([level])
|
@@index([level])
|
||||||
|
@@index([views])
|
||||||
@@index([important])
|
@@index([important])
|
||||||
@@map("post")
|
@@map("post")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue