This commit is contained in:
ditiqi 2025-02-25 16:23:36 +08:00
parent 9251422e09
commit c2a5a13063
3 changed files with 165 additions and 417 deletions

View File

@ -6,7 +6,7 @@ import {
PlayCircleOutlined, PlayCircleOutlined,
TeamOutlined, TeamOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { CourseDto } from "@nice/common"; import { CourseDto, TaxonomySlug } from "@nice/common";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
interface CourseCardProps { 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"> className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0">
{course.terms?.[0].name} {course.terms?.[0].name}
</Tag> </Tag>
<Tag <Tag
color={ color={
course.terms?.[1].name === "入门" course.terms?.[1].name === "入门"

View File

@ -1,286 +1,178 @@
import React, { useState, useMemo, useEffect } from 'react'; import React, { useState, useMemo, useEffect } from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import { Button, Card, Typography, Tag, Spin, Empty } from 'antd'; import { Button, Card, Typography, Tag, Spin, Empty } from "antd";
import { import {
PlayCircleOutlined, PlayCircleOutlined,
UserOutlined, TeamOutlined,
ClockCircleOutlined, ArrowRightOutlined,
TeamOutlined, } from "@ant-design/icons";
StarOutlined, import { CourseDto, TaxonomySlug, TermDto } from "@nice/common";
ArrowRightOutlined, import { api } from "@nice/client";
EyeOutlined, import CourseCard from "../../courses/components/CourseCard";
} from '@ant-design/icons';
import { CourseDto, TaxonomySlug, TermDto } from '@nice/common';
import { api } from '@nice/client';
interface GetTaxonomyProps { interface GetTaxonomyProps {
categories: string[]; categories: string[];
isLoading: boolean; isLoading: boolean;
} }
function useGetTaxonomy({ type }): GetTaxonomyProps { function useGetTaxonomy({ type }): GetTaxonomyProps {
const { data, isLoading }: { data: TermDto[], isLoading: boolean } = api.term.findMany.useQuery({ const { data, isLoading }: { data: TermDto[]; isLoading: boolean } =
where: { api.term.findMany.useQuery({
taxonomy: { where: {
//TaxonomySlug.CATEGORY taxonomy: {
slug: type slug: type,
} },
}, },
include: { include: {
children: true children: true,
}, },
take: 10, // 只取前10个 take: 10, // 只取前10个
orderBy: { orderBy: {},
createdAt: 'desc', // 按创建时间降序排列 });
}, const categories = useMemo(() => {
}) const allCategories = isLoading
const categories = useMemo(() => { ? []
const allCategories = isLoading ? [] : data?.map((course) => course.name); : data?.map((course) => course.name);
return [...Array.from(new Set(allCategories))]; return [...Array.from(new Set(allCategories))];
}, [data]); }, [data]);
return { categories, isLoading } return { categories, isLoading };
} }
// 不同分类跳转 // 不同分类跳转
function useFetchCoursesByCategory(category: string) { function useFetchCoursesByCategory(category: string) {
const isAll = category === '全部'; const isAll = category === "全部";
const { data, isLoading }: { data: CourseDto[], isLoading: boolean } = api.post.findMany.useQuery({ const { data, isLoading }: { data: CourseDto[]; isLoading: boolean } =
where: isAll ? {} : { api.post.findMany.useQuery({
terms: { where: isAll
some: { ? {}
name: category : {
}, terms: {
}, some: {
}, name: category,
take: 8, },
include: { },
terms: true, },
depts:true take: 8,
} include: {
}); terms: true,
depts: true,
},
});
return { data, isLoading }; return { data, isLoading };
} }
const { Title, Text } = Typography; 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 { interface CoursesSectionProps {
title: string; title: string;
description: string; description: string;
courses: Course[]; initialVisibleCoursesCount?: number;
isLoading: boolean
initialVisibleCoursesCount?: number;
} }
const CoursesSection: React.FC<CoursesSectionProps> = ({ const CoursesSection: React.FC<CoursesSectionProps> = ({
title, title,
description, description,
initialVisibleCoursesCount = 8,
}) => { }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [selectedCategory, setSelectedCategory] = useState<string>('全部'); const [selectedCategory, setSelectedCategory] = useState<string>("全部");
const [visibleCourses, setVisibleCourses] = useState(initialVisibleCoursesCount); const gateGory: GetTaxonomyProps = useGetTaxonomy({
const gateGory: GetTaxonomyProps = useGetTaxonomy({ type: TaxonomySlug.CATEGORY,
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 (
<section className="relative py-20 overflow-hidden bg-gradient-to-b from-gray-50 to-white">
<div className="max-w-screen-2xl mx-auto px-6 relative">
<div className="flex justify-between items-end mb-16">
<div>
<Title
level={2}
className="font-bold text-5xl mb-6 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
{title}
</Title>
<Text
type="secondary"
className="text-xl font-light text-gray-600">
{description}
</Text>
</div>
</div>
const { data, isLoading: isDataLoading } = useFetchCoursesByCategory(selectedCategory); <div className="mb-12 flex flex-wrap gap-4">
// useEffect(() => { {gateGory.isLoading ? (
// console.log('data:', data) <Spin className="m-3" />
// }) ) : (
const handleClick = (course: CourseDto) => { <>
navigate(`/course/${course.id}/detail`); <Tag
} color={
selectedCategory === "全部"
? "blue"
: "default"
}
onClick={() => 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"
}`}>
</Tag>
{gateGory.categories.map((category) => (
<Tag
key={category}
color={
selectedCategory === category
? "blue"
: "default"
}
onClick={() => {
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}
</Tag>
))}
</>
)}
</div>
useEffect(() => { <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
console.log('data:', data) {displayedCourses.length === 0 ? (
}) <div className="col-span-full">
<Empty
// const { data: depts, isLoading: isDeptLoading }: { data: CourseDto[], isLoading: boolean } = api.post.findMany.useQuery({ description="暂无课程"
// where: {}, image={Empty.PRESENTED_IMAGE_DEFAULT}
// include: { />
// depts: true, </div>
// }, ) : (
// orderBy: { displayedCourses?.map((course) => (
// createdAt: 'desc' // 按创建时间降序排列 <CourseCard course={course}></CourseCard>
// }, ))
// take: 8 // 只获取前8个课程 )}
// }); </div>
{
<div className="flex items-center gap-4 justify-between mt-12">
<div className="h-[1px] flex-grow bg-gradient-to-r from-transparent via-gray-300 to-transparent"></div>
const filteredCourses = useMemo(() => { <div className="flex justify-end">
return selectedCategory === '全部' <Button
? data type="link"
: data?.filter(c => c.terms.some(t => t.name === selectedCategory)); onClick={() => navigate("/courses")}
}, [selectedCategory, data]); className="flex items-center gap-2 text-gray-600 hover:text-blue-600 font-medium transition-colors duration-300">
const displayedCourses = isDataLoading ? [] : filteredCourses?.slice(0, visibleCourses); <ArrowRightOutlined />
</Button>
return ( </div>
<section className="relative py-20 overflow-hidden bg-gradient-to-b from-gray-50 to-white"> </div>
<div className="max-w-screen-2xl mx-auto px-6 relative"> }
<div className="flex justify-between items-end mb-16"> </div>
<div> </section>
<Title );
level={2}
className="font-bold text-5xl mb-6 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
>
{title}
</Title>
<Text type="secondary" className="text-xl font-light text-gray-600">
{description}
</Text>
</div>
</div>
<div className="mb-12 flex flex-wrap gap-4">
{gateGory.isLoading ? <Spin className='m-3' /> :
(
<>
<Tag
color={selectedCategory === "全部" ? 'blue' : 'default'}
onClick={() => 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'
}`}
></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.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}
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="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>
}
>
<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>
</Title>
<div className="flex items-center mb-4 p-2 rounded-lg transition-all duration-300 hover:bg-blue-50 group">
<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 truncate max-w-[120px]">
{course?.depts[0]?.name}
</Text>
</div>
<span className="text-xs font-medium text-gray-500">
{course?.meta?.views}
</span>
</div>
<div className="pt-4 border-t border-gray-100 text-center">
<Button
type="primary"
size="large"
className="w-full shadow-[0_8px_20px_-6px_rgba(59,130,246,0.5)] hover:shadow-[0_12px_24px_-6px_rgba(59,130,246,0.6)]
transform hover:translate-y-[-2px] transition-all duration-500 ease-out"
>
</Button>
</div>
</div>
</Card>
))}
</div>
{filteredCourses?.length >= visibleCourses && (
<div className='flex items-center gap-4 justify-between mt-12'>
<div className='h-[1px] flex-grow bg-gradient-to-r from-transparent via-gray-300 to-transparent'></div>
<div className="flex justify-end">
<Button
type="link"
onClick={() => navigate('/courses')}
className="flex items-center gap-2 text-gray-600 hover:text-blue-600 font-medium transition-colors duration-300"
>
<ArrowRightOutlined />
</Button>
</div>
</div>
)}
</div>
</section>
);
}; };
export default CoursesSection; export default CoursesSection;

View File

@ -1,163 +1,18 @@
import HeroSection from "./components/HeroSection"; import HeroSection from "./components/HeroSection";
import CategorySection from "./components/CategorySection"; import CategorySection from "./components/CategorySection";
import CoursesSection from "./components/CoursesSection"; 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 ( return (
<div className="min-h-screen"> <div className="min-h-screen">
<HeroSection /> <HeroSection />
<CoursesSection <CoursesSection
title="推荐课程" title="推荐课程"
description="最受欢迎的精品课程,助你快速成长" description="最受欢迎的精品课程,助你快速成长"
courses={data}
isLoading={isLoading}
/> />
{/* {formattedCourses.map((course)=>{
return (
<>
<span>course.title</span>
</>
)
})} */}
{/* <CoursesSection
title="热门课程"
description="最受欢迎的精品课程,助你快速成长"
courses={mockCourses}
/> */}
<CategorySection /> <CategorySection />
{/* <FeaturedTeachersSection /> */}
</div> </div>
); );
}; };