This commit is contained in:
longdayi 2025-02-27 09:27:56 +08:00
commit b9bbf5c55b
11 changed files with 239 additions and 116 deletions

View File

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

View File

@ -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);
@ -35,10 +35,10 @@ const CategorySection = () => {
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">
@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ export default function CourseDetailLayout() {
const [isSyllabusOpen, setIsSyllabusOpen] = useState(true);
return (
<div className="relative">
<CourseDetailHeader />
{/* <CourseDetailHeader /> */}
{/* 添加 Header 组件 */}
{/* 主内容区域 */}

View File

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

View File

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

View File

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