This commit is contained in:
Rao 2025-02-24 11:50:56 +08:00
parent c031e4f1c1
commit 074ca5ca22
9 changed files with 170 additions and 124 deletions

View File

@ -15,7 +15,7 @@ async function bootstrap() {
const trpc = app.get(TrpcRouter); const trpc = app.get(TrpcRouter);
trpc.applyMiddleware(app); trpc.applyMiddleware(app);
const port = process.env.SERVER_PORT || 3001; const port = process.env.SERVER_PORT || 3000;
await app.listen(port); await app.listen(port);
} }

View File

@ -19,6 +19,7 @@ import { setCourseInfo, setPostRelation } from './utils';
import EventBus, { CrudOperation } from '@server/utils/event-bus'; import EventBus, { CrudOperation } from '@server/utils/event-bus';
import { BaseTreeService } from '../base/base.tree.service'; import { BaseTreeService } from '../base/base.tree.service';
import { z } from 'zod'; import { z } from 'zod';
import { DefaultArgs } from '@prisma/client/runtime/library';
@Injectable() @Injectable()
export class PostService extends BaseTreeService<Prisma.PostDelegate> { export class PostService extends BaseTreeService<Prisma.PostDelegate> {
@ -215,7 +216,15 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
return { ...result, items }; return { ...result, items };
}); });
} }
async findManyWithPagination(args:
{ page?: number;
pageSize?: number;
where?: Prisma.PostWhereInput;
select?: Prisma.PostSelect<DefaultArgs>;
}): Promise<{ items: { id: string; type: string | null; level: string | null; state: string | null; title: string | null; subTitle: string | null; content: string | null; important: boolean | null; domainId: string | null; order: number | null; duration: number | null; rating: number | null; createdAt: Date; publishedAt: Date | null; updatedAt: Date; deletedAt: Date | null; authorId: string | null; parentId: string | null; hasChildren: boolean | null; meta: Prisma.JsonValue | null; }[]; totalPages: number; }>
{
return super.findManyWithPagination(args);
}
protected async setPerms(data: Post, staff?: UserProfile) { protected async setPerms(data: Post, staff?: UserProfile) {
if (!staff) return; if (!staff) return;
const perms: ResPerm = { const perms: ResPerm = {

View File

@ -2,8 +2,9 @@ import { Checkbox, Divider, Radio, Space , Spin} from 'antd';
import { categories, levels } from '../mockData'; import { categories, levels } from '../mockData';
import { TaxonomySlug, TermDto } from '@nice/common'; import { TaxonomySlug, TermDto } from '@nice/common';
import { useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { api } from '@nice/client'; import { api } from '@nice/client';
import { useSearchParams } from 'react-router-dom';
interface FilterSectionProps { interface FilterSectionProps {
selectedCategory: string; selectedCategory: string;
@ -53,6 +54,12 @@ export default function FilterSection({
const levels : GetTaxonomyProps = useGetTaxonomy({ const levels : GetTaxonomyProps = useGetTaxonomy({
type: TaxonomySlug.LEVEL, type: TaxonomySlug.LEVEL,
}) })
const [searchParams,setSearchParams] = useSearchParams()
useEffect(() => {
if(searchParams.get('category')) onCategoryChange(searchParams.get('category'))
},[searchParams.get('category')])
return ( return (
<div className="bg-white p-6 rounded-lg shadow-sm space-y-6"> <div className="bg-white p-6 rounded-lg shadow-sm space-y-6">
<div> <div>

View File

@ -66,6 +66,7 @@ export default function CoursesPage() {
selectedCategory={selectedCategory} selectedCategory={selectedCategory}
selectedLevel={selectedLevel} selectedLevel={selectedLevel}
onCategoryChange={(category) => { onCategoryChange={(category) => {
console.log(category);
setSelectedCategory(category); setSelectedCategory(category);
setCurrentPage(1); setCurrentPage(1);
}} }}

View File

@ -1,7 +1,9 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react'; import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { Typography, Button } from 'antd'; import { Typography, Button, Spin } from 'antd';
import { stringToColor, TaxonomySlug, TermDto } from '@nice/common'; import { stringToColor, TaxonomySlug, TermDto } from '@nice/common';
import { api } from '@nice/client'; import { api,} from '@nice/client';
import { ControlOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -11,45 +13,45 @@ interface CourseCategory {
description: string; description: string;
} }
const courseCategories: CourseCategory[] = [ // const courseCategories: CourseCategory[] = [
{ // {
name: '计算机基础', // name: '计算机基础',
count: 120, // count: 120,
description: '计算机组成原理、操作系统、网络等基础知识' // description: '计算机组成原理、操作系统、网络等基础知识'
}, // },
{ // {
name: '编程语言', // name: '编程语言',
count: 85, // count: 85,
description: 'Python、Java、JavaScript等主流编程语言' // description: 'Python、Java、JavaScript等主流编程语言'
}, // },
{ // {
name: '人工智能', // name: '人工智能',
count: 65, // count: 65,
description: '机器学习、深度学习、自然语言处理等前沿技术' // description: '机器学习、深度学习、自然语言处理等前沿技术'
}, // },
{ // {
name: '数据科学', // name: '数据科学',
count: 45, // count: 45,
description: '数据分析、数据可视化、商业智能等' // description: '数据分析、数据可视化、商业智能等'
}, // },
{ // {
name: '云计算', // name: '云计算',
count: 38, // count: 38,
description: '云服务、容器化、微服务架构等' // description: '云服务、容器化、微服务架构等'
}, // },
{ // {
name: '网络安全', // name: '网络安全',
count: 42, // count: 42,
description: '网络安全基础、渗透测试、安全防护等' // description: '网络安全基础、渗透测试、安全防护等'
} // }
]; // ];
const CategorySection = () => { const CategorySection = () => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [showAll, setShowAll] = useState(false); const [showAll, setShowAll] = useState(false);
/** //获得分类
* const {data,isLoading} :{data:TermDto[],isLoading:boolean}= api.term.findMany.useQuery({ const {data:courseCategoriesData,isLoading} :{data:TermDto[],isLoading:boolean}= api.term.findMany.useQuery({
where:{ where:{
taxonomy: { taxonomy: {
slug:TaxonomySlug.CATEGORY slug:TaxonomySlug.CATEGORY
@ -57,16 +59,32 @@ const CategorySection = () => {
}, },
include:{ include:{
children :true children :true
} },
orderBy: {
createdAt: 'desc', // 按创建时间降序排列
},
take:10
}) })
const courseCategories: CourseCategory[] = useMemo(() => { // 分类展示
return data?.map((term) => ({ const [displayedCategories,setDisplayedCategories] = useState<TermDto[]>([])
name: term.name, useEffect(() => {
count: term.hasChildren ? term.children.length : 0, console.log(courseCategoriesData);
description: term.description if(!isLoading){
})) || []; if(showAll){
},[data]) setDisplayedCategories(courseCategoriesData)
*/ }else{
setDisplayedCategories(courseCategoriesData.slice(0,8))
}
}
}, [courseCategoriesData,showAll]);
// const courseCategories: CourseCategory[] = useMemo(() => {
// return data?.map((term) => ({
// name: term.name,
// count: term.hasChildren ? term.children.length : 0,
// description: term.description
// })) || [];
// },[data])
const handleMouseEnter = useCallback((index: number) => { const handleMouseEnter = useCallback((index: number) => {
@ -77,9 +95,7 @@ const CategorySection = () => {
setHoveredIndex(null); setHoveredIndex(null);
}, []); }, []);
const displayedCategories = showAll const navigate = useNavigate()
? courseCategories
: courseCategories.slice(0, 8);
return ( return (
<section className="py-32 relative overflow-hidden"> <section className="py-32 relative overflow-hidden">
@ -93,19 +109,25 @@ const CategorySection = () => {
</Text> </Text>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{displayedCategories.map((category, index) => { {
isLoading ? <Spin></Spin> :
(displayedCategories.map((category, index) => {
const categoryColor = stringToColor(category.name); const categoryColor = stringToColor(category.name);
const isHovered = hoveredIndex === index; const isHovered = hoveredIndex === index;
return ( return (
<div <div
key={index} key={index}
className="group relative rounded-2xl transition-all duration-700 ease-out cursor-pointer will-change-transform hover:-translate-y-2" className="group relative min-h-[130px] rounded-2xl transition-all duration-700 ease-out cursor-pointer will-change-transform hover:-translate-y-2"
onMouseEnter={() => handleMouseEnter(index)} onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label={`查看${category.name}课程类别`} aria-label={`查看${category.name}课程类别`}
onClick={()=>{
console.log(category.name)
navigate(`/courses?category=${category.name}`)
}}
> >
<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" /> <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" />
<div <div
@ -120,16 +142,16 @@ const CategorySection = () => {
/> />
<div <div
className={`absolute top-0 left-1/2 -translate-x-1/2 h-1 rounded-full transition-all duration-500 ease-out ${ className={`absolute top-0 left-1/2 -translate-x-1/2 h-1 rounded-full transition-all duration-500 ease-out ${
isHovered ? 'w-36 opacity-90' : 'w-24 opacity-60' false ? 'w-36 opacity-90' : 'w-24 opacity-60'
}`} }`}
style={{ backgroundColor: categoryColor }} style={{ backgroundColor: categoryColor }}
/> />
<div className="relative p-6"> <div className="relative w-full h-full p-6">
<div className="flex flex-col space-y-4 mb-4"> <div className="flex w-2/3 absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 flex-col space-y-4 mb-4">
<Text strong className="text-xl font-semibold tracking-tight"> <Text strong className="text-xl font-medium tracking-wide">
{category.name} {category.name}
</Text> </Text>
<span {/* <span
className={`px-3 py-1 rounded-full text-sm w-fit font-medium transition-all duration-500 ease-out ${ className={`px-3 py-1 rounded-full text-sm w-fit font-medium transition-all duration-500 ease-out ${
isHovered ? 'shadow-md scale-105' : '' isHovered ? 'shadow-md scale-105' : ''
}`} }`}
@ -138,22 +160,22 @@ const CategorySection = () => {
color: categoryColor color: categoryColor
}} }}
> >
{category.count} {category.children.length}
</span> </span> */}
</div> </div>
<Text type="secondary" className="block text-sm leading-relaxed opacity-90"> <Text type="secondary" className="block text-sm leading-relaxed opacity-90">
{category.description} {category.description}
</Text> </Text>
<div <div
className={`mt-6 text-sm font-medium flex items-center space-x-2 transition-all duration-500 ease-out ${ className={` mt-6 absolute bottom-4 right-6 text-sm font-medium flex items-center space-x-2 transition-all duration-500 ease-out ${
isHovered ? 'translate-x-2' : '' false ? 'translate-x-2' : ''
}`} }`}
style={{ color: categoryColor }} style={{ color: categoryColor }}
> >
<span></span> <span></span>
<span <span
className={`transform transition-all duration-500 ease-out ${ className={`transform transition-all duration-500 ease-out ${
isHovered ? 'translate-x-2' : '' false ? 'translate-x-2' : ''
}`} }`}
> >
@ -162,9 +184,11 @@ const CategorySection = () => {
</div> </div>
</div> </div>
); );
})} }))
}
</div> </div>
{courseCategories.length > 8 && ( {!isLoading && courseCategoriesData.length > 8 && (
<div className="flex justify-center mt-12"> <div className="flex justify-center mt-12">
<Button <Button
type="default" type="default"

View File

@ -6,7 +6,8 @@ import {
StarOutlined, StarOutlined,
ClockCircleOutlined, ClockCircleOutlined,
LeftOutlined, LeftOutlined,
RightOutlined RightOutlined,
EyeOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { CarouselRef } from 'antd/es/carousel'; import type { CarouselRef } from 'antd/es/carousel';
@ -46,8 +47,8 @@ const carouselItems: CarouselItem[] = [
const platformStats: PlatformStat[] = [ const platformStats: PlatformStat[] = [
{ icon: <TeamOutlined />, value: '50,000+', label: '注册学员' }, { icon: <TeamOutlined />, value: '50,000+', label: '注册学员' },
{ icon: <BookOutlined />, value: '1,000+', label: '精品课程' }, { icon: <BookOutlined />, value: '1,000+', label: '精品课程' },
{ icon: <StarOutlined />, value: '98%', label: '好评度' }, // { icon: <StarOutlined />, value: '98%', label: '好评度' },
{ icon: <ClockCircleOutlined />, value: '100万+', label: '学习时长' } { icon: <EyeOutlined />, value: '100万+', label: '观看次数' }
]; ];
const HeroSection = () => { const HeroSection = () => {
@ -132,8 +133,8 @@ const HeroSection = () => {
</div> </div>
{/* Stats Container */} {/* Stats Container */}
<div className="absolute -bottom-24 left-1/2 -translate-x-1/2 w-full max-w-6xl px-4"> <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-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]"> <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]">
{platformStats.map((stat, index) => ( {platformStats.map((stat, index) => (
<div <div
key={index} key={index}
@ -145,7 +146,9 @@ const HeroSection = () => {
<div className="text-2xl font-bold bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent mb-1.5"> <div className="text-2xl font-bold bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent mb-1.5">
{stat.value} {stat.value}
</div> </div>
<div className="text-gray-600 font-medium">{stat.label}</div> <div className="text-gray-600 font-medium">
{stat.label}
</div>
</div> </div>
))} ))}
</div> </div>

View File

@ -2,6 +2,8 @@ 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 FeaturedTeachersSection from './components/FeaturedTeachersSection';
import { api } from '@nice/client';
import { useEffect } from 'react';
const HomePage = () => { const HomePage = () => {
const mockCourses = [ const mockCourses = [
{ {

View File

@ -6,8 +6,8 @@ export function useLocalSettings() {
return `${protocol}://${env.SERVER_IP}:${port}`; return `${protocol}://${env.SERVER_IP}:${port}`;
}, []); }, []);
const tusUrl = useMemo(() => getBaseUrl('http', 8080), [getBaseUrl]); const tusUrl = useMemo(() => getBaseUrl('http', 8080), [getBaseUrl]);
const apiUrl = useMemo(() => getBaseUrl('http', 3000), [getBaseUrl]); const apiUrl = useMemo(() => getBaseUrl('http', parseInt(env.SERVER_PORT)), [getBaseUrl]);
const websocketUrl = useMemo(() => getBaseUrl('ws', 3000), [getBaseUrl]); const websocketUrl = useMemo(() => getBaseUrl('ws', parseInt(env.SERVER_PORT)), [getBaseUrl]);
const checkIsTusUrl = useCallback((url: string) => { const checkIsTusUrl = useCallback((url: string) => {
return url.startsWith(tusUrl) return url.startsWith(tusUrl)
}, [tusUrl]) }, [tusUrl])

View File

@ -1,6 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import { env } from '../env'; import { env } from '../env';
const BASE_URL = `http://${env.SERVER_IP}:3000` const BASE_URL = `http://${env.SERVER_IP}:${env?.SERVER_PORT}`
const apiClient = axios.create({ const apiClient = axios.create({
baseURL: BASE_URL, baseURL: BASE_URL,
// withCredentials: true, // withCredentials: true,