diff --git a/apps/server/src/models/app-config/app-config.router.ts b/apps/server/src/models/app-config/app-config.router.ts index 882fa16..350d047 100755 --- a/apps/server/src/models/app-config/app-config.router.ts +++ b/apps/server/src/models/app-config/app-config.router.ts @@ -36,7 +36,7 @@ export class AppConfigRouter { .mutation(async ({ input }) => { return await this.appConfigService.deleteMany(input); }), - findFirst: this.trpc.protectProcedure + findFirst: this.trpc.procedure .input(AppConfigFindFirstArgsSchema) .query(async ({ input }) => { return await this.appConfigService.findFirst(input); diff --git a/apps/server/src/models/term/term.router.ts b/apps/server/src/models/term/term.router.ts index 43afff5..1b15a3d 100755 --- a/apps/server/src/models/term/term.router.ts +++ b/apps/server/src/models/term/term.router.ts @@ -61,7 +61,7 @@ export class TermRouter { .input(TermMethodSchema.getSimpleTree) .query(async ({ input, ctx }) => { const { staff } = ctx; - return await this.termService.getChildSimpleTree(staff, input); + return await this.termService.getChildSimpleTree(input, staff); }), getParentSimpleTree: this.trpc.procedure .input(TermMethodSchema.getSimpleTree) @@ -69,7 +69,7 @@ export class TermRouter { const { staff } = ctx; return await this.termService.getParentSimpleTree(staff, input); }), - getTreeData: this.trpc.protectProcedure + getTreeData: this.trpc.procedure .input(TermMethodSchema.getTreeData) .query(async ({ input }) => { return await this.termService.getTreeData(input); diff --git a/apps/server/src/models/term/term.service.ts b/apps/server/src/models/term/term.service.ts index 7f21041..7cbcc5f 100755 --- a/apps/server/src/models/term/term.service.ts +++ b/apps/server/src/models/term/term.service.ts @@ -269,10 +269,10 @@ export class TermService extends BaseTreeService { } async getChildSimpleTree( - staff: UserProfile, data: z.infer, + staff?: UserProfile, ) { - const { domainId = null, permissions } = staff; + const domainId = staff?.domainId || null; const hasAnyPerms = staff?.permissions?.includes(RolePerms.MANAGE_ANY_TERM) || staff?.permissions?.includes(RolePerms.READ_ANY_TERM); @@ -352,7 +352,9 @@ export class TermService extends BaseTreeService { staff: UserProfile, data: z.infer, ) { - const { domainId = null, permissions } = staff; + // const { domainId = null, permissions } = staff; + const permissions = staff?.permissions || []; + const domainId = staff?.domainId || null; const hasAnyPerms = permissions.includes(RolePerms.READ_ANY_TERM) || permissions.includes(RolePerms.MANAGE_ANY_TERM); diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 9a9f37d..963c691 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -1,5 +1,4 @@ import { Card, Rate, Tag, Typography, Button } from "antd"; -import { Course } from "../mockData"; import { UserOutlined, ClockCircleOutlined, @@ -18,6 +17,7 @@ export default function CourseCard({ course }: CourseCardProps) { const handleClick = (course: CourseDto) => { navigate(`/course/${course.id}/detail`); }; + return ( handleClick(course)} @@ -39,10 +39,30 @@ export default function CourseCard({ course }: CourseCardProps) { }>
- { + return ( + + {term.name} + + ); + })} + {/* {course.terms?.[0].name} + {course.terms?.[1].name} - + */}
<div className="ml-2 flex items-center flex-grow"> <Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]"> - {course?.depts?.[0]?.name} + { + course?.depts.length > 1 ?`${course.depts[0].name}等`:course.depts[0].name + } + {/* {course?.depts?.map((dept) => {return dept.name.length > 1 ?`${dept.name.slice}等`: dept.name})} */} + {/* {course?.depts?.map((dept)=>{return dept.name})} */} </Text> </div> <span className="text-xs font-medium text-gray-500"> - 观看次数{course?.meta?.views}次 + {course?.meta?.views ? `观看次数 ${course?.meta?.views}` : null} </span> </div> diff --git a/apps/web/src/app/main/courses/components/CoursesContainer.tsx b/apps/web/src/app/main/courses/components/CoursesContainer.tsx new file mode 100644 index 0000000..9e2d871 --- /dev/null +++ b/apps/web/src/app/main/courses/components/CoursesContainer.tsx @@ -0,0 +1,52 @@ +import CourseList from "@web/src/components/models/course/list/CourseList"; +import { useMainContext } from "../../layout/MainProvider"; +import { PostType, Prisma } from "@nice/common"; +import { useMemo } from "react"; + +export function CoursesContainer() { + const { searchValue, selectedTerms } = 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 ( + <> + <CourseList + params={{ + pageSize: 12, + where: { + type: PostType.COURSE, + AND: termFilters.map((termFilter) => ({ + terms: { + some: { + id: { + in: termFilter, // 确保至少有一个 term.id 在当前 termFilter 中 + }, + }, + }, + })), + OR: [ + { title: searchCondition }, + { subTitle: searchCondition }, + { content: searchCondition }, + { + terms: { + some: { + name: searchCondition, + }, + }, + }, + ], + }, + }} + cols={4}></CourseList> + </> + ); +} + +export default CoursesContainer; diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index 69fd828..203e1d1 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -1,90 +1,55 @@ import { Checkbox, Divider, Radio, Space, Spin } from "antd"; -import { categories, levels } from "../mockData"; + import { TaxonomySlug, TermDto } from "@nice/common"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { api } from "@nice/client"; import { useSearchParams } from "react-router-dom"; -import TermTree from "@web/src/components/models/term/term-tree"; import TermSelect from "@web/src/components/models/term/term-select"; +import { useMainContext } from "../../layout/MainProvider"; -interface FilterSectionProps { - selectedCategory: string; - selectedLevel: string; - onCategoryChange: (category: string) => void; - onLevelChange: (level: string) => void; -} - -interface GetTaxonomyProps { - categories: string[]; - isLoading: boolean; -} - -function useGetTaxonomy({ type }): GetTaxonomyProps { - const { data, isLoading }: { data: TermDto[]; isLoading: boolean } = - api.term.findMany.useQuery({ - where: { - taxonomy: { - //TaxonomySlug.CATEGORY - slug: type, - }, - }, - include: { - children: true, - }, - take: 10, // 只取前10个 - orderBy: { - createdAt: "desc", // 按创建时间降序排列 - }, - }); - const categories = useMemo(() => { - const allCategories = isLoading - ? [] - : data?.map((course) => course.name); - return [...Array.from(new Set(allCategories))]; - }, [data]); - return { categories, isLoading }; -} - -export default function FilterSection({ - selectedCategory, - selectedLevel, - onCategoryChange, - onLevelChange, -}: FilterSectionProps) { - const gateGory: GetTaxonomyProps = useGetTaxonomy({ - type: TaxonomySlug.CATEGORY, - }); - const levels: GetTaxonomyProps = useGetTaxonomy({ - type: TaxonomySlug.LEVEL, - }); - - const [searchParams, setSearchParams] = useSearchParams(); - useEffect(() => { - if (searchParams.get("category")) - onCategoryChange(searchParams.get("category")); - }, [searchParams.get("category")]); - +export default function FilterSection() { const { data: taxonomies } = api.taxonomy.getAll.useQuery({}); - + const { selectedTerms, setSelectedTerms } = useMainContext(); + const handleTermChange = (slug: string, selected: string[]) => { + setSelectedTerms({ + ...selectedTerms, + [slug]: selected, // 更新对应 slug 的选择 + }); + }; return ( - <div className="bg-white p-6 rounded-lg shadow-sm space-y-6"> - {taxonomies?.map((tax) => { + <div className="bg-white p-6 rounded-lg shadow-sm space-y-6 h-full"> + {taxonomies?.map((tax, index) => { + const items = Object.entries(selectedTerms).find( + ([key, items]) => key === tax.slug + )?.[1]; return ( - <> - <div> - <h3 className="text-lg font-medium mb-4"> - {tax?.name} - </h3> - <TermSelect - multiple - taxonomyId={tax?.id}></TermSelect> - </div> - </> + <div key={index}> + <h3 className="text-lg font-medium mb-4"> + {tax?.name} + </h3> + <TermSelect + // open + className="w-72" + value={items} + dropdownRender={(menu) => ( + <div style={{ padding: "8px" }}>{menu}</div> + )} + dropdownStyle={{ maxHeight: 400, overflow: "auto" }} + multiple + taxonomyId={tax?.id} + onChange={(selected) => + handleTermChange( + tax?.slug, + selected as string[] + ) + }></TermSelect> + {index < taxonomies.length - 1 && ( + <Divider className="my-6" /> + )} + </div> ); })} - - <Divider className="my-6" /> </div> ); } diff --git a/apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx b/apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx new file mode 100644 index 0000000..5062731 --- /dev/null +++ b/apps/web/src/app/main/courses/layout/AllCoursesLayout.tsx @@ -0,0 +1,19 @@ +import FilterSection from "../components/FilterSection"; +import CoursesContainer from "../components/CoursesContainer"; +export function AllCoursesLayout() { + return ( + <> + <div className="min-h-screen bg-gray-50"> + <div className=" flex"> + <div className="w-1/6"> + <FilterSection></FilterSection> + </div> + <div className="w-5/6 p-4"> + <CoursesContainer></CoursesContainer> + </div> + </div> + </div> + </> + ); +} +export default AllCoursesLayout; diff --git a/apps/web/src/app/main/courses/mockData.ts b/apps/web/src/app/main/courses/mockData.ts deleted file mode 100755 index 096d174..0000000 --- a/apps/web/src/app/main/courses/mockData.ts +++ /dev/null @@ -1,44 +0,0 @@ -export interface Course { - id: string; - title: string; - description: string; - instructor: string; - price: number; - originalPrice: number; - category: string; - level: string; - thumbnail: string; - rating: number; - enrollments: number; - duration: string; -} - -export const categories = [ - "计算机科学", - "数据科学", - "商业管理", - "人工智能", - "软件开发", - "网络安全", - "云计算", - "前端开发", - "后端开发", - "移动开发" -]; - -export const levels = ["入门", "初级", "中级", "高级"]; - -export const mockCourses: Course[] = Array.from({ length: 50 }, (_, i) => ({ - id: `course-${i + 1}`, - title: `${categories[i % categories.length]}课程 ${i + 1}`, - description: "本课程将带你深入了解该领域的核心概念和实践应用,通过实战项目提升你的专业技能。", - instructor: `讲师 ${i + 1}`, - price: Math.floor(Math.random() * 500 + 99), - originalPrice: Math.floor(Math.random() * 1000 + 299), - category: categories[i % categories.length], - level: levels[i % levels.length], - thumbnail: `/api/placeholder/280/160`, - rating: Number((Math.random() * 2 + 3).toFixed(1)), - enrollments: Math.floor(Math.random() * 10000), - duration: `${Math.floor(Math.random() * 20 + 10)}小时` -})); \ No newline at end of file diff --git a/apps/web/src/app/main/courses/page.tsx b/apps/web/src/app/main/courses/page.tsx index fa2fe9a..9c9225e 100755 --- a/apps/web/src/app/main/courses/page.tsx +++ b/apps/web/src/app/main/courses/page.tsx @@ -1,35 +1,8 @@ -import { useState, useMemo, useEffect } from "react"; -import { mockCourses } from "./mockData"; -import FilterSection from "./components/FilterSection"; -import CourseList from "./components/CourseList"; -import { api } from "@nice/client"; -import { - courseDetailSelect, - CourseDto, - LectureType, - PostType, -} from "@nice/common"; -import { useSearchParams } from "react-router-dom"; -import { set } from "idb-keyval"; -import { useMainContext } from "../layout/MainProvider"; - -interface paginationData { - items: CourseDto[]; - totalPages: number; -} +import AllCoursesLayout from "./layout/AllCoursesLayout"; export default function CoursesPage() { - const { searchValue, setSearchValue } = useMainContext(); - return ( <> - <div className="min-h-screen bg-gray-50"> - <div>{searchValue}</div> - <CourseList - params={{ - page: 1, - pageSize: 12, - }}></CourseList> - </div> + <AllCoursesLayout></AllCoursesLayout> </> ); } diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index d0735fa..f3ed9ec 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -1,212 +1,85 @@ -import React, { useState, useCallback, useEffect, useMemo } from 'react'; -import { Typography, Button, Spin } from 'antd'; -import { stringToColor, TaxonomySlug, TermDto } from '@nice/common'; -import { api,} from '@nice/client'; -import { ControlOutlined } from '@ant-design/icons'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +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; - -interface CourseCategory { - name: string; - count: number; - description: string; -} - -// const courseCategories: CourseCategory[] = [ -// { -// name: '计算机基础', -// count: 120, -// description: '计算机组成原理、操作系统、网络等基础知识' -// }, -// { -// name: '编程语言', -// count: 85, -// description: 'Python、Java、JavaScript等主流编程语言' -// }, -// { -// name: '人工智能', -// count: 65, -// description: '机器学习、深度学习、自然语言处理等前沿技术' -// }, -// { -// name: '数据科学', -// count: 45, -// description: '数据分析、数据可视化、商业智能等' -// }, -// { -// name: '云计算', -// count: 38, -// description: '云服务、容器化、微服务架构等' -// }, -// { -// name: '网络安全', -// count: 42, -// description: '网络安全基础、渗透测试、安全防护等' -// } -// ]; - - const CategorySection = () => { - const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); - const [showAll, setShowAll] = useState(false); - //获得分类 - const {data:courseCategoriesData,isLoading} :{data:TermDto[],isLoading:boolean}= api.term.findMany.useQuery({ - where:{ - taxonomy: { - slug:TaxonomySlug.CATEGORY - } - }, - include:{ - children :true - }, - orderBy: { - createdAt: 'desc', // 按创建时间降序排列 - }, - take:8 - }) - // 分类展示 - const [displayedCategories,setDisplayedCategories] = useState<TermDto[]>([]) - useEffect(() => { - console.log(courseCategoriesData); - // 如果 showAll 为 true,则显示所有分类数据, -// 如果 showAll 为 false,则只显示前 8 个分类数据, - if(!isLoading){ - if(showAll){ - setDisplayedCategories(courseCategoriesData) - }else{ - setDisplayedCategories(courseCategoriesData.slice(0,8)) - } - } - }, [courseCategoriesData,showAll]); - // const courseCategories: CourseCategory[] = useMemo(() => { - // return data?.map((term) => ({ - // name: term.name, - // count: term.hasChildren ? term.children.length : 0, - // description: term.description - // })) || []; - // },[data]) - const handleMouseEnter = useCallback((index: number) => { - setHoveredIndex(index); - }, []); + const [hoveredIndex, setHoveredIndex] = useState<number | null>(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 handleMouseLeave = useCallback(() => { - setHoveredIndex(null); - }, []); + const handleMouseEnter = useCallback((index: number) => { + setHoveredIndex(index); + }, []); - const navigate = useNavigate() + const handleMouseLeave = useCallback(() => { + setHoveredIndex(null); + }, []); - return ( - <section className="py-32 relative overflow-hidden"> - <div className="max-w-screen-2xl mx-auto px-4 relative"> - <div className="text-center mb-24"> - <Title level={2} className="font-bold text-5xl mb-6 bg-gradient-to-r from-gray-900 via-gray-700 to-gray-800 bg-clip-text text-transparent motion-safe:animate-gradient-x"> - 探索课程分类 - - - 选择你感兴趣的方向,开启学习之旅 - -
-
- { - isLoading ? : - (displayedCategories.map((category, index) => { - const categoryColor = stringToColor(category.name); - const isHovered = hoveredIndex === index; - - return ( -
handleMouseEnter(index)} - onMouseLeave={handleMouseLeave} - role="button" - tabIndex={0} - aria-label={`查看${category.name}课程类别`} - onClick={()=>{ - console.log(category.name) - navigate(`/courses?category=${category.name}`) - window.scrollTo({ top: 0, behavior: 'smooth' }) - }} - > -
-
-
-
-
-
- - {category.name} - - {/* - {category.children.length} 门课程 - */} -
- - {category.description} - -
- 了解更多 - - → - -
-
-
- ); - })) - } - -
- {!isLoading && courseCategoriesData.length > 8 && ( -
- -
- )} -
- - ); + const handleMouseClick = useCallback((categoryId:string) => { + setSelectedTerms({ + [TaxonomySlug.CATEGORY] : [categoryId] + }) + navigate('/courses') + window.scrollTo({top: 0,behavior: "smooth",}) + },[]); + return ( +
+
+
+ + 探索课程分类 + + + 选择你感兴趣的方向,开启学习之旅 + +
+
+ {isLoading ? ( + + ) : ( + courseCategoriesData.map((category, index) => { + const categoryColor = stringToColor(category.name); + const isHovered = hoveredIndex === index; + + return ( + + ); + }) + )} +
+ +
+
+ ); }; -export default CategorySection; \ No newline at end of file +export default CategorySection; diff --git a/apps/web/src/app/main/home/components/CategorySectionCard.tsx b/apps/web/src/app/main/home/components/CategorySectionCard.tsx new file mode 100644 index 0000000..e3e1273 --- /dev/null +++ b/apps/web/src/app/main/home/components/CategorySectionCard.tsx @@ -0,0 +1,58 @@ +import { useNavigate } from "react-router-dom"; +import { Typography } from "antd"; +export default function CategorySectionCard({handleMouseClick, index,handleMouseEnter,handleMouseLeave,category,categoryColor,isHovered,}) { + const navigate = useNavigate() + const { Title, Text } = Typography; + return ( +
handleMouseEnter(index)} + onMouseLeave={handleMouseLeave} + role="button" + tabIndex={0} + aria-label={`查看${category.name}课程类别`} + onClick={()=>{ + handleMouseClick(category.id) + }} + > +
+
+
+
+
+
+ + {category.name} + +
+
+ 了解更多 + + → + +
+
+
+ ) +} \ No newline at end of file diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index 2117baa..aa2bf07 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -1,14 +1,10 @@ -import React, { useState, useMemo, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import { Button, Card, Typography, Tag, Spin, Empty } from "antd"; -import { - PlayCircleOutlined, - TeamOutlined, - ArrowRightOutlined, -} from "@ant-design/icons"; -import { CourseDto, TaxonomySlug, TermDto } from "@nice/common"; +import React, { useState, useMemo } from "react"; +import { Typography, Skeleton } from "antd"; +import { TaxonomySlug, TermDto } from "@nice/common"; import { api } from "@nice/client"; -import CourseCard from "../../courses/components/CourseCard"; +import { CoursesSectionTag } from "./CoursesSectionTag"; +import CourseList from "@web/src/components/models/course/list/CourseList"; +import LookForMore from "./LookForMore"; interface GetTaxonomyProps { categories: string[]; isLoading: boolean; @@ -21,11 +17,7 @@ function useGetTaxonomy({ type }): GetTaxonomyProps { slug: type, }, }, - include: { - children: true, - }, take: 10, // 只取前10个 - orderBy: {}, }); const categories = useMemo(() => { const allCategories = isLoading @@ -35,29 +27,6 @@ function useGetTaxonomy({ type }): GetTaxonomyProps { }, [data]); return { categories, isLoading }; } -// 不同分类跳转 -function useFetchCoursesByCategory(category: string) { - const isAll = category === "全部"; - const { data, isLoading }: { data: CourseDto[]; isLoading: boolean } = - api.post.findMany.useQuery({ - where: isAll - ? {} - : { - terms: { - some: { - name: category, - }, - }, - }, - take: 8, - include: { - terms: true, - depts: true, - }, - }); - - return { data, isLoading }; -} const { Title, Text } = Typography; interface CoursesSectionProps { title: string; @@ -67,22 +36,12 @@ interface CoursesSectionProps { const CoursesSection: React.FC = ({ title, description, + initialVisibleCoursesCount = 8, }) => { - const navigate = useNavigate(); const [selectedCategory, setSelectedCategory] = useState("全部"); const gateGory: GetTaxonomyProps = useGetTaxonomy({ type: TaxonomySlug.CATEGORY, }); - const { data, isLoading: isDataLoading } = - useFetchCoursesByCategory(selectedCategory); - const filteredCourses = useMemo(() => { - return selectedCategory === "全部" - ? data - : data?.filter((c) => - c.terms.some((t) => t.name === selectedCategory) - ); - }, [selectedCategory, data]); - const displayedCourses = isDataLoading ? [] : filteredCourses; return (
@@ -100,77 +59,43 @@ const CoursesSection: React.FC = ({
-
{gateGory.isLoading ? ( - + ) : ( <> - setSelectedCategory("全部")} - className={`px-6 py-2 text-base cursor-pointer rounded-full transition-all duration-300 ${ - selectedCategory === "全部" - ? "bg-blue-600 text-white shadow-lg" - : "bg-white text-gray-600 hover:bg-gray-100" - }`}> - 全部 - - {gateGory.categories.map((category) => ( - { - setSelectedCategory(category); - }} - className={`px-6 py-2 text-base cursor-pointer rounded-full transition-all duration-300 ${ - selectedCategory === category - ? "bg-blue-600 text-white shadow-lg" - : "bg-white text-gray-600 hover:bg-gray-100" - }`}> - {category} - - ))} + {["全部", ...gateGory.categories].map( + (category, idx) => ( + + ) + )} )}
- -
- {displayedCourses.length === 0 ? ( -
- -
- ) : ( - displayedCourses?.map((course) => ( - - )) - )} -
- { -
-
-
- -
-
- } + +
); diff --git a/apps/web/src/app/main/home/components/CoursesSectionTag.tsx b/apps/web/src/app/main/home/components/CoursesSectionTag.tsx new file mode 100644 index 0000000..c03d718 --- /dev/null +++ b/apps/web/src/app/main/home/components/CoursesSectionTag.tsx @@ -0,0 +1,24 @@ +import { Tag } from "antd"; + +export function CoursesSectionTag({category, selectedCategory, setSelectedCategory}) { + return ( + <> + { + setSelectedCategory(category); + }} + className={`px-6 py-2 text-base cursor-pointer rounded-full transition-all duration-300 ${selectedCategory === category + ? "bg-blue-600 text-white shadow-lg" + : "bg-white text-gray-600 hover:bg-gray-100" + }`}> + {category} + + + ) +} diff --git a/apps/web/src/app/main/home/components/LookForMore.tsx b/apps/web/src/app/main/home/components/LookForMore.tsx new file mode 100644 index 0000000..88e1b41 --- /dev/null +++ b/apps/web/src/app/main/home/components/LookForMore.tsx @@ -0,0 +1,24 @@ +import { ArrowRightOutlined } from "@ant-design/icons"; +import { Button } from "antd"; +import { useNavigate } from "react-router-dom"; + +export default function LookForMore({to}:{to:string}) { + const navigate = useNavigate(); + return ( + <> +
+
+
+ +
+
+ + ) + +} diff --git a/apps/web/src/app/main/layout/MainProvider.tsx b/apps/web/src/app/main/layout/MainProvider.tsx index d503c39..0952268 100644 --- a/apps/web/src/app/main/layout/MainProvider.tsx +++ b/apps/web/src/app/main/layout/MainProvider.tsx @@ -1,8 +1,13 @@ import React, { createContext, ReactNode, useContext, useState } from "react"; +interface SelectedTerms { + [key: string]: string[]; // 每个 slug 对应一个 string 数组 +} interface MainContextType { searchValue?: string; + selectedTerms?: SelectedTerms; setSearchValue?: React.Dispatch>; + setSelectedTerms?: React.Dispatch>; } const MainContext = createContext(null); @@ -12,11 +17,14 @@ interface MainProviderProps { export function MainProvider({ children }: MainProviderProps) { const [searchValue, setSearchValue] = useState(""); + const [selectedTerms, setSelectedTerms] = useState({}); // 初始化状态 return ( {children} diff --git a/apps/web/src/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx index b1a4fb6..53efdb3 100755 --- a/apps/web/src/app/main/layout/NavigationMenu.tsx +++ b/apps/web/src/app/main/layout/NavigationMenu.tsx @@ -1,31 +1,37 @@ -import { Menu } from 'antd'; -import { useNavigate, useLocation } from 'react-router-dom'; +import { Menu } from "antd"; +import { useNavigate, useLocation } from "react-router-dom"; const menuItems = [ - { key: 'home', path: '/', label: '首页' }, - { key: 'courses', path: '/courses', label: '全部课程' }, - { key: 'paths', path: '/paths', label: '学习路径' } + { key: "home", path: "/", label: "首页" }, + { key: "courses", path: "/courses", label: "全部课程" }, + { key: "paths", path: "/paths", label: "学习路径" }, ]; export const NavigationMenu = () => { - const navigate = useNavigate(); - const { pathname } = useLocation(); - const selectedKey = menuItems.find(item => item.path === pathname)?.key || ''; - return ( - { - const selectedItem = menuItems.find(item => item.key === key); - if (selectedItem) navigate(selectedItem.path); - }} - > - {menuItems.map(({ key, label }) => ( - - {label} - - ))} - - ); -}; \ No newline at end of file + const navigate = useNavigate(); + const { pathname } = useLocation(); + const selectedKey = + menuItems.find((item) => item.path === pathname)?.key || ""; + return ( + { + const selectedItem = menuItems.find((item) => item.key === key); + if (selectedItem) navigate(selectedItem.path); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }}> + {menuItems.map(({ key, label }) => ( + + {label} + + ))} + + ); +}; diff --git a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx index 508fdaa..ea23902 100755 --- a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx @@ -221,21 +221,19 @@ export function UserMenu() { focus:outline-none focus:ring-2 focus:ring-[#00538E]/20 group relative overflow-hidden - active:scale-[0.99] - ${ - item.label === "注销" + active:scale-[0.99] + ${item.label === "注销" ? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700" : "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]" - }`}> + }`}> + group-hover:translate-x-0.5 ${item.label === "注销" + ? "group-hover:text-red-600" + : "group-hover:text-[#003F6A]" + }`}> {item.icon} {item.label} diff --git a/apps/web/src/components/common/editor/MindEditor.tsx b/apps/web/src/components/common/editor/MindEditor.tsx index 732cfae..cd06b8e 100755 --- a/apps/web/src/components/common/editor/MindEditor.tsx +++ b/apps/web/src/components/common/editor/MindEditor.tsx @@ -13,6 +13,7 @@ export default function MindEditor() { toolBar: true, // default true nodeMenu: true, // default true keypress: true, // default true + locale: "zh_CN", }); // instance.install(NodeMenu); instance.init(MindElixir.new("新主题")); diff --git a/apps/web/src/components/layout/element/usermenu/usermenu.tsx b/apps/web/src/components/layout/element/usermenu/usermenu.tsx index d2611dd..31460cd 100755 --- a/apps/web/src/components/layout/element/usermenu/usermenu.tsx +++ b/apps/web/src/components/layout/element/usermenu/usermenu.tsx @@ -229,20 +229,18 @@ export function UserMenu() { focus:ring-2 focus:ring-[#00538E]/20 group relative overflow-hidden active:scale-[0.99] - ${ - item.label === "注销" + ${item.label === "注销" ? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700" : "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]" - }`}> + }`}> + group-hover:translate-x-0.5 ${item.label === "注销" + ? "group-hover:text-red-600" + : "group-hover:text-[#003F6A]" + }`}> {item.icon} {item.label} diff --git a/apps/web/src/app/main/courses/components/CourseList.tsx b/apps/web/src/components/models/course/list/CourseList.tsx similarity index 56% rename from apps/web/src/app/main/courses/components/CourseList.tsx rename to apps/web/src/components/models/course/list/CourseList.tsx index 4550795..fb1f74e 100755 --- a/apps/web/src/app/main/courses/components/CourseList.tsx +++ b/apps/web/src/components/models/course/list/CourseList.tsx @@ -1,6 +1,6 @@ -import { Pagination, Empty } from "antd"; -import CourseCard from "./CourseCard"; -import { CourseDto, Prisma } from "@nice/common"; +import { Pagination, Empty, Skeleton } from "antd"; +import CourseCard from "../../../../app/main/courses/components/CourseCard"; +import { courseDetailSelect, CourseDto, Prisma } from "@nice/common"; import { api } from "@nice/client"; import { DefaultArgs } from "@prisma/client/runtime/library"; import { useEffect, useMemo, useState } from "react"; @@ -11,6 +11,8 @@ interface CourseListProps { where?: Prisma.PostWhereInput; select?: Prisma.PostSelect; }; + cols?: number; + showPagination?: boolean; } interface CoursesPagnationProps { data: { @@ -19,10 +21,15 @@ interface CoursesPagnationProps { }; isLoading: boolean; } -export default function CourseList({ params }: CourseListProps) { +export default function CourseList({ + params, + cols = 3, + showPagination = true, +}: CourseListProps) { const [currentPage, setCurrentPage] = useState(params?.page || 1); const { data, isLoading }: CoursesPagnationProps = api.post.findManyWithPagination.useQuery({ + select: courseDetailSelect, ...params, page: currentPage, }); @@ -42,7 +49,7 @@ export default function CourseList({ params }: CourseListProps) { useEffect(() => { setCurrentPage(params?.page || 1); - }, [params?.page]); + }, [params?.page, params]); function onPageChange(page: number, pageSize: number) { setCurrentPage(page); window.scrollTo({ top: 0, behavior: "smooth" }); @@ -51,20 +58,26 @@ export default function CourseList({ params }: CourseListProps) {
{courses.length > 0 ? ( <> -
- {courses.map((course) => ( - - ))} -
-
- +
+ {isLoading ? ( + + ) : ( + courses.map((course) => ( + + )) + )}
+ {showPagination && ( +
+ +
+ )} ) : ( diff --git a/apps/web/src/components/models/course/list/course-list.tsx b/apps/web/src/components/models/course/list/course-list.tsx deleted file mode 100755 index 64c8e83..0000000 --- a/apps/web/src/components/models/course/list/course-list.tsx +++ /dev/null @@ -1,61 +0,0 @@ -// CourseList.tsx -import { motion } from "framer-motion"; -import { Course, CourseDto } from "@nice/common"; -import { EmptyState } from "@web/src/components/common/space/Empty"; -import { Pagination } from "@web/src/components/common/element/Pagination"; -import React from "react"; - -interface CourseListProps { - courses?: CourseDto[]; - renderItem: (course: CourseDto) => React.ReactNode; - emptyComponent?: React.ReactNode; - // 新增分页相关属性 - currentPage: number; - totalPages: number; - onPageChange: (page: number) => void; -} - -const container = { - hidden: { opacity: 0 }, - show: { - opacity: 1, - transition: { - staggerChildren: 0.05, - duration: 0.3, - }, - }, -}; -export const CourseList = ({ - courses, - renderItem, - emptyComponent: EmptyComponent, - currentPage, - totalPages, - onPageChange, -}: CourseListProps) => { - if (!courses || courses.length === 0) { - return EmptyComponent || ; - } - - return ( -
- - {courses.map((course) => ( - - {renderItem(course)} - - ))} - - - -
- ); -}; diff --git a/apps/web/src/components/models/term/term-select.tsx b/apps/web/src/components/models/term/term-select.tsx index ce5d9be..3896d8e 100755 --- a/apps/web/src/components/models/term/term-select.tsx +++ b/apps/web/src/components/models/term/term-select.tsx @@ -1,5 +1,12 @@ import { TreeSelect, TreeSelectProps } from "antd"; -import React, { useEffect, useState, useCallback, useRef } from "react"; +import React, { + useEffect, + useState, + useCallback, + useRef, + ReactElement, + JSXElementConstructor, +} from "react"; import { getUniqueItems } from "@nice/common"; import { api } from "@nice/client"; import { DefaultOptionType } from "antd/es/select"; @@ -8,29 +15,35 @@ interface TermSelectProps { defaultValue?: string | string[]; value?: string | string[]; onChange?: (value: string | string[]) => void; - placeholder?: string; multiple?: boolean; - // rootId?: string; - // domain?: boolean; taxonomyId?: string; disabled?: boolean; - className?: string; domainId?: string; + dropdownStyle?: React.CSSProperties; + style?: React.CSSProperties; + open?: boolean; + showSearch?: boolean; + dropdownRender?: ( + menu: ReactElement> + ) => ReactElement>; } export default function TermSelect({ defaultValue, value, onChange, - className, placeholder = "选择分类", multiple = false, taxonomyId, + open = undefined, + showSearch = true, domainId, - // rootId = null, + dropdownStyle, + style, disabled = false, - // domain = undefined, -}: TermSelectProps) { + dropdownRender, + ...treeSelectProps +}: TermSelectProps & TreeSelectProps) { const utils = api.useUtils(); const [listTreeData, setListTreeData] = useState< Omit[] @@ -57,7 +70,7 @@ export default function TermSelect({ throw error; } }, - [utils] + [utils, value] ); const fetchTerms = useCallback(async () => { @@ -124,37 +137,35 @@ export default function TermSelect({ } }; - const handleExpand = async (keys: React.Key[]) => { - // console.log(keys); - try { - const allKeyIds = - keys.map((key) => key.toString()).filter(Boolean) || []; - // const expandedNodes = await Promise.all( - // keys.map(async (key) => { - // return await utils.department.getChildSimpleTree.fetch({ - // deptId: key.toString(), - // domain, - // }); - // }) - // ); - // - //上面那样一个个拉会拉爆,必须直接拉deptIds - const expandedNodes = await utils.term.getChildSimpleTree.fetch({ - termIds: allKeyIds, - taxonomyId, - domainId, - }); - const flattenedNodes = expandedNodes.flat(); - const newItems = getUniqueItems( - [...listTreeData, ...flattenedNodes], - "id" - ); - setListTreeData(newItems); - } catch (error) { - console.error("Error expanding nodes with keys", keys, ":", error); - } - }; - + const handleExpand = useCallback( + async (keys: React.Key[]) => { + try { + const allKeyIds = + keys.map((key) => key.toString()).filter(Boolean) || []; + const expandedNodes = await utils.term.getChildSimpleTree.fetch( + { + termIds: allKeyIds, + taxonomyId, + domainId, + } + ); + const flattenedNodes = expandedNodes.flat(); + const newItems = getUniqueItems( + [...listTreeData, ...flattenedNodes], + "id" + ); + setListTreeData(newItems); + } catch (error) { + console.error( + "Error expanding nodes with keys", + keys, + ":", + error + ); + } + }, + [value] + ); const handleDropdownVisibleChange = async (open: boolean) => { if (open) { // This will attempt to expand all nodes and fetch their children when the dropdown opens @@ -162,22 +173,19 @@ export default function TermSelect({ await handleExpand(allKeys); } }; - return ( handleChange(multiple ? [] : undefined)} onTreeExpand={handleExpand} onDropdownVisibleChange={handleDropdownVisibleChange} + {...treeSelectProps} /> ); } diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 9a36b03..34aca4f 100755 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -69,10 +69,10 @@ export const InitTaxonomies: { // name: "研判单元", // slug: TaxonomySlug.UNIT, // }, - { - name: "标签", - slug: TaxonomySlug.TAG, - }, + // { + // name: "标签", + // slug: TaxonomySlug.TAG, + // }, ]; export const InitAppConfigs: Prisma.AppConfigCreateInput[] = [ { diff --git a/packages/common/src/models/post.ts b/packages/common/src/models/post.ts index 6994d4b..0d2bf07 100755 --- a/packages/common/src/models/post.ts +++ b/packages/common/src/models/post.ts @@ -77,7 +77,7 @@ export type Course = Post & { export type CourseDto = Course & { enrollments?: Enrollment[]; sections?: SectionDto[]; - terms: Term[]; + terms: TermDto[]; lectureCount?: number; depts:Department[] }; diff --git a/packages/common/src/models/term.ts b/packages/common/src/models/term.ts index cef8b61..37c5d7f 100755 --- a/packages/common/src/models/term.ts +++ b/packages/common/src/models/term.ts @@ -1,8 +1,10 @@ -import { Term } from "@prisma/client"; +import { Taxonomy, Term } from "@prisma/client"; import { ResPerm } from "./rbac"; +import { TaxonomySlug } from "../enum"; export type TermDto = Term & { permissions: ResPerm; children: TermDto[]; hasChildren: boolean; + taxonomy: Taxonomy }; \ No newline at end of file