add
This commit is contained in:
parent
4c89a43197
commit
a7bbc88e99
|
@ -7,11 +7,14 @@ export default function MyLearningPage() {
|
|||
<>
|
||||
<div className="p-4">
|
||||
<CourseList
|
||||
edit
|
||||
params={{
|
||||
pageSize: 12,
|
||||
where: {
|
||||
authorId: user.id,
|
||||
students: {
|
||||
some: {
|
||||
id: user?.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
cols={4}></CourseList>
|
||||
|
|
|
@ -28,6 +28,7 @@ interface CourseDetailContextType {
|
|||
isHeaderVisible: boolean; // 新增
|
||||
setIsHeaderVisible: (visible: boolean) => void; // 新增
|
||||
canEdit?: boolean;
|
||||
userIsLearning?: boolean;
|
||||
}
|
||||
|
||||
interface CourseFormProviderProps {
|
||||
|
@ -43,30 +44,25 @@ export function CourseDetailProvider({
|
|||
}: CourseFormProviderProps) {
|
||||
const navigate = useNavigate();
|
||||
const { read } = useVisitor();
|
||||
const { user, hasSomePermissions } = useAuth();
|
||||
const { user, hasSomePermissions, isAuthenticated } = useAuth();
|
||||
const { lectureId } = useParams();
|
||||
|
||||
const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } =
|
||||
(api.post as any).findFirst.useQuery(
|
||||
{
|
||||
where: { id: editId },
|
||||
// include: {
|
||||
// // sections: { include: { lectures: true } },
|
||||
// enrollments: true,
|
||||
// terms:true
|
||||
// },
|
||||
|
||||
select:courseDetailSelect
|
||||
select: courseDetailSelect,
|
||||
},
|
||||
{ enabled: Boolean(editId) }
|
||||
);
|
||||
|
||||
const userIsLearning = useMemo(() => {
|
||||
return (course?.studentIds || []).includes(user?.id);
|
||||
}, [user, course, isLoading]);
|
||||
const canEdit = useMemo(() => {
|
||||
const isAuthor = user?.id === course?.authorId;
|
||||
const isDept = course?.depts
|
||||
?.map((dept) => dept.id)
|
||||
.includes(user?.deptId);
|
||||
const isAuthor = isAuthenticated && user?.id === course?.authorId;
|
||||
const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST);
|
||||
return isAuthor || isDept || isRoot;
|
||||
return isAuthor || isRoot;
|
||||
}, [user, course]);
|
||||
const [selectedLectureId, setSelectedLectureId] = useState<
|
||||
string | undefined
|
||||
|
@ -109,6 +105,7 @@ export function CourseDetailProvider({
|
|||
isHeaderVisible,
|
||||
setIsHeaderVisible,
|
||||
canEdit,
|
||||
userIsLearning,
|
||||
}}>
|
||||
{children}
|
||||
</CourseDetailContext.Provider>
|
||||
|
|
|
@ -10,17 +10,18 @@ import { useAuth } from "@web/src/providers/auth-provider";
|
|||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu";
|
||||
import { CourseDetailContext } from "../CourseDetailContext";
|
||||
|
||||
import { usePost, useStaff } from "@nice/client";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
export function CourseDetailHeader() {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const { id } = useParams();
|
||||
const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } =
|
||||
useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { course, canEdit } = useContext(CourseDetailContext);
|
||||
const { course, canEdit, userIsLearning } = useContext(CourseDetailContext);
|
||||
const { update } = useStaff();
|
||||
|
||||
return (
|
||||
<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 /> */}
|
||||
</div>
|
||||
<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 && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const url = id
|
||||
? `/course/${id}/editor`
|
||||
: "/course/editor";
|
||||
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"
|
||||
icon={<EditFilled />}>
|
||||
{"编辑课程"}
|
||||
</Button>
|
||||
</>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const url = id
|
||||
? `/course/${id}/editor`
|
||||
: "/course/editor";
|
||||
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"
|
||||
icon={<EditFilled />}>
|
||||
{"编辑课程"}
|
||||
</Button>
|
||||
)}
|
||||
{isAuthenticated ? (
|
||||
<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: {
|
||||
connect: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
||||
set: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
||||
},
|
||||
depts: {
|
||||
connect: deptIds.map((id) => ({ id })),
|
||||
set: deptIds.map((id) => ({ id })),
|
||||
},
|
||||
};
|
||||
// 删除原始的 taxonomy 字段
|
||||
|
|
|
@ -83,7 +83,7 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
|||
: undefined,
|
||||
},
|
||||
resources: {
|
||||
connect: [videoUrlId, ...fileIds]
|
||||
set: [videoUrlId, ...fileIds]
|
||||
.filter(Boolean)
|
||||
.map((fileId) => ({
|
||||
fileId,
|
||||
|
@ -109,7 +109,7 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
|||
: undefined,
|
||||
},
|
||||
resources: {
|
||||
connect: [videoUrlId, ...fileIds]
|
||||
set: [videoUrlId, ...fileIds]
|
||||
.filter(Boolean)
|
||||
.map((fileId) => ({
|
||||
fileId,
|
||||
|
|
Loading…
Reference in New Issue