diff --git a/apps/web/src/app/main/courses/components/CoursesContainer.tsx b/apps/web/src/app/main/courses/components/CoursesContainer.tsx index 237e8b3..5f16cc1 100755 --- a/apps/web/src/app/main/courses/components/CoursesContainer.tsx +++ b/apps/web/src/app/main/courses/components/CoursesContainer.tsx @@ -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 ( <> diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index d589d35..4c8141e 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -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 (
@@ -57,7 +57,7 @@ const CategorySection = () => { {isLoading ? ( ) : ( - courseCategoriesData.map((category, index) => { + courseCategoriesData?.map((category, index) => { const categoryColor = stringToColor(category.name); const isHovered = hoveredIndex === index; diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx index dfbf5e2..d6d288f 100755 --- a/apps/web/src/app/main/layout/MainHeader.tsx +++ b/apps/web/src/app/main/layout/MainHeader.tsx @@ -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,76 +18,77 @@ export function MainHeader() { const navigate = useNavigate(); const { searchValue, setSearchValue } = useMainContext(); - return ( -
- -
-
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"> - 烽火慕课 +
+
+
+
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"> + 烽火慕课 +
+
-
- - } - placeholder="搜索课程" - className="w-96 rounded-full" - value={searchValue} - onChange={(e) => setSearchValue(e.target.value)} - onPressEnter={(e) => { - if ( - !window.location.pathname.startsWith( - "/courses/" - ) - ) { - navigate(`/courses/`); - window.scrollTo({ - top: 0, - behavior: "smooth", - }); +
+ } - }} - /> -
- {isAuthenticated && ( - <> + placeholder="搜索课程" + className="w-96 rounded-full" + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} + onPressEnter={(e) => { + if ( + !window.location.pathname.startsWith("/courses/") && + !window.location.pathname.startsWith("my") + ) { + navigate(`/courses/`); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + } + }} + /> +
+ {isAuthenticated && ( + <> + + + )} + {isAuthenticated && ( - - )} - { - isAuthenticated && - } - {isAuthenticated ? ( - - ) : ( - - )} + )} + {isAuthenticated ? ( + + ) : ( + + )} +
); diff --git a/apps/web/src/app/main/layout/MainProvider.tsx b/apps/web/src/app/main/layout/MainProvider.tsx index 0952268..979d110 100755 --- a/apps/web/src/app/main/layout/MainProvider.tsx +++ b/apps/web/src/app/main/layout/MainProvider.tsx @@ -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>; setSelectedTerms?: React.Dispatch>; + searchCondition?: Prisma.PostWhereInput; } const MainContext = createContext(null); @@ -18,6 +26,29 @@ interface MainProviderProps { export function MainProvider({ children }: MainProviderProps) { const [searchValue, setSearchValue] = useState(""); const [selectedTerms, setSelectedTerms] = useState({}); // 初始化状态 + + 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 ( {children} diff --git a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx index e3ef21f..c25fc53 100755 --- a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx @@ -90,14 +90,14 @@ export function UserMenu() { icon: , label: "我创建的课程", action: () => { - navigate("/my/duty"); + navigate("/my-duty"); }, }, { icon: , label: "我学习的课程", action: () => { - navigate("/my/learning"); + navigate("/my-learning"); }, }, canManageAnyStaff && { diff --git a/apps/web/src/app/main/my-duty/page.tsx b/apps/web/src/app/main/my-duty/page.tsx index 64a529f..1031d79 100755 --- a/apps/web/src/app/main/my-duty/page.tsx +++ b/apps/web/src/app/main/my-duty/page.tsx @@ -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 ( <>
} - + renderItem={(post) => ( + + )} params={{ pageSize: 12, where: { authorId: user.id, + ...searchCondition, }, }} cols={4}> diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx index 14278e8..5f0a7a8 100755 --- a/apps/web/src/app/main/my-learning/page.tsx +++ b/apps/web/src/app/main/my-learning/page.tsx @@ -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 ( <>
} + renderItem={(post) => ( + + )} params={{ pageSize: 12, where: { @@ -17,6 +21,7 @@ export default function MyLearningPage() { id: user?.id, }, }, + ...searchCondition, }, }} cols={4}> diff --git a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx index 6154bbe..cac3bf8 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx @@ -19,7 +19,7 @@ export default function CourseDetailLayout() { const [isSyllabusOpen, setIsSyllabusOpen] = useState(true); return (
- + {/* */} {/* 添加 Header 组件 */} {/* 主内容区域 */} diff --git a/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx b/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx index 3ebc083..0b8da3b 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx @@ -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() {
{course?.title}
-
+
{"创建于:"} {dayjs(course?.createdAt).format("YYYY年M月D日")}
- + {"更新于:"} {dayjs(course?.updatedAt).format("YYYY年M月D日")}
@@ -38,19 +45,7 @@ export default function CourseDetailTitle() {
{`学习人数${course?.studentIds?.length || 0}`}
- {canEdit && ( -
{ - const url = course?.id - ? `/course/${course?.id}/editor` - : "/course/editor"; - navigate(url); - }}> - - {"点击编辑课程"} -
- )} +
); diff --git a/apps/web/src/components/models/course/detail/JoinLearingButton.tsx b/apps/web/src/components/models/course/detail/JoinLearingButton.tsx new file mode 100644 index 0000000..68c4243 --- /dev/null +++ b/apps/web/src/components/models/course/detail/JoinLearingButton.tsx @@ -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 && ( +
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 ? ( + + ) : ( + + ) + ) : ( + + )} + + {userIsLearning + ? isHovered + ? "退出学习" + : "正在学习" + : "加入学习"} + +
+ )} + {canEdit && ( +
{ + const url = course?.id + ? `/course/${course?.id}/editor` + : "/course/editor"; + navigate(url); + }}> + + {"编辑课程"} +
+ )} + + ); +} diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index f011db6..d4d4afe 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -92,6 +92,10 @@ export const routes: CustomRouteObject[] = [ ), }, + { + path: "course/:id?/detail/:lectureId?", // 使用 ? 表示 id 参数是可选的 + element: , + }, ], }, @@ -125,10 +129,6 @@ export const routes: CustomRouteObject[] = [ }, ], }, - { - path: ":id?/detail/:lectureId?", // 使用 ? 表示 id 参数是可选的 - element: , - }, ], }, adminRoute,