diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index 6630e1b..83e8f13 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -1,86 +1,86 @@ -import React, { useState, useCallback, useEffect, useMemo } from "react"; -import { Typography, Skeleton } from "antd"; -import { stringToColor, TaxonomySlug, TermDto } from "@nice/common"; -import { api } from "@nice/client"; -import LookForMore from "./LookForMore"; -import CategorySectionCard from "./CategorySectionCard"; -import { useNavigate } from "react-router-dom"; -import { useMainContext } from "../../layout/MainProvider"; +// import React, { useState, useCallback, useEffect, useMemo } from "react"; +// import { Typography, Skeleton } from "antd"; +// import { stringToColor, TaxonomySlug, TermDto } from "@nice/common"; +// import { api } from "@nice/client"; +// import LookForMore from "./LookForMore"; +// import CategorySectionCard from "./CategorySectionCard"; +// import { useNavigate } from "react-router-dom"; +// import { useMainContext } from "../../layout/MainProvider"; -const { Title, Text } = Typography; -const CategorySection = () => { - const [hoveredIndex, setHoveredIndex] = useState(null); - const { selectedTerms, setSelectedTerms } = useMainContext(); - const { - data: courseCategoriesData, - isLoading, - }: { data: TermDto[]; isLoading: boolean } = api.term.findMany.useQuery({ - where: { - taxonomy: { - slug: TaxonomySlug.CATEGORY, - }, - parentId: null, - }, - take: 8, - }); - const navigate = useNavigate(); +// const { Title, Text } = Typography; +// const CategorySection = () => { +// const [hoveredIndex, setHoveredIndex] = useState(null); +// const { selectedTerms, setSelectedTerms } = useMainContext(); +// const { +// data: courseCategoriesData, +// isLoading, +// }: { data: TermDto[]; isLoading: boolean } = api.term.findMany.useQuery({ +// where: { +// taxonomy: { +// slug: TaxonomySlug.CATEGORY, +// }, +// parentId: null, +// }, +// take: 8, +// }); +// const navigate = useNavigate(); - const handleMouseEnter = useCallback((index: number) => { - setHoveredIndex(index); - }, []); +// const handleMouseEnter = useCallback((index: number) => { +// setHoveredIndex(index); +// }, []); - const handleMouseLeave = useCallback(() => { - setHoveredIndex(null); - }, []); +// const handleMouseLeave = useCallback(() => { +// setHoveredIndex(null); +// }, []); - const handleMouseClick = useCallback((categoryId: string) => { - setSelectedTerms({ - ...selectedTerms, - [TaxonomySlug.CATEGORY]: [categoryId], - }); - navigate("/courses"); - window.scrollTo({ top: 0, behavior: "smooth" }); - }, []); - return ( -
-
-
- - 探索课程分类 - - - 选择你感兴趣的方向,开启学习之旅 - -
-
- {isLoading ? ( - - ) : ( - courseCategoriesData?.filter(c=>!c.deletedAt)?.map((category, index) => { - const categoryColor = stringToColor(category.name); - const isHovered = hoveredIndex === index; +// const handleMouseClick = useCallback((categoryId: string) => { +// setSelectedTerms({ +// ...selectedTerms, +// [TaxonomySlug.CATEGORY]: [categoryId], +// }); +// navigate("/courses"); +// window.scrollTo({ top: 0, behavior: "smooth" }); +// }, []); +// return ( +//
+//
+//
+// +// 探索课程分类 +// +// +// 选择你感兴趣的方向,开启学习之旅 +// +//
+//
+// {isLoading ? ( +// +// ) : ( +// courseCategoriesData?.filter(c=>!c.deletedAt)?.map((category, index) => { +// const categoryColor = stringToColor(category.name); +// const isHovered = hoveredIndex === index; - return ( - - ); - }) - )} -
- -
-
- ); -}; +// return ( +// +// ); +// }) +// )} +//
+// +//
+//
+// ); +// }; -export default CategorySection; +// export default CategorySection; diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx deleted file mode 100755 index c4f4f7d..0000000 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { useState, useMemo, ReactNode } from "react"; -import { Typography, Skeleton } from "antd"; -import { TaxonomySlug, TermDto } from "@nice/common"; -import { api } from "@nice/client"; -import { CoursesSectionTag } from "./CoursesSectionTag"; -import LookForMore from "./LookForMore"; -import PostList from "@web/src/components/models/course/list/PostList"; -interface GetTaxonomyProps { - categories: string[]; - isLoading: boolean; -} -function useGetTaxonomy({ type }): GetTaxonomyProps { - const { data, isLoading }: { data: TermDto[]; isLoading: boolean } = - api.term.findMany.useQuery({ - where: { - taxonomy: { - slug: type, - }, - parentId: null, - }, - take: 11, // 只取前10个 - }); - const categories = useMemo(() => { - const allCategories = isLoading - ? [] - : data?.filter(c=>!c.deletedAt)?.map((course) => course.name); - return [...Array.from(new Set(allCategories))]; - }, [data]); - return { categories, isLoading }; -} -const { Title, Text } = Typography; -interface CoursesSectionProps { - title: string; - description: string; - initialVisibleCoursesCount?: number; - postType:string; - render?:(post)=>ReactNode; - to:string -} -const CoursesSection: React.FC = ({ - title, - description, - initialVisibleCoursesCount = 8, - postType, - render, - to -}) => { - const [selectedCategory, setSelectedCategory] = useState("全部"); - const gateGory: GetTaxonomyProps = useGetTaxonomy({ - type: TaxonomySlug.CATEGORY, - }); - return ( -
-
-
-
- - {title} - - - {description} - -
-
-
- {gateGory.isLoading ? ( - - ) : ( - <> - {["全部", ...gateGory.categories].map( - (category, idx) => ( - - ) - )} - - )} -
- render(post)} - params={{ - page: 1, - pageSize: initialVisibleCoursesCount, - where: { - deletedAt:null, - terms: !(selectedCategory === "全部") - ? { - some: { - name: selectedCategory, - }, - } - : {}, - type: postType - }, - }} - showPagination={false} - cols={4}> - -
-
- ); -}; -export default CoursesSection; diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index 4482872..fb6b8d1 100755 --- a/apps/web/src/app/main/home/page.tsx +++ b/apps/web/src/app/main/home/page.tsx @@ -1,34 +1,34 @@ -import HeroSection from "./components/HeroSection"; -import CategorySection from "./components/CategorySection"; -import CoursesSection from "./components/CoursesSection"; -import { PostType } from "@nice/common"; -import PathCard from "@web/src/components/models/post/SubPost/PathCard"; -import CourseCard from "@web/src/components/models/post/SubPost/CourseCard"; +// import HeroSection from "./components/HeroSection"; +// import CategorySection from "./components/CategorySection"; +// import CoursesSection from "./components/CoursesSection"; +// import { PostType } from "@nice/common"; +// import PathCard from "@web/src/components/models/post/SubPost/PathCard"; +// import CourseCard from "@web/src/components/models/post/SubPost/CourseCard"; -const HomePage = () => { +// const HomePage = () => { - return ( -
- - } - to={"path"} - /> - } - to={"/courses"} - /> +// return ( +//
+// +// } +// to={"path"} +// /> +// } +// to={"/courses"} +// /> - -
- ); -}; +// +//
+// ); +// }; -export default HomePage; +// export default HomePage; diff --git a/apps/web/src/app/main/layout/BasePost/BasePostLayout.tsx b/apps/web/src/app/main/layout/BasePost/BasePostLayout.tsx deleted file mode 100755 index 09c7852..0000000 --- a/apps/web/src/app/main/layout/BasePost/BasePostLayout.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { ReactNode, useEffect } from "react"; -import FilterSection from "./FilterSection"; -import { useMainContext } from "../MainProvider"; - -export function BasePostLayout({ - children, - showSearchMode = false, -}: { - children: ReactNode; - showSearchMode?: boolean; -}) { - const { setShowSearchMode } = useMainContext(); - useEffect(() => { - setShowSearchMode(showSearchMode); - }, [showSearchMode]); - return ( - <> -
-
-
- -
-
{children}
-
-
- - ); -} -export default BasePostLayout; diff --git a/apps/web/src/app/main/layout/BasePost/FilterSection.tsx b/apps/web/src/app/main/layout/BasePost/FilterSection.tsx deleted file mode 100755 index 084878c..0000000 --- a/apps/web/src/app/main/layout/BasePost/FilterSection.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Divider } from "antd"; -import { api } from "@nice/client"; -import { useMainContext } from "../MainProvider"; -import TermParentSelector from "@web/src/components/models/term/term-parent-selector"; -import SearchModeRadio from "./SearchModeRadio"; -export default function FilterSection() { - const { data: taxonomies } = api.taxonomy.getAll.useQuery({}); - const { selectedTerms, setSelectedTerms, showSearchMode } = - useMainContext(); - const handleTermChange = (slug: string, selected: string[]) => { - setSelectedTerms({ - ...selectedTerms, - [slug]: selected, // 更新对应 slug 的选择 - }); - }; - return ( -
- {showSearchMode && } - {taxonomies?.map((tax, index) => { - const items = Object.entries(selectedTerms).find( - ([key, items]) => key === tax.slug - )?.[1]; - return ( -
-

- {tax?.name} - {/* {JSON.stringify(items)} */} -

- - handleTermChange( - tax?.slug, - selected as string[] - ) - } - taxonomyId={tax?.id}> - -
- ); - })} -
- ); -} diff --git a/apps/web/src/app/main/layout/BasePost/SearchModeRadio.tsx b/apps/web/src/app/main/layout/BasePost/SearchModeRadio.tsx deleted file mode 100755 index 507e94f..0000000 --- a/apps/web/src/app/main/layout/BasePost/SearchModeRadio.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useMainContext } from "../MainProvider"; -import { Radio, Space, Typography } from "antd"; -import { PostType } from "@nice/common"; // Assuming PostType is defined in this path - -export default function SearchModeRadio() { - const { searchMode, setSearchMode } = useMainContext(); - - const handleModeChange = (e) => { - setSearchMode(e.target.value); - }; - - return ( - -

