Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
b9bbf5c55b
|
@ -5,16 +5,13 @@ import { useMemo } from "react";
|
|||
import CourseCard from "./CourseCard";
|
||||
|
||||
export function CoursesContainer() {
|
||||
const { searchValue, selectedTerms } = useMainContext();
|
||||
const { selectedTerms, searchCondition } = useMainContext();
|
||||
const termFilters = useMemo(() => {
|
||||
return Object.entries(selectedTerms)
|
||||
.filter(([, terms]) => terms.length > 0)
|
||||
.map(([, terms]) => terms);
|
||||
}, [selectedTerms]);
|
||||
const searchCondition: Prisma.StringNullableFilter = {
|
||||
contains: searchValue,
|
||||
mode: "insensitive" as Prisma.QueryMode, // 使用类型断言
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostList
|
||||
|
@ -32,18 +29,7 @@ export function CoursesContainer() {
|
|||
},
|
||||
},
|
||||
})),
|
||||
OR: [
|
||||
{ title: searchCondition },
|
||||
{ subTitle: searchCondition },
|
||||
{ content: searchCondition },
|
||||
{
|
||||
terms: {
|
||||
some: {
|
||||
name: searchCondition,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
...searchCondition,
|
||||
},
|
||||
}}
|
||||
cols={4}></PostList>
|
||||
|
|
|
@ -19,11 +19,11 @@ const CategorySection = () => {
|
|||
taxonomy: {
|
||||
slug: TaxonomySlug.CATEGORY,
|
||||
},
|
||||
parentId : null
|
||||
parentId: null,
|
||||
},
|
||||
take: 8,
|
||||
});
|
||||
const navigate = useNavigate()
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleMouseEnter = useCallback((index: number) => {
|
||||
setHoveredIndex(index);
|
||||
|
@ -33,13 +33,13 @@ const CategorySection = () => {
|
|||
setHoveredIndex(null);
|
||||
}, []);
|
||||
|
||||
const handleMouseClick = useCallback((categoryId:string) => {
|
||||
const handleMouseClick = useCallback((categoryId: string) => {
|
||||
setSelectedTerms({
|
||||
[TaxonomySlug.CATEGORY] : [categoryId]
|
||||
})
|
||||
navigate('/courses')
|
||||
window.scrollTo({top: 0,behavior: "smooth",})
|
||||
},[]);
|
||||
[TaxonomySlug.CATEGORY]: [categoryId],
|
||||
});
|
||||
navigate("/courses");
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, []);
|
||||
return (
|
||||
<section className="py-8 relative overflow-hidden">
|
||||
<div className="max-w-screen-2xl mx-auto px-4 relative">
|
||||
|
@ -57,7 +57,7 @@ const CategorySection = () => {
|
|||
{isLoading ? (
|
||||
<Skeleton paragraph={{ rows: 4 }}></Skeleton>
|
||||
) : (
|
||||
courseCategoriesData.map((category, index) => {
|
||||
courseCategoriesData?.map((category, index) => {
|
||||
const categoryColor = stringToColor(category.name);
|
||||
const isHovered = hoveredIndex === index;
|
||||
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import { Input, Layout, Avatar, Button, Dropdown } from "antd";
|
||||
import { EditFilled, PlusOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
EditFilled,
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { UserMenu } from "./UserMenu/UserMenu";
|
||||
import { NavigationMenu } from "./NavigationMenu";
|
||||
import { useMainContext } from "./MainProvider";
|
||||
import { Header } from "antd/es/layout/layout";
|
||||
|
||||
export function MainHeader() {
|
||||
const { isAuthenticated, user } = useAuth();
|
||||
|
@ -12,11 +18,10 @@ export function MainHeader() {
|
|||
const navigate = useNavigate();
|
||||
const { searchValue, setSearchValue } = useMainContext();
|
||||
|
||||
|
||||
return (
|
||||
<div className="select-none flex items-center justify-between p-4 bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
|
||||
<div className="w-full max-w-screen-3xl px-4 md:px-6 mx-auto flex items-center justify-between h-full">
|
||||
<div className="flex items-center space-x-8">
|
||||
<div
|
||||
onClick={() => navigate("/")}
|
||||
className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent hover:scale-105 transition-transform cursor-pointer">
|
||||
|
@ -24,6 +29,8 @@ export function MainHeader() {
|
|||
</div>
|
||||
<NavigationMenu />
|
||||
</div>
|
||||
</div>
|
||||
<div className=" flex justify-end gap-4 mr-2">
|
||||
<Input
|
||||
size="large"
|
||||
prefix={
|
||||
|
@ -35,9 +42,8 @@ export function MainHeader() {
|
|||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onPressEnter={(e) => {
|
||||
if (
|
||||
!window.location.pathname.startsWith(
|
||||
"/courses/"
|
||||
)
|
||||
!window.location.pathname.startsWith("/courses/") &&
|
||||
!window.location.pathname.startsWith("my")
|
||||
) {
|
||||
navigate(`/courses/`);
|
||||
window.scrollTo({
|
||||
|
@ -63,15 +69,15 @@ export function MainHeader() {
|
|||
</Button>
|
||||
</>
|
||||
)}
|
||||
{
|
||||
isAuthenticated && <Button
|
||||
{isAuthenticated && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
||||
window.location.href = "/path/editor";
|
||||
}}
|
||||
|
||||
icon={<PlusOutlined></PlusOutlined>} >创建学习路径</Button>
|
||||
}
|
||||
icon={<PlusOutlined></PlusOutlined>}>
|
||||
创建学习路径
|
||||
</Button>
|
||||
)}
|
||||
{isAuthenticated ? (
|
||||
<UserMenu />
|
||||
) : (
|
||||
|
@ -84,5 +90,6 @@ export function MainHeader() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import React, { createContext, ReactNode, useContext, useState } from "react";
|
||||
import { Prisma } from "packages/common/dist";
|
||||
import React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
interface SelectedTerms {
|
||||
[key: string]: string[]; // 每个 slug 对应一个 string 数组
|
||||
}
|
||||
|
@ -8,6 +15,7 @@ interface MainContextType {
|
|||
selectedTerms?: SelectedTerms;
|
||||
setSearchValue?: React.Dispatch<React.SetStateAction<string>>;
|
||||
setSelectedTerms?: React.Dispatch<React.SetStateAction<SelectedTerms>>;
|
||||
searchCondition?: Prisma.PostWhereInput;
|
||||
}
|
||||
|
||||
const MainContext = createContext<MainContextType | null>(null);
|
||||
|
@ -18,6 +26,29 @@ interface MainProviderProps {
|
|||
export function MainProvider({ children }: MainProviderProps) {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [selectedTerms, setSelectedTerms] = useState<SelectedTerms>({}); // 初始化状态
|
||||
|
||||
const searchCondition: Prisma.PostWhereInput = useMemo(() => {
|
||||
const containTextCondition: Prisma.StringNullableFilter = {
|
||||
contains: searchValue,
|
||||
mode: "insensitive" as Prisma.QueryMode, // 使用类型断言
|
||||
};
|
||||
return searchValue
|
||||
? {
|
||||
OR: [
|
||||
{ title: containTextCondition },
|
||||
{ subTitle: containTextCondition },
|
||||
{ content: containTextCondition },
|
||||
{
|
||||
terms: {
|
||||
some: {
|
||||
name: containTextCondition,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {};
|
||||
}, [searchValue]);
|
||||
return (
|
||||
<MainContext.Provider
|
||||
value={{
|
||||
|
@ -25,6 +56,7 @@ export function MainProvider({ children }: MainProviderProps) {
|
|||
setSearchValue,
|
||||
selectedTerms,
|
||||
setSelectedTerms,
|
||||
searchCondition,
|
||||
}}>
|
||||
{children}
|
||||
</MainContext.Provider>
|
||||
|
|
|
@ -90,14 +90,14 @@ export function UserMenu() {
|
|||
icon: <UserOutlined className="text-lg" />,
|
||||
label: "我创建的课程",
|
||||
action: () => {
|
||||
navigate("/my/duty");
|
||||
navigate("/my-duty");
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <UserOutlined className="text-lg" />,
|
||||
label: "我学习的课程",
|
||||
action: () => {
|
||||
navigate("/my/learning");
|
||||
navigate("/my-learning");
|
||||
},
|
||||
},
|
||||
canManageAnyStaff && {
|
||||
|
|
|
@ -1,19 +1,23 @@
|
|||
import PostList from "@web/src/components/models/course/list/PostList";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { useMainContext } from "../layout/MainProvider";
|
||||
import CourseCard from "../courses/components/CourseCard";
|
||||
|
||||
export default function MyDutyPage() {
|
||||
const { user } = useAuth();
|
||||
const { searchCondition } = useMainContext();
|
||||
return (
|
||||
<>
|
||||
<div className="p-4">
|
||||
<PostList
|
||||
renderItem={post=><CourseCard edit course={post}></CourseCard>}
|
||||
|
||||
renderItem={(post) => (
|
||||
<CourseCard edit course={post}></CourseCard>
|
||||
)}
|
||||
params={{
|
||||
pageSize: 12,
|
||||
where: {
|
||||
authorId: user.id,
|
||||
...searchCondition,
|
||||
},
|
||||
}}
|
||||
cols={4}></PostList>
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import PostList from "@web/src/components/models/course/list/PostList";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { useMainContext } from "../layout/MainProvider";
|
||||
import CourseCard from "../courses/components/CourseCard";
|
||||
|
||||
export default function MyLearningPage() {
|
||||
const { user } = useAuth();
|
||||
const { searchCondition } = useMainContext();
|
||||
return (
|
||||
<>
|
||||
<div className="p-4">
|
||||
<PostList
|
||||
renderItem={post => <CourseCard edit={false} course={post}></CourseCard>}
|
||||
renderItem={(post) => (
|
||||
<CourseCard edit={false} course={post}></CourseCard>
|
||||
)}
|
||||
params={{
|
||||
pageSize: 12,
|
||||
where: {
|
||||
|
@ -17,6 +21,7 @@ export default function MyLearningPage() {
|
|||
id: user?.id,
|
||||
},
|
||||
},
|
||||
...searchCondition,
|
||||
},
|
||||
}}
|
||||
cols={4}></PostList>
|
||||
|
|
|
@ -19,7 +19,7 @@ export default function CourseDetailLayout() {
|
|||
const [isSyllabusOpen, setIsSyllabusOpen] = useState(true);
|
||||
return (
|
||||
<div className="relative">
|
||||
<CourseDetailHeader />
|
||||
{/* <CourseDetailHeader /> */}
|
||||
|
||||
{/* 添加 Header 组件 */}
|
||||
{/* 主内容区域 */}
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import { useContext } from "react";
|
||||
import { CourseDetailContext } from "./CourseDetailContext";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { BookOutlined, CalendarOutlined, EditTwoTone, EyeOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
BookOutlined,
|
||||
CalendarOutlined,
|
||||
EditTwoTone,
|
||||
EyeOutlined,
|
||||
ReloadOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import CourseOperationBtns from "./JoinLearingButton";
|
||||
|
||||
export default function CourseDetailTitle() {
|
||||
const {
|
||||
|
@ -19,14 +26,14 @@ export default function CourseDetailTitle() {
|
|||
<div className="flex justify-start w-full text-2xl font-bold">
|
||||
{course?.title}
|
||||
</div>
|
||||
<div className="text-gray-600 flex w-full justify-start gap-5">
|
||||
<div className="text-gray-600 flex w-full justify-start items-center gap-5">
|
||||
<div className="flex gap-1">
|
||||
<CalendarOutlined></CalendarOutlined>
|
||||
{"创建于:"}
|
||||
{dayjs(course?.createdAt).format("YYYY年M月D日")}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<ReloadOutlined></ReloadOutlined>
|
||||
<ReloadOutlined spin></ReloadOutlined>
|
||||
{"更新于:"}
|
||||
{dayjs(course?.updatedAt).format("YYYY年M月D日")}
|
||||
</div>
|
||||
|
@ -38,19 +45,7 @@ export default function CourseDetailTitle() {
|
|||
<BookOutlined />
|
||||
<div>{`学习人数${course?.studentIds?.length || 0}`}</div>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<div
|
||||
className="flex gap-1 text-primary hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
const url = course?.id
|
||||
? `/course/${course?.id}/editor`
|
||||
: "/course/editor";
|
||||
navigate(url);
|
||||
}}>
|
||||
<EditTwoTone></EditTwoTone>
|
||||
{"点击编辑课程"}
|
||||
</div>
|
||||
)}
|
||||
<CourseOperationBtns />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { useContext, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { CourseDetailContext } from "./CourseDetailContext";
|
||||
import { useStaff } from "@nice/client";
|
||||
import {
|
||||
CheckCircleFilled,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleFilled,
|
||||
CloseCircleOutlined,
|
||||
EditFilled,
|
||||
EditTwoTone,
|
||||
LoginOutlined,
|
||||
} from "@ant-design/icons";
|
||||
|
||||
export default function CourseOperationBtns() {
|
||||
const { id } = useParams();
|
||||
const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } =
|
||||
useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { course, canEdit, userIsLearning } = useContext(CourseDetailContext);
|
||||
const { update } = useStaff();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const toggleLearning = 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isAuthenticated && (
|
||||
<div
|
||||
onClick={toggleLearning}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={`flex px-1 py-0.5 gap-1 hover:cursor-pointer transition-all ${
|
||||
userIsLearning
|
||||
? isHovered
|
||||
? "text-red-500 border-red-500 rounded-md "
|
||||
: "text-green-500 "
|
||||
: "text-primary "
|
||||
}`}>
|
||||
{userIsLearning ? (
|
||||
isHovered ? (
|
||||
<CloseCircleOutlined />
|
||||
) : (
|
||||
<CheckCircleOutlined />
|
||||
)
|
||||
) : (
|
||||
<LoginOutlined />
|
||||
)}
|
||||
<span>
|
||||
{userIsLearning
|
||||
? isHovered
|
||||
? "退出学习"
|
||||
: "正在学习"
|
||||
: "加入学习"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{canEdit && (
|
||||
<div
|
||||
className="flex gap-1 px-1 py-0.5 text-primary hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
const url = course?.id
|
||||
? `/course/${course?.id}/editor`
|
||||
: "/course/editor";
|
||||
navigate(url);
|
||||
}}>
|
||||
<EditTwoTone></EditTwoTone>
|
||||
{"编辑课程"}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -92,6 +92,10 @@ export const routes: CustomRouteObject[] = [
|
|||
</WithAuth>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "course/:id?/detail/:lectureId?", // 使用 ? 表示 id 参数是可选的
|
||||
element: <CourseDetailPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
@ -125,10 +129,6 @@ export const routes: CustomRouteObject[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: ":id?/detail/:lectureId?", // 使用 ? 表示 id 参数是可选的
|
||||
element: <CourseDetailPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminRoute,
|
||||
|
|
Loading…
Reference in New Issue