This commit is contained in:
Rao 2025-02-27 23:07:53 +08:00
commit 711ab6290e
22 changed files with 226 additions and 203 deletions

View File

@ -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'];

View File

@ -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);
} }

View File

@ -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",

View File

@ -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>;
} }

View File

@ -57,7 +57,7 @@ const HeroSection = () => {
{ {
icon: <EyeOutlined />, icon: <EyeOutlined />,
value: statistics.reads, value: statistics.reads,
label: "观看次数", label: "播放次数",
}, },
]; ];
}, [statistics]); }, [statistics]);

View File

@ -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(

View File

@ -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>
); );

View File

@ -30,44 +30,44 @@ export function MainHeader() {
<NavigationMenu /> <NavigationMenu />
</div> </div>
{/* 中间搜索区域 - 允许适当收缩但保持可用性 */}
<div className="mx-4 flex-shrink md:flex-shrink-0 md:w-auto w-auto">
<Input
size="large"
prefix={
<SearchOutlined className="text-gray-400 group-hover:text-blue-500 transition-colors" />
}
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",
});
}
}}
/>
</div>
{/* 右侧区域 - 可以灵活收缩 */} {/* 右侧区域 - 可以灵活收缩 */}
<div className="flex justify-end gap-2 md:gap-4 flex-shrink"> <div className="flex justify-end gap-2 md:gap-4 flex-shrink">
<div className="flex items-center gap-2 md:gap-4"> <div className="flex items-center gap-2 md:gap-4">
<Input
size="large"
prefix={
<SearchOutlined className="text-gray-400 group-hover:text-blue-500 transition-colors" />
}
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 && (
<> <>
<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>

View File

@ -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]);

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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 && ( {
<div isLoading && (
className="py-64 justify-center flex" <div
style={{ height: "calc(100vh - 287px)" }}> className="py-64 justify-center flex"
<Spin size="large"></Spin> style={{ height: "calc(100vh - 287px)" }}>
</div> <Spin size="large"></Spin>
)} </div>
{!post && id && !isLoading && ( )
<div }
className="py-64" {
style={{ height: "calc(100vh - 287px)" }}> !post && id && !isLoading && (
<Empty></Empty> <div
</div> className="py-64"
)} style={{ height: "calc(100vh - 287px)" }}>
</div> <Empty></Empty>
</div>
)
}
</div >
); );
} }

View File

@ -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",
},
},
};

View File

@ -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}>

View File

@ -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 = () => {
// 创建滚动动画效果 // 创建滚动动画效果

View File

@ -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 */}

View File

@ -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 />

View File

@ -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;

View File

@ -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={{

View File

@ -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,

View File

@ -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: (

View File

@ -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")
} }