From 4eb87d511ada539b3a0227153377b94ec350ed9d Mon Sep 17 00:00:00 2001 From: Rao <1227431568@qq.com> Date: Tue, 25 Feb 2025 10:01:17 +0800 Subject: [PATCH 01/14] rht02251001 --- .../main/home/components/CoursesSection.tsx | 63 +++++++++++-------- .../app/main/home/components/HeroSection.tsx | 18 +++--- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index c6a2969..9fe682d 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -10,7 +10,7 @@ import { ArrowRightOutlined, EyeOutlined, } from '@ant-design/icons'; -import { TaxonomySlug, TermDto } from '@nice/common'; +import { CourseDto, TaxonomySlug, TermDto } from '@nice/common'; import { api } from '@nice/client'; // const {courseId} = useParams(); interface GetTaxonomyProps { @@ -41,6 +41,24 @@ function useGetTaxonomy({ type }): GetTaxonomyProps { 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 + } + }); + + return { data, isLoading}; +} const { Title, Text } = Typography; @@ -77,6 +95,10 @@ const CoursesSection: React.FC = ({ const gateGory: GetTaxonomyProps = useGetTaxonomy({ type: TaxonomySlug.CATEGORY, }) + const { data ,isLoading : isDataLoading} = useFetchCoursesByCategory(selectedCategory); + useEffect(()=>{ + console.log('data:', data) + }) // const { data } = api.post.findMany.useQuery({ // where: {}, // 必选参数 // include: { // 关联查询 @@ -100,17 +122,17 @@ const CoursesSection: React.FC = ({ // id: course.id, // title: course.title // })); - const handleClick = (course: Course) => { + const handleClick = (course: CourseDto) => { navigate(`/course?courseId=${course.id}/detail`); } const filteredCourses = useMemo(() => { return selectedCategory === '全部' - ? courses - : courses.filter((course) => course.category === selectedCategory); - }, [selectedCategory, courses]); + ? data + : data?.filter(c => c.terms.some(t => t.name === selectedCategory)); + }, [selectedCategory, data]); - const displayedCourses = isLoading ? [] : filteredCourses.slice(0, visibleCourses); + const displayedCourses = isDataLoading ? [] : filteredCourses?.slice(0, visibleCourses); return (
@@ -165,7 +187,7 @@ const CoursesSection: React.FC = ({
- {displayedCourses.map((course) => ( + {displayedCourses?.map((course) => ( handleClick(course)} key={course.id} @@ -176,23 +198,10 @@ const CoursesSection: React.FC = ({
- {course.progress > 0 && ( -
- {/* */} -
- )}
} > @@ -202,19 +211,19 @@ const CoursesSection: React.FC = ({ color="blue" className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0" > - {course.category} + {course.terms[0].name} - {course.level} + {course.terms[1].name}
= ({ <TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" /> <div className="ml-2 flex items-center flex-grow"> <Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300"> - {course.instructor} + {/* {course.} */} </Text> </div> <span className="flex items-center bg-blue-100 px-2 py-1 rounded-full text-blue-600 hover:bg-blue-200 transition-colors duration-300"> <EyeOutlined className="ml-1.5 text-sm" /> - <span className="text-xs font-medium">观看次数{course.progress}次</span> + <span className="text-xs font-medium">观看次数{course?.meta?.views}次</span> </span> </div> <div className="pt-4 border-t border-gray-100 text-center"> diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx index 5a58d12..e0e443d 100755 --- a/apps/web/src/app/main/home/components/HeroSection.tsx +++ b/apps/web/src/app/main/home/components/HeroSection.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useCallback } from "react"; +import React, { useRef, useCallback, useEffect } from "react"; import { Button, Carousel, Typography } from "antd"; import { TeamOutlined, @@ -62,7 +62,11 @@ const HeroSection = () => { const handleNext = useCallback(() => { carouselRef.current?.next(); }, []); - //const {slides:carouselItems} = useAppConfig() + const { slides } = useAppConfig() + useEffect(() => { + //slides.push(('https://s.cn.bing.net/th?id=OHR.GiantCuttlefish_ZH-CN0670915878_1920x1080.webp&qlt=50')) + //console.log(slides) + }, []) return ( <section className="relative "> <div className="group"> @@ -74,20 +78,20 @@ const HeroSection = () => { dots={{ className: "carousel-dots !bottom-32 !z-20", }}> - {Array.isArray(carouselItems)? - (carouselItems.map((item, index) => ( + {Array.isArray(slides)? + (slides.map((item, index) => ( <div key={index} className="relative h-[600px]"> <div className="absolute inset-0 bg-cover bg-center transform transition-[transform,filter] duration-[2000ms] group-hover:scale-105 group-hover:brightness-110 will-change-[transform,filter]" style={{ //backgroundImage: `url(https://s.cn.bing.net/th?id=OHR.GiantCuttlefish_ZH-CN0670915878_1920x1080.webp&qlt=50)`, - backgroundImage: `url(${item.image})`, + backgroundImage: `url(${item})`, backfaceVisibility: "hidden", }} /> - <div + {/* <div className={`absolute inset-0 bg-gradient-to-r ${item.color} to-transparent opacity-90 mix-blend-overlay transition-opacity duration-500`} - /> + /> */} <div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" /> {/* Content Container */} From 79f51e1b55216214db36cd8d1d6e4b63f7b5270f Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Tue, 25 Feb 2025 10:02:40 +0800 Subject: [PATCH 02/14] add. --- .../main/home/components/CategorySection.tsx | 3 - .../main/home/components/CoursesSection.tsx | 79 +++++++++++-------- apps/web/src/app/main/home/page.tsx | 6 +- 3 files changed, 49 insertions(+), 39 deletions(-) diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index c2eff20..81b201a 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -84,9 +84,6 @@ const CategorySection = () => { // description: term.description // })) || []; // },[data]) - - - const handleMouseEnter = useCallback((index: number) => { setHoveredIndex(index); }, []); diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index 1b7edd7..efa7270 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useEffect } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { Button, Card, Typography, Tag, Progress, Spin } from 'antd'; +import { Button, Card, Typography, Tag, Progress, Spin, Empty } from 'antd'; import { PlayCircleOutlined, UserOutlined, @@ -17,7 +17,6 @@ interface GetTaxonomyProps { categories: string[]; isLoading: boolean; } - function useGetTaxonomy({ type }): GetTaxonomyProps { const { data, isLoading }: { data: TermDto[], isLoading: boolean } = api.term.findMany.useQuery({ where: { @@ -40,8 +39,11 @@ function useGetTaxonomy({ type }): GetTaxonomyProps { }, [data]); return { categories, isLoading } } +// 修改useGetName中的查询条件 +// 不同分类跳转 + const { Title, Text } = Typography; interface Course { @@ -63,6 +65,25 @@ interface CoursesSectionProps { isLoading:boolean initialVisibleCoursesCount?: number; } +function useFetchCoursesByCategory(category: string) { + const isAll = category === '全部'; + const { data, isLoading } = api.post.findMany.useQuery({ + where: isAll?{}:{ + terms: { + some: { + name:category + }, + }, + }, + take: 8, + include:{ + terms:true + } + }); + + return { data, isLoading }; +} + const CoursesSection: React.FC<CoursesSectionProps> = ({ title, @@ -77,39 +98,22 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ const gateGory: GetTaxonomyProps = useGetTaxonomy({ type: TaxonomySlug.CATEGORY, }) - // const { data } = api.post.findMany.useQuery({ - // where: {}, // 必选参数 - // include: { // 关联查询 - // instructors: true - // }, - // orderBy: { // 排序规则 - // createdAt: 'desc' - // }, - // take: 8 - // }) - // useEffect(() => { - // // 添加空值保护 - // if (data && data.length > 0) { - // console.log('有效数据:', data) - // // 执行后续操作 - // } else { - // console.log('无数据或加载中') - // } - // }, [data]) - // const formatted = data?.map(course => ({ - // id: course.id, - // title: course.title - // })); - const handleClick = (course: Course) => { - navigate(`/course?courseId=${course.id}/detail`); - } + const handleClick = (course: Course) => { + navigate(`/course/${course.id}/detail`); + } + const { data } = useFetchCoursesByCategory(selectedCategory); + + useEffect(()=>{ + console.log('data:', data) + }) const filteredCourses = useMemo(() => { return selectedCategory === '全部' ? courses : courses.filter((course) => course.category === selectedCategory); }, [selectedCategory, courses]); + const displayedCourses = isLoading ? [] : filteredCourses.slice(0, visibleCourses); return ( <section className="relative py-20 overflow-hidden bg-gradient-to-b from-gray-50 to-white"> @@ -140,12 +144,22 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ : 'bg-white text-gray-600 hover:bg-gray-100' }`} >全部</Tag> - { + {gateGory.categories.length === 0 && ( + <Empty + description="暂无课程分类" + image={Empty.PRESENTED_IMAGE_DEFAULT} + /> + )}:{ gateGory.categories.map((category) => ( <Tag key={category} color={selectedCategory === category ? 'blue' : 'default'} - onClick={() => setSelectedCategory(category)} + onClick={() => { + setSelectedCategory(category) + // console.log(gateGory) + } + } + 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' @@ -155,6 +169,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ </Tag> )) } + </> ) } @@ -229,7 +244,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ </div> <span className="flex items-center bg-blue-100 px-2 py-1 rounded-full text-blue-600 hover:bg-blue-200 transition-colors duration-300"> <EyeOutlined className="ml-1.5 text-sm" /> - <span className="text-xs font-medium">观看次数{course.progress}次</span> + <span className="text-xs font-medium">观看次数{course.progress}</span> </span> </div> <div className="pt-4 border-t border-gray-100 text-center"> @@ -262,7 +277,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ </Button> </div> </div> - )} +)} </div> </section> ); diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index 3a7b2af..b1a2b82 100755 --- a/apps/web/src/app/main/home/page.tsx +++ b/apps/web/src/app/main/home/page.tsx @@ -127,12 +127,10 @@ const HomePage = () => { }); useEffect(() => { if (data) { - console.log('mockCourses data:', data); + console.log('Courses data:', data); } }, [data]); - - // 数据处理逻辑 -// 修正依赖数组 + return ( <div className="min-h-screen"> <HeroSection /> From 4fda0ecdef0238743cc5a90c198562be254d0f83 Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Tue, 25 Feb 2025 10:08:43 +0800 Subject: [PATCH 03/14] add. --- apps/web/src/app/main/home/components/CoursesSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index 0c04870..fc7e3a9 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -126,7 +126,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ // title: course.title // })); const handleClick = (course: CourseDto) => { - navigate(`/course?courseId=${course.id}/detail`); + navigate(`/course/${course.id}/detail`); } useEffect(()=>{ From 9db19fa02a0420527b1795559f70de185f0db884 Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Tue, 25 Feb 2025 10:58:35 +0800 Subject: [PATCH 04/14] li --- .../main/home/components/CoursesSection.tsx | 100 +++++++----------- 1 file changed, 37 insertions(+), 63 deletions(-) diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index fc7e3a9..b4938b0 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -39,28 +39,27 @@ function useGetTaxonomy({ type }): GetTaxonomyProps { }, [data]); return { categories, isLoading } } -// 修改useGetName中的查询条件 - +// 不同分类跳转 function useFetchCoursesByCategory(category: string) { const isAll = category === '全部'; - const { data, isLoading}:{data:CourseDto[],isLoading:boolean} = api.post.findMany.useQuery({ - where: isAll?{}:{ + const { data, isLoading }: { data: CourseDto[], isLoading: boolean } = api.post.findMany.useQuery({ + where: isAll ? {} : { terms: { some: { - name:category + name: category }, }, }, take: 8, - include:{ - terms:true + include: { + terms: true } }); - return { data, isLoading}; + return { data, isLoading }; } -// 不同分类跳转 + const { Title, Text } = Typography; @@ -80,7 +79,7 @@ interface CoursesSectionProps { title: string; description: string; courses: Course[]; - isLoading:boolean + isLoading: boolean initialVisibleCoursesCount?: number; } @@ -98,38 +97,15 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ const gateGory: GetTaxonomyProps = useGetTaxonomy({ type: TaxonomySlug.CATEGORY, }) - const { data ,isLoading : isDataLoading} = useFetchCoursesByCategory(selectedCategory); - useEffect(()=>{ + const { data, isLoading: isDataLoading } = useFetchCoursesByCategory(selectedCategory); + useEffect(() => { console.log('data:', data) }) - // const { data } = api.post.findMany.useQuery({ - // where: {}, // 必选参数 - // include: { // 关联查询 - // instructors: true - // }, - // orderBy: { // 排序规则 - // createdAt: 'desc' - // }, - // take: 8 - // }) - // useEffect(() => { - // // 添加空值保护 - // if (data && data.length > 0) { - // console.log('有效数据:', data) - // // 执行后续操作 - // } else { - // console.log('无数据或加载中') - // } - // }, [data]) - // const formatted = data?.map(course => ({ - // id: course.id, - // title: course.title - // })); const handleClick = (course: CourseDto) => { navigate(`/course/${course.id}/detail`); } - useEffect(()=>{ + useEffect(() => { console.log('data:', data) }) const filteredCourses = useMemo(() => { @@ -168,38 +144,37 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ : 'bg-white text-gray-600 hover:bg-gray-100' }`} >全部</Tag> - {gateGory.categories.length === 0 && ( - <Empty - description="暂无课程分类" - image={Empty.PRESENTED_IMAGE_DEFAULT} - /> - )}:{ - gateGory.categories.map((category) => ( - <Tag - key={category} - color={selectedCategory === category ? 'blue' : 'default'} - onClick={() => { - setSelectedCategory(category) - console.log(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} - </Tag> - )) + {gateGory.categories.map((category) => ( + <Tag + key={category} + color={selectedCategory === category ? 'blue' : 'default'} + onClick={() => { + setSelectedCategory(category) + console.log(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} + </Tag> + + )) } - + </> ) } </div> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8"> - {displayedCourses?.map((course) => ( + {displayedCourses.length=== 0 ? ( + <div className="col-span-full"> + <Empty description="暂无课程" image={Empty.PRESENTED_IMAGE_DEFAULT} /> + </div> + ) : displayedCourses?.map((course) => ( <Card onClick={() => handleClick(course)} key={course.id} @@ -249,7 +224,6 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ <TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" /> <div className="ml-2 flex items-center flex-grow"> <Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300"> - {/* {course.} */} </Text> </div> <span className="flex items-center bg-blue-100 px-2 py-1 rounded-full text-blue-600 hover:bg-blue-200 transition-colors duration-300"> @@ -287,7 +261,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({ </Button> </div> </div> -)} + )} </div> </section> ); From b36d90d01109fa3bc746545e49e02709ae655c7b Mon Sep 17 00:00:00 2001 From: ditiqi <ditiqi@163.com> Date: Tue, 25 Feb 2025 12:31:37 +0800 Subject: [PATCH 05/14] add --- apps/server/src/models/term/term.service.ts | 76 ++--- .../main/courses/components/FilterSection.tsx | 180 +++++------ apps/web/src/app/main/courses/page.tsx | 123 ++++--- apps/web/src/app/main/home/page.tsx | 300 +++++++++--------- .../components/models/term/term-select.tsx | 4 +- .../src/components/models/term/term-tree.tsx | 199 ++++++++++++ apps/web/src/components/models/term/util.ts | 27 +- 7 files changed, 567 insertions(+), 342 deletions(-) create mode 100644 apps/web/src/components/models/term/term-tree.tsx diff --git a/apps/server/src/models/term/term.service.ts b/apps/server/src/models/term/term.service.ts index cc119e6..7f21041 100755 --- a/apps/server/src/models/term/term.service.ts +++ b/apps/server/src/models/term/term.service.ts @@ -298,12 +298,12 @@ export class TermService extends BaseTreeService<Prisma.TermDelegate> { ...(hasAnyPerms ? {} // 当有全局权限时,不添加任何额外条件 : { - // 当无全局权限时,添加域ID过滤 - OR: [ - { domainId: null }, // 通用记录 - { domainId: domainId }, // 特定域记录 - ], - }), + // 当无全局权限时,添加域ID过滤 + OR: [ + { domainId: null }, // 通用记录 + { domainId: domainId }, // 特定域记录 + ], + }), }, ancestorId: parentId, relDepth: 1, @@ -315,29 +315,29 @@ export class TermService extends BaseTreeService<Prisma.TermDelegate> { }), termIds ? db.term.findMany({ - where: { - ...(termIds && { - OR: [ - ...(validTermIds.length - ? [{ id: { in: validTermIds } }] - : []), - ], - }), - taxonomyId: taxonomyId, - // 动态权限控制条件 - ...(hasAnyPerms - ? {} // 当有全局权限时,不添加任何额外条件 - : { - // 当无全局权限时,添加域ID过滤 + where: { + ...(termIds && { OR: [ - { domainId: null }, // 通用记录 - { domainId: domainId }, // 特定域记录 + ...(validTermIds.length + ? [{ id: { in: validTermIds } }] + : []), ], }), - }, - include: { children: true }, - orderBy: { order: 'asc' }, - }) + taxonomyId: taxonomyId, + // 动态权限控制条件 + ...(hasAnyPerms + ? {} // 当有全局权限时,不添加任何额外条件 + : { + // 当无全局权限时,添加域ID过滤 + OR: [ + { domainId: null }, // 通用记录 + { domainId: domainId }, // 特定域记录 + ], + }), + }, + include: { children: true }, + orderBy: { order: 'asc' }, + }) : [], ]); const children = childrenData @@ -371,12 +371,12 @@ export class TermService extends BaseTreeService<Prisma.TermDelegate> { ...(hasAnyPerms ? {} // 当有全局权限时,不添加任何额外条件 : { - // 当无全局权限时,添加域ID过滤 - OR: [ - { domainId: null }, // 通用记录 - { domainId: domainId }, // 特定域记录 - ], - }), + // 当无全局权限时,添加域ID过滤 + OR: [ + { domainId: null }, // 通用记录 + { domainId: domainId }, // 特定域记录 + ], + }), }, }, include: { @@ -398,12 +398,12 @@ export class TermService extends BaseTreeService<Prisma.TermDelegate> { ...(hasAnyPerms ? {} // 当有全局权限时,不添加任何额外条件 : { - // 当无全局权限时,添加域ID过滤 - OR: [ - { domainId: null }, // 通用记录 - { domainId: domainId }, // 特定域记录 - ], - }), + // 当无全局权限时,添加域ID过滤 + OR: [ + { domainId: null }, // 通用记录 + { domainId: domainId }, // 特定域记录 + ], + }), }, include: { children: true }, // 包含子节点信息 orderBy: { order: 'asc' }, // 按顺序升序排序 diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index 6fe06c4..c64a1f8 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -1,10 +1,12 @@ -import { Checkbox, Divider, Radio, Space , Spin} from 'antd'; -import { categories, levels } from '../mockData'; -import { TaxonomySlug, TermDto } from '@nice/common'; +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 { api } from '@nice/client'; -import { useSearchParams } from 'react-router-dom'; +import { useEffect, useMemo } 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"; interface FilterSectionProps { selectedCategory: string; @@ -14,105 +16,99 @@ interface FilterSectionProps { } interface GetTaxonomyProps { - categories: string[]; - isLoading: boolean; + 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} +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 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')]) + const [searchParams, setSearchParams] = useSearchParams(); + useEffect(() => { + if (searchParams.get("category")) + onCategoryChange(searchParams.get("category")); + }, [searchParams.get("category")]); - return ( - <div className="bg-white p-6 rounded-lg shadow-sm space-y-6"> - <div> - <h3 className="text-lg font-medium mb-4">课程分类</h3> - <Radio.Group - value={selectedCategory} - onChange={(e) => onCategoryChange(e.target.value)} - className="flex flex-col space-y-3" - > - { - gateGory.isLoading? - (<Spin/>) - : - ( - <> - <Radio value="" >全部课程</Radio> - {gateGory.categories.map(category => ( - <Radio key={category} value={category}> - {category} - </Radio> - ))} - </>) - } - - </Radio.Group> - </div> + const { data: taxonomies } = api.taxonomy.getAll.useQuery({}); + + return ( + <div className="bg-white p-6 rounded-lg shadow-sm space-y-6"> + <Form> + + </Form> + {taxonomies.map((tax) => { + return ( + <> + <div> + <h3 className="text-lg font-medium mb-4"> + {tax?.name} + </h3> + <TermSelect + multiple + taxonomyId={tax?.id}></TermSelect> + </div> + </> + ); + })} <Divider className="my-6" /> - <div> - <h3 className="text-lg font-medium mb-4">难度等级</h3> - <Radio.Group - value={selectedLevel} - onChange={(e) => onLevelChange(e.target.value)} - className="flex flex-col space-y-3" - > - { - levels.isLoading ? - (<Spin/>) - : - ( - <> - <Radio value="">全部难度</Radio> - {levels.categories.map(level => ( - <Radio key={level} value={level}> - {level} - </Radio> - ))} - </>) - } - - </Radio.Group> - </div> - </div> - ); + <div> + <h3 className="text-lg font-medium mb-4">难度等级</h3> + <Radio.Group + value={selectedLevel} + onChange={(e) => onLevelChange(e.target.value)} + className="flex flex-col space-y-3"> + {levels.isLoading ? ( + <Spin /> + ) : ( + <> + <Radio value="">全部难度</Radio> + {levels.categories.map((level) => ( + <Radio key={level} value={level}> + {level} + </Radio> + ))} + </> + )} + </Radio.Group> + </div> + </div> + ); } diff --git a/apps/web/src/app/main/courses/page.tsx b/apps/web/src/app/main/courses/page.tsx index a411d60..1c9df69 100755 --- a/apps/web/src/app/main/courses/page.tsx +++ b/apps/web/src/app/main/courses/page.tsx @@ -3,80 +3,107 @@ 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 { + courseDetailSelect, + CourseDto, + LectureType, + PostType, +} from "@nice/common"; import { useSearchParams } from "react-router-dom"; import { set } from "idb-keyval"; - export default function CoursesPage() { const [currentPage, setCurrentPage] = useState(1); const [selectedCategory, setSelectedCategory] = useState(""); const [selectedLevel, setSelectedLevel] = useState(""); const pageSize = 9; - const [isAll,setIsAll] = useState(true) + const [isAll, setIsAll] = useState(true); const [searchParams, setSearchParams] = useSearchParams(); - let coursesData = [] - let isCourseLoading = false - if(!searchParams.get('searchValue')){ - console.log('no category') - const {data,isLoading} = api.post.findManyWithPagination.useQuery({ + let coursesData = []; + let isCourseLoading = false; + if (!searchParams.get("searchValue")) { + console.log("no category"); + const { data, isLoading } = api.post.findManyWithPagination.useQuery({ where: { type: PostType.COURSE, - terms:isAll?{}:{ - some: { - OR : [ - selectedCategory?{name:selectedCategory}:{}, - selectedLevel?{name:selectedLevel}:{} - ], - }, - }, - + terms: isAll + ? {} + : { + some: { + OR: [ + selectedCategory + ? { name: selectedCategory } + : {}, + selectedLevel + ? { name: selectedLevel } + : {}, + ], + }, + }, }, - select:courseDetailSelect + select: courseDetailSelect, }); - coursesData = data?.items - isCourseLoading = isLoading - }else{ - console.log('searchValue:'+searchParams.get('searchValue')) - const searchValue = searchParams.get('searchValue') - const {data,isLoading} = api.post.findManyWithPagination.useQuery({ + coursesData = data?.items; + isCourseLoading = isLoading; + } else { + console.log("searchValue:" + searchParams.get("searchValue")); + const searchValue = searchParams.get("searchValue"); + const { data, isLoading } = api.post.findManyWithPagination.useQuery({ where: { type: PostType.COURSE, - OR:[ - { title: { contains: searchValue, mode: 'insensitive' } }, - { subTitle: { contains: searchValue, mode: 'insensitive' } }, - { content: { contains: searchValue, mode: 'insensitive' } }, - { terms: { some: { name: { contains: searchValue, mode: 'insensitive' } } } } - ] + OR: [ + { title: { contains: searchValue, mode: "insensitive" } }, + { + subTitle: { + contains: searchValue, + mode: "insensitive", + }, + }, + { content: { contains: searchValue, mode: "insensitive" } }, + { + terms: { + some: { + name: { + contains: searchValue, + mode: "insensitive", + }, + }, + }, + }, + ], }, - select:courseDetailSelect - }) - coursesData = data?.items - isCourseLoading = isLoading + select: courseDetailSelect, + }); + coursesData = data?.items; + isCourseLoading = isLoading; } useEffect(() => { - if(searchParams.get('searchValue')==''){ - setSelectedCategory(''); - setSelectedLevel('') + if (searchParams.get("searchValue") == "") { + setSelectedCategory(""); + setSelectedLevel(""); } - }, [searchParams.get('searchValue')]); + }, [searchParams.get("searchValue")]); const filteredCourses = useMemo(() => { return isCourseLoading ? [] : coursesData; }, [isCourseLoading, coursesData, selectedCategory, selectedLevel]); - const paginatedCourses :CourseDto[]= useMemo(() => { + const paginatedCourses: CourseDto[] = useMemo(() => { const startIndex = (currentPage - 1) * pageSize; - return isCourseLoading ? [] : (filteredCourses.slice(startIndex, startIndex + pageSize) as any as CourseDto[]); + return isCourseLoading + ? [] + : (filteredCourses.slice( + startIndex, + startIndex + pageSize + ) as any as CourseDto[]); }, [filteredCourses, currentPage]); const handlePageChange = (page: number) => { setCurrentPage(page); window.scrollTo({ top: 0, behavior: "smooth" }); }; - useEffect(()=>{ - setCurrentPage(1) - },[]) - + useEffect(() => { + setCurrentPage(1); + }, []); return ( <div className="min-h-screen bg-gray-50"> @@ -92,14 +119,14 @@ export default function CoursesPage() { console.log(category); setSelectedCategory(category); setCurrentPage(1); - setIsAll(!category) - setSearchParams({ searchValue: ''}); + setIsAll(!category); + setSearchParams({ searchValue: "" }); }} onLevelChange={(level) => { setSelectedLevel(level); setCurrentPage(1); - setIsAll(!level) - setSearchParams({ searchValue: ''}); + setIsAll(!level); + setSearchParams({ searchValue: "" }); }} /> </div> diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index 3a7b2af..9fcac1d 100755 --- a/apps/web/src/app/main/home/page.tsx +++ b/apps/web/src/app/main/home/page.tsx @@ -1,163 +1,165 @@ -import HeroSection from './components/HeroSection'; -import CategorySection from './components/CategorySection'; -import CoursesSection from './components/CoursesSection'; -import FeaturedTeachersSection from './components/FeaturedTeachersSection'; -import { api } from '@nice/client'; -import { useEffect, useState } from 'react'; -interface Courses{ - id: number; - title: string; - instructor: string; - students: number; - rating: number; - level: string; - duration: string; - category: string; - progress: number; - thumbnail: string; +import HeroSection from "./components/HeroSection"; +import CategorySection from "./components/CategorySection"; +import CoursesSection from "./components/CoursesSection"; +import FeaturedTeachersSection from "./components/FeaturedTeachersSection"; +import { api } from "@nice/client"; +import { useEffect, useState } from "react"; +import TermTree from "@web/src/components/models/term/term-tree"; +interface Courses { + id: number; + title: string; + instructor: string; + students: number; + rating: number; + level: string; + duration: string; + category: string; + progress: number; + thumbnail: string; } const HomePage = () => { - // const mockCourses = [ - // { - // id: 1, - // title: 'Python 零基础入门', - // instructor: '张教授', - // students: 12000, - // rating: 4.8, - // level: '入门', - // duration: '36小时', - // category: '编程语言', - // progress: 16, - // thumbnail: '/images/course1.jpg', - // }, - // { - // id: 2, - // title: '数据结构与算法', - // instructor: '李教授', - // students: 8500, - // rating: 4.9, - // level: '进阶', - // duration: '48小时', - // category: '计算机基础', - // progress: 35, - // thumbnail: '/images/course2.jpg', - // }, - // { - // id: 3, - // title: '前端开发实战', - // instructor: '王教授', - // students: 10000, - // rating: 4.7, - // level: '中级', - // duration: '42小时', - // category: '前端开发', - // progress: 68, - // thumbnail: '/images/course3.jpg', - // }, - // { - // id: 4, - // title: 'Java企业级开发', - // instructor: '刘教授', - // students: 9500, - // rating: 4.6, - // level: '高级', - // duration: '56小时', - // category: '编程语言', - // progress: 15, - // thumbnail: '/images/course4.jpg', - // }, - // { - // id: 5, - // title: '人工智能基础', - // instructor: '陈教授', - // students: 11000, - // rating: 4.9, - // level: '中级', - // duration: '45小时', - // category: '人工智能', - // progress: 20, - // thumbnail: '/images/course5.jpg', - // }, - // { - // id: 6, - // title: '大数据分析', - // instructor: '赵教授', - // students: 8000, - // rating: 4.8, - // level: '进阶', - // duration: '50小时', - // category: '数据科学', - // progress: 45, - // thumbnail: '/images/course6.jpg', - // }, - // { - // id: 7, - // title: '云计算实践', - // instructor: '孙教授', - // students: 7500, - // rating: 4.7, - // level: '高级', - // duration: '48小时', - // category: '云计算', - // progress: 15, - // thumbnail: '/images/course7.jpg', - // }, - // { - // id: 8, - // title: '移动应用开发', - // instructor: '周教授', - // students: 9000, - // rating: 4.8, - // level: '中级', - // duration: '40小时', - // category: '移动开发', - // progress: 70, - // thumbnail: '/images/course8.jpg', - // }, - // ]; - const { data ,isLoading}: { data: Courses[] , isLoading:boolean} = api.post.findMany.useQuery({ - where: {}, - include: { - instructors: true, - }, - orderBy: { - createdAt: 'desc' // 按创建时间降序排列 - }, - take: 8 // 只获取前8个课程 - }); - useEffect(() => { - if (data) { - console.log('mockCourses data:', data); - } - }, [data]); - - // 数据处理逻辑 -// 修正依赖数组 -return ( - <div className="min-h-screen"> - <HeroSection /> - <CoursesSection - title="推荐课程" - description="最受欢迎的精品课程,助你快速成长" - courses={data} - isLoading={isLoading} - /> - {/* {formattedCourses.map((course)=>{ + // { + // id: 1, + // title: 'Python 零基础入门', + // instructor: '张教授', + // students: 12000, + // rating: 4.8, + // level: '入门', + // duration: '36小时', + // category: '编程语言', + // progress: 16, + // thumbnail: '/images/course1.jpg', + // }, + // { + // id: 2, + // title: '数据结构与算法', + // instructor: '李教授', + // students: 8500, + // rating: 4.9, + // level: '进阶', + // duration: '48小时', + // category: '计算机基础', + // progress: 35, + // thumbnail: '/images/course2.jpg', + // }, + // { + // id: 3, + // title: '前端开发实战', + // instructor: '王教授', + // students: 10000, + // rating: 4.7, + // level: '中级', + // duration: '42小时', + // category: '前端开发', + // progress: 68, + // thumbnail: '/images/course3.jpg', + // }, + // { + // id: 4, + // title: 'Java企业级开发', + // instructor: '刘教授', + // students: 9500, + // rating: 4.6, + // level: '高级', + // duration: '56小时', + // category: '编程语言', + // progress: 15, + // thumbnail: '/images/course4.jpg', + // }, + // { + // id: 5, + // title: '人工智能基础', + // instructor: '陈教授', + // students: 11000, + // rating: 4.9, + // level: '中级', + // duration: '45小时', + // category: '人工智能', + // progress: 20, + // thumbnail: '/images/course5.jpg', + // }, + // { + // id: 6, + // title: '大数据分析', + // instructor: '赵教授', + // students: 8000, + // rating: 4.8, + // level: '进阶', + // duration: '50小时', + // category: '数据科学', + // progress: 45, + // thumbnail: '/images/course6.jpg', + // }, + // { + // id: 7, + // title: '云计算实践', + // instructor: '孙教授', + // students: 7500, + // rating: 4.7, + // level: '高级', + // duration: '48小时', + // category: '云计算', + // progress: 15, + // thumbnail: '/images/course7.jpg', + // }, + // { + // id: 8, + // title: '移动应用开发', + // instructor: '周教授', + // students: 9000, + // rating: 4.8, + // level: '中级', + // duration: '40小时', + // category: '移动开发', + // progress: 70, + // thumbnail: '/images/course8.jpg', + // }, + // ]; + const { data, isLoading }: { data: Courses[]; isLoading: boolean } = + api.post.findMany.useQuery({ + where: {}, + include: { + instructors: true, + }, + orderBy: { + createdAt: "desc", // 按创建时间降序排列 + }, + take: 8, // 只获取前8个课程 + }); + useEffect(() => { + if (data) { + console.log("mockCourses data:", data); + } + }, [data]); + + // 数据处理逻辑 + // 修正依赖数组 + return ( + <div className="min-h-screen"> + <HeroSection /> + + <CoursesSection + title="推荐课程" + description="最受欢迎的精品课程,助你快速成长" + courses={data} + isLoading={isLoading} + /> + {/* {formattedCourses.map((course)=>{ return ( <> <span>course.title</span> </> ) })} */} - {/* <CoursesSection + {/* <CoursesSection title="热门课程" description="最受欢迎的精品课程,助你快速成长" courses={mockCourses} /> */} - <CategorySection /> - {/* <FeaturedTeachersSection /> */} - </div> -); + <CategorySection /> + {/* <FeaturedTeachersSection /> */} + </div> + ); }; -export default HomePage; \ No newline at end of file +export default HomePage; diff --git a/apps/web/src/components/models/term/term-select.tsx b/apps/web/src/components/models/term/term-select.tsx index 045e0e1..ce5d9be 100755 --- a/apps/web/src/components/models/term/term-select.tsx +++ b/apps/web/src/components/models/term/term-select.tsx @@ -23,7 +23,7 @@ export default function TermSelect({ value, onChange, className, - placeholder = "选择单位", + placeholder = "选择分类", multiple = false, taxonomyId, domainId, @@ -190,4 +190,4 @@ export default function TermSelect({ onDropdownVisibleChange={handleDropdownVisibleChange} /> ); -} \ No newline at end of file +} diff --git a/apps/web/src/components/models/term/term-tree.tsx b/apps/web/src/components/models/term/term-tree.tsx new file mode 100644 index 0000000..ae88a68 --- /dev/null +++ b/apps/web/src/components/models/term/term-tree.tsx @@ -0,0 +1,199 @@ +import React, { useEffect, useState, useCallback } from "react"; +import { Tree } from "antd"; +import type { DataNode, TreeProps } from "antd/es/tree"; +import { getUniqueItems } from "@nice/common"; +import { api } from "@nice/client"; + +interface TermData { + value?: string; + children?: TermData[]; + key?: string; + hasChildren?: boolean; + isLeaf?: boolean; + pId?: string; + title?: React.ReactNode; + data?: any; + order?: string; + id?: string; +} + +interface TermTreeProps { + defaultValue?: string | string[]; + value?: string | string[]; + onChange?: (value: string | string[]) => void; + multiple?: boolean; + taxonomyId?: string; + disabled?: boolean; + className?: string; + domainId?: string; + style?: React.CSSProperties; +} + +const TermTree: React.FC<TermTreeProps> = ({ + defaultValue, + value, + onChange, + className, + multiple = false, + taxonomyId, + domainId, + disabled = false, + style, +}) => { + const utils = api.useUtils(); + const [treeData, setTreeData] = useState<TermData[]>([]); + + const processTermData = (terms: TermData[]): TermData[] => { + return terms.map((term) => ({ + ...term, + key: term.key || term.id || "", + title: term.title || term.value, + children: term.children + ? processTermData(term.children) + : undefined, + })); + }; + + const fetchParentTerms = useCallback( + async (termIds: string | string[], taxonomyId?: string) => { + const idsArray = Array.isArray(termIds) + ? termIds + : [termIds].filter(Boolean); + try { + const result = await utils.term.getParentSimpleTree.fetch({ + termIds: idsArray, + taxonomyId, + domainId, + }); + return processTermData(result); + } catch (error) { + console.error( + "Error fetching parent terms for termIds", + idsArray, + ":", + error + ); + throw error; + } + }, + [utils, domainId] + ); + + const fetchTerms = useCallback(async () => { + try { + const rootTerms = await utils.term.getChildSimpleTree.fetch({ + taxonomyId, + domainId, + }); + let combinedTerms = processTermData(rootTerms); + if (defaultValue) { + const defaultTerms = await fetchParentTerms( + defaultValue, + taxonomyId + ); + combinedTerms = getUniqueItems( + [...treeData, ...combinedTerms, ...defaultTerms], + "key" + ); + } + if (value) { + const valueTerms = await fetchParentTerms(value, taxonomyId); + combinedTerms = getUniqueItems( + [...treeData, ...combinedTerms, ...valueTerms], + "key" + ); + } + + setTreeData(combinedTerms); + } catch (error) { + console.error("Error fetching terms:", error); + } + }, [ + defaultValue, + value, + taxonomyId, + utils, + fetchParentTerms, + domainId, + treeData, + ]); + + useEffect(() => { + fetchTerms(); + }, [fetchTerms]); + + const onLoadData = async ({ key }: any) => { + try { + const result = await utils.term.getChildSimpleTree.fetch({ + termIds: [key], + taxonomyId, + domainId, + }); + const processedResult = processTermData(result); + const newItems = getUniqueItems( + [...treeData, ...processedResult], + "key" + ); + setTreeData(newItems); + } catch (error) { + console.error( + "Error loading data for node with key", + key, + ":", + error + ); + } + }; + + const handleCheck: TreeProps["onCheck"] = (checkedKeys, info) => { + if (onChange) { + if (multiple) { + onChange(checkedKeys as string[]); + } else { + onChange((checkedKeys as string[])[0] || ""); + } + } + }; + + const handleExpand = async (expandedKeys: React.Key[]) => { + try { + const allKeyIds = expandedKeys + .map((key) => key.toString()) + .filter(Boolean); + const expandedNodes = await utils.term.getChildSimpleTree.fetch({ + termIds: allKeyIds, + taxonomyId, + domainId, + }); + const processedNodes = processTermData(expandedNodes); + const newItems = getUniqueItems( + [...treeData, ...processedNodes], + "key" + ); + setTreeData(newItems); + } catch (error) { + console.error( + "Error expanding nodes with keys", + expandedKeys, + ":", + error + ); + } + }; + + return ( + <Tree + checkable={multiple} + disabled={disabled} + className={className} + style={style} + treeData={treeData as DataNode[]} + checkedKeys={Array.isArray(value) ? value : value ? [value] : []} + onCheck={handleCheck} + loadData={onLoadData} + onExpand={handleExpand} + /> + ); +}; + +export default TermTree; diff --git a/apps/web/src/components/models/term/util.ts b/apps/web/src/components/models/term/util.ts index c54910a..f7aebc8 100755 --- a/apps/web/src/components/models/term/util.ts +++ b/apps/web/src/components/models/term/util.ts @@ -1,15 +1,16 @@ -import { TreeDataNode } from "@nice/common" +import { TreeDataNode } from "@nice/common"; +import React from "react"; export const treeVisitor = ( - data: TreeDataNode[], - key: React.Key, - callback: (node: TreeDataNode, i: number, data: TreeDataNode[]) => void + data: TreeDataNode[], + key: React.Key, + callback: (node: TreeDataNode, i: number, data: TreeDataNode[]) => void ) => { - for (let i = 0; i < data.length; i++) { - if (data[i].key === key) { - return callback(data[i], i, data); - } - if (data[i].children) { - treeVisitor(data[i].children!, key, callback); - } - } -}; \ No newline at end of file + for (let i = 0; i < data.length; i++) { + if (data[i].key === key) { + return callback(data[i], i, data); + } + if (data[i].children) { + treeVisitor(data[i].children!, key, callback); + } + } +}; From 512b2420e32a51e718adf87e0c92ec5ebc0cfc82 Mon Sep 17 00:00:00 2001 From: Rao <1227431568@qq.com> Date: Tue, 25 Feb 2025 12:56:53 +0800 Subject: [PATCH 06/14] rht02251256 --- .../main/courses/components/FilterSection.tsx | 2 +- apps/web/src/app/main/courses/page.tsx | 61 +++++++++++++------ .../main/home/components/CategorySection.tsx | 3 +- .../main/home/components/CoursesSection.tsx | 2 +- .../app/main/home/components/HeroSection.tsx | 13 ++-- apps/web/src/app/main/layout/MainHeader.tsx | 9 ++- 6 files changed, 59 insertions(+), 31 deletions(-) diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index 6fe06c4..a5d9113 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -29,7 +29,7 @@ function useGetTaxonomy({type}) : GetTaxonomyProps { include:{ children :true }, - take:10, // 只取前10个 + take:20, // 只取前10个 orderBy: { createdAt: 'desc', // 按创建时间降序排列 }, diff --git a/apps/web/src/app/main/courses/page.tsx b/apps/web/src/app/main/courses/page.tsx index a411d60..2deace0 100755 --- a/apps/web/src/app/main/courses/page.tsx +++ b/apps/web/src/app/main/courses/page.tsx @@ -7,19 +7,24 @@ import { courseDetailSelect, CourseDto, LectureType, PostType } from "@nice/comm import { useSearchParams } from "react-router-dom"; import { set } from "idb-keyval"; - +interface paginationData { + items:CourseDto[], + totalPages:number +} export default function CoursesPage() { const [currentPage, setCurrentPage] = useState(1); const [selectedCategory, setSelectedCategory] = useState(""); const [selectedLevel, setSelectedLevel] = useState(""); - const pageSize = 9; - const [isAll,setIsAll] = useState(true) + const pageSize = 12; + const [isAll,setIsAll] = useState(true) const [searchParams, setSearchParams] = useSearchParams(); - let coursesData = [] - let isCourseLoading = false - if(!searchParams.get('searchValue')){ - console.log('no category') - const {data,isLoading} = api.post.findManyWithPagination.useQuery({ + + const [coursesData, setCoursesData] = useState<CourseDto[]>([]); + const [isCourseLoading, setIsCourseLoading] = useState(false); + const [totalPagesNum, setTotalPagesNum] = useState(0); + + if(!searchParams.get('searchValue') && !searchParams.get('searchValue')){ + const {data,isLoading} :{ data:paginationData,isLoading:boolean} = api.post.findManyWithPagination.useQuery({ where: { type: PostType.COURSE, terms:isAll?{}:{ @@ -32,14 +37,21 @@ export default function CoursesPage() { }, }, - select:courseDetailSelect + pageSize, + page:currentPage, + select:courseDetailSelect, }); - coursesData = data?.items - isCourseLoading = isLoading + console.log(data) + useEffect(()=>{ + console.log(currentPage); + setIsCourseLoading(isLoading) + setCoursesData(data?.items) + setTotalPagesNum(data?.totalPages) + },[currentPage,data]) }else{ console.log('searchValue:'+searchParams.get('searchValue')) const searchValue = searchParams.get('searchValue') - const {data,isLoading} = api.post.findManyWithPagination.useQuery({ + const {data,isLoading} :{ data:paginationData,isLoading:boolean}= api.post.findManyWithPagination.useQuery({ where: { type: PostType.COURSE, OR:[ @@ -49,10 +61,15 @@ export default function CoursesPage() { { terms: { some: { name: { contains: searchValue, mode: 'insensitive' } } } } ] }, - select:courseDetailSelect + select:courseDetailSelect, + pageSize, + page:currentPage, }) - coursesData = data?.items - isCourseLoading = isLoading + useEffect(()=>{ + setIsCourseLoading(isLoading) + setCoursesData(data?.items) + setTotalPagesNum(data?.totalPages) + },[currentPage]) } useEffect(() => { if(searchParams.get('searchValue')==''){ @@ -93,13 +110,19 @@ export default function CoursesPage() { setSelectedCategory(category); setCurrentPage(1); setIsAll(!category) - setSearchParams({ searchValue: ''}); + setSearchParams(prev => { + prev.delete('searchValue'); + return prev; + }); }} onLevelChange={(level) => { setSelectedLevel(level); setCurrentPage(1); setIsAll(!level) - setSearchParams({ searchValue: ''}); + setSearchParams(prev => { + prev.delete('searchValue'); + return prev; + }); }} /> </div> @@ -110,12 +133,12 @@ export default function CoursesPage() { <div className="bg-white p-6 rounded-lg shadow-sm"> <div className="flex justify-between items-center mb-6"> <span className="text-gray-600"> - 共找到 {filteredCourses.length} 门课程 + 共找到 {totalPagesNum * pageSize || 0} 门课程 </span> </div> <CourseList courses={paginatedCourses} - total={filteredCourses.length} + total={totalPagesNum * pageSize} pageSize={pageSize} currentPage={currentPage} onPageChange={handlePageChange} diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index 81b201a..e06a5da 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -3,7 +3,7 @@ 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 } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; const { Title, Text } = Typography; @@ -124,6 +124,7 @@ const CategorySection = () => { onClick={()=>{ console.log(category.name) navigate(`/courses?category=${category.name}`) + window.scrollTo({ top: 0, behavior: 'smooth' }) }} > <div className="absolute -inset-0.5 bg-gradient-to-r from-gray-200 to-gray-300 opacity-50 rounded-2xl transition-all duration-700 group-hover:opacity-75" /> diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index 0c04870..b965815 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -56,7 +56,7 @@ function useFetchCoursesByCategory(category: string) { terms:true } }); - + console.log("ss",data) return { data, isLoading}; } diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx index f73357d..10ecbfe 100755 --- a/apps/web/src/app/main/home/components/HeroSection.tsx +++ b/apps/web/src/app/main/home/components/HeroSection.tsx @@ -48,12 +48,11 @@ const carouselItems: CarouselItem[] = [ const HeroSection = () => { const carouselRef = useRef<CarouselRef>(null); const { statistics, baseSetting } = useAppConfig(); - const platformStats: PlatformStat[] = [ - { icon: <TeamOutlined />, value: "50,000+", label: "注册学员" }, - { icon: <BookOutlined />, value: "1,000+", label: "精品课程" }, - // { icon: <StarOutlined />, value: '98%', label: '好评度' }, - { icon: <EyeOutlined />, value: "4552", label: "观看次数" }, + { icon: <TeamOutlined />, value: statistics.staffs.toString(), label: "注册学员" }, + { icon: <StarOutlined />, value: statistics.courses.toString(), label: "精品课程" }, + { icon: <BookOutlined />, value: statistics.lectures.toString(), label: '课程章节' }, + { icon: <EyeOutlined />, value: statistics.reads.toString(), label: "观看次数" }, ]; const handlePrev = useCallback(() => { carouselRef.current?.prev(); @@ -119,8 +118,8 @@ const HeroSection = () => { </div> {/* Stats Container */} - <div className="absolute -bottom-24 left-1/2 -translate-x-1/2 w-1/2 max-w-6xl px-4"> - <div className="rounded-2xl grid grid-cols-2 md:grid-cols-3 gap-4 md:gap-8 p-6 md:p-8 bg-white border shadow-xl hover:shadow-2xl transition-shadow duration-500 will-change-[transform,box-shadow]"> + <div className="absolute -bottom-20 left-1/2 -translate-x-1/2 w-2/3 max-w-6xl px-4"> + <div className="rounded-2xl grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-8 p-6 md:p-8 bg-white border shadow-xl hover:shadow-2xl transition-shadow duration-500 will-change-[transform,box-shadow]"> {platformStats.map((stat, index) => ( <div key={index} diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx index 76a15b4..40053e1 100755 --- a/apps/web/src/app/main/layout/MainHeader.tsx +++ b/apps/web/src/app/main/layout/MainHeader.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { Input, Layout, Avatar, Button, Dropdown } from "antd"; import { EditFilled, SearchOutlined, UserOutlined } from "@ant-design/icons"; import { useAuth } from "@web/src/providers/auth-provider"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { UserMenu } from "./UserMenu/UserMenu"; import { NavigationMenu } from "./NavigationMenu"; @@ -12,6 +12,7 @@ export function MainHeader() { const [searchValue, setSearchValue] = useState(""); const { isAuthenticated, user } = useAuth(); const navigate = useNavigate(); + const [searchParams,setSearchParams] = useSearchParams(); return ( <Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30"> @@ -37,7 +38,11 @@ export function MainHeader() { onChange={(e) => setSearchValue(e.target.value)} onPressEnter={(e) => { //console.log(e) - setSearchValue(""); + //setSearchValue(""); + setSearchParams((prev)=>{ + if(searchParams.get("category")) prev.delete("category") + return prev + }) navigate( `/courses/?searchValue=${searchValue}` ); From 6484ac80ad63e549ab5f52173c1303bdd7ae8055 Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Tue, 25 Feb 2025 13:30:59 +0800 Subject: [PATCH 07/14] li --- .../main/courses/components/CourseCard.tsx | 115 ++++++++++++------ .../main/home/components/CoursesSection.tsx | 48 +++++--- packages/common/src/models/post.ts | 1 + 3 files changed, 111 insertions(+), 53 deletions(-) diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 5d7c3cd..84688f3 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -1,49 +1,90 @@ -import { Card, Rate, Tag } from 'antd'; +import { Card, Rate, Tag ,Typography,Button} from 'antd'; import { Course } from '../mockData'; -import { UserOutlined, ClockCircleOutlined } from '@ant-design/icons'; +import { UserOutlined, ClockCircleOutlined, PlayCircleOutlined, TeamOutlined } from '@ant-design/icons'; import { CourseDto } from '@nice/common'; +import { useNavigate } from 'react-router-dom'; + interface CourseCardProps { course: CourseDto; } - +const { Title, Text } = Typography; export default function CourseCard({ course }: CourseCardProps) { + const navigate = useNavigate(); + const handleClick = (course: CourseDto) => { + navigate(`/course/${course.id}/detail`); + } return ( - <Card - hoverable - className="w-full h-full transition-all duration-300 hover:shadow-lg" - cover={ - <img - alt={course.title} - src={course?.meta?.thumbnail} - className="object-cover w-full h-40" + <Card + onClick={() => handleClick(course)} + key={course.id} + hoverable + className="group overflow-hidden rounded-2xl border border-gray-200 bg-white + shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" + cover={ + <div className="relative h-56 bg-gradient-to-br from-gray-900 to-gray-800 overflow-hidden"> + <div + className="absolute inset-0 bg-cover bg-center transform transition-all duration-700 ease-out group-hover:scale-110" + style={{ backgroundImage: `url(${course?.meta?.thumbnail})` }} /> - } - > - <div className="space-y-3"> - <h3 className="text-lg font-semibold line-clamp-2 hover:text-blue-600 transition-colors"> - {course.title} - </h3> - <p className="text-gray-500 text-sm">{course.subTitle}</p> - <div className="flex items-center space-x-2"> - <Rate disabled defaultValue={course.rating} className="text-sm" /> - <span className="text-gray-500 text-sm">{course.rating}</span> - </div> - <div className="flex items-center justify-between text-sm text-gray-500"> - <div className="flex items-center space-x-1"> - <UserOutlined className="text-gray-400" /> - <span>{course.enrollments?.length} 人在学</span> - </div> - <div className="flex items-center space-x-1"> - <ClockCircleOutlined className="text-gray-400" /> - <span>{course.duration}</span> - </div> - </div> - <div className="flex flex-wrap gap-2 pt-2"> - <Tag color="blue" className="rounded-full px-3">{course.terms[0].name}</Tag> - <Tag color="green" className="rounded-full px-3">{course.terms[1].name}</Tag> - </div> + <div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-80 group-hover:opacity-60 transition-opacity duration-300" /> + <PlayCircleOutlined className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-6xl text-white/90 opacity-0 group-hover:opacity-100 transition-all duration-300" /> </div> - </Card> + } + > + <div className="px-4"> + <div className="flex gap-2 mb-4"> + <Tag + color="blue" + className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0" + > + {course.terms[0].name} + </Tag> + <Tag + color={ + course.terms[1].name === '入门' + ? 'green' + : course.terms[1].name === '中级' + ? 'blue' + : 'purple' + } + className="px-3 py-1 rounded-full border-0" + > + {course.terms[1].name} + </Tag> + </div> + <Title + level={4} + className="mb-4 line-clamp-2 font-bold leading-snug text-gray-800 hover:text-blue-600 transition-colors duration-300 group-hover:scale-[1.02] transform origin-left" + > + <button > {course.title}</button> + + +
+ +
+ + {course?.depts[0]?.name} + +
+ + 观看次数{course?.meta?.views}次 + +
+ + +
+ +
+ +
+
); } diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index b4938b0..ec2e759 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useEffect } from 'react'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { Button, Card, Typography, Tag, Progress, Spin, Empty } from 'antd'; +import { useNavigate } from 'react-router-dom'; +import { Button, Card, Typography, Tag, Spin, Empty } from 'antd'; import { PlayCircleOutlined, UserOutlined, @@ -12,7 +12,6 @@ import { } from '@ant-design/icons'; import { CourseDto, TaxonomySlug, TermDto } from '@nice/common'; import { api } from '@nice/client'; -// const {courseId} = useParams(); interface GetTaxonomyProps { categories: string[]; isLoading: boolean; @@ -39,6 +38,7 @@ function useGetTaxonomy({ type }): GetTaxonomyProps { }, [data]); return { categories, isLoading } } + // 不同分类跳转 function useFetchCoursesByCategory(category: string) { const isAll = category === '全部'; @@ -52,7 +52,8 @@ function useFetchCoursesByCategory(category: string) { }, take: 8, include: { - terms: true + terms: true, + depts:true } }); @@ -83,12 +84,9 @@ interface CoursesSectionProps { initialVisibleCoursesCount?: number; } - const CoursesSection: React.FC = ({ title, description, - courses, - isLoading, initialVisibleCoursesCount = 8, }) => { const navigate = useNavigate(); @@ -97,10 +95,11 @@ const CoursesSection: React.FC = ({ const gateGory: GetTaxonomyProps = useGetTaxonomy({ type: TaxonomySlug.CATEGORY, }) + const { data, isLoading: isDataLoading } = useFetchCoursesByCategory(selectedCategory); - useEffect(() => { - console.log('data:', data) - }) + // useEffect(() => { + // console.log('data:', data) + // }) const handleClick = (course: CourseDto) => { navigate(`/course/${course.id}/detail`); } @@ -108,6 +107,20 @@ const CoursesSection: React.FC = ({ useEffect(() => { console.log('data:', data) }) + + // const { data: depts, isLoading: isDeptLoading }: { data: CourseDto[], isLoading: boolean } = api.post.findMany.useQuery({ + // where: {}, + // include: { + // depts: true, + // }, + // orderBy: { + // createdAt: 'desc' // 按创建时间降序排列 + // }, + // take: 8 // 只获取前8个课程 + // }); + + + const filteredCourses = useMemo(() => { return selectedCategory === '全部' ? data @@ -115,6 +128,7 @@ const CoursesSection: React.FC = ({ }, [selectedCategory, data]); const displayedCourses = isDataLoading ? [] : filteredCourses?.slice(0, visibleCourses); + return (
@@ -170,8 +184,8 @@ const CoursesSection: React.FC = ({
- {displayedCourses.length=== 0 ? ( -
+ {displayedCourses.length === 0 ? ( +
) : displayedCourses?.map((course) => ( @@ -223,14 +237,16 @@ const CoursesSection: React.FC = ({
- + + {course?.depts[0]?.name}
- - - 观看次数{course?.meta?.views}次 + + 观看次数{course?.meta?.views}次
+ +
-
- -
- - ); +
+ +
+
+ + ); } diff --git a/apps/web/src/app/main/courses/components/CourseList.tsx b/apps/web/src/app/main/courses/components/CourseList.tsx index f3e3177..4550795 100755 --- a/apps/web/src/app/main/courses/components/CourseList.tsx +++ b/apps/web/src/app/main/courses/components/CourseList.tsx @@ -1,44 +1,74 @@ -import { Pagination, Empty } from 'antd'; -import { Course } from '../mockData'; -import CourseCard from './CourseCard'; -import {CourseDto} from '@nice/common' +import { Pagination, Empty } from "antd"; +import CourseCard from "./CourseCard"; +import { CourseDto, Prisma } from "@nice/common"; +import { api } from "@nice/client"; +import { DefaultArgs } from "@prisma/client/runtime/library"; +import { useEffect, useMemo, useState } from "react"; interface CourseListProps { - courses: CourseDto[]; - total: number; - pageSize: number; - currentPage: number; - onPageChange: (page: number) => void; + params?: { + page?: number; + pageSize?: number; + where?: Prisma.PostWhereInput; + select?: Prisma.PostSelect; + }; } +interface CoursesPagnationProps { + data: { + items: CourseDto[]; + totalPages: number; + }; + isLoading: boolean; +} +export default function CourseList({ params }: CourseListProps) { + const [currentPage, setCurrentPage] = useState(params?.page || 1); + const { data, isLoading }: CoursesPagnationProps = + api.post.findManyWithPagination.useQuery({ + ...params, + page: currentPage, + }); + const totalPages = useMemo(() => { + if (data && !isLoading) { + return data?.totalPages; + } + return 1; + }, [data, isLoading]); -export default function CourseList({ - courses, - total, - pageSize, - currentPage, - onPageChange, -}: CourseListProps) { - return ( -
- {courses.length > 0 ? ( - <> -
- {courses.map(course => ( - - ))} -
-
- -
- - ) : ( - - )} -
- ); -} \ No newline at end of file + const courses = useMemo(() => { + if (data && !isLoading) { + return data?.items; + } + return []; + }, [data, isLoading]); + + useEffect(() => { + setCurrentPage(params?.page || 1); + }, [params?.page]); + function onPageChange(page: number, pageSize: number) { + setCurrentPage(page); + window.scrollTo({ top: 0, behavior: "smooth" }); + } + return ( +
+ {courses.length > 0 ? ( + <> +
+ {courses.map((course) => ( + + ))} +
+
+ +
+ + ) : ( + + )} +
+ ); +} diff --git a/apps/web/src/app/main/courses/instructor/page.tsx b/apps/web/src/app/main/courses/instructor/page.tsx deleted file mode 100755 index a914925..0000000 --- a/apps/web/src/app/main/courses/instructor/page.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { PlusIcon } from "@heroicons/react/24/outline"; -import { CourseList } from "@web/src/components/models/course/list/course-list"; -import { Button } from "@web/src/components/common/element/Button"; -import { api } from "@nice/client"; -import { useState } from "react"; -import { CourseCard } from "@web/src/components/models/course/card/CourseCard"; -import { useNavigate } from "react-router-dom"; -import { useAuth } from "@web/src/providers/auth-provider"; - -export default function InstructorCoursesPage() { - const navigate = useNavigate(); - const [currentPage, setCurrentPage] = useState(1); - const { user } = useAuth(); - - const { data: paginationRes, refetch } = - api.post.findManyWithPagination.useQuery({ - page: currentPage, - pageSize: 8, - where: { - instructors: { - some: { - instructorId: user?.id, - }, - }, - }, - }); - - const handlePageChange = (page: number) => { - setCurrentPage(page); - refetch(); - }; - - return ( -
-
-
-

- 我教授的课程 -

- -
- ( - { - navigate(`/course/${course.id}/editor`, { - replace: true, - }); - }} - course={course} - /> - )} - /> -
-
- ); -} diff --git a/apps/web/src/app/main/courses/student/page.tsx b/apps/web/src/app/main/courses/student/page.tsx deleted file mode 100755 index 2508c50..0000000 --- a/apps/web/src/app/main/courses/student/page.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { CourseList } from "@web/src/components/models/course/list/course-list"; -import { api } from "@nice/client"; -import { useState } from "react"; -import { CourseCard } from "@web/src/components/models/course/card/CourseCard"; -import { useAuth } from "@web/src/providers/auth-provider"; - -export default function StudentCoursesPage() { - const [currentPage, setCurrentPage] = useState(1); - const { user } = useAuth(); - - const { data: paginationRes, refetch } = - api.post.findManyWithPagination.useQuery({ - page: currentPage, - pageSize: 8, - where: { - enrollments: { - some: { - studentId: user?.id, - }, - }, - }, - }); - - const handlePageChange = (page: number) => { - setCurrentPage(page); - refetch(); - }; - - return ( -
-
-
-

- 我参加的课程 -

-
- ( - - )}> -
-
- ); -} diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx index 40053e1..47503b5 100755 --- a/apps/web/src/app/main/layout/MainHeader.tsx +++ b/apps/web/src/app/main/layout/MainHeader.tsx @@ -1,19 +1,17 @@ -import { useState } from "react"; +import { useContext, useState } from "react"; import { Input, Layout, Avatar, Button, Dropdown } from "antd"; import { EditFilled, SearchOutlined, UserOutlined } from "@ant-design/icons"; import { useAuth } from "@web/src/providers/auth-provider"; import { useNavigate, useSearchParams } from "react-router-dom"; import { UserMenu } from "./UserMenu/UserMenu"; import { NavigationMenu } from "./NavigationMenu"; - +import { useMainContext } from "./MainProvider"; const { Header } = Layout; export function MainHeader() { - const [searchValue, setSearchValue] = useState(""); const { isAuthenticated, user } = useAuth(); const navigate = useNavigate(); - const [searchParams,setSearchParams] = useSearchParams(); - + const { searchValue, setSearchValue } = useMainContext(); return (
@@ -37,16 +35,17 @@ export function MainHeader() { value={searchValue} onChange={(e) => setSearchValue(e.target.value)} onPressEnter={(e) => { - //console.log(e) - //setSearchValue(""); - setSearchParams((prev)=>{ - if(searchParams.get("category")) prev.delete("category") - return prev - }) - navigate( - `/courses/?searchValue=${searchValue}` - ); - window.scrollTo({ top: 0, behavior: "smooth" }); + if ( + !window.location.pathname.startsWith( + "/courses/" + ) + ) { + navigate(`/courses/`); + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + } }} />
diff --git a/apps/web/src/app/main/layout/MainLayout.tsx b/apps/web/src/app/main/layout/MainLayout.tsx index 09f5670..0a0074b 100755 --- a/apps/web/src/app/main/layout/MainLayout.tsx +++ b/apps/web/src/app/main/layout/MainLayout.tsx @@ -1,19 +1,21 @@ -import { Layout } from 'antd'; -import { Outlet } from 'react-router-dom'; -import { MainHeader } from './MainHeader'; -import { MainFooter } from './MainFooter'; +import { Layout } from "antd"; +import { Outlet } from "react-router-dom"; +import { MainHeader } from "./MainHeader"; +import { MainFooter } from "./MainFooter"; +import { MainProvider } from "./MainProvider"; const { Content } = Layout; export function MainLayout() { - return ( - - - - - - - - - ); -} \ No newline at end of file + return ( + + + + + + + + + + ); +} diff --git a/apps/web/src/app/main/layout/MainProvider.tsx b/apps/web/src/app/main/layout/MainProvider.tsx new file mode 100644 index 0000000..d503c39 --- /dev/null +++ b/apps/web/src/app/main/layout/MainProvider.tsx @@ -0,0 +1,31 @@ +import React, { createContext, ReactNode, useContext, useState } from "react"; + +interface MainContextType { + searchValue?: string; + setSearchValue?: React.Dispatch>; +} + +const MainContext = createContext(null); +interface MainProviderProps { + children: ReactNode; +} + +export function MainProvider({ children }: MainProviderProps) { + const [searchValue, setSearchValue] = useState(""); + return ( + + {children} + + ); +} +export const useMainContext = () => { + const context = useContext(MainContext); + if (!context) { + throw new Error("useMainContext must be used within MainProvider"); + } + return context; +}; diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 70f60a8..07c7c9c 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -8,8 +8,6 @@ import { import ErrorPage from "../app/error"; import WithAuth from "../components/utils/with-auth"; import LoginPage from "../app/login"; -import StudentCoursesPage from "../app/main/courses/student/page"; -import InstructorCoursesPage from "../app/main/courses/instructor/page"; import HomePage from "../app/main/home/page"; import { CourseDetailPage } from "../app/main/course/detail/page"; import { CourseBasicForm } from "../components/models/course/editor/form/CourseBasicForm"; @@ -41,7 +39,6 @@ export type CustomRouteObject = | CustomIndexRouteObject | CustomNonIndexRouteObject; export const routes: CustomRouteObject[] = [ - { path: "/", errorElement: , @@ -66,39 +63,16 @@ export const routes: CustomRouteObject[] = [ path: "courses", element: , }, - { - path: "my-courses", - }, + { path: "profiles", }, - { - path: "courses", - children: [ - { - path: "student", - element: ( - - - - ), - }, - { - path: "instructor", - element: ( - - - - ), - }, - ], - }, + // 课程预览页面 { path: "coursePreview/:id?", - element: - } - + element: , + }, ], }, { @@ -106,20 +80,27 @@ export const routes: CustomRouteObject[] = [ children: [ { path: ":id?/editor", - element: , + element: ( + + + + ), children: [ { index: true, - element: , + element: ( + + + + ), }, - // { - // path: "goal", - // element: , - // }, + { path: "content", element: ( - + + + ), }, // { @@ -146,5 +127,4 @@ export const routes: CustomRouteObject[] = [ }, ]; - export const router = createBrowserRouter(routes); From c2a5a13063cdd2c8285d7f56c27f29364ea1e744 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Tue, 25 Feb 2025 16:23:36 +0800 Subject: [PATCH 12/14] add --- .../main/courses/components/CourseCard.tsx | 3 +- .../main/home/components/CoursesSection.tsx | 428 +++++++----------- apps/web/src/app/main/home/page.tsx | 151 +----- 3 files changed, 165 insertions(+), 417 deletions(-) diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index bd9af23..9a9f37d 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -6,7 +6,7 @@ import { PlayCircleOutlined, TeamOutlined, } from "@ant-design/icons"; -import { CourseDto } from "@nice/common"; +import { CourseDto, TaxonomySlug } from "@nice/common"; import { useNavigate } from "react-router-dom"; interface CourseCardProps { @@ -44,6 +44,7 @@ export default function CourseCard({ course }: CourseCardProps) { className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0"> {course.terms?.[0].name} + { - const allCategories = isLoading ? [] : data?.map((course) => course.name); - return [...Array.from(new Set(allCategories))]; - }, [data]); - return { categories, isLoading } + const { data, isLoading }: { data: TermDto[]; isLoading: boolean } = + api.term.findMany.useQuery({ + where: { + taxonomy: { + slug: type, + }, + }, + include: { + children: true, + }, + take: 10, // 只取前10个 + orderBy: {}, + }); + const categories = useMemo(() => { + const allCategories = isLoading + ? [] + : data?.map((course) => course.name); + return [...Array.from(new Set(allCategories))]; + }, [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 - } - }); + 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 }; + return { data, isLoading }; } - - - const { Title, Text } = Typography; - -interface Course { - id: number; - title: string; - instructor: string; - students: number; - rating: number; - level: string; - duration: string; - category: string; - progress: number; - thumbnail: string; -} interface CoursesSectionProps { - title: string; - description: string; - courses: Course[]; - isLoading: boolean - initialVisibleCoursesCount?: number; + title: string; + description: string; + initialVisibleCoursesCount?: number; } - const CoursesSection: React.FC = ({ - title, - description, - initialVisibleCoursesCount = 8, + title, + description, }) => { - const navigate = useNavigate(); - const [selectedCategory, setSelectedCategory] = useState('全部'); - const [visibleCourses, setVisibleCourses] = useState(initialVisibleCoursesCount); - const gateGory: GetTaxonomyProps = useGetTaxonomy({ - type: TaxonomySlug.CATEGORY, - }) + 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 ( +
+
+
+
+ + {title} + + + {description} + +
+
- const { data, isLoading: isDataLoading } = useFetchCoursesByCategory(selectedCategory); - // useEffect(() => { - // console.log('data:', data) - // }) - const handleClick = (course: CourseDto) => { - navigate(`/course/${course.id}/detail`); - } +
+ {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} + + ))} + + )} +
- useEffect(() => { - console.log('data:', data) - }) - - // const { data: depts, isLoading: isDeptLoading }: { data: CourseDto[], isLoading: boolean } = api.post.findMany.useQuery({ - // where: {}, - // include: { - // depts: true, - // }, - // orderBy: { - // createdAt: 'desc' // 按创建时间降序排列 - // }, - // take: 8 // 只获取前8个课程 - // }); - - - - const filteredCourses = useMemo(() => { - return selectedCategory === '全部' - ? data - : data?.filter(c => c.terms.some(t => t.name === selectedCategory)); - }, [selectedCategory, data]); - - const displayedCourses = isDataLoading ? [] : filteredCourses?.slice(0, visibleCourses); - - return ( -
-
-
-
- - {title} - - - {description} - -
-
- -
- {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) - console.log(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} - - - )) - } - - - ) - } -
- -
- {displayedCourses.length === 0 ? ( -
- -
- ) : displayedCourses?.map((course) => ( - handleClick(course)} - key={course.id} - hoverable - className="group overflow-hidden rounded-2xl border border-gray-200 bg-white - shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" - cover={ -
-
-
- -
- } - > -
-
- - {course.terms[0].name} - - - {course.terms[1].name} - -
- - <button > {course.title}</button> - - -
- -
- - {course?.depts[0]?.name} - -
- - 观看次数{course?.meta?.views}次 - -
- - -
- -
- -
- - ))} -
- - {filteredCourses?.length >= visibleCourses && ( -
-
-
- -
-
- )} -
-
- ); +
+ {displayedCourses.length === 0 ? ( +
+ +
+ ) : ( + displayedCourses?.map((course) => ( + + )) + )} +
+ { +
+
+
+ +
+
+ } +
+
+ ); }; - export default CoursesSection; diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index 7250975..0cf5123 100755 --- a/apps/web/src/app/main/home/page.tsx +++ b/apps/web/src/app/main/home/page.tsx @@ -1,163 +1,18 @@ import HeroSection from "./components/HeroSection"; import CategorySection from "./components/CategorySection"; import CoursesSection from "./components/CoursesSection"; -import FeaturedTeachersSection from "./components/FeaturedTeachersSection"; -import { api } from "@nice/client"; -import { useEffect, useState } from "react"; -import TermTree from "@web/src/components/models/term/term-tree"; -interface Courses { - id: number; - title: string; - instructor: string; - students: number; - rating: number; - level: string; - duration: string; - category: string; - progress: number; - thumbnail: string; -} -const HomePage = () => { - // { - // id: 1, - // title: 'Python 零基础入门', - // instructor: '张教授', - // students: 12000, - // rating: 4.8, - // level: '入门', - // duration: '36小时', - // category: '编程语言', - // progress: 16, - // thumbnail: '/images/course1.jpg', - // }, - // { - // id: 2, - // title: '数据结构与算法', - // instructor: '李教授', - // students: 8500, - // rating: 4.9, - // level: '进阶', - // duration: '48小时', - // category: '计算机基础', - // progress: 35, - // thumbnail: '/images/course2.jpg', - // }, - // { - // id: 3, - // title: '前端开发实战', - // instructor: '王教授', - // students: 10000, - // rating: 4.7, - // level: '中级', - // duration: '42小时', - // category: '前端开发', - // progress: 68, - // thumbnail: '/images/course3.jpg', - // }, - // { - // id: 4, - // title: 'Java企业级开发', - // instructor: '刘教授', - // students: 9500, - // rating: 4.6, - // level: '高级', - // duration: '56小时', - // category: '编程语言', - // progress: 15, - // thumbnail: '/images/course4.jpg', - // }, - // { - // id: 5, - // title: '人工智能基础', - // instructor: '陈教授', - // students: 11000, - // rating: 4.9, - // level: '中级', - // duration: '45小时', - // category: '人工智能', - // progress: 20, - // thumbnail: '/images/course5.jpg', - // }, - // { - // id: 6, - // title: '大数据分析', - // instructor: '赵教授', - // students: 8000, - // rating: 4.8, - // level: '进阶', - // duration: '50小时', - // category: '数据科学', - // progress: 45, - // thumbnail: '/images/course6.jpg', - // }, - // { - // id: 7, - // title: '云计算实践', - // instructor: '孙教授', - // students: 7500, - // rating: 4.7, - // level: '高级', - // duration: '48小时', - // category: '云计算', - // progress: 15, - // thumbnail: '/images/course7.jpg', - // }, - // { - // id: 8, - // title: '移动应用开发', - // instructor: '周教授', - // students: 9000, - // rating: 4.8, - // level: '中级', - // duration: '40小时', - // category: '移动开发', - // progress: 70, - // thumbnail: '/images/course8.jpg', - // }, - // ]; - const { data, isLoading }: { data: Courses[]; isLoading: boolean } = - api.post.findMany.useQuery({ - where: {}, - include: { - instructors: true, - }, - orderBy: { - createdAt: "desc", // 按创建时间降序排列 - }, - take: 8, // 只获取前8个课程 - }); - useEffect(() => { - if (data) { - console.log("mockCourses data:", data); - } - }, [data]); - // 数据处理逻辑 - // 修正依赖数组 + +const HomePage = () => { + return (
- - {/* {formattedCourses.map((course)=>{ - return ( - <> - course.title - - ) - })} */} - {/* */} - {/* */}
); }; From b0d0597d86cad7176dcfdbb252dc8c1d9f5c2ccc Mon Sep 17 00:00:00 2001 From: Rao <1227431568@qq.com> Date: Tue, 25 Feb 2025 16:26:59 +0800 Subject: [PATCH 13/14] rht02251626 --- .../main/courses/components/FilterSection.tsx | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index c64a1f8..eac6b59 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -69,10 +69,10 @@ export default function FilterSection({ return (
-
+ {/* -
- {taxonomies.map((tax) => { + */} + {taxonomies?.map((tax) => { return ( <>
@@ -88,27 +88,6 @@ export default function FilterSection({ })} - -
-

难度等级

- onLevelChange(e.target.value)} - className="flex flex-col space-y-3"> - {levels.isLoading ? ( - - ) : ( - <> - 全部难度 - {levels.categories.map((level) => ( - - {level} - - ))} - - )} - -
); } From 668a3a015b67433cc2b8b7d17a73b418e20c45c9 Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Tue, 25 Feb 2025 16:28:17 +0800 Subject: [PATCH 14/14] add. Li --- .../main/courses/components/FilterSection.tsx | 6 +-- .../main/home/components/CategorySection.tsx | 2 + .../main/home/components/CoursesSection.tsx | 37 +++++++------------ 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index c64a1f8..9c74447 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -69,10 +69,10 @@ export default function FilterSection({ return (
-
+ {/* -
- {taxonomies.map((tax) => { + */} + {taxonomies?.map((tax) => { return ( <>
diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index e06a5da..d0735fa 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -69,6 +69,8 @@ const CategorySection = () => { const [displayedCategories,setDisplayedCategories] = useState([]) useEffect(() => { console.log(courseCategoriesData); + // 如果 showAll 为 true,则显示所有分类数据, +// 如果 showAll 为 false,则只显示前 8 个分类数据, if(!isLoading){ if(showAll){ setDisplayedCategories(courseCategoriesData) diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index ec2e759..1fe12af 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -54,7 +54,10 @@ function useFetchCoursesByCategory(category: string) { include: { terms: true, depts:true - } + }, + orderBy: { + createdAt: 'desc', // 按创建时间降序排列 + }, }); return { data, isLoading }; @@ -97,38 +100,21 @@ const CoursesSection: React.FC = ({ }) const { data, isLoading: isDataLoading } = useFetchCoursesByCategory(selectedCategory); - // useEffect(() => { - // console.log('data:', data) - // }) + + useEffect(() => { + console.log('dept data:', data) + }) const handleClick = (course: CourseDto) => { navigate(`/course/${course.id}/detail`); } - useEffect(() => { - console.log('data:', data) - }) - - // const { data: depts, isLoading: isDeptLoading }: { data: CourseDto[], isLoading: boolean } = api.post.findMany.useQuery({ - // where: {}, - // include: { - // depts: true, - // }, - // orderBy: { - // createdAt: 'desc' // 按创建时间降序排列 - // }, - // take: 8 // 只获取前8个课程 - // }); - - const filteredCourses = useMemo(() => { return selectedCategory === '全部' ? data : data?.filter(c => c.terms.some(t => t.name === selectedCategory)); }, [selectedCategory, data]); - const displayedCourses = isDataLoading ? [] : filteredCourses?.slice(0, visibleCourses); - return (
@@ -164,7 +150,7 @@ const CoursesSection: React.FC = ({ color={selectedCategory === category ? 'blue' : 'default'} onClick={() => { setSelectedCategory(category) - console.log(category) + // console.log(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' @@ -269,7 +255,10 @@ const CoursesSection: React.FC = ({