只搜索

- - 视频课程 - 思维导图 - 所有资源 - -
- ); -} diff --git a/apps/web/src/app/main/my-duty-path/components/MyDutyPathContainer.tsx b/apps/web/src/app/main/my-duty-path/components/MyDutyPathContainer.tsx deleted file mode 100755 index 38e381e..0000000 --- a/apps/web/src/app/main/my-duty-path/components/MyDutyPathContainer.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import PostList from "@web/src/components/models/course/list/PostList"; -import { useAuth } from "@web/src/providers/auth-provider"; -import { useMainContext } from "../../layout/MainProvider"; -import { PostType } from "@nice/common"; -import PathCard from "@web/src/components/models/post/SubPost/PathCard"; - -export default function MyDutyPathContainer() { - const { user } = useAuth(); - const { searchCondition, termsCondition } = useMainContext(); - return ( - <> - } - params={{ - pageSize: 12, - where: { - deletedAt: null, - type: PostType.PATH, - authorId: user?.id, - ...termsCondition, - ...searchCondition, - }, - }} - cols={4}> - - ); -} diff --git a/apps/web/src/app/main/my-duty-path/page.tsx b/apps/web/src/app/main/my-duty-path/page.tsx deleted file mode 100755 index 24d7931..0000000 --- a/apps/web/src/app/main/my-duty-path/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect } from "react"; -import BasePostLayout from "../layout/BasePost/BasePostLayout"; -import { useMainContext } from "../layout/MainProvider"; -import { PostType } from "@nice/common"; -import MyDutyPathContainer from "./components/MyDutyPathContainer"; - -export default function MyDutyPathPage() { - const { setSearchMode } = useMainContext(); - useEffect(() => { - setSearchMode(PostType.PATH); - }, [setSearchMode]); - return ( - - - - ); -} diff --git a/apps/web/src/app/main/my-duty/components/MyDutyListContainer.tsx b/apps/web/src/app/main/my-duty/components/MyDutyListContainer.tsx deleted file mode 100755 index 79258a8..0000000 --- a/apps/web/src/app/main/my-duty/components/MyDutyListContainer.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import PostList from "@web/src/components/models/course/list/PostList"; -import { useAuth } from "@web/src/providers/auth-provider"; -import { PostType } from "@nice/common"; -import { useMainContext } from "../../layout/MainProvider"; -import PostCard from "@web/src/components/models/post/PostCard"; -import CourseCard from "@web/src/components/models/post/SubPost/CourseCard"; - -export default function MyDutyListContainer() { - const { user } = useAuth(); - const { searchCondition, termsCondition } = useMainContext(); - return ( - <> - } - params={{ - pageSize: 12, - where: { - deletedAt:null, - type: PostType.COURSE, - authorId: user.id, - ...termsCondition, - ...searchCondition, - }, - }} - cols={4}> - - ); -} diff --git a/apps/web/src/app/main/my-duty/page.tsx b/apps/web/src/app/main/my-duty/page.tsx deleted file mode 100755 index fff4c91..0000000 --- a/apps/web/src/app/main/my-duty/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import BasePostLayout from "../layout/BasePost/BasePostLayout"; -import MyDutyListContainer from "./components/MyDutyListContainer"; -import { useEffect } from "react"; -import { useMainContext } from "../layout/MainProvider"; -import { PostType } from "@nice/common"; -export default function MyDutyPage() { - const { setSearchMode } = useMainContext(); - useEffect(() => { - setSearchMode(PostType.COURSE); - }, [setSearchMode]); - return ( - - - - ); -} diff --git a/apps/web/src/app/main/my-learning/components/MyLearningListContainer.tsx b/apps/web/src/app/main/my-learning/components/MyLearningListContainer.tsx deleted file mode 100755 index 19c5266..0000000 --- a/apps/web/src/app/main/my-learning/components/MyLearningListContainer.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import PostList from "@web/src/components/models/course/list/PostList"; -import { useAuth } from "@web/src/providers/auth-provider"; -import { useMainContext } from "../../layout/MainProvider"; -import { PostType } from "@nice/common"; -import PostCard from "@web/src/components/models/post/PostCard"; -import CourseCard from "@web/src/components/models/post/SubPost/CourseCard"; - -export default function MyLearningListContainer() { - const { user } = useAuth(); - const { searchCondition, termsCondition } = useMainContext(); - return ( - <> - } - params={{ - pageSize: 12, - where: { - deletedAt: null, - type: PostType.COURSE, - students: { - some: { - id: user?.id, - }, - }, - ...termsCondition, - ...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 deleted file mode 100755 index cee2c94..0000000 --- a/apps/web/src/app/main/my-learning/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect } from "react"; -import BasePostLayout from "../layout/BasePost/BasePostLayout"; -import { useMainContext } from "../layout/MainProvider"; -import MyLearningListContainer from "./components/MyLearningListContainer"; -import { PostType } from "@nice/common"; - -export default function MyLearningPage() { - const { setSearchMode } = useMainContext(); - useEffect(() => { - setSearchMode(PostType.COURSE); - }, [setSearchMode]); - return ( - - - - ); -} diff --git a/apps/web/src/app/main/my-path/components/MyPathListContainer.tsx b/apps/web/src/app/main/my-path/components/MyPathListContainer.tsx deleted file mode 100755 index a481263..0000000 --- a/apps/web/src/app/main/my-path/components/MyPathListContainer.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import PostList from "@web/src/components/models/course/list/PostList"; -import { useAuth } from "@web/src/providers/auth-provider"; - -import { PostType } from "@nice/common"; -import { useMainContext } from "../../layout/MainProvider"; -import PostCard from "@web/src/components/models/post/PostCard"; -import PathCard from "@web/src/components/models/post/SubPost/PathCard"; - -export default function MyPathListContainer() { - const { user } = useAuth(); - const { searchCondition, termsCondition } = useMainContext(); - return ( - <> - } - params={{ - pageSize: 12, - where: { - type: PostType.PATH, - students: { - some: { - id: user?.id, - }, - }, - deletedAt: null, - ...termsCondition, - ...searchCondition, - }, - }} - cols={4}> - - ); -} diff --git a/apps/web/src/app/main/my-path/page.tsx b/apps/web/src/app/main/my-path/page.tsx deleted file mode 100755 index 81f7298..0000000 --- a/apps/web/src/app/main/my-path/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect } from "react"; -import BasePostLayout from "../layout/BasePost/BasePostLayout"; -import { useMainContext } from "../layout/MainProvider"; -import MyPathListContainer from "./components/MyPathListContainer"; -import { PostType } from "@nice/common"; - -export default function MyPathPage() { - const { setSearchMode } = useMainContext(); - useEffect(() => { - setSearchMode(PostType.PATH); - }, [setSearchMode]); - return ( - - - - ); -} diff --git a/apps/web/src/app/main/path/components/DeptInfo.tsx b/apps/web/src/app/main/path/components/DeptInfo.tsx deleted file mode 100755 index ed6510b..0000000 --- a/apps/web/src/app/main/path/components/DeptInfo.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { BookOutlined, EyeOutlined, TeamOutlined } from "@ant-design/icons"; -import { Typography } from "antd"; -import { PostDto } from "@nice/common"; - -const { Title, Text } = Typography; -const DeptInfo = ({ post }: { post: PostDto }) => { - return ( -
-
- - {post?.depts && post?.depts?.length > 0 ? ( - - {post?.depts?.length > 1 - ? `${post.depts[0].name}等` - : post?.depts?.[0]?.name} - - ) : ( - - 未设置单位 - - )} -
- {post && ( -
- - 浏览量 - - {`${post?.views || 0}`} - - {post?.studentIds && post?.studentIds?.length > 0 && ( - - - {`${post?.studentIds?.length || 0}`} - - )} -
- )} -
- ); -}; - -export default DeptInfo; diff --git a/apps/web/src/app/main/path/components/PathListContainer.tsx b/apps/web/src/app/main/path/components/PathListContainer.tsx deleted file mode 100755 index 98f8eeb..0000000 --- a/apps/web/src/app/main/path/components/PathListContainer.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import PostList from "@web/src/components/models/course/list/PostList"; -import { useMainContext } from "../../layout/MainProvider"; -import { PostType, Prisma } from "@nice/common"; -import PostCard from "@web/src/components/models/post/PostCard"; -import PathCard from "@web/src/components/models/post/SubPost/PathCard"; - -export function PathListContainer() { - const { searchCondition, termsCondition } = useMainContext(); - return ( - <> - } - params={{ - pageSize: 12, - where: { - type: PostType.PATH, - ...termsCondition, - ...searchCondition, - deletedAt: null, - }, - }} - cols={4} - > - - ); -} -export default PathListContainer; diff --git a/apps/web/src/app/main/path/components/TermInfo.tsx b/apps/web/src/app/main/path/components/TermInfo.tsx deleted file mode 100755 index 21fed6d..0000000 --- a/apps/web/src/app/main/path/components/TermInfo.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Tag } from "antd"; -import { PostDto, TaxonomySlug, TermDto } from "@nice/common"; - -const TermInfo = ({ terms = [] }: { terms?: TermDto[] }) => { - return ( -
- {terms && terms?.length > 0 ? ( -
- {terms - ?.sort((a, b) => - String(a?.taxonomy?.id || "").localeCompare( - String(b?.taxonomy?.id || "") - ) - ) - ?.map((term: any) => { - return ( - - {term.name} - - ); - })} -
- ) : ( -
- - {"未设置分类"} - -
- )} -
- ); -}; - -export default TermInfo; diff --git a/apps/web/src/app/main/path/editor/page.tsx b/apps/web/src/app/main/path/editor/page.tsx deleted file mode 100755 index 16c106e..0000000 --- a/apps/web/src/app/main/path/editor/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import MindEditor from "@web/src/components/common/editor/MindEditor"; -import { PostDetailProvider } from "@web/src/components/models/course/detail/PostDetailContext"; -import { useParams } from "react-router-dom"; - -export default function PathEditorPage() { - const { id } = useParams(); - return ( - - - - ); -} diff --git a/apps/web/src/app/main/path/page.tsx b/apps/web/src/app/main/path/page.tsx deleted file mode 100755 index 542689b..0000000 --- a/apps/web/src/app/main/path/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useEffect } from "react"; -import BasePostLayout from "../layout/BasePost/BasePostLayout"; -import { useMainContext } from "../layout/MainProvider"; -import PathListContainer from "./components/PathListContainer"; -import { PostType } from "@nice/common"; -import { PostDetailProvider } from "@web/src/components/models/course/detail/PostDetailContext"; -import { useParams } from "react-router-dom"; - -export default function PathPage() { - const { setSearchMode } = useMainContext(); - useEffect(() => { - setSearchMode(PostType.PATH); - }, [setSearchMode]); - const { id } = useParams(); - return ( - - - - ); -} diff --git a/apps/web/src/app/main/search/components/SearchContainer.tsx b/apps/web/src/app/main/search/components/SearchContainer.tsx deleted file mode 100755 index d227fba..0000000 --- a/apps/web/src/app/main/search/components/SearchContainer.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import PostList from "@web/src/components/models/course/list/PostList"; -import { useMainContext } from "../../layout/MainProvider"; -import PostCard from "@web/src/components/models/post/PostCard"; -import { PostType } from "@nice/common"; -import CourseCard from "@web/src/components/models/post/SubPost/CourseCard"; -import PathCard from "@web/src/components/models/post/SubPost/PathCard"; -const POST_TYPE_COMPONENTS = { - [PostType.COURSE]: CourseCard, - [PostType.PATH]: PathCard, -}; -export default function SearchListContainer() { - const { searchCondition, termsCondition, searchMode } = useMainContext(); - - return ( - <> - { - const Component = - POST_TYPE_COMPONENTS[post.type] || PostCard; - return ; - }} - params={{ - pageSize: 12, - where: { - type: searchMode === "both" ? { in: [PostType.COURSE, PostType.PATH] } : searchMode, - ...termsCondition, - ...searchCondition, - deletedAt: null, - }, - }} - cols={4}> - - ); -} diff --git a/apps/web/src/app/main/search/page.tsx b/apps/web/src/app/main/search/page.tsx deleted file mode 100755 index cfb6153..0000000 --- a/apps/web/src/app/main/search/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useEffect } from "react"; -import BasePostLayout from "../layout/BasePost/BasePostLayout"; -import SearchListContainer from "./components/SearchContainer"; -import { useMainContext } from "../layout/MainProvider"; - -export default function SearchPage() { - const { setShowSearchMode, setSearchValue } = useMainContext(); - useEffect(() => { - setShowSearchMode(true); - return () => { - setShowSearchMode(false); - setSearchValue(""); - }; - }, [setShowSearchMode]); - return ( - - - - ); -} diff --git a/apps/web/src/app/main/self/courses/page.tsx b/apps/web/src/app/main/self/courses/page.tsx deleted file mode 100755 index 36b358f..0000000 --- a/apps/web/src/app/main/self/courses/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function MyCoursePage() { - return ( -
- My Course Page -
- ) -} \ No newline at end of file diff --git a/apps/web/src/app/main/self/profiles/page.tsx b/apps/web/src/app/main/self/profiles/page.tsx deleted file mode 100755 index 70ac75b..0000000 --- a/apps/web/src/app/main/self/profiles/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ProfilesPage() { - return <>Profiles -} \ No newline at end of file diff --git a/apps/web/src/components/common/editor/MindEditor.tsx b/apps/web/src/components/common/editor/MindEditor.tsx deleted file mode 100755 index f71f925..0000000 --- a/apps/web/src/components/common/editor/MindEditor.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import { Button, Empty, Form, Modal, Spin } from "antd"; -import NodeMenu from "./NodeMenu"; -import { api, usePost, useVisitor } from "@nice/client"; -import { - ObjectType, - PathDto, - postDetailSelect, - PostType, - Prisma, - RolePerms, - VisitType, -} from "@nice/common"; -import TermSelect from "../../models/term/term-select"; -import DepartmentSelect from "../../models/department/department-select"; -import { useContext, useEffect, useMemo, useRef, useState } from "react"; -import toast from "react-hot-toast"; -import { MindElixirInstance } from "mind-elixir"; -import MindElixir from "mind-elixir"; -import { useTusUpload } from "@web/src/hooks/useTusUpload"; -import { useNavigate } from "react-router-dom"; -import { useAuth } from "@web/src/providers/auth-provider"; -import { MIND_OPTIONS } from "./constant"; -import { ExclamationCircleFilled, LinkOutlined, SaveOutlined } from "@ant-design/icons"; -import JoinButton from "../../models/course/detail/CourseOperationBtns/JoinButton"; -import { CourseDetailContext } from "../../models/course/detail/PostDetailContext"; -import ReactDOM from "react-dom"; -import { createRoot } from "react-dom/client"; -import { useQueryClient } from "@tanstack/react-query"; -import { getQueryKey } from "@trpc/react-query"; -export default function MindEditor({ id }: { id?: string }) { - const containerRef = useRef(null); - const { confirm } = Modal; - const { - post, - isLoading, - // userIsLearning, - // setUserIsLearning, - } = useContext(CourseDetailContext); - const [instance, setInstance] = useState(null); - const { isAuthenticated, user, hasSomePermissions } = useAuth(); - const { read } = useVisitor(); - const queryClient = useQueryClient(); - useEffect(() => { - console.log("post", post) - console.log("user", user) - console.log(canEdit) - }) - // const { data: post, isLoading }: { data: PathDto; isLoading: boolean } = - // api.post.findFirst.useQuery( - // { - // where: { - // id, - // }, - // select: postDetailSelect, - // }, - // { enabled: Boolean(id) } - // ); - const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({ - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) }); - } - }) - const canEdit: boolean = useMemo(() => { - const isAuth = isAuthenticated && user?.id === post?.author?.id; - - return ( - isAuthenticated && - (!id || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST)) - ); - }, [user, post]); - - - const navigate = useNavigate(); - const { create, update } = usePost(); - const { data: taxonomies } = api.taxonomy.getAll.useQuery({ - type: ObjectType.COURSE, - }); - const { handleFileUpload } = useTusUpload(); - const [form] = Form.useForm(); - const handleIcon = () => { - const hyperLinkElement = document.querySelectorAll(".hyper-link"); - console.log("hyperLinkElement", hyperLinkElement); - hyperLinkElement.forEach((item) => { - const hyperLinkDom = createRoot(item); - hyperLinkDom.render(); - }); - } - const CustomLinkIconPlugin = (mind) => { - mind.bus.addListener("operation", handleIcon) - }; - useEffect(() => { - if (post?.id && id) { - read.mutateAsync({ - data: { - visitorId: user?.id || null, - postId: post?.id, - type: VisitType.READED, - }, - }); - } - }, [post]); - useEffect(() => { - if (post && form && instance && id) { - instance.refresh((post as any).meta); - const deptIds = (post?.depts || [])?.map((dept) => dept.id); - const formData = { - title: post.title, - deptIds: deptIds, - }; - // post.terms?.forEach((term) => { - // formData[term.taxonomyId] = term.id; // 假设 taxonomyName是您在 Form.Item 中使用的name - // }); - - // 按 taxonomyId 分组所有 terms - const termsByTaxonomy = {}; - post.terms?.forEach((term) => { - if (!termsByTaxonomy[term.taxonomyId]) { - termsByTaxonomy[term.taxonomyId] = []; - } - termsByTaxonomy[term.taxonomyId].push(term.id); - }); - - // 将分组后的 terms 设置到 formData - Object.entries(termsByTaxonomy).forEach(([taxonomyId, termIds]) => { - formData[taxonomyId] = termIds; - }); - form.setFieldsValue(formData); - } - }, [post, form, instance, id]); - useEffect(() => { - if (!containerRef.current) return; - const mind = new MindElixir({ - ...MIND_OPTIONS, - el: containerRef.current, - before: { - beginEdit() { - return canEdit; - }, - }, - draggable: canEdit, // 禁用拖拽 - contextMenu: canEdit, // 禁用右键菜单 - toolBar: canEdit, // 禁用工具栏 - nodeMenu: canEdit, // 禁用节点右键菜单 - keypress: canEdit, // 禁用键盘快捷键 - }); - mind.install(CustomLinkIconPlugin); - mind.init(MindElixir.new("新思维导图")); - containerRef.current.hidden = true; - //挂载实例 - setInstance(mind); - - }, [canEdit, post]); - useEffect(() => { - handleIcon() - }); - useEffect(() => { - if ((!id || post) && instance) { - containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`; - containerRef.current.style.width = `100%`; - containerRef.current.hidden = false; - instance.toCenter(); - if ((post as any as PathDto)?.meta?.nodeData) { - instance.refresh((post as any as PathDto)?.meta); - } - } - }, [id, post, instance]); - - //保存 按钮 函数 - const handleSave = async () => { - if (!instance) return; - const values = form.getFieldsValue(); - //以图片格式导出思维导图以作为思维导图封面 - const imgBlob = await instance?.exportPng(); - handleFileUpload( - imgBlob, - async (result) => { - const termIds = taxonomies - .flatMap((tax) => values[tax.id] || []) // 获取每个 taxonomy 对应的选中值并展平 - .filter((id) => id); // 过滤掉空值 - const deptIds = (values?.deptIds || []) as string[]; - const { theme, ...data } = instance.getData(); - try { - if (post && id) { - const params: Prisma.PostUpdateArgs = { - where: { - id, - }, - data: { - //authorId: post.authorId, - title: data.nodeData.topic, - meta: { - ...data, - thumbnail: result.compressedUrl, - }, - terms: { - set: termIds.map((id) => ({ id })), - }, - depts: { - set: deptIds.map((id) => ({ id })), - }, - updatedAt: new Date(), - }, - }; - await update.mutateAsync(params); - toast.success("更新成功"); - } else { - const params: Prisma.PostCreateInput = { - type: PostType.PATH, - title: data.nodeData.topic, - meta: { ...data, thumbnail: result.compressedUrl }, - terms: { - connect: termIds.map((id) => ({ id })), - }, - depts: { - connect: deptIds.map((id) => ({ id })), - }, - updatedAt: new Date(), - }; - - const res = await create.mutateAsync({ data: params }); - navigate(`/path/editor/${res.id}`, { replace: true }); - toast.success("创建成功"); - } - } catch (error) { - toast.error("保存失败"); - throw error; - } - console.log(result); - }, - (error) => { }, - `mind-thumb-${new Date().toString()}` - ); - }; - const handleDelete = async () => { - await softDeletePostDescendant.mutateAsync({ - ancestorId: id, - }); - navigate("/path"); - } - - - const showDeleteConfirm = () => { - confirm({ - title: '确定删除该思维导图吗', - icon: , - content: '', - okText: '删除', - okType: 'danger', - cancelText: '取消', - async onOk() { - console.log('OK'); - await handleDelete() - toast.success('思维导图已删除') - }, - onCancel() { - console.log('Cancel'); - }, - }); - }; - useEffect(() => { - containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`; - }, []); - return ( -
- {taxonomies && ( -
-
-
- {taxonomies.map((tax, index) => ( - - - - ))} - - - - {post && id ? : <>} -
-
- {canEdit && ( - <> - { - id && ( - - ) - } - - - )} -
-
-
- )} -
e.preventDefault()} - /> - {canEdit && instance && } - {isLoading && ( -
- -
- )} - {!post && id && !isLoading && ( -
- -
- )} -
- ); -} diff --git a/apps/web/src/components/common/editor/NodeMenu.tsx b/apps/web/src/components/common/editor/NodeMenu.tsx deleted file mode 100755 index 2519c7e..0000000 --- a/apps/web/src/components/common/editor/NodeMenu.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import React, { useState, useEffect, useRef, useMemo } from "react"; -import { Input, Button, ColorPicker, Select } from "antd"; -import { - FontSizeOutlined, - BoldOutlined, - LinkOutlined, - GlobalOutlined, - SwapOutlined, -} from "@ant-design/icons"; -import type { MindElixirInstance, NodeObj } from "mind-elixir"; -import PostSelect from "../../models/post/PostSelect/PostSelect"; -import { Lecture, PostType } from "@nice/common"; -import { xmindColorPresets } from "./constant"; -import { api } from "@nice/client"; -import { useAuth } from "@web/src/providers/auth-provider"; - -interface NodeMenuProps { - mind: MindElixirInstance; -} - -//管理节点样式状态 -const NodeMenu: React.FC = ({ mind }) => { - - const [isOpen, setIsOpen] = useState(false); - const [selectedFontColor, setSelectedFontColor] = useState(""); - const [selectedBgColor, setSelectedBgColor] = useState(""); - const [selectedSize, setSelectedSize] = useState(""); - const [isBold, setIsBold] = useState(false); - const { user} = useAuth(); - const [urlMode, setUrlMode] = useState<"URL" | "POSTURL">("POSTURL"); - const [url, setUrl] = useState(""); - const [postId, setPostId] = useState(""); - const containerRef = useRef(null); - const { data: lecture, isLoading }: { data: Lecture; isLoading: boolean } = - api.post.findFirst.useQuery( - { - where: { id: postId }, - }, - { enabled: !!postId } - ); - useEffect(() => { - { - if(lecture?.courseId && lecture?.id){ - if (urlMode === "POSTURL"){ - setUrl(`/course/${lecture?.courseId}/detail/${lecture?.id}`); - } - mind.reshapeNode(mind.currentNode, { - hyperLink: `/course/${lecture?.courseId}/detail/${lecture?.id}`, - }); - } - } - }, [postId, lecture, isLoading, urlMode]); - - //监听思维导图节点选择事件,更新节点菜单状态 - useEffect(() => { - const handleSelectNode = (nodeObj: NodeObj) => { - setIsOpen(true); - const style = nodeObj.style || {}; - setSelectedFontColor(style.color || ""); - setSelectedBgColor(style.background || ""); - setSelectedSize(style.fontSize || "24"); - setIsBold(style.fontWeight === "bold"); - setUrl(nodeObj.hyperLink || ""); - }; - const handleUnselectNode = () => { - setIsOpen(false); - }; - mind.bus.addListener("selectNode", handleSelectNode); - mind.bus.addListener("unselectNode", handleUnselectNode); - }, [mind]); - - useEffect(() => { - const handleSelectNode = (nodeObj: NodeObj) => { - setIsOpen(true); - const style = nodeObj.style || {}; - setSelectedFontColor(style.color || ""); - setSelectedBgColor(style.background || ""); - setSelectedSize(style.fontSize || "24"); - setIsBold(style.fontWeight === "bold"); - setUrl(nodeObj.hyperLink || ""); - }; - const handleUnselectNode = () => { - setIsOpen(false); - }; - mind.bus.addListener("selectNode", handleSelectNode); - mind.bus.addListener("unselectNode", handleUnselectNode); - }, [mind]); - - useEffect(() => { - if (containerRef.current && mind.container) { - mind.container.appendChild(containerRef.current); - } - }, [mind.container]); - - const handleColorChange = (type: "font" | "background", color: string) => { - if (type === "font") { - setSelectedFontColor(color); - } else { - setSelectedBgColor(color); - } - const patch = { style: {} as any }; - if (type === "font") { - patch.style.color = color; - } else { - patch.style.background = color; - } - mind.reshapeNode(mind.currentNode, patch); - }; - - const handleSizeChange = (size: string) => { - setSelectedSize(size); - mind.reshapeNode(mind.currentNode, { style: { fontSize: size } }); - }; - - const handleBoldToggle = () => { - const fontWeight = isBold ? "" : "bold"; - setIsBold(!isBold); - mind.reshapeNode(mind.currentNode, { style: { fontWeight } }); - }; - - const handleUrlChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setUrl(value); - mind.reshapeNode(mind.currentNode, { - hyperLink: value, - }); - }; - - return ( -
-
- {/* Font Size Selector */} -
-

