rhtwork
This commit is contained in:
parent
c031e4f1c1
commit
074ca5ca22
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue