Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
6042002280
|
@ -166,19 +166,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
||||||
);
|
);
|
||||||
return transDto;
|
return transDto;
|
||||||
}
|
}
|
||||||
// async findMany(args: Prisma.PostFindManyArgs, staff?: UserProfile) {
|
|
||||||
// if (!args.where) args.where = {};
|
|
||||||
// args.where.OR = await this.preFilter(args.where.OR, staff);
|
|
||||||
// return this.wrapResult(super.findMany(args), async (result) => {
|
|
||||||
// await Promise.all(
|
|
||||||
// result.map(async (item) => {
|
|
||||||
// await setPostRelation({ data: item, staff });
|
|
||||||
// await this.setPerms(item, staff);
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
// return { ...result };
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) {
|
async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) {
|
||||||
if (!args.where) args.where = {};
|
if (!args.where) args.where = {};
|
||||||
args.where.OR = await this.preFilter(args.where.OR, staff);
|
args.where.OR = await this.preFilter(args.where.OR, staff);
|
||||||
|
@ -255,6 +243,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
||||||
// 批量执行更新
|
// 批量执行更新
|
||||||
return updates.length > 0 ? await db.$transaction(updates) : [];
|
return updates.length > 0 ? await db.$transaction(updates) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async setPerms(data: Post, staff?: UserProfile) {
|
protected async setPerms(data: Post, staff?: UserProfile) {
|
||||||
if (!staff) return;
|
if (!staff) return;
|
||||||
const perms: ResPerm = {
|
const perms: ResPerm = {
|
||||||
|
|
|
@ -168,6 +168,21 @@ export async function setCourseInfo({ data }: { data: Post }) {
|
||||||
(lecture) => lecture.parentId === section.id,
|
(lecture) => lecture.parentId === section.id,
|
||||||
) as any as Lecture[];
|
) as any as Lecture[];
|
||||||
});
|
});
|
||||||
Object.assign(data, { sections, lectureCount });
|
|
||||||
|
const students = await db.staff.findMany({
|
||||||
|
where: {
|
||||||
|
learningPosts: {
|
||||||
|
some: {
|
||||||
|
id: data.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const studentIds = (students || []).map((student) => student?.id);
|
||||||
|
Object.assign(data, { sections, lectureCount, studentIds });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Card, Tag, Typography, Button } from "antd";
|
import { Card, Tag, Typography, Button } from "antd";
|
||||||
import {
|
import {
|
||||||
|
BookOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
|
@ -73,10 +74,10 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) {
|
||||||
<button> {course.title}</button>
|
<button> {course.title}</button>
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<div className="flex items-center mb-4 p-2 rounded-lg transition-all duration-300 hover:bg-blue-50 group">
|
<div className="flex items-center mb-4 rounded-lg transition-all duration-300 hover:bg-blue-50 group">
|
||||||
<TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" />
|
<TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" />
|
||||||
<div className="ml-2 flex items-center flex-grow">
|
<div className="ml-2 flex items-center flex-grow">
|
||||||
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
|
<Text className="font-medium text-blue-500 transition-colors duration-300 truncate max-w-[120px]">
|
||||||
{course?.depts?.length > 1
|
{course?.depts?.length > 1
|
||||||
? `${course.depts[0].name}等`
|
? `${course.depts[0].name}等`
|
||||||
: course?.depts?.[0]?.name}
|
: course?.depts?.[0]?.name}
|
||||||
|
@ -84,10 +85,16 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) {
|
||||||
{/* {course?.depts?.map((dept)=>{return dept.name})} */}
|
{/* {course?.depts?.map((dept)=>{return dept.name})} */}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs font-medium text-gray-500 flex items-center">
|
<span className="text-xs font-medium text-gray-500 flex items-center">
|
||||||
<EyeOutlined className="mr-1" />
|
<EyeOutlined />
|
||||||
{`观看次数 ${course?.meta?.views || 0}`}
|
{`观看次数 ${course?.meta?.views || 0}`}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="text-xs font-medium text-gray-500 flex items-center">
|
||||||
|
<BookOutlined />
|
||||||
|
{`学习人数 ${course?.studentIds?.length || 0}`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-4 border-t border-gray-100 text-center">
|
<div className="pt-4 border-t border-gray-100 text-center">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -7,11 +7,14 @@ export default function MyLearningPage() {
|
||||||
<>
|
<>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<CourseList
|
<CourseList
|
||||||
edit
|
|
||||||
params={{
|
params={{
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
where: {
|
where: {
|
||||||
authorId: user.id,
|
students: {
|
||||||
|
some: {
|
||||||
|
id: user?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
cols={4}></CourseList>
|
cols={4}></CourseList>
|
||||||
|
|
|
@ -28,6 +28,7 @@ interface CourseDetailContextType {
|
||||||
isHeaderVisible: boolean; // 新增
|
isHeaderVisible: boolean; // 新增
|
||||||
setIsHeaderVisible: (visible: boolean) => void; // 新增
|
setIsHeaderVisible: (visible: boolean) => void; // 新增
|
||||||
canEdit?: boolean;
|
canEdit?: boolean;
|
||||||
|
userIsLearning?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CourseFormProviderProps {
|
interface CourseFormProviderProps {
|
||||||
|
@ -43,30 +44,25 @@ export function CourseDetailProvider({
|
||||||
}: CourseFormProviderProps) {
|
}: CourseFormProviderProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { read } = useVisitor();
|
const { read } = useVisitor();
|
||||||
const { user, hasSomePermissions } = useAuth();
|
const { user, hasSomePermissions, isAuthenticated } = useAuth();
|
||||||
const { lectureId } = useParams();
|
const { lectureId } = useParams();
|
||||||
|
|
||||||
const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } =
|
const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } =
|
||||||
(api.post as any).findFirst.useQuery(
|
(api.post as any).findFirst.useQuery(
|
||||||
{
|
{
|
||||||
where: { id: editId },
|
where: { id: editId },
|
||||||
// include: {
|
select: courseDetailSelect,
|
||||||
// // sections: { include: { lectures: true } },
|
|
||||||
// enrollments: true,
|
|
||||||
// terms:true
|
|
||||||
// },
|
|
||||||
|
|
||||||
select:courseDetailSelect
|
|
||||||
},
|
},
|
||||||
{ enabled: Boolean(editId) }
|
{ enabled: Boolean(editId) }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const userIsLearning = useMemo(() => {
|
||||||
|
return (course?.studentIds || []).includes(user?.id);
|
||||||
|
}, [user, course, isLoading]);
|
||||||
const canEdit = useMemo(() => {
|
const canEdit = useMemo(() => {
|
||||||
const isAuthor = user?.id === course?.authorId;
|
const isAuthor = isAuthenticated && user?.id === course?.authorId;
|
||||||
const isDept = course?.depts
|
|
||||||
?.map((dept) => dept.id)
|
|
||||||
.includes(user?.deptId);
|
|
||||||
const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST);
|
const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST);
|
||||||
return isAuthor || isDept || isRoot;
|
return isAuthor || isRoot;
|
||||||
}, [user, course]);
|
}, [user, course]);
|
||||||
const [selectedLectureId, setSelectedLectureId] = useState<
|
const [selectedLectureId, setSelectedLectureId] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
|
@ -109,6 +105,7 @@ export function CourseDetailProvider({
|
||||||
isHeaderVisible,
|
isHeaderVisible,
|
||||||
setIsHeaderVisible,
|
setIsHeaderVisible,
|
||||||
canEdit,
|
canEdit,
|
||||||
|
userIsLearning,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</CourseDetailContext.Provider>
|
</CourseDetailContext.Provider>
|
||||||
|
|
|
@ -3,11 +3,13 @@ import React, { useContext, 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 {
|
import {
|
||||||
|
BookOutlined,
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
EditTwoTone,
|
EditTwoTone,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
|
TeamOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
@ -23,7 +25,8 @@ export const CourseDetailDescription: React.FC = () => {
|
||||||
const { canEdit } = useContext(CourseDetailContext);
|
const { canEdit } = useContext(CourseDetailContext);
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-white shadow-md rounded-lg border border-gray-200 p-5 my-4">
|
// <div className="w-full bg-white shadow-md rounded-lg border border-gray-200 p-5 my-4">
|
||||||
|
<div className="w-full p-5 my-4">
|
||||||
{isLoading || !course ? (
|
{isLoading || !course ? (
|
||||||
<Skeleton active paragraph={{ rows: 4 }} />
|
<Skeleton active paragraph={{ rows: 4 }} />
|
||||||
) : (
|
) : (
|
||||||
|
@ -41,53 +44,61 @@ export const CourseDetailDescription: React.FC = () => {
|
||||||
setSelectedLectureId(firstLectureId);
|
setSelectedLectureId(firstLectureId);
|
||||||
}}
|
}}
|
||||||
className="w-full h-full absolute top-0 z-10 bg-black opacity-30 transition-opacity duration-300 ease-in-out hover:opacity-70 cursor-pointer">
|
className="w-full h-full absolute top-0 z-10 bg-black opacity-30 transition-opacity duration-300 ease-in-out hover:opacity-70 cursor-pointer">
|
||||||
<PlayCircleOutlined className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-4xl z-10" />
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-4xl z-10">
|
||||||
|
点击进入学习
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="text-lg font-bold">{"课程简介:"}</div>
|
<div className="text-lg font-bold">{"课程简介:"}</div>
|
||||||
<div className="flex gap-2 flex-wrap items-center">
|
<div className="flex flex-col gap-2">
|
||||||
<div>{course?.subTitle}</div>
|
<div className="flex gap-2 flex-wrap items-center float-start">
|
||||||
{
|
{course?.subTitle && <div>{course?.subTitle}</div>}
|
||||||
course.terms.map((term) => {
|
{course.terms.map((term) => {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
key={term.id}
|
key={term.id}
|
||||||
// color={term.taxonomy.slug===TaxonomySlug.CATEGORY? "blue" : "green"}
|
// color={term.taxonomy.slug===TaxonomySlug.CATEGORY? "blue" : "green"}
|
||||||
color={
|
color={
|
||||||
term?.taxonomy?.slug ===
|
term?.taxonomy?.slug ===
|
||||||
TaxonomySlug.CATEGORY
|
TaxonomySlug.CATEGORY
|
||||||
? "blue"
|
? "blue"
|
||||||
: term?.taxonomy?.slug ===
|
: term?.taxonomy?.slug ===
|
||||||
TaxonomySlug.LEVEL
|
TaxonomySlug.LEVEL
|
||||||
? "green"
|
? "green"
|
||||||
: "orange"
|
: "orange"
|
||||||
}
|
}
|
||||||
className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0">
|
className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0">
|
||||||
{term.name}
|
{term.name}
|
||||||
</Tag>
|
</Tag>
|
||||||
)
|
);
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-800 flex justify-start gap-5">
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<CalendarOutlined></CalendarOutlined>
|
|
||||||
{dayjs(course?.createdAt).format("YYYY年M月D日")}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="text-gray-600 flex justify-start gap-5">
|
||||||
<ReloadOutlined></ReloadOutlined>
|
<div className="flex gap-1">
|
||||||
{dayjs(course?.updatedAt).format("YYYY年M月D日")}
|
<CalendarOutlined></CalendarOutlined>
|
||||||
</div>
|
{"创建于:"}
|
||||||
<div className="flex gap-1">
|
{dayjs(course?.createdAt).format(
|
||||||
<EyeOutlined></EyeOutlined>
|
"YYYY年M月D日"
|
||||||
<div>{course?.meta?.views || 0}</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
{
|
<div className="flex gap-1">
|
||||||
canEdit && (
|
<ReloadOutlined></ReloadOutlined>
|
||||||
|
{"更新于:"}
|
||||||
|
{dayjs(course?.updatedAt).format(
|
||||||
|
"YYYY年M月D日"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<EyeOutlined></EyeOutlined>
|
||||||
|
<div>{`观看次数${course?.meta?.views || 0}`}</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<BookOutlined />
|
||||||
|
<div>{`学习人数${course?.studentIds?.length || 0}`}</div>
|
||||||
|
</div>
|
||||||
|
{canEdit && (
|
||||||
<div
|
<div
|
||||||
className="flex gap-1 text-primary hover:cursor-pointer"
|
className="flex gap-1 text-primary hover:cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -95,13 +106,12 @@ export const CourseDetailDescription: React.FC = () => {
|
||||||
? `/course/${id}/editor`
|
? `/course/${id}/editor`
|
||||||
: "/course/editor";
|
: "/course/editor";
|
||||||
navigate(url);
|
navigate(url);
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<EditTwoTone></EditTwoTone>
|
<EditTwoTone></EditTwoTone>
|
||||||
{"点击编辑课程"}
|
{"点击编辑课程"}
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Paragraph
|
<Paragraph
|
||||||
className="text-gray-600"
|
className="text-gray-600"
|
||||||
|
|
|
@ -10,17 +10,18 @@ import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu";
|
import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu";
|
||||||
import { CourseDetailContext } from "../CourseDetailContext";
|
import { CourseDetailContext } from "../CourseDetailContext";
|
||||||
|
import { usePost, useStaff } from "@nice/client";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
const { Header } = Layout;
|
const { Header } = Layout;
|
||||||
|
|
||||||
export function CourseDetailHeader() {
|
export function CourseDetailHeader() {
|
||||||
const [searchValue, setSearchValue] = useState("");
|
|
||||||
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 } = useContext(CourseDetailContext);
|
const { course, canEdit, userIsLearning } = useContext(CourseDetailContext);
|
||||||
|
const { update } = useStaff();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
|
<Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
|
||||||
|
@ -39,20 +40,48 @@ export function CourseDetailHeader() {
|
||||||
{/* <NavigationMenu /> */}
|
{/* <NavigationMenu /> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-6">
|
<div className="flex items-center space-x-6">
|
||||||
|
{isAuthenticated && (
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
if (!userIsLearning) {
|
||||||
|
await update.mutateAsync({
|
||||||
|
where: { id: user?.id },
|
||||||
|
data: {
|
||||||
|
learningPosts: {
|
||||||
|
connect: { id: course.id },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await update.mutateAsync({
|
||||||
|
where: { id: user?.id },
|
||||||
|
data: {
|
||||||
|
learningPosts: {
|
||||||
|
disconnect: {
|
||||||
|
id: course.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
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={<EditFilled />}>
|
||||||
|
{userIsLearning ? "退出学习" : "加入学习"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<>
|
<Button
|
||||||
<Button
|
onClick={() => {
|
||||||
onClick={() => {
|
const url = id
|
||||||
const url = id
|
? `/course/${id}/editor`
|
||||||
? `/course/${id}/editor`
|
: "/course/editor";
|
||||||
: "/course/editor";
|
navigate(url);
|
||||||
navigate(url);
|
}}
|
||||||
}}
|
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"
|
||||||
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={<EditFilled />}>
|
||||||
icon={<EditFilled />}>
|
{"编辑课程"}
|
||||||
{"编辑课程"}
|
</Button>
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<UserMenu />
|
<UserMenu />
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
// // components/Header.tsx
|
|
||||||
// import { motion, useScroll, useTransform } from "framer-motion";
|
|
||||||
// import { useContext, useEffect, useState } from "react";
|
|
||||||
// import { CourseDetailContext } from "../CourseDetailContext";
|
|
||||||
// import { Avatar, Button, Dropdown } from "antd";
|
|
||||||
// import { UserOutlined } from "@ant-design/icons";
|
|
||||||
// import { UserMenu } from "@web/src/app/main/layout/UserMenu";
|
|
||||||
// import { useAuth } from "@web/src/providers/auth-provider";
|
|
||||||
|
|
||||||
// export const CourseDetailHeader = () => {
|
|
||||||
// const { scrollY } = useScroll();
|
|
||||||
// const { user, isAuthenticated } = useAuth();
|
|
||||||
// const [lastScrollY, setLastScrollY] = useState(0);
|
|
||||||
// const { course, isHeaderVisible, setIsHeaderVisible, lecture } =
|
|
||||||
// useContext(CourseDetailContext);
|
|
||||||
// useEffect(() => {
|
|
||||||
// const updateHeader = () => {
|
|
||||||
// const current = scrollY.get();
|
|
||||||
// const direction = current > lastScrollY ? "down" : "up";
|
|
||||||
|
|
||||||
// if (direction === "down" && current > 100) {
|
|
||||||
// setIsHeaderVisible(false);
|
|
||||||
// } else if (direction === "up") {
|
|
||||||
// setIsHeaderVisible(true);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setLastScrollY(current);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // 使用 requestAnimationFrame 来优化性能
|
|
||||||
// const unsubscribe = scrollY.on("change", () => {
|
|
||||||
// requestAnimationFrame(updateHeader);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// unsubscribe();
|
|
||||||
// };
|
|
||||||
// }, [lastScrollY, scrollY, setIsHeaderVisible]);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <motion.header
|
|
||||||
// initial={{ y: 0 }}
|
|
||||||
// animate={{ y: isHeaderVisible ? 0 : -100 }}
|
|
||||||
// transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
|
||||||
// className="fixed top-0 left-0 w-full h-16 bg-slate-900 backdrop-blur-sm z-50 shadow-sm">
|
|
||||||
// <div className="w-full mx-auto px-4 h-full flex items-center justify-between">
|
|
||||||
// <div className="flex items-center space-x-4">
|
|
||||||
// <h1 className="text-white text-xl ">{course?.title}</h1>
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// {isAuthenticated ? (
|
|
||||||
// <Dropdown
|
|
||||||
// overlay={<UserMenu />}
|
|
||||||
// trigger={["click"]}
|
|
||||||
// placement="bottomRight">
|
|
||||||
// <Avatar
|
|
||||||
// size="large"
|
|
||||||
// className="cursor-pointer hover:scale-105 transition-all bg-gradient-to-r from-blue-500 to-blue-600 text-white font-semibold">
|
|
||||||
// {(user?.showname ||
|
|
||||||
// user?.username ||
|
|
||||||
// "")[0]?.toUpperCase()}
|
|
||||||
// </Avatar>
|
|
||||||
// </Dropdown>
|
|
||||||
// ) : (
|
|
||||||
// <Button
|
|
||||||
// onClick={() => navigator("/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 />}>
|
|
||||||
// 登录
|
|
||||||
// </Button>
|
|
||||||
// )}
|
|
||||||
// </div>
|
|
||||||
// </motion.header>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export default CourseDetailHeader;
|
|
|
@ -99,10 +99,10 @@ export function CourseFormProvider({
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
terms: {
|
terms: {
|
||||||
connect: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
set: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
||||||
},
|
},
|
||||||
depts: {
|
depts: {
|
||||||
connect: deptIds.map((id) => ({ id })),
|
set: deptIds.map((id) => ({ id })),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// 删除原始的 taxonomy 字段
|
// 删除原始的 taxonomy 字段
|
||||||
|
|
|
@ -32,7 +32,7 @@ export function CourseBasicForm() {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="subTitle"
|
name="subTitle"
|
||||||
label="课程副标题"
|
label="课程副标题"
|
||||||
rules={[{ max: 10, message: "副标题最多10个字符" }]}>
|
rules={[{ max: 20, message: "副标题最多20个字符" }]}>
|
||||||
<Input placeholder="请输入课程副标题" />
|
<Input placeholder="请输入课程副标题" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name={["meta", "thumbnail"]} label="课程封面">
|
<Form.Item name={["meta", "thumbnail"]} label="课程封面">
|
||||||
|
|
|
@ -5,39 +5,48 @@ import { ObjectType, Staff } from "@nice/common";
|
||||||
import { findQueryData } from "../utils";
|
import { findQueryData } from "../utils";
|
||||||
import { CrudOperation, emitDataChange } from "../../event";
|
import { CrudOperation, emitDataChange } from "../../event";
|
||||||
export function useStaff() {
|
export function useStaff() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const queryKey = getQueryKey(api.staff);
|
const queryKey = getQueryKey(api.staff);
|
||||||
|
|
||||||
const create = api.staff.create.useMutation({
|
const create = api.staff.create.useMutation({
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
emitDataChange(ObjectType.STAFF, result as any, CrudOperation.CREATED)
|
emitDataChange(
|
||||||
},
|
ObjectType.STAFF,
|
||||||
});
|
result as any,
|
||||||
const updateUserDomain = api.staff.updateUserDomain.useMutation({
|
CrudOperation.CREATED
|
||||||
onSuccess: async (result) => {
|
);
|
||||||
queryClient.invalidateQueries({ queryKey });
|
},
|
||||||
},
|
});
|
||||||
});
|
const updateUserDomain = api.staff.updateUserDomain.useMutation({
|
||||||
const update = api.staff.update.useMutation({
|
onSuccess: async (result) => {
|
||||||
onSuccess: (result) => {
|
queryClient.invalidateQueries({ queryKey });
|
||||||
queryClient.invalidateQueries({ queryKey });
|
},
|
||||||
emitDataChange(ObjectType.STAFF, result as any, CrudOperation.UPDATED)
|
});
|
||||||
},
|
const update = api.staff.update.useMutation({
|
||||||
});
|
onSuccess: (result) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey });
|
||||||
|
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
|
||||||
|
emitDataChange(
|
||||||
|
ObjectType.STAFF,
|
||||||
|
result as any,
|
||||||
|
CrudOperation.UPDATED
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
const softDeleteByIds = api.staff.softDeleteByIds.useMutation({
|
const softDeleteByIds = api.staff.softDeleteByIds.useMutation({
|
||||||
onSuccess: (result, variables) => {
|
onSuccess: (result, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const getStaff = (key: string) => {
|
const getStaff = (key: string) => {
|
||||||
return findQueryData<Staff>(queryClient, api.staff, key);
|
return findQueryData<Staff>(queryClient, api.staff, key);
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
softDeleteByIds,
|
softDeleteByIds,
|
||||||
getStaff,
|
getStaff,
|
||||||
updateUserDomain
|
updateUserDomain,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ model Staff {
|
||||||
posts Post[]
|
posts Post[]
|
||||||
|
|
||||||
|
|
||||||
learningPost Post[] @relation("post_student")
|
learningPosts Post[] @relation("post_student")
|
||||||
sentMsgs Message[] @relation("message_sender")
|
sentMsgs Message[] @relation("message_sender")
|
||||||
receivedMsgs Message[] @relation("message_receiver")
|
receivedMsgs Message[] @relation("message_receiver")
|
||||||
registerToken String?
|
registerToken String?
|
||||||
|
|
|
@ -83,4 +83,5 @@ export type CourseDto = Course & {
|
||||||
terms: TermDto[];
|
terms: TermDto[];
|
||||||
lectureCount?: number;
|
lectureCount?: number;
|
||||||
depts: Department[];
|
depts: Department[];
|
||||||
|
studentIds: string[];
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue