rht02242053

This commit is contained in:
Rao 2025-02-24 20:53:42 +08:00
parent c1fd27061e
commit ce086bc97d
7 changed files with 88 additions and 62 deletions

View File

@ -1,9 +1,10 @@
import { Card, Rate, Tag } from 'antd'; import { Card, Rate, Tag } from 'antd';
import { Course } from '../mockData'; import { Course } from '../mockData';
import { UserOutlined, ClockCircleOutlined } from '@ant-design/icons'; import { UserOutlined, ClockCircleOutlined } from '@ant-design/icons';
import { CourseDto } from '@nice/common';
interface CourseCardProps { interface CourseCardProps {
course: Course; course: CourseDto;
} }
export default function CourseCard({ course }: CourseCardProps) { export default function CourseCard({ course }: CourseCardProps) {
@ -14,7 +15,7 @@ export default function CourseCard({ course }: CourseCardProps) {
cover={ cover={
<img <img
alt={course.title} alt={course.title}
src={course.thumbnail} src={course?.meta?.thumbnail}
className="object-cover w-full h-40" className="object-cover w-full h-40"
/> />
} }
@ -23,7 +24,7 @@ export default function CourseCard({ course }: CourseCardProps) {
<h3 className="text-lg font-semibold line-clamp-2 hover:text-blue-600 transition-colors"> <h3 className="text-lg font-semibold line-clamp-2 hover:text-blue-600 transition-colors">
{course.title} {course.title}
</h3> </h3>
<p className="text-gray-500 text-sm">{course.instructor}</p> <p className="text-gray-500 text-sm">{course.subTitle}</p>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Rate disabled defaultValue={course.rating} className="text-sm" /> <Rate disabled defaultValue={course.rating} className="text-sm" />
<span className="text-gray-500 text-sm">{course.rating}</span> <span className="text-gray-500 text-sm">{course.rating}</span>
@ -31,7 +32,7 @@ export default function CourseCard({ course }: CourseCardProps) {
<div className="flex items-center justify-between text-sm text-gray-500"> <div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<UserOutlined className="text-gray-400" /> <UserOutlined className="text-gray-400" />
<span>{course.enrollments} </span> <span>{course.enrollments?.length} </span>
</div> </div>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
<ClockCircleOutlined className="text-gray-400" /> <ClockCircleOutlined className="text-gray-400" />
@ -39,8 +40,8 @@ export default function CourseCard({ course }: CourseCardProps) {
</div> </div>
</div> </div>
<div className="flex flex-wrap gap-2 pt-2"> <div className="flex flex-wrap gap-2 pt-2">
<Tag color="blue" className="rounded-full px-3">{course.category}</Tag> <Tag color="blue" className="rounded-full px-3">{course.terms[0].name}</Tag>
<Tag color="green" className="rounded-full px-3">{course.level}</Tag> <Tag color="green" className="rounded-full px-3">{course.terms[1].name}</Tag>
</div> </div>
</div> </div>
</Card> </Card>

View File

@ -1,9 +1,9 @@
import { Pagination, Empty } from 'antd'; import { Pagination, Empty } from 'antd';
import { Course } from '../mockData'; import { Course } from '../mockData';
import CourseCard from './CourseCard'; import CourseCard from './CourseCard';
import {CourseDto} from '@nice/common'
interface CourseListProps { interface CourseListProps {
courses: Course[]; courses: CourseDto[];
total: number; total: number;
pageSize: number; pageSize: number;
currentPage: number; currentPage: number;

View File

@ -75,7 +75,7 @@ export default function FilterSection({
: :
( (
<> <>
<Radio value=""></Radio> <Radio value="" ></Radio>
{gateGory.categories.map(category => ( {gateGory.categories.map(category => (
<Radio key={category} value={category}> <Radio key={category} value={category}>
{category} {category}

View File

@ -1,9 +1,11 @@
import { useState, useMemo } from "react"; import { useState, useMemo, useEffect } from "react";
import { mockCourses } from "./mockData"; import { mockCourses } from "./mockData";
import FilterSection from "./components/FilterSection"; import FilterSection from "./components/FilterSection";
import CourseList from "./components/CourseList"; import CourseList from "./components/CourseList";
import { api } from "@nice/client"; import { api } from "@nice/client";
import { 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() { export default function CoursesPage() {
@ -11,43 +13,57 @@ export default function CoursesPage() {
const [selectedCategory, setSelectedCategory] = useState(""); const [selectedCategory, setSelectedCategory] = useState("");
const [selectedLevel, setSelectedLevel] = useState(""); const [selectedLevel, setSelectedLevel] = useState("");
const pageSize = 12; const pageSize = 12;
const { data, isLoading } = api.post.findManyWithPagination.useQuery({ const [isAll,setIsAll] = useState(true)
where: { const [searchParams, setSearchParams] = useSearchParams();
type: PostType.COURSE, let coursesData = []
terms: { let isCourseLoading = false
some: { if(!searchParams.get('searchValue')){
AND: [ console.log('no category')
...(selectedCategory const {data,isLoading} = api.post.findManyWithPagination.useQuery({
? [ where: {
{ type: PostType.COURSE,
name: selectedCategory, terms:isAll?{}:{
}, some: {
] OR : [
: []), selectedCategory?{name:selectedCategory}:{},
...(selectedLevel selectedLevel?{name:selectedLevel}:{}
? [ ],
{ },
name: selectedLevel,
},
]
: []),
],
}, },
}, },
}, select:courseDetailSelect
});
const filteredCourses = useMemo(() => {
return mockCourses.filter((course) => {
const matchCategory =
!selectedCategory || course.category === selectedCategory;
const matchLevel = !selectedLevel || course.level === selectedLevel;
return matchCategory && matchLevel;
}); });
}, [selectedCategory, selectedLevel]); 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' } } } }
]
},
select:courseDetailSelect
})
coursesData = data?.items
isCourseLoading = isLoading
}
useEffect(() => {
console.log(coursesData)
}, [coursesData]);
const filteredCourses = useMemo(() => {
return isCourseLoading ? [] : coursesData;
}, [isCourseLoading, coursesData, selectedCategory, selectedLevel]);
const paginatedCourses = useMemo(() => { const paginatedCourses :CourseDto[]= useMemo(() => {
const startIndex = (currentPage - 1) * pageSize; const startIndex = (currentPage - 1) * pageSize;
return filteredCourses.slice(startIndex, startIndex + pageSize); return isCourseLoading ? [] : (filteredCourses.slice(startIndex, startIndex + pageSize) as any as CourseDto[]);
}, [filteredCourses, currentPage]); }, [filteredCourses, currentPage]);
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
@ -55,6 +71,8 @@ export default function CoursesPage() {
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
}; };
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
@ -69,10 +87,14 @@ export default function CoursesPage() {
console.log(category); console.log(category);
setSelectedCategory(category); setSelectedCategory(category);
setCurrentPage(1); setCurrentPage(1);
setIsAll(!category)
setSearchParams({ searchValue: ''});
}} }}
onLevelChange={(level) => { onLevelChange={(level) => {
setSelectedLevel(level); setSelectedLevel(level);
setCurrentPage(1); setCurrentPage(1);
setIsAll(!level)
setSearchParams({ searchValue: ''});
}} }}
/> />
</div> </div>

View File

@ -35,6 +35,10 @@ export function MainHeader() {
className="w-72 rounded-full" className="w-72 rounded-full"
value={searchValue} value={searchValue}
onChange={(e) => setSearchValue(e.target.value)} onChange={(e) => setSearchValue(e.target.value)}
onPressEnter={()=>{
//console.log(searchValue)
navigate(`/courses/?searchValue=${searchValue}`)
}}
/> />
</div> </div>
{isAuthenticated && ( {isAuthenticated && (

View File

@ -77,5 +77,5 @@ export type Course = Post & {
export type CourseDto = Course & { export type CourseDto = Course & {
enrollments?: Enrollment[]; enrollments?: Enrollment[];
sections?: SectionDto[]; sections?: SectionDto[];
terms: Term[]; terms: TermDto[];
}; };

View File

@ -70,28 +70,27 @@ export const courseDetailSelect: Prisma.PostSelect = {
title: true, title: true,
subTitle: true, subTitle: true,
content: true, content: true,
level: true,
// requirements: true,
// objectives: true,
// skills: true,
// audiences: true,
// totalDuration: true,
// totalLectures: true,
// averageRating: true,
// numberOfReviews: true,
// numberOfStudents: true,
// completionRate: true,
state: true,
// isFeatured: true, // isFeatured: true,
createdAt: true, createdAt: true,
publishedAt: true, updatedAt: true,
// 关联表选择 // 关联表选择
children: { terms:{
include: { select:{
children: true, id:true,
name:true,
taxonomy:{
select:{
id:true,
slug:true
}
}
}
},
enrollments: {
select: {
id: true,
}, },
}, },
enrollments: true,
meta: true, meta: true,
rating: true,
}; };