- 文字样式 -

-
- } - /> - )} - - {urlMode === "URL" && - url && - !/^(https?:\/\/\S+|\/|\.\/|\.\.\/)?\S+$/.test(url) && ( -

- 请输入有效的URL地址 -

- )} -
-
-
- ); -}; - -export default NodeMenu; diff --git a/apps/web/src/components/common/editor/constant.ts b/apps/web/src/components/common/editor/constant.ts deleted file mode 100755 index eb0766e..0000000 --- a/apps/web/src/components/common/editor/constant.ts +++ /dev/null @@ -1,54 +0,0 @@ -import MindElixir from "mind-elixir"; -export const MIND_OPTIONS = { - direction: MindElixir.SIDE, - draggable: true, - contextMenu: true, - toolBar: true, - nodeMenu: true, - keypress: true, - locale: "zh_CN" as const, - theme: { - name: "Latte", - palette: [ - "#dd7878", - "#ea76cb", - "#8839ef", - "#e64553", - "#fe640b", - "#df8e1d", - "#40a02b", - "#209fb5", - "#1e66f5", - "#7287fd", - ], - cssVar: { - "--main-color": "#444446", - "--main-bgcolor": "#ffffff", - "--color": "#777777", - "--bgcolor": "#f6f6f6", - "--panel-color": "#444446", - "--panel-bgcolor": "#ffffff", - "--panel-border-color": "#eaeaea", - }, - }, -}; - -export const xmindColorPresets = [ - // 经典16色 - "#FFFFFF", - "#F5F5F5", // 白色系 - "#2196F3", - "#1976D2", // 蓝色系 - "#4CAF50", - "#388E3C", // 绿色系 - "#FF9800", - "#F57C00", // 橙色系 - "#F44336", - "#D32F2F", // 红色系 - "#9C27B0", - "#7B1FA2", // 紫色系 - "#424242", - "#757575", // 灰色系 - "#FFEB3B", - "#FBC02D", // 黄色系 -]; diff --git a/apps/web/src/components/common/editor/quill/QuillCharCounter.tsx b/apps/web/src/components/common/editor/quill/QuillCharCounter.tsx deleted file mode 100755 index ed409b7..0000000 --- a/apps/web/src/components/common/editor/quill/QuillCharCounter.tsx +++ /dev/null @@ -1,42 +0,0 @@ -interface QuillCharCounterProps { - currentCount: number; - maxLength?: number; - minLength?: number; -} - -const QuillCharCounter: React.FC = ({ - currentCount, - maxLength, - minLength = 0 -}) => { - const getStatusColor = () => { - if (currentCount > (maxLength || Infinity)) return 'text-red-500'; - if (currentCount < minLength) return 'text-amber-500'; - return 'text-gray-500'; - }; - - return ( -
- {currentCount} - {maxLength && ( - <> - / - {maxLength} - - )} - 字符 - {minLength > 0 && currentCount < minLength && ( - - 至少输入 {minLength} 字符 - - )} -
- ); -}; - -export default QuillCharCounter \ No newline at end of file diff --git a/apps/web/src/components/common/editor/quill/QuillEditor.tsx b/apps/web/src/components/common/editor/quill/QuillEditor.tsx deleted file mode 100755 index 78ca9ff..0000000 --- a/apps/web/src/components/common/editor/quill/QuillEditor.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import Quill from "quill"; -import "quill/dist/quill.snow.css"; // 引入默认样式 -import QuillCharCounter from "./QuillCharCounter"; -import { defaultModules } from "./constants"; - -interface QuillEditorProps { - value?: string; - onChange?: (content: string) => void; - placeholder?: string; - readOnly?: boolean; - theme?: "snow" | "bubble"; - modules?: any; - className?: string; - style?: React.CSSProperties; - onFocus?: () => void; - onBlur?: () => void; - onKeyDown?: (event: KeyboardEvent) => void; - onKeyUp?: (event: KeyboardEvent) => void; - maxLength?: number; - minLength?: number; - minRows?: number; - maxRows?: number; -} -const QuillEditor: React.FC = ({ - value = "", - onChange, - placeholder = "请输入内容...", - readOnly = false, - theme = "snow", - modules = defaultModules, - className = "", - style = {}, - onFocus, - onBlur, - onKeyDown, - onKeyUp, - maxLength, - minLength = 0, - minRows = 1, - maxRows, -}) => { - const editorRef = useRef(null); - const quillRef = useRef(null); - const isMounted = useRef(false); - const [charCount, setCharCount] = useState(0); // 添加字符计数状态 - const handleTextChange = () => { - if (!quillRef.current) return; - const editor = quillRef.current; - // 获取文本并处理换行符 - const text = editor.getText().replace(/\n$/, ""); - const textLength = text.length; - - // 处理最大长度限制 - if (maxLength && textLength > maxLength) { - // 暂时移除事件监听器 - editor.off("text-change", handleTextChange); - - // 获取当前选区 - const selection = editor.getSelection(); - const delta = editor.getContents(); - let length = 0; - const newDelta = delta.ops?.reduce((acc: any, op: any) => { - if (typeof op.insert === "string") { - const remainingLength = maxLength - length; - if (length < maxLength) { - const truncatedText = op.insert.slice( - 0, - remainingLength - ); - length += truncatedText.length; - acc.push({ ...op, insert: truncatedText }); - } - } else { - acc.push(op); - } - return acc; - }, []); - // 更新内容 - editor.setContents({ ops: newDelta } as any); - // 恢复光标位置 - if (selection) { - editor.setSelection(Math.min(selection.index, maxLength)); - } - // 重新计算截断后的实际长度 - const finalText = editor.getText().replace(/\n$/, ""); - setCharCount(finalText.length); - - // 重新绑定事件监听器 - editor.on("text-change", handleTextChange); - } else { - // 如果没有超出最大长度,直接更新字符计数 - setCharCount(textLength); - } - - onChange?.(quillRef.current.root.innerHTML); - }; - - useEffect(() => { - if (!editorRef.current) return; - if (!isMounted.current) { - // 初始化 Quill 编辑器 - quillRef.current = new Quill(editorRef.current, { - theme, - modules, - placeholder, - readOnly, - }); - // 设置初始内容 - quillRef.current.root.innerHTML = value; - if (onFocus) { - quillRef.current.on(Quill.events.SELECTION_CHANGE, (range) => { - if (range) { - onFocus(); - } - }); - } - if (onBlur) { - quillRef.current.on(Quill.events.SELECTION_CHANGE, (range) => { - if (!range) { - onBlur(); - } - }); - } - quillRef.current.on(Quill.events.TEXT_CHANGE, handleTextChange); - if (onKeyDown) { - quillRef.current.root.addEventListener("keydown", onKeyDown); - } - if (onKeyUp) { - quillRef.current.root.addEventListener("keyup", onKeyUp); - } - isMounted.current = true; - } - }, [ - theme, - modules, - placeholder, - readOnly, - onFocus, - onBlur, - onKeyDown, - onKeyUp, - maxLength, - minLength, - ]); // 添加所有相关的依赖 - useEffect(() => { - if (quillRef.current) { - const editor = editorRef.current?.querySelector( - ".ql-editor" - ) as HTMLElement; - if (editor) { - const lineHeight = parseInt( - window.getComputedStyle(editor).lineHeight, - 10 - ); - const paddingTop = parseInt( - window.getComputedStyle(editor).paddingTop, - 10 - ); - const paddingBottom = parseInt( - window.getComputedStyle(editor).paddingBottom, - 10 - ); - const minHeight = - lineHeight * minRows + paddingTop + paddingBottom; - editor.style.minHeight = `${minHeight}px`; - if (maxRows) { - const maxHeight = - lineHeight * maxRows + paddingTop + paddingBottom; - editor.style.maxHeight = `${maxHeight}px`; - editor.style.overflowY = "auto"; - } - } - } - }, [minRows, maxRows, quillRef.current]); - - // 监听 value 属性变化 - useEffect(() => { - if (quillRef.current && value !== quillRef.current.root.innerHTML) { - quillRef.current.root.innerHTML = value; - } - }, [value]); - - return ( -
-
- {(maxLength || minLength > 0) && ( - - )} -
- ); -}; - -export default QuillEditor; diff --git a/apps/web/src/components/common/editor/quill/constants.ts b/apps/web/src/components/common/editor/quill/constants.ts deleted file mode 100755 index fd92bd1..0000000 --- a/apps/web/src/components/common/editor/quill/constants.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const defaultModules = { - toolbar: [ - [{ header: [1, 2, 3, 4, 5, 6, false] }], - ["bold", "italic", "underline", "strike"], - [{ list: "ordered" }, { list: "bullet" }], - [{ color: [] }, { background: [] }], - [{ align: [] }], - ["link"], - ["clean"], - ], -}; diff --git a/apps/web/src/components/common/editor/types.ts b/apps/web/src/components/common/editor/types.ts deleted file mode 100755 index 3810ba7..0000000 --- a/apps/web/src/components/common/editor/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { MindElixirInstance, MindElixirData } from 'mind-elixir'; -import { PostType, ObjectType } from '@nice/common'; - -export interface MindEditorProps { - initialData?: MindElixirData; - onSave?: (data: MindElixirData) => Promise; - taxonomyType?: ObjectType; -} - -export interface MindEditorState { - instance: MindElixirInstance | null; - isSaving: boolean; - error: Error | null; -} diff --git a/apps/web/src/components/common/form/FormInput.tsx b/apps/web/src/components/common/form/FormInput.tsx index 325eeba..bd3762f 100755 --- a/apps/web/src/components/common/form/FormInput.tsx +++ b/apps/web/src/components/common/form/FormInput.tsx @@ -3,6 +3,7 @@ import { useRef, useState } from 'react'; import { CheckIcon, PencilIcon, XMarkIcon } from '@heroicons/react/24/outline'; import FormError from './FormError'; import { Button } from '../element/Button'; +import React from 'react'; export interface FormInputProps extends Omit, 'type'> { name: string; label?: string; diff --git a/apps/web/src/components/common/form/FormQuillInput.tsx b/apps/web/src/components/common/form/FormQuillInput.tsx index 7820f55..4cc07e0 100755 --- a/apps/web/src/components/common/form/FormQuillInput.tsx +++ b/apps/web/src/components/common/form/FormQuillInput.tsx @@ -1,7 +1,7 @@ import { useFormContext, Controller } from 'react-hook-form'; import FormError from './FormError'; import { useState } from 'react'; -import QuillEditor from '../editor/quill/QuillEditor'; +// import QuillEditor from '@web/src/components/common/editor/quill/QuillEditor'; export interface FormQuillInputProps { name: string; @@ -54,7 +54,7 @@ export function FormQuillInput({
- ( @@ -69,9 +69,9 @@ export function FormQuillInput({ readOnly={readOnly} onFocus={() => setIsFocused(true)} onBlur={handleBlur} - /> - )} - /> + /> */} + {/* )} */} + {/* /> */}
diff --git a/apps/web/src/components/models/course/card/CourseHeader.tsx b/apps/web/src/components/models/course/card/CourseHeader.tsx deleted file mode 100755 index 95780a1..0000000 --- a/apps/web/src/components/models/course/card/CourseHeader.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { - CalendarIcon, - UserGroupIcon, - AcademicCapIcon, -} from "@heroicons/react/24/outline"; -import { CourseLevelLabel } from "@nice/common"; - -interface CourseHeaderProps { - title: string; - subTitle?: string; - thumbnail?: string; - level?: string; - numberOfStudents?: number; - publishedAt?: Date; -} - -export const CourseHeader = ({ - title, - subTitle, - thumbnail, - level, - numberOfStudents, - publishedAt, -}: CourseHeaderProps) => { - return ( -
- {thumbnail && ( -
- {title} -
- )} -
-

{title}

- {subTitle &&

{subTitle}

} -
- {level && ( -
- - {CourseLevelLabel[level]} -
- )} - {numberOfStudents !== undefined && ( -
- - {numberOfStudents} 人学习中 -
- )} - {publishedAt && ( -
- - {publishedAt.toLocaleDateString()} -
- )} -
-
-
- ); -}; diff --git a/apps/web/src/components/models/course/card/CourseStats.tsx b/apps/web/src/components/models/course/card/CourseStats.tsx deleted file mode 100755 index 51f547a..0000000 --- a/apps/web/src/components/models/course/card/CourseStats.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { StarIcon, ChartBarIcon, ClockIcon } from '@heroicons/react/24/solid'; - -interface CourseStatsProps { - averageRating?: number; - numberOfReviews?: number; - completionRate?: number; - totalDuration?: number; -} -export const CourseStats = ({ - averageRating, - numberOfReviews, - completionRate, - totalDuration, -}: CourseStatsProps) => { - return ( -
- {averageRating !== undefined && ( -
- -
-
- {averageRating.toFixed(1)} -
-
- {numberOfReviews} 观看量 -
-
-
- )} - {completionRate !== undefined && ( -
- -
-
- {completionRate}% -
-
- 完成率 -
-
-
- )} - {totalDuration !== undefined && ( -
- -
-
- {Math.floor(totalDuration / 60)}h {totalDuration % 60}m -
-
- 总时长 -
-
-
- )} -
- ); -}; \ No newline at end of file diff --git a/apps/web/src/components/models/course/detail/CourseDetail.tsx b/apps/web/src/components/models/course/detail/CourseDetail.tsx deleted file mode 100755 index a9bc127..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetail.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { PostDetailProvider } from "./PostDetailContext"; -import CourseDetailLayout from "./CourseDetailLayout"; - -export default function CourseDetail({ - id, - lectureId, -}: { - id?: string; - lectureId?: string; -}) { - return ( - <> - - - - - ); -} diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx deleted file mode 100755 index 06e976f..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Course, CourseDto, TaxonomySlug } from "@nice/common"; -import React, { useContext, useEffect, useMemo } from "react"; -import { Image, Typography, Skeleton, Tag } from "antd"; // 引入 antd 组件 -import { CourseDetailContext } from "./PostDetailContext"; -import { useNavigate, useParams } from "react-router-dom"; -import { useStaff } from "@nice/client"; -import { useAuth } from "@web/src/providers/auth-provider"; -import TermInfo from "@web/src/app/main/path/components/TermInfo"; -import { PictureOutlined } from "@ant-design/icons"; - -export const CourseDetailDescription: React.FC = () => { - const { - post, - canEdit, - isLoading, - selectedLectureId, - setSelectedLectureId, - userIsLearning, - lecture = null, - } = useContext(CourseDetailContext); - const { Paragraph } = Typography; - const { user } = useAuth(); - const { update } = useStaff(); - const firstLectureId = useMemo(() => { - return (post as CourseDto)?.sections?.[0]?.lectures?.[0]?.id; - }, [post]); - const navigate = useNavigate(); - const { id } = useParams(); - return ( - //
-
- {isLoading || !post ? ( - - ) : ( -
- {!selectedLectureId && ( -
- { -
- } -
{ - setSelectedLectureId(firstLectureId); - if (!userIsLearning) { - await update.mutateAsync({ - where: { id: user?.id }, - data: { - learningPosts: { - connect: { - id: post.id, - }, - }, - }, - }); - } - }} - className="absolute rounded-xl top-0 left-0 right-0 bottom-0 z-10 bg-[rgba(0,0,0,0.3)] transition-all duration-300 ease-in-out hover:bg-[rgba(0,0,0,0.7)] cursor-pointer group"> -
- 点击进入学习 -
-
-
- )} -
{"课程简介:"}
-
-
- -
-
- {post?.subTitle &&
{post?.subTitle}
} - {/* */} -
-
- console.log("展开"), - }}> - {post?.content} - -
- )} -
- ); -}; diff --git a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx deleted file mode 100755 index cc6303a..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx +++ /dev/null @@ -1,88 +0,0 @@ -// components/CourseDetailDisplayArea.tsx -import { motion, useScroll, useTransform } from "framer-motion"; -import React, { useContext, useEffect, useRef, useState } from "react"; -import { VideoPlayer } from "@web/src/components/presentation/video-player/VideoPlayer"; -import { CourseDetailDescription } from "./CourseDetailDescription"; -import { Course, LectureType, PostType } from "@nice/common"; -import { CourseDetailContext } from "./PostDetailContext"; -import CollapsibleContent from "@web/src/components/common/container/CollapsibleContent"; -import { Skeleton } from "antd"; -import ResourcesShower from "@web/src/components/common/uploader/ResourceShower"; -import { useNavigate } from "react-router-dom"; -import CourseDetailTitle from "./CourseDetailTitle"; -import ReactPlayer from "react-player"; -export const CourseDetailDisplayArea: React.FC = () => { - // 创建滚动动画效果 - const { - - isLoading, - canEdit, - lecture, - lectureIsLoading, - selectedLectureId, - } = useContext(CourseDetailContext); - const navigate = useNavigate(); - const { scrollY } = useScroll(); - const videoOpacity = useTransform(scrollY, [0, 200], [1, 0.8]); - return ( -
- {/* 固定的视频区域 */} - {lectureIsLoading && ( - - )} - - {selectedLectureId && - !lectureIsLoading && - lecture?.meta?.type === LectureType.VIDEO && ( -
- -
- {/* { - console.log(error); - }} - /> */} - -
-
-
- )} - {!lectureIsLoading && - selectedLectureId && - ( -
-
- {lecture?.meta?.type === LectureType.ARTICLE && ( - - )} -
- -
-
-
- )} -
- -
- {/* 课程内容区域 */} -
- ); -}; - -export default CourseDetailDisplayArea; diff --git a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx deleted file mode 100755 index 40059de..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { motion } from "framer-motion"; -import { useContext, useState } from "react"; -import { CourseDetailContext } from "./PostDetailContext"; - -import CourseDetailDisplayArea from "./CourseDetailDisplayArea"; -import { CourseSyllabus } from "./CourseSyllabus/CourseSyllabus"; -import { CourseDto } from "packages/common/dist"; - -export default function CourseDetailLayout() { - const { - post, - - setSelectedLectureId, - } = useContext(CourseDetailContext); - - const handleLectureClick = (lectureId: string) => { - setSelectedLectureId(lectureId); - }; - const [isSyllabusOpen, setIsSyllabusOpen] = useState(true); - return ( -
-
- {" "} - {/* 添加这个包装 div */} - - - - {/* 课程大纲侧边栏 */} - setIsSyllabusOpen(!isSyllabusOpen)} - /> -
-
- ); -} diff --git a/apps/web/src/components/models/course/detail/CourseDetailSkeleton.tsx b/apps/web/src/components/models/course/detail/CourseDetailSkeleton.tsx deleted file mode 100755 index ad58cb8..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetailSkeleton.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { - SkeletonItem, - SkeletonSection, -} from "@web/src/components/presentation/Skeleton"; -import { api } from "packages/client/dist"; - -export const CourseDetailSkeleton = () => { - return ( -
- {/* 标题骨架屏 */} -
- - -
- - {/* 描述骨架屏 */} - - - {/* 学习目标骨架屏 */} - - - {/* 适合人群骨架屏 */} - - - {/* 技能骨架屏 */} -
- -
- {Array.from({ length: 5 }).map((_, i) => ( - - ))} -
-
-
- ); -}; -export default CourseDetailSkeleton; diff --git a/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx b/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx deleted file mode 100755 index f775b31..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetailTitle.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { useContext } from "react"; -import { CourseDetailContext } from "./PostDetailContext"; -import { useNavigate } from "react-router-dom"; -import { - BookOutlined, - CalendarOutlined, - EditTwoTone, - EyeOutlined, - ReloadOutlined, -} from "@ant-design/icons"; -import dayjs from "dayjs"; -import CourseOperationBtns from "./CourseOperationBtns/CourseOperationBtns"; - -export default function CourseDetailTitle() { - const { - post: course, - lecture, - selectedLectureId, - } = useContext(CourseDetailContext); - const navigate = useNavigate(); - return ( -
-
- {!selectedLectureId ? course?.title : `${course?.title} : ${lecture?.title}`} -
-
- {course?.depts && course?.depts?.length > 0 && ( -
- 发布单位: - {course?.depts?.map((dept) => dept.name)} -
- )} -
-
-
- - {"发布于:"} - {dayjs( - !selectedLectureId - ? course?.createdAt - : lecture?.createdAt - ).format("YYYY年M月D日")} -
-
- {"最后更新:"} - {dayjs( - !selectedLectureId - ? course?.updatedAt - : lecture?.updatedAt - ).format("YYYY年M月D日")} -
-
- -
{`观看次数${ - !selectedLectureId - ? course?.views || 0 - : lecture?.views || 0 - }`}
-
-
- -
{`学习人数${course?.studentIds?.length || 0}`}
-
- -
-
- ); -} diff --git a/apps/web/src/components/models/course/detail/CourseOperationBtns/CourseOperationBtns.tsx b/apps/web/src/components/models/course/detail/CourseOperationBtns/CourseOperationBtns.tsx deleted file mode 100755 index 912386d..0000000 --- a/apps/web/src/components/models/course/detail/CourseOperationBtns/CourseOperationBtns.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useContext, useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { CourseDetailContext } from "../PostDetailContext"; -import { api } from "@nice/client"; -import { - DeleteTwoTone, - EditTwoTone, - ExclamationCircleFilled, -} from "@ant-design/icons"; -import toast from "react-hot-toast"; -import JoinButton from "./JoinButton"; -import { Modal } from "antd"; -import { useQueryClient } from "@tanstack/react-query"; -import { getQueryKey } from "@trpc/react-query"; - -export default function CourseOperationBtns() { - const navigate = useNavigate(); - const { post, canEdit } = useContext(CourseDetailContext); - return ( - <> - - {canEdit && ( - <> -
{ - const url = post?.id - ? `/course/${post?.id}/editor` - : "/course/editor"; - navigate(url); - }}> - - {"编辑课程"} -
- - - - )} - - ); -} diff --git a/apps/web/src/components/models/course/detail/CourseOperationBtns/JoinButton.tsx b/apps/web/src/components/models/course/detail/CourseOperationBtns/JoinButton.tsx deleted file mode 100755 index d351156..0000000 --- a/apps/web/src/components/models/course/detail/CourseOperationBtns/JoinButton.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useAuth } from "@web/src/providers/auth-provider"; -import { useStaff } from "@nice/client"; -import { useContext, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { CourseDetailContext } from "../PostDetailContext"; -import toast from "react-hot-toast"; -import { - CheckCircleOutlined, - CloseCircleOutlined, - LoginOutlined, -} from "@ant-design/icons"; - -export default function JoinButton() { - const { isAuthenticated, user } = useAuth(); - const navigate = useNavigate(); - const { post, canEdit, userIsLearning, setUserIsLearning } = - 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: post.id }, - }, - }, - }); - setUserIsLearning(true); - toast.success("加入学习成功"); - } else { - await update.mutateAsync({ - where: { id: user?.id }, - data: { - learningPosts: { - disconnect: { - id: post.id, - }, - }, - }, - }); - toast.success("退出学习成功"); - setUserIsLearning(false); - } - }; - 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 - ? "退出学习" - : "正在学习" - : "加入学习"} - -
- )} - - ); -} diff --git a/apps/web/src/components/models/course/detail/CoursePreview/CoursePreview.tsx b/apps/web/src/components/models/course/detail/CoursePreview/CoursePreview.tsx deleted file mode 100755 index 64baa66..0000000 --- a/apps/web/src/components/models/course/detail/CoursePreview/CoursePreview.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useContext, useEffect } from "react"; -import { CoursePreviewMsg } from "@web/src/app/main/course/preview/type.ts"; -import { Button, Tabs, Image, Skeleton } from "antd"; -import type { TabsProps } from "antd"; -import { PlayCircleOutlined } from "@ant-design/icons"; -import { CourseDetailContext } from "../PostDetailContext"; -export function CoursePreview() { - const { post, isLoading, lecture, lectureIsLoading, selectedLectureId } = - useContext(CourseDetailContext); - return ( -
-
-
- example -
- -
-
-
- {isLoading ? ( - - ) : ( - <> - - {post.title} - - - {post.subTitle} - - - {post.content} - - - )} - - -
-
-
- ); -} diff --git a/apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx b/apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx deleted file mode 100755 index 8f32fce..0000000 --- a/apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Checkbox, List } from 'antd'; -import React from 'react'; - -export function CoursePreviewTabmsg({data}){ - - - const renderItem = (item) => ( - - - - ); - - return( -
- -
- ) -} \ No newline at end of file diff --git a/apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx b/apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx deleted file mode 100755 index b13e87d..0000000 --- a/apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { MenuProps } from 'antd'; -import { Menu } from 'antd'; - -type MenuItem = Required['items'][number]; - -export function CourseCatalog(){ - return ( - <> - - ) -} \ No newline at end of file diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/CollapsedButton.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/CollapsedButton.tsx deleted file mode 100755 index 08f04b3..0000000 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/CollapsedButton.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { BookOpenIcon } from "@heroicons/react/24/outline"; -import { motion } from "framer-motion"; -import React from "react"; -interface CollapsedButtonProps { - onToggle: () => void; -} - -export const CollapsedButton: React.FC = ({ - onToggle, -}) => ( - - - -); diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx deleted file mode 100755 index 2dcd706..0000000 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { XMarkIcon, BookOpenIcon } from "@heroicons/react/24/outline"; -import { - ChevronDownIcon, - ClockIcon, - PlayCircleIcon, -} from "@heroicons/react/24/outline"; -import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; -import React, { useState, useRef, useContext } from "react"; -import { SectionDto, TaxonomySlug } from "@nice/common"; -import { SyllabusHeader } from "./SyllabusHeader"; -import { SectionItem } from "./SectionItem"; -import { CollapsedButton } from "./CollapsedButton"; -import { CourseDetailContext } from "../PostDetailContext"; -import { api } from "@nice/client"; - -interface CourseSyllabusProps { - sections: SectionDto[]; - onLectureClick?: (lectureId: string) => void; - isOpen: boolean; - onToggle: () => void; -} - -export const CourseSyllabus: React.FC = ({ - sections, - onLectureClick, - isOpen, - onToggle, -}) => { - const { isHeaderVisible } = useContext(CourseDetailContext); - const [expandedSections, setExpandedSections] = useState( - sections.map((section) => section.id) // 默认展开所有章节 - ); - const sectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); - - const toggleSection = (sectionId: string) => { - setExpandedSections((prev) => - prev.includes(sectionId) - ? prev.filter((id) => id !== sectionId) - : [...prev, sectionId] - ); - - // 直接滚动,无需延迟 - sectionRefs.current[sectionId]?.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - }; - return ( - <> - {/* 收起按钮直接显示 */} - {!isOpen && ( -
- -
- )} - -
- {isOpen && ( -
- - -
-
- {sections.map((section, index) => ( - - (sectionRefs.current[section.id] = - el) - } - index={index + 1} - section={section} - isExpanded={expandedSections.includes( - section.id - )} - onToggle={toggleSection} - onLectureClick={onLectureClick} - /> - ))} -
-
-
- )} -
- - ); -}; diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx deleted file mode 100755 index 9f41c82..0000000 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ /dev/null @@ -1,65 +0,0 @@ -// components/CourseSyllabus/LectureItem.tsx - -import { Lecture, LectureType, LessonTypeLabel } from "@nice/common"; -import React, { useMemo } from "react"; -import { - ClockCircleOutlined, - EyeOutlined, - FileTextOutlined, - PlayCircleOutlined, -} from "@ant-design/icons"; // 使用 Ant Design 图标 -import { useParams } from "react-router-dom"; - -interface LectureItemProps { - lecture: Lecture; - onClick: (lectureId: string) => void; -} - -export const LectureItem: React.FC = ({ - lecture, - onClick, -}) => { - const { lectureId } = useParams(); - const isReading = useMemo(() => { - return lecture?.id === lectureId; - }, [lectureId, lecture]); - return ( -
onClick(lecture.id)}> - {lecture?.meta?.type === LectureType.VIDEO && ( -
- - {LessonTypeLabel[lecture?.meta?.type]} -
- )} - {lecture?.meta?.type === LectureType.ARTICLE && ( -
- {" "} - {LessonTypeLabel[lecture?.meta?.type]} -
- )} -
-

- {lecture.title} -

- {lecture.subTitle && ( - - {lecture.subTitle} - - )} -
- - - {lecture?.views ? lecture?.views : 0} - -
-
-
- ); -}; diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx deleted file mode 100755 index 3c0a4c5..0000000 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { ChevronDownIcon } from "@heroicons/react/24/outline"; -import { SectionDto } from "@nice/common"; -import { AnimatePresence, motion } from "framer-motion"; -import React, { useMemo } from "react"; -import { LectureItem } from "./LectureItem"; -import { useParams } from "react-router-dom"; -interface SectionItemProps { - section: SectionDto; - index?: number; - isExpanded: boolean; - onToggle: (sectionId: string) => void; - onLectureClick: (lectureId: string) => void; - ref: React.RefObject; -} -export const SectionItem = React.forwardRef( - ({ section, index, isExpanded, onToggle, onLectureClick }, ref) => { - const { lectureId } = useParams(); - const isReading = useMemo(() => { - return (section?.lectures || []) - ?.map((lecture) => lecture?.id) - .includes(lectureId); - }, [lectureId, section]); - return ( -
-
onToggle(section.id)}> -
- - 第{index}章 - -
-

- {section.title} -

-

- {section?.lectures?.length}节课 · -

-
-
- -
- {isReading && ( - - 正在学习中 - - )} - - - -
-
- - - {isExpanded && ( - - {section.lectures.map((lecture) => ( - - ))} - - )} - -
- ); - } -); diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/SyllabusHeader.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/SyllabusHeader.tsx deleted file mode 100755 index 1b4d6bf..0000000 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/SyllabusHeader.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// components/CourseSyllabus/SyllabusHeader.tsx -import React from "react"; - -import { XMarkIcon } from "@heroicons/react/24/outline"; -interface SyllabusHeaderProps { - onToggle: () => void; -} - -export const SyllabusHeader: React.FC = ({ onToggle }) => ( -
-

