Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
821bf1ec3e
|
@ -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,6 +182,7 @@ 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<{
|
}): Promise<{
|
||||||
items: {
|
items: {
|
||||||
|
@ -197,6 +199,9 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
||||||
duration: number | null;
|
duration: number | null;
|
||||||
rating: number | null;
|
rating: number | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
views: number;
|
||||||
|
hates: number;
|
||||||
|
likes: number;
|
||||||
publishedAt: Date | null;
|
publishedAt: Date | null;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
deletedAt: Date | null;
|
deletedAt: Date | null;
|
||||||
|
|
|
@ -15,13 +15,13 @@ export class VisitRouter {
|
||||||
private readonly visitService: VisitService,
|
private readonly visitService: VisitService,
|
||||||
) {}
|
) {}
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
create: this.trpc.protectProcedure
|
create: this.trpc.procedure
|
||||||
.input(VisitCreateArgsSchema)
|
.input(VisitCreateArgsSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.visitService.create(input, staff);
|
return await this.visitService.create(input, staff);
|
||||||
}),
|
}),
|
||||||
createMany: this.trpc.protectProcedure
|
createMany: this.trpc.procedure
|
||||||
.input(z.array(VisitCreateManyInputSchema))
|
.input(z.array(VisitCreateManyInputSchema))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
|
|
|
@ -9,17 +9,22 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
||||||
}
|
}
|
||||||
async create(args: Prisma.VisitCreateArgs, staff?: UserProfile) {
|
async create(args: Prisma.VisitCreateArgs, staff?: UserProfile) {
|
||||||
const { postId, lectureId, messageId } = args.data;
|
const { postId, lectureId, messageId } = args.data;
|
||||||
const visitorId = args.data.visitorId || staff?.id;
|
const visitorId = args.data?.visitorId || staff?.id;
|
||||||
let result;
|
let result;
|
||||||
|
console.log(args.data.type);
|
||||||
|
console.log(visitorId);
|
||||||
|
console.log(postId);
|
||||||
const existingVisit = await db.visit.findFirst({
|
const existingVisit = await db.visit.findFirst({
|
||||||
where: {
|
where: {
|
||||||
type: args.data.type,
|
type: args.data.type,
|
||||||
visitorId,
|
// visitorId: visitorId ? visitorId : null,
|
||||||
OR: [{ postId }, { lectureId }, { messageId }],
|
OR: [{ postId }, { messageId }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log('result', existingVisit);
|
||||||
if (!existingVisit) {
|
if (!existingVisit) {
|
||||||
result = await super.create(args);
|
result = await super.create(args);
|
||||||
|
console.log('createdResult', result);
|
||||||
} else if (args.data.type === VisitType.READED) {
|
} else if (args.data.type === VisitType.READED) {
|
||||||
result = await super.update({
|
result = await super.update({
|
||||||
where: { id: existingVisit.id },
|
where: { id: existingVisit.id },
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
export async function updateTotalCourseViewCount(type: VisitType) {
|
export async function updateTotalCourseViewCount(type: VisitType) {
|
||||||
const posts = await db.post.findMany({
|
const posts = await db.post.findMany({
|
||||||
where: {
|
where: {
|
||||||
type: { in: [PostType.COURSE, PostType.LECTURE] },
|
// type: { in: [PostType.COURSE, PostType.LECTURE,] },
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
select: { id: true, type: true },
|
select: { id: true, type: true },
|
||||||
|
@ -66,27 +66,34 @@ export async function updatePostViewCount(id: string, type: VisitType) {
|
||||||
where: { id },
|
where: { id },
|
||||||
select: { id: true, meta: true, type: true },
|
select: { id: true, meta: true, type: true },
|
||||||
});
|
});
|
||||||
|
console.log(post?.type);
|
||||||
|
console.log('updatePostViewCount');
|
||||||
const metaFieldMap = {
|
const metaFieldMap = {
|
||||||
[VisitType.READED]: 'views',
|
[VisitType.READED]: 'views',
|
||||||
[VisitType.LIKE]: 'likes',
|
[VisitType.LIKE]: 'likes',
|
||||||
[VisitType.HATE]: 'hates',
|
[VisitType.HATE]: 'hates',
|
||||||
};
|
};
|
||||||
if (post?.type === PostType.LECTURE) {
|
if (post?.type === PostType.LECTURE) {
|
||||||
const course = await db.postAncestry.findFirst({
|
const courseAncestry = await db.postAncestry.findFirst({
|
||||||
where: {
|
where: {
|
||||||
descendantId: post?.id,
|
descendantId: post?.id,
|
||||||
ancestor: {
|
ancestor: {
|
||||||
type: PostType.COURSE,
|
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 } },
|
where: { ancestorId: course.id, descendant: { type: PostType.LECTURE } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
descendantId: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const lectures = lecturesAncestry.map((ancestry) => ({
|
||||||
|
id: ancestry.descendantId,
|
||||||
|
}));
|
||||||
const courseViews = await db.visit.aggregate({
|
const courseViews = await db.visit.aggregate({
|
||||||
_sum: {
|
_sum: {
|
||||||
views: true,
|
views: true,
|
||||||
|
@ -98,9 +105,11 @@ export async function updatePostViewCount(id: string, type: VisitType) {
|
||||||
type: type,
|
type: type,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log(courseViews);
|
||||||
await db.post.update({
|
await db.post.update({
|
||||||
where: { id: course.id },
|
where: { id: course.id },
|
||||||
data: {
|
data: {
|
||||||
|
[metaFieldMap[type]]: courseViews._sum.views || 0,
|
||||||
meta: {
|
meta: {
|
||||||
...((post?.meta as any) || {}),
|
...((post?.meta as any) || {}),
|
||||||
[metaFieldMap[type]]: courseViews._sum.views || 0,
|
[metaFieldMap[type]]: courseViews._sum.views || 0,
|
||||||
|
@ -117,9 +126,11 @@ export async function updatePostViewCount(id: string, type: VisitType) {
|
||||||
type: type,
|
type: type,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log('totalViews', totalViews);
|
||||||
await db.post.update({
|
await db.post.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
|
[metaFieldMap[type]]: totalViews._sum.views || 0,
|
||||||
meta: {
|
meta: {
|
||||||
...((post?.meta as any) || {}),
|
...((post?.meta as any) || {}),
|
||||||
[metaFieldMap[type]]: totalViews._sum.views || 0,
|
[metaFieldMap[type]]: totalViews._sum.views || 0,
|
||||||
|
|
|
@ -31,6 +31,7 @@ export class GenDevService {
|
||||||
domainDepts: Record<string, Department[]> = {};
|
domainDepts: Record<string, Department[]> = {};
|
||||||
staffs: Staff[] = [];
|
staffs: Staff[] = [];
|
||||||
deptGeneratedCount = 0;
|
deptGeneratedCount = 0;
|
||||||
|
courseGeneratedCount = 1;
|
||||||
constructor(
|
constructor(
|
||||||
private readonly appConfigService: AppConfigService,
|
private readonly appConfigService: AppConfigService,
|
||||||
|
|
||||||
|
@ -194,8 +195,9 @@ export class GenDevService {
|
||||||
cate.id,
|
cate.id,
|
||||||
randomLevelId,
|
randomLevelId,
|
||||||
);
|
);
|
||||||
|
this.courseGeneratedCount++;
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Generated ${this.deptGeneratedCount}/${total} departments`,
|
`Generated ${this.courseGeneratedCount}/${total} course`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ const CategorySection = () => {
|
||||||
|
|
||||||
const handleMouseClick = useCallback((categoryId: string) => {
|
const handleMouseClick = useCallback((categoryId: string) => {
|
||||||
setSelectedTerms({
|
setSelectedTerms({
|
||||||
|
...selectedTerms,
|
||||||
[TaxonomySlug.CATEGORY]: [categoryId],
|
[TaxonomySlug.CATEGORY]: [categoryId],
|
||||||
});
|
});
|
||||||
navigate("/courses");
|
navigate("/courses");
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import React, { useState, useMemo } from "react";
|
import React, { useState, useMemo, ReactNode } from "react";
|
||||||
import { Typography, Skeleton } from "antd";
|
import { Typography, Skeleton } from "antd";
|
||||||
import { TaxonomySlug, TermDto } from "@nice/common";
|
import { TaxonomySlug, TermDto } from "@nice/common";
|
||||||
import { api } from "@nice/client";
|
import { api } from "@nice/client";
|
||||||
import { CoursesSectionTag } from "./CoursesSectionTag";
|
import { CoursesSectionTag } from "./CoursesSectionTag";
|
||||||
import LookForMore from "./LookForMore";
|
import LookForMore from "./LookForMore";
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
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 {
|
interface GetTaxonomyProps {
|
||||||
categories: string[];
|
categories: string[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
@ -35,11 +33,17 @@ interface CoursesSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
initialVisibleCoursesCount?: number;
|
initialVisibleCoursesCount?: number;
|
||||||
|
postType:string;
|
||||||
|
render?:(post)=>ReactNode;
|
||||||
|
to:string
|
||||||
}
|
}
|
||||||
const CoursesSection: React.FC<CoursesSectionProps> = ({
|
const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
initialVisibleCoursesCount = 8,
|
initialVisibleCoursesCount = 8,
|
||||||
|
postType,
|
||||||
|
render,
|
||||||
|
to
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>("全部");
|
const [selectedCategory, setSelectedCategory] = useState<string>("全部");
|
||||||
const gateGory: GetTaxonomyProps = useGetTaxonomy({
|
const gateGory: GetTaxonomyProps = useGetTaxonomy({
|
||||||
|
@ -83,7 +87,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<PostList
|
<PostList
|
||||||
renderItem={(post) => <CourseCard post={post}></CourseCard>}
|
renderItem={(post) => render(post)}
|
||||||
params={{
|
params={{
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: initialVisibleCoursesCount,
|
pageSize: initialVisibleCoursesCount,
|
||||||
|
@ -95,11 +99,12 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {},
|
: {},
|
||||||
|
type: postType
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
showPagination={false}
|
showPagination={false}
|
||||||
cols={4}></PostList>
|
cols={4}></PostList>
|
||||||
<LookForMore to={"/courses"}></LookForMore>
|
<LookForMore to={to}></LookForMore>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
@ -57,7 +57,7 @@ const HeroSection = () => {
|
||||||
{
|
{
|
||||||
icon: <EyeOutlined />,
|
icon: <EyeOutlined />,
|
||||||
value: statistics.reads,
|
value: statistics.reads,
|
||||||
label: "观看次数",
|
label: "播放次数",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [statistics]);
|
}, [statistics]);
|
||||||
|
|
|
@ -11,7 +11,10 @@ export default function LookForMore({to}:{to:string}) {
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
onClick={() => navigate(to)}
|
onClick={() => {
|
||||||
|
navigate(to)
|
||||||
|
window.scrollTo({top: 0,behavior: "smooth"});
|
||||||
|
}}
|
||||||
className="flex items-center gap-2 text-gray-600 hover:text-blue-600 font-medium transition-colors duration-300">
|
className="flex items-center gap-2 text-gray-600 hover:text-blue-600 font-medium transition-colors duration-300">
|
||||||
查看更多
|
查看更多
|
||||||
<ArrowRightOutlined />
|
<ArrowRightOutlined />
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import HeroSection from "./components/HeroSection";
|
import HeroSection from "./components/HeroSection";
|
||||||
import CategorySection from "./components/CategorySection";
|
import CategorySection from "./components/CategorySection";
|
||||||
import CoursesSection from "./components/CoursesSection";
|
import CoursesSection from "./components/CoursesSection";
|
||||||
|
import { PostType } from "@nice/common";
|
||||||
|
import PathCard from "@web/src/components/models/post/SubPost/PathCard";
|
||||||
|
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
|
||||||
|
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
|
@ -8,10 +11,21 @@ const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
|
<CoursesSection
|
||||||
|
title="最受欢迎的思维导图"
|
||||||
|
description="深受追捧的思维导图,点亮你的智慧人生"
|
||||||
|
postType={PostType.PATH}
|
||||||
|
render={(post)=><PathCard post={post}></PathCard>}
|
||||||
|
to={"path"}
|
||||||
|
/>
|
||||||
<CoursesSection
|
<CoursesSection
|
||||||
title="推荐课程"
|
title="推荐课程"
|
||||||
description="最受欢迎的精品课程,助你快速成长"
|
description="最受欢迎的精品课程,助你快速成长"
|
||||||
|
postType={PostType.COURSE}
|
||||||
|
render={(post)=> <CourseCard post={post}></CourseCard>}
|
||||||
|
to={"/courses"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CategorySection />
|
<CategorySection />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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(
|
||||||
|
@ -24,10 +24,11 @@ export default function FilterSection() {
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<h3 className="text-lg font-medium mb-4">
|
<h3 className="text-lg font-medium mb-4">
|
||||||
{tax?.name}
|
{tax?.name}
|
||||||
|
{/* {JSON.stringify(items)} */}
|
||||||
</h3>
|
</h3>
|
||||||
<TermParentSelector
|
<TermParentSelector
|
||||||
value={items}
|
value={items}
|
||||||
slug={tax?.slug}
|
// slug={tax?.slug}
|
||||||
className="w-70 max-h-[400px] overscroll-contain overflow-x-hidden"
|
className="w-70 max-h-[400px] overscroll-contain overflow-x-hidden"
|
||||||
onChange={(selected) =>
|
onChange={(selected) =>
|
||||||
handleTermChange(
|
handleTermChange(
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import { Input, Layout, Avatar, Button, Dropdown } from "antd";
|
import { Input, Layout, Avatar, Button, Dropdown } from "antd";
|
||||||
import {
|
import {
|
||||||
EditFilled,
|
EditFilled,
|
||||||
|
@ -30,71 +29,79 @@ 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`
|
||||||
: "/course/editor";
|
: "/course/editor";
|
||||||
navigate(url);
|
navigate(url);
|
||||||
}}
|
}}
|
||||||
type="primary"
|
type="primary">
|
||||||
>
|
|
||||||
{id ? "编辑课程" : "创建课程"}
|
{id ? "编辑课程" : "创建课程"}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{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>
|
||||||
|
@ -104,4 +111,3 @@ export function MainHeader() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export function MainLayout() {
|
||||||
<MainProvider>
|
<MainProvider>
|
||||||
<div className=" min-h-screen bg-gray-100">
|
<div className=" min-h-screen bg-gray-100">
|
||||||
<MainHeader />
|
<MainHeader />
|
||||||
<Content className="min-h-screen flex-grow pt-14 bg-gray-50 ">
|
<Content className=" flex-grow pt-16 bg-gray-50 ">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Content>
|
</Content>
|
||||||
<MainFooter />
|
<MainFooter />
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -12,7 +12,7 @@ import toast from "react-hot-toast";
|
||||||
export default function StaffForm() {
|
export default function StaffForm() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { create, update } = useStaff(); // Ensure you have these methods in your hooks
|
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 {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
@ -68,7 +68,7 @@ export default function StaffForm() {
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
console.log('cc',data);
|
console.log('cc', data);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
form.setFieldValue("username", data.username);
|
form.setFieldValue("username", data.username);
|
||||||
|
@ -121,7 +121,7 @@ export default function StaffForm() {
|
||||||
name={"showname"}
|
name={"showname"}
|
||||||
label="名称">
|
label="名称">
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入名称"
|
placeholder="请输入姓名"
|
||||||
allowClear
|
allowClear
|
||||||
autoComplete="new-name" // 使用非标准的自动完成值
|
autoComplete="new-name" // 使用非标准的自动完成值
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -23,8 +23,9 @@ const DeptInfo = ({ post }: { post: PostDto }) => {
|
||||||
{post && (
|
{post && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="gap-1 text-xs font-medium text-gray-500 flex items-center">
|
<span className="gap-1 text-xs font-medium text-gray-500 flex items-center">
|
||||||
|
浏览量
|
||||||
<EyeOutlined />
|
<EyeOutlined />
|
||||||
{`${post?.meta?.views || 0}`}
|
{`${post?.views || 0}`}
|
||||||
</span>
|
</span>
|
||||||
{post?.studentIds && post?.studentIds?.length > 0 && (
|
{post?.studentIds && post?.studentIds?.length > 0 && (
|
||||||
<span className="gap-1 text-xs font-medium text-gray-500 flex items-center">
|
<span className="gap-1 text-xs font-medium text-gray-500 flex items-center">
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import { Tag } from "antd";
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{post?.terms && post?.terms?.length > 0 ? (
|
{terms && terms?.length > 0 ? (
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
{post?.terms?.map((term: any) => {
|
{terms?.map((term: any) => {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
key={term.id}
|
key={term.id}
|
||||||
color={
|
color={
|
||||||
term?.taxonomy?.slug ===
|
term?.taxonomy?.slug ===
|
||||||
TaxonomySlug.CATEGORY
|
TaxonomySlug.CATEGORY
|
||||||
? "green"
|
? "green"
|
||||||
: term?.taxonomy?.slug ===
|
: term?.taxonomy?.slug ===
|
||||||
TaxonomySlug.LEVEL
|
TaxonomySlug.LEVEL
|
||||||
? "blue"
|
? "blue"
|
||||||
: "orange"
|
: "orange"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,9 @@ import MindEditor from "@web/src/components/common/editor/MindEditor";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
export default function PathEditorPage() {
|
export default function PathEditorPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
return (
|
return <div className="">
|
||||||
<div className="p-2 min-h-screen">
|
<MindEditor id={id}></MindEditor>
|
||||||
<MindEditor id={id}></MindEditor>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 NodeMenu from "./NodeMenu";
|
||||||
import { api, usePost } from "@nice/client";
|
import { api, usePost, useVisitor } from "@nice/client";
|
||||||
import {
|
import {
|
||||||
ObjectType,
|
ObjectType,
|
||||||
PathDto,
|
PathDto,
|
||||||
|
@ -8,6 +8,7 @@ import {
|
||||||
PostType,
|
PostType,
|
||||||
Prisma,
|
Prisma,
|
||||||
RolePerms,
|
RolePerms,
|
||||||
|
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,19 +20,20 @@ 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";
|
||||||
import { MIND_OPTIONS } from "./constant";
|
import { MIND_OPTIONS } from "./constant";
|
||||||
|
import { SaveOutlined } from "@ant-design/icons";
|
||||||
export default function MindEditor({ id }: { id?: string }) {
|
export default function MindEditor({ id }: { id?: string }) {
|
||||||
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 { read } = useVisitor()
|
||||||
const { data: post, isLoading }: { data: PathDto; isLoading: boolean } =
|
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新建模式
|
|
||||||
const isAuth = isAuthenticated && user?.id === post?.author?.id;
|
const isAuth = isAuthenticated && user?.id === post?.author?.id;
|
||||||
return !!id || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST);
|
return !!id || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
@ -42,9 +44,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 = {
|
||||||
|
@ -52,8 +64,8 @@ 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]);
|
||||||
|
@ -73,8 +85,9 @@ 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(() => {
|
||||||
|
@ -86,6 +99,7 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [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();
|
||||||
|
@ -145,19 +159,23 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
}
|
}
|
||||||
console.log(result);
|
console.log(result);
|
||||||
},
|
},
|
||||||
(error) => {},
|
(error) => { },
|
||||||
`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 - 271)}px`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 flex-col w-[90vw] my-5 h-[80vh] border rounded-lg mx-auto">
|
|
||||||
|
<div className={` flex-col flex `}>
|
||||||
{canEdit && taxonomies && (
|
{canEdit && taxonomies && (
|
||||||
<Form form={form} className=" bg-white p-4 ">
|
<Form
|
||||||
<div className="flex items-center justify-between gap-4">
|
form={form}
|
||||||
|
className=" bg-white p-4 border-b">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{taxonomies.map((tax, index) => (
|
{taxonomies.map((tax, index) => (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
@ -166,6 +184,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}
|
||||||
|
@ -177,19 +196,21 @@ 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
|
{canEdit && <Button
|
||||||
ghost
|
ghost
|
||||||
type="primary"
|
type="primary"
|
||||||
|
icon={<SaveOutlined></SaveOutlined>}
|
||||||
onSubmit={(e) => e.preventDefault()}
|
onSubmit={(e) => e.preventDefault()}
|
||||||
onClick={handleSave}>
|
onClick={handleSave}>
|
||||||
{id ? "更新" : "保存"}
|
{id ? "更新" : "保存"}
|
||||||
</Button>
|
</Button>}
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
|
@ -199,20 +220,24 @@ export default function MindEditor({ id }: { id?: string }) {
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
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 - 271px)" }}>
|
||||||
</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 - 271px)" }}>
|
||||||
</div>
|
<Empty></Empty>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,23 +35,18 @@ const NodeMenu: React.FC<NodeMenuProps> = ({ mind }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleSelectNode = (nodeObj: NodeObj) => {
|
const handleSelectNode = (nodeObj: NodeObj) => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
|
|
||||||
const style = nodeObj.style || {};
|
const style = nodeObj.style || {};
|
||||||
setSelectedFontColor(style.color || '');
|
setSelectedFontColor(style.color || '');
|
||||||
setSelectedBgColor(style.background || '');
|
setSelectedBgColor(style.background || '');
|
||||||
|
|
||||||
setSelectedSize(style.fontSize || '24');
|
setSelectedSize(style.fontSize || '24');
|
||||||
setIsBold(style.fontWeight === 'bold');
|
setIsBold(style.fontWeight === 'bold');
|
||||||
setUrl(nodeObj.hyperLink || '');
|
setUrl(nodeObj.hyperLink || '');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUnselectNode = () => {
|
const handleUnselectNode = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
mind.bus.addListener('selectNode', handleSelectNode);
|
mind.bus.addListener('selectNode', handleSelectNode);
|
||||||
mind.bus.addListener('unselectNode', handleUnselectNode);
|
mind.bus.addListener('unselectNode', handleUnselectNode);
|
||||||
|
|
||||||
}, [mind]);
|
}, [mind]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -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
|
|
|
@ -108,15 +108,15 @@ export default function ResourcesShower({
|
||||||
{resource.title?.slice(0, 12) ||
|
{resource.title?.slice(0, 12) ||
|
||||||
"未命名"}
|
"未命名"}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-between text-xs text-gray-500">
|
<div className="flex items-center justify-between text-xs text-gray-500 ">
|
||||||
<span className="bg-gray-100 px-0.5 rounded-sm">
|
<span className="bg-gray-100 px-0.5 rounded-sm mr-2 whitespace-pre-wrap">
|
||||||
{resource.url
|
{resource.url
|
||||||
.split(".")
|
.split(".")
|
||||||
.pop()
|
.pop()
|
||||||
?.slice(0, 4)
|
?.slice(0, 4)
|
||||||
.toUpperCase()}
|
.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span className="flex bg-gray-100 px-0.5 rounded-sm justify-items-center whitespace-pre-wrap">
|
||||||
{resource.meta.size &&
|
{resource.meta.size &&
|
||||||
formatFileSize(
|
formatFileSize(
|
||||||
resource.meta.size
|
resource.meta.size
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -29,6 +29,7 @@ interface CourseDetailContextType {
|
||||||
setIsHeaderVisible: (visible: boolean) => void; // 新增
|
setIsHeaderVisible: (visible: boolean) => void; // 新增
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
userIsLearning?: boolean;
|
userIsLearning?: boolean;
|
||||||
|
setUserIsLearning:(learning: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CourseFormProviderProps {
|
interface CourseFormProviderProps {
|
||||||
|
@ -56,9 +57,14 @@ export function CourseDetailProvider({
|
||||||
{ enabled: Boolean(editId) }
|
{ enabled: Boolean(editId) }
|
||||||
);
|
);
|
||||||
|
|
||||||
const userIsLearning = useMemo(() => {
|
// const userIsLearning = useMemo(() => {
|
||||||
return (course?.studentIds || []).includes(user?.id);
|
// return (course?.studentIds || []).includes(user?.id);
|
||||||
}, [user, course, isLoading]);
|
// }, [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 canEdit = useMemo(() => {
|
||||||
const isAuthor = isAuthenticated && user?.id === course?.authorId;
|
const isAuthor = isAuthenticated && user?.id === course?.authorId;
|
||||||
const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST);
|
const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST);
|
||||||
|
@ -79,16 +85,28 @@ export function CourseDetailProvider({
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lecture?.id) {
|
if (lectureId) {
|
||||||
|
console.log(123);
|
||||||
|
console.log(lectureId);
|
||||||
read.mutateAsync({
|
read.mutateAsync({
|
||||||
data: {
|
data: {
|
||||||
visitorId: user?.id || null,
|
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,
|
type: VisitType.READED,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [course]);
|
}, [editId, lectureId]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lectureId !== selectedLectureId) {
|
if (lectureId !== selectedLectureId) {
|
||||||
navigate(`/course/${editId}/detail/${selectedLectureId}`);
|
navigate(`/course/${editId}/detail/${selectedLectureId}`);
|
||||||
|
@ -109,6 +127,7 @@ export function CourseDetailProvider({
|
||||||
setIsHeaderVisible,
|
setIsHeaderVisible,
|
||||||
canEdit,
|
canEdit,
|
||||||
userIsLearning,
|
userIsLearning,
|
||||||
|
setUserIsLearning
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</CourseDetailContext.Provider>
|
</CourseDetailContext.Provider>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Course, TaxonomySlug } from "@nice/common";
|
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 { Image, Typography, Skeleton, Tag } from "antd"; // 引入 antd 组件
|
||||||
import { CourseDetailContext } from "./CourseDetailContext";
|
import { CourseDetailContext } from "./CourseDetailContext";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
@ -36,11 +36,11 @@ export const CourseDetailDescription: React.FC = () => {
|
||||||
{!selectedLectureId && (
|
{!selectedLectureId && (
|
||||||
<div className="relative mb-4 overflow-hidden flex justify-center items-center">
|
<div className="relative mb-4 overflow-hidden flex justify-center items-center">
|
||||||
{
|
{
|
||||||
<Image
|
<div
|
||||||
src={course.meta.thumbnail}
|
className="w-full rounded-xl aspect-video bg-cover bg-center z-0"
|
||||||
preview={false}
|
style={{
|
||||||
className="w-full h-full object-cover z-0"
|
backgroundImage: `url(${course?.meta?.thumbnail || "/placeholder.webp"})`,
|
||||||
fallback="/placeholder.webp"
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<div
|
<div
|
||||||
|
@ -59,7 +59,7 @@ export const CourseDetailDescription: React.FC = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
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">
|
||||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-4xl z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-4xl z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||||
点击进入学习
|
点击进入学习
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,7 +70,7 @@ export const CourseDetailDescription: React.FC = () => {
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex gap-2 flex-wrap items-center float-start">
|
<div className="flex gap-2 flex-wrap items-center float-start">
|
||||||
{course?.subTitle && <div>{course?.subTitle}</div>}
|
{course?.subTitle && <div>{course?.subTitle}</div>}
|
||||||
<TermInfo post={course}></TermInfo>
|
<TermInfo terms={course.terms}></TermInfo>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Paragraph
|
<Paragraph
|
||||||
|
|
|
@ -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 */}
|
||||||
|
|
|
@ -12,11 +12,12 @@ import dayjs from "dayjs";
|
||||||
import CourseOperationBtns from "./JoinLearingButton";
|
import CourseOperationBtns from "./JoinLearingButton";
|
||||||
|
|
||||||
export default function CourseDetailTitle() {
|
export default function CourseDetailTitle() {
|
||||||
const { course } = useContext(CourseDetailContext);
|
const { course, lecture, selectedLectureId } =
|
||||||
|
useContext(CourseDetailContext);
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center flex-col items-center gap-2 w-full my-2 px-6">
|
<div className="flex justify-center flex-col items-center gap-2 w-full my-2 px-6">
|
||||||
<div className="flex justify-start w-full text-2xl font-bold">
|
<div className="flex justify-start w-full text-2xl font-bold">
|
||||||
{course?.title}
|
{!selectedLectureId ? course?.title : lecture?.title}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-600 flex w-full justify-start items-center gap-5">
|
<div className="text-gray-600 flex w-full justify-start items-center gap-5">
|
||||||
{course?.author?.showname && (
|
{course?.author?.showname && (
|
||||||
|
@ -36,15 +37,27 @@ export default function CourseDetailTitle() {
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<CalendarOutlined></CalendarOutlined>
|
<CalendarOutlined></CalendarOutlined>
|
||||||
{"发布于:"}
|
{"发布于:"}
|
||||||
{dayjs(course?.createdAt).format("YYYY年M月D日")}
|
{dayjs(
|
||||||
|
!selectedLectureId
|
||||||
|
? course?.createdAt
|
||||||
|
: lecture?.createdAt
|
||||||
|
).format("YYYY年M月D日")}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{"最后更新:"}
|
{"最后更新:"}
|
||||||
{dayjs(course?.updatedAt).format("YYYY年M月D日")}
|
{dayjs(
|
||||||
|
!selectedLectureId
|
||||||
|
? course?.updatedAt
|
||||||
|
: lecture?.updatedAt
|
||||||
|
).format("YYYY年M月D日")}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<EyeOutlined></EyeOutlined>
|
<EyeOutlined></EyeOutlined>
|
||||||
<div>{`观看次数${course?.meta?.views || 0}`}</div>
|
<div>{`观看次数${
|
||||||
|
!selectedLectureId
|
||||||
|
? course?.views || 0
|
||||||
|
: lecture?.views || 0
|
||||||
|
}`}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<BookOutlined />
|
<BookOutlined />
|
||||||
|
|
|
@ -45,7 +45,9 @@ export const LectureItem: React.FC<LectureItemProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-grow flex justify-between items-center w-2/3 realative">
|
<div className="flex-grow flex justify-between items-center w-2/3 realative">
|
||||||
<h4 className="font-medium text-gray-800 w-4/5">{lecture.title}</h4>
|
<h4 className="font-medium text-gray-800 w-4/5">
|
||||||
|
{lecture.title}
|
||||||
|
</h4>
|
||||||
{lecture.subTitle && (
|
{lecture.subTitle && (
|
||||||
<span className="text-sm text-gray-500 mt-1 w-4/5">
|
<span className="text-sm text-gray-500 mt-1 w-4/5">
|
||||||
{lecture.subTitle}
|
{lecture.subTitle}
|
||||||
|
@ -53,7 +55,9 @@ export const LectureItem: React.FC<LectureItemProps> = ({
|
||||||
)}
|
)}
|
||||||
<div className="text-gray-500 whitespace-normal">
|
<div className="text-gray-500 whitespace-normal">
|
||||||
<EyeOutlined></EyeOutlined>
|
<EyeOutlined></EyeOutlined>
|
||||||
<span className="ml-2">{lecture?.meta?.views ? lecture?.meta?.views : 0}</span>
|
<span className="ml-2">
|
||||||
|
{lecture?.views ? lecture?.views : 0}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,15 +12,17 @@ import {
|
||||||
EditTwoTone,
|
EditTwoTone,
|
||||||
LoginOutlined,
|
LoginOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
export default function CourseOperationBtns() {
|
export default function CourseOperationBtns() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } =
|
const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } =
|
||||||
useAuth();
|
useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { course, canEdit, userIsLearning } = useContext(CourseDetailContext);
|
const { course, canEdit, userIsLearning, setUserIsLearning} = useContext(CourseDetailContext);
|
||||||
const { update } = useStaff();
|
const { update } = useStaff();
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
const toggleLearning = async () => {
|
const toggleLearning = async () => {
|
||||||
if (!userIsLearning) {
|
if (!userIsLearning) {
|
||||||
await update.mutateAsync({
|
await update.mutateAsync({
|
||||||
|
@ -31,7 +33,10 @@ export default function CourseOperationBtns() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
setUserIsLearning(true)
|
||||||
|
toast.success("加入学习成功");
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
await update.mutateAsync({
|
await update.mutateAsync({
|
||||||
where: { id: user?.id },
|
where: { id: user?.id },
|
||||||
data: {
|
data: {
|
||||||
|
@ -42,6 +47,8 @@ export default function CourseOperationBtns() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
toast.success("退出学习成功");
|
||||||
|
setUserIsLearning(false)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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
|
||||||
|
@ -101,13 +101,17 @@ export function CourseFormProvider({
|
||||||
terms:
|
terms:
|
||||||
termIds?.length > 0
|
termIds?.length > 0
|
||||||
? {
|
? {
|
||||||
set: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
[editId ? "set" : "connect"]: termIds.map((id) => ({
|
||||||
|
id,
|
||||||
|
})), // 转换成 connect 格式
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
depts:
|
depts:
|
||||||
deptIds?.length > 0
|
deptIds?.length > 0
|
||||||
? {
|
? {
|
||||||
set: deptIds.map((id) => ({ id })),
|
[editId ? "set" : "connect"]: deptIds.map((id) => ({
|
||||||
|
id,
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
@ -149,6 +153,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,
|
||||||
|
|
|
@ -115,7 +115,7 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
||||||
resources:
|
resources:
|
||||||
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
|
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
|
||||||
? {
|
? {
|
||||||
connect: [videoUrlId, ...fileIds]
|
set: [videoUrlId, ...fileIds]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((fileId) => ({
|
.map((fileId) => ({
|
||||||
fileId,
|
fileId,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Card, Typography, Button, Empty } from "antd";
|
import { Typography, Button, Empty, Card } from "antd";
|
||||||
|
|
||||||
import { PostDto } from "@nice/common";
|
import { PostDto } from "@nice/common";
|
||||||
import DeptInfo from "@web/src/app/main/path/components/DeptInfo";
|
import DeptInfo from "@web/src/app/main/path/components/DeptInfo";
|
||||||
|
@ -19,9 +19,9 @@ export default function PostCard({ post, onClick }: PostCardProps) {
|
||||||
onClick={() => handleClick(post)}
|
onClick={() => handleClick(post)}
|
||||||
key={post?.id}
|
key={post?.id}
|
||||||
hoverable
|
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={
|
cover={
|
||||||
<div className="relative h-56 bg-gradient-to-br from-gray-900 to-gray-800 overflow-hidden group">
|
<div className="relative h-56 overflow-hidden group">
|
||||||
{post?.meta?.thumbnail ? (
|
{post?.meta?.thumbnail ? (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center transform transition-all duration-700 ease-out group-hover:scale-110"
|
className="absolute inset-0 bg-cover bg-center transform transition-all duration-700 ease-out group-hover:scale-110"
|
||||||
|
@ -39,7 +39,7 @@ export default function PostCard({ post, onClick }: PostCardProps) {
|
||||||
<div className="px-4 ">
|
<div className="px-4 ">
|
||||||
<div className="overflow-hidden hover:overflow-auto">
|
<div className="overflow-hidden hover:overflow-auto">
|
||||||
<div className="flex gap-2 h-7 whiteSpace-nowrap">
|
<div className="flex gap-2 h-7 whiteSpace-nowrap">
|
||||||
<TermInfo post={post}></TermInfo>
|
<TermInfo terms={post.terms}></TermInfo>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Title
|
<Title
|
||||||
|
@ -53,6 +53,7 @@ export default function PostCard({ post, onClick }: PostCardProps) {
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-100 text-center">
|
<div className="pt-4 border-t border-gray-100 text-center">
|
||||||
<Button
|
<Button
|
||||||
|
shape="round"
|
||||||
type="primary"
|
type="primary"
|
||||||
size="large"
|
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)]
|
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)]
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default function TaxonomyForm() {
|
||||||
setTaxonomyModalOpen(false)
|
setTaxonomyModalOpen(false)
|
||||||
}}>
|
}}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
rules={[{ required: true, message: "请输入名称" }]}
|
rules={[{ required: true, message: "请输入姓名" }]}
|
||||||
name={"name"}
|
name={"name"}
|
||||||
label="名称">
|
label="名称">
|
||||||
<Input></Input>
|
<Input></Input>
|
||||||
|
|
|
@ -1,50 +1,56 @@
|
||||||
import { api } from "@nice/client/";
|
import { api } from "@nice/client/";
|
||||||
import { Checkbox, Form } from "antd";
|
import { Checkbox, Skeleton } from "antd";
|
||||||
import { TermDto } from "@nice/common";
|
import { TermDto } from "@nice/common";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import React from "react";
|
||||||
export default function TermParentSelector({
|
export default function TermParentSelector({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
placeholder = "选择分类",
|
taxonomyId,
|
||||||
multiple = true,
|
domainId = undefined,
|
||||||
taxonomyId,
|
style,
|
||||||
domainId,
|
}: {
|
||||||
style,
|
value?: string[];
|
||||||
}: any) {
|
onChange?: (value: string[]) => void;
|
||||||
const [selectedValues, setSelectedValues] = useState<string[]>([]); // 用于存储选中的值
|
className?: string;
|
||||||
const {
|
taxonomyId: string;
|
||||||
data,
|
domainId?: string;
|
||||||
isLoading,
|
style?: React.CSSProperties;
|
||||||
}: { data: TermDto[]; isLoading: boolean } = api.term.findMany.useQuery({
|
}) {
|
||||||
where: {
|
const { data, isLoading }: { data: TermDto[]; isLoading: boolean } =
|
||||||
taxonomy: {
|
api.term.findMany.useQuery({
|
||||||
id: taxonomyId,
|
where: {
|
||||||
},
|
taxonomyId: taxonomyId,
|
||||||
parentId: null
|
parentId: null,
|
||||||
},
|
domainId,
|
||||||
});
|
},
|
||||||
const handleCheckboxChange = (checkedValues: string[]) => {
|
});
|
||||||
setSelectedValues(checkedValues); // 更新选中的值
|
const handleCheckboxChange = (checkedValues: string[]) => {
|
||||||
if (onChange) {
|
// setSelectedValues(checkedValues); // 更新选中的值
|
||||||
onChange(checkedValues); // 调用外部传入的 onChange 回调
|
if (onChange) {
|
||||||
}
|
onChange(checkedValues); // 调用外部传入的 onChange 回调
|
||||||
};
|
}
|
||||||
return (
|
};
|
||||||
<div className={className} style={style}>
|
return (
|
||||||
<Form onFinish={null}>
|
<div className={className} style={style}>
|
||||||
<Form.Item name="categories">
|
{isLoading ? (
|
||||||
<Checkbox.Group onChange={handleCheckboxChange}>
|
<Skeleton
|
||||||
{data?.map((category) => (
|
paragraph={{
|
||||||
<div className="w-full h-9 p-2 my-1">
|
rows: 4,
|
||||||
<Checkbox className="text-base text-slate-700" key={category.id} value={category.id}>
|
}}></Skeleton>
|
||||||
{category.name}
|
) : (
|
||||||
</Checkbox>
|
<Checkbox.Group value={value} onChange={handleCheckboxChange}>
|
||||||
</div>
|
{data?.map((category) => (
|
||||||
))}
|
<div className="w-full h-9 p-2 my-1" key={category.id}>
|
||||||
</Checkbox.Group>
|
<Checkbox
|
||||||
</Form.Item>
|
className="text-base text-slate-700"
|
||||||
</Form>
|
value={category.id}>
|
||||||
</div>
|
{category.name}
|
||||||
)
|
</Checkbox>
|
||||||
}
|
</div>
|
||||||
|
))}
|
||||||
|
</Checkbox.Group>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -70,7 +70,9 @@ export const routes: CustomRouteObject[] = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "editor/:id?",
|
path: "editor/:id?",
|
||||||
element: <PathEditorPage></PathEditorPage>,
|
element: <WithAuth>
|
||||||
|
<PathEditorPage></PathEditorPage>
|
||||||
|
</WithAuth>,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -86,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,9 @@ 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")
|
||||||
|
hates Int @default(0) @map("hates")
|
||||||
|
likes Int @default(0) @map("likes")
|
||||||
// 索引
|
// 索引
|
||||||
// 日期时间类型字段
|
// 日期时间类型字段
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
@ -238,6 +241,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")
|
||||||
}
|
}
|
||||||
|
@ -282,8 +286,8 @@ model Visit {
|
||||||
views Int @default(1) @map("views")
|
views Int @default(1) @map("views")
|
||||||
// sourceIP String? @map("source_ip")
|
// sourceIP String? @map("source_ip")
|
||||||
// 关联关系
|
// 关联关系
|
||||||
visitorId String @map("visitor_id")
|
visitorId String? @map("visitor_id")
|
||||||
visitor Staff @relation(fields: [visitorId], references: [id])
|
visitor Staff? @relation(fields: [visitorId], references: [id])
|
||||||
postId String? @map("post_id")
|
postId String? @map("post_id")
|
||||||
post Post? @relation(fields: [postId], references: [id])
|
post Post? @relation(fields: [postId], references: [id])
|
||||||
message Message? @relation(fields: [messageId], references: [id])
|
message Message? @relation(fields: [messageId], references: [id])
|
||||||
|
|
|
@ -45,11 +45,13 @@ export const postDetailSelect: Prisma.PostSelect = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
meta: true,
|
meta: true,
|
||||||
|
views: true,
|
||||||
};
|
};
|
||||||
export const postUnDetailSelect: Prisma.PostSelect = {
|
export const postUnDetailSelect: Prisma.PostSelect = {
|
||||||
id: true,
|
id: true,
|
||||||
type: true,
|
type: true,
|
||||||
title: true,
|
title: true,
|
||||||
|
views: true,
|
||||||
parent: true,
|
parent: true,
|
||||||
parentId: true,
|
parentId: true,
|
||||||
content: true,
|
content: true,
|
||||||
|
@ -79,6 +81,7 @@ export const messageDetailSelect: Prisma.MessageSelect = {
|
||||||
id: true,
|
id: true,
|
||||||
sender: true,
|
sender: true,
|
||||||
content: true,
|
content: true,
|
||||||
|
|
||||||
title: true,
|
title: true,
|
||||||
url: true,
|
url: true,
|
||||||
option: true,
|
option: true,
|
||||||
|
@ -88,6 +91,7 @@ export const courseDetailSelect: Prisma.PostSelect = {
|
||||||
id: true,
|
id: true,
|
||||||
title: true,
|
title: true,
|
||||||
subTitle: true,
|
subTitle: true,
|
||||||
|
views: true,
|
||||||
type: true,
|
type: true,
|
||||||
author: true,
|
author: true,
|
||||||
authorId: true,
|
authorId: true,
|
||||||
|
@ -124,6 +128,7 @@ export const lectureDetailSelect: Prisma.PostSelect = {
|
||||||
subTitle: true,
|
subTitle: true,
|
||||||
content: true,
|
content: true,
|
||||||
resources: true,
|
resources: true,
|
||||||
|
views: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
// 关联表选择
|
// 关联表选择
|
||||||
|
|
Loading…
Reference in New Issue