课程大纲

- -
-); diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/index.ts b/apps/web/src/components/models/course/detail/CourseSyllabus/index.ts deleted file mode 100755 index a294db3..0000000 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./CourseSyllabus"; diff --git a/apps/web/src/components/models/course/detail/PostDetailContext.tsx b/apps/web/src/components/models/course/detail/PostDetailContext.tsx deleted file mode 100755 index ad181f6..0000000 --- a/apps/web/src/components/models/course/detail/PostDetailContext.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { api, useVisitor } from "@nice/client"; -import { - courseDetailSelect, - CourseDto, - Lecture, - lectureDetailSelect, - PostDto, - RolePerms, - VisitType, -} from "@nice/common"; -import { useAuth } from "@web/src/providers/auth-provider"; -import React, { - createContext, - ReactNode, - useEffect, - useMemo, - useState, -} from "react"; -import { useNavigate, useParams } from "react-router-dom"; - -interface CourseDetailContextType { - editId?: string; // 添加 editId - post?: PostDto; - lecture?: Lecture; - selectedLectureId?: string | undefined; - setSelectedLectureId?: React.Dispatch>; - isLoading?: boolean; - lectureIsLoading?: boolean; - isHeaderVisible: boolean; // 新增 - setIsHeaderVisible: (visible: boolean) => void; // 新增 - canEdit?: boolean; - userIsLearning?: boolean; - setUserIsLearning: (learning: boolean) => void; -} - -interface CourseFormProviderProps { - children: ReactNode; - editId?: string; // 添加 editId 参数 -} - -export const CourseDetailContext = - createContext(null); -export function PostDetailProvider({ - children, - editId, -}: CourseFormProviderProps) { - const navigate = useNavigate(); - const { read } = useVisitor(); - const { user, hasSomePermissions, isAuthenticated } = useAuth(); - const { lectureId } = useParams(); - - const { data: post, isLoading }: { data: PostDto; isLoading: boolean } = ( - api.post as any - ).findFirst.useQuery( - { - where: { id: editId }, - select: courseDetailSelect, - }, - { enabled: Boolean(editId) } - ); - const [userIsLearning, setUserIsLearning] = useState(false); - useEffect(() => { - setUserIsLearning((post?.studentIds || []).includes(user?.id)); - }, [user, post, isLoading]); - const canEdit = useMemo(() => { - const isAuthor = isAuthenticated && user?.id === post?.authorId; - const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST); - return isAuthor || isRoot; - }, [user, post]); - - const [selectedLectureId, setSelectedLectureId] = useState< - string | undefined - >(lectureId || undefined); - const { data: lecture, isLoading: lectureIsLoading } = ( - api.post as any - ).findFirst.useQuery( - { - where: { id: selectedLectureId }, - select: lectureDetailSelect, - }, - { enabled: Boolean(editId) } - ); - - useEffect(() => { - if (lectureId) { - read.mutateAsync({ - data: { - visitorId: user?.id || null, - postId: lectureId, - type: VisitType.READED, - }, - }); - } else { - read.mutateAsync({ - data: { - visitorId: user?.id || null, - postId: editId, - type: VisitType.READED, - }, - }); - } - }, [editId, lectureId]); - useEffect(() => { - if (lectureId !== selectedLectureId) { - navigate(`/course/${editId}/detail/${selectedLectureId}`); - } - }, [selectedLectureId, editId]); - const [isHeaderVisible, setIsHeaderVisible] = useState(true); // 新增 - useEffect(() => { - console.log("post", post); - }, [post]); - return ( - - {children} - - ); -} diff --git a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx deleted file mode 100755 index c5c30b9..0000000 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { createContext, useContext, ReactNode, useEffect } from "react"; -import { Form, FormInstance, message } from "antd"; -import { - courseDetailSelect, - CourseDto, - CourseMeta, - CourseStatus, - ObjectType, - PostType, - Taxonomy, -} from "@nice/common"; -import { api, usePost } from "@nice/client"; -import { useNavigate } from "react-router-dom"; -import { z } from "zod"; -import { useAuth } from "@web/src/providers/auth-provider"; -import { getQueryKey } from "@trpc/react-query"; -import { useQueryClient } from "@tanstack/react-query"; - -export type CourseFormData = { - title: string; - subTitle?: string; - content?: string; - thumbnail?: string; - requirements?: string[]; - objectives?: string[]; - sections: any; -}; - -interface CourseEditorContextType { - onSubmit: (values: CourseFormData) => Promise; - handleDeleteCourse: () => Promise; - editId?: string; - course?: CourseDto; - taxonomies?: Taxonomy[]; // 根据实际类型调整 - form: FormInstance; // 添加 form 到上下文 -} - -interface CourseFormProviderProps { - children: ReactNode; - editId?: string; -} - -const CourseEditorContext = createContext(null); - -export function CourseFormProvider({ - children, - editId, -}: CourseFormProviderProps) { - const [form] = Form.useForm(); - const { create, update, createCourse } = usePost(); - const queryClient = useQueryClient(); - const { user } = useAuth(); - const { data: course }: { data: CourseDto } = api.post.findFirst.useQuery( - { - where: { id: editId }, - select: courseDetailSelect, - }, - { enabled: Boolean(editId) } - ); - const { - data: taxonomies, - }: { - data: Taxonomy[]; - } = api.taxonomy.getAll.useQuery({ - type: ObjectType.COURSE, - }); - const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({ - onSuccess:()=>{ - queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) }); - } - }) - const navigate = useNavigate(); - - useEffect(() => { - if (course) { - const deptIds = (course?.depts || [])?.map((dept) => dept.id); - const formData = { - title: course.title, - subTitle: course.subTitle, - content: course.content, - deptIds: deptIds, - meta: { - thumbnail: course?.meta?.thumbnail, - }, - }; - - // 按 taxonomyId 分组所有 terms - const termsByTaxonomy = {}; - course.terms?.forEach((term) => { - if (!termsByTaxonomy[term.taxonomyId]) { - termsByTaxonomy[term.taxonomyId] = []; - } - termsByTaxonomy[term.taxonomyId].push(term.id); - }); - - // 将分组后的 terms 设置到 formData - Object.entries(termsByTaxonomy).forEach(([taxonomyId, termIds]) => { - formData[taxonomyId] = termIds; - }); - - form.setFieldsValue(formData); - } - }, [course, form]); - const handleDeleteCourse = async () => { - if(editId){ - await softDeletePostDescendant.mutateAsync({ - ancestorId: editId, - }); - - navigate("/courses"); - } - } - const onSubmit = async (values: any) => { - const sections = values?.sections || []; - const deptIds = values?.deptIds || []; - const termIds = taxonomies - .flatMap((tax) => values[tax.id] || []) // 获取每个 taxonomy 对应的选中值并展平 - .filter((id) => id); // 过滤掉空值 - - const formattedValues = { - ...values, - type: PostType.COURSE, - meta: { - ...((course?.meta as CourseMeta) || {}), - ...(values?.meta?.thumbnail !== undefined && { - thumbnail: values?.meta?.thumbnail, - }), - }, - terms: - termIds?.length > 0 - ? { - [editId ? "set" : "connect"]: termIds.map((id) => ({ - id, - })), // 转换成 connect 格式 - } - : undefined, - depts: - deptIds?.length > 0 - ? { - [editId ? "set" : "connect"]: deptIds.map((id) => ({ - id, - })), - } - : undefined, - }; - // 删除原始的 taxonomy 字段 - taxonomies.forEach((tax) => { - delete formattedValues[tax.id]; - }); - delete formattedValues.sections; - delete formattedValues.deptIds; - - try { - if (editId) { - const result = await update.mutateAsync({ - where: { id: editId }, - data: formattedValues, - }); - message.success("课程更新成功!"); - navigate(`/course/${result.id}/editor/content`); - } else { - const result = await createCourse.mutateAsync({ - courseDetail: { - data: { - title: formattedValues.title, - - // state: CourseStatus.DRAFT, - type: PostType.COURSE, - ...formattedValues, - }, - }, - sections, - }); - message.success("课程创建成功!"); - navigate(`/course/${result.id}/editor/content`); - } - form.resetFields(); - } catch (error) { - console.error("Error submitting form:", error); - message.error("操作失败,请重试!"); - } - }; - - return ( - -
- {children} -
-
- ); -} - -export const useCourseEditor = () => { - const context = useContext(CourseEditorContext); - if (!context) { - throw new Error( - "useCourseEditor must be used within CourseFormProvider" - ); - } - return context; -}; diff --git a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx deleted file mode 100755 index 8ac7dbd..0000000 --- a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Form, Input, Select } from "antd"; -import { CourseLevel, CourseLevelLabel } from "@nice/common"; -import { convertToOptions } from "@nice/client"; -import TermSelect from "../../../term/term-select"; -import { useCourseEditor } from "../context/CourseEditorContext"; -import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader"; -import DepartmentSelect from "../../../department/department-select"; - -const { TextArea } = Input; - -export function CourseBasicForm() { - // 将 CourseLevelLabel 使用 Object.entries 将 CourseLevelLabel 对象转换为键值对数组。 - const levelOptions = Object.entries(CourseLevelLabel).map( - ([key, value]) => ({ - label: value, - value: key as CourseLevel, - }) - ); - - const { form, taxonomies } = useCourseEditor(); - return ( -
- - - - - - - - - - -