This commit is contained in:
Rao 2025-02-26 20:00:17 +08:00
commit aac476cfdd
9 changed files with 174 additions and 123 deletions

View File

@ -64,9 +64,50 @@ export async function updateTotalCourseViewCount(type: VisitType) {
export async function updatePostViewCount(id: string, type: VisitType) { export async function updatePostViewCount(id: string, type: VisitType) {
const post = await db.post.findFirst({ const post = await db.post.findFirst({
where: { id }, where: { id },
select: { id: true, meta: true }, select: { id: true, meta: true, type: true },
}); });
const metaFieldMap = {
[VisitType.READED]: 'views',
[VisitType.LIKE]: 'likes',
[VisitType.HATE]: 'hates',
};
if (post?.type === PostType.LECTURE) {
const course = await db.postAncestry.findFirst({
where: {
descendantId: post?.id,
ancestor: {
type: PostType.COURSE,
},
},
select: { id: true },
});
const lectures = await db.postAncestry.findMany({
where: { ancestorId: course.id, descendant: { type: PostType.LECTURE } },
select: {
id: true,
},
});
const courseViews = await db.visit.aggregate({
_sum: {
views: true,
},
where: {
postId: {
in: [course.id, ...lectures.map((lecture) => lecture.id)],
},
type: type,
},
});
await db.post.update({
where: { id: course.id },
data: {
meta: {
...((post?.meta as any) || {}),
[metaFieldMap[type]]: courseViews._sum.views || 0,
},
},
});
}
const totalViews = await db.visit.aggregate({ const totalViews = await db.visit.aggregate({
_sum: { _sum: {
views: true, views: true,
@ -76,42 +117,13 @@ export async function updatePostViewCount(id: string, type: VisitType) {
type: type, type: type,
}, },
}); });
if (type === VisitType.READED) { await db.post.update({
await db.post.update({ where: { id },
where: { data: {
id: id, meta: {
...((post?.meta as any) || {}),
[metaFieldMap[type]]: totalViews._sum.views || 0,
}, },
data: { },
meta: { });
...((post?.meta as any) || {}),
views: totalViews._sum.views || 0,
}, // Use 0 if no visits exist
},
});
console.log('readed');
} else if (type === VisitType.LIKE) {
await db.post.update({
where: {
id: id,
},
data: {
meta: {
...((post?.meta as any) || {}),
likes: totalViews._sum.views || 0, // Use 0 if no visits exist
},
},
});
} else if (type === VisitType.HATE) {
await db.post.update({
where: {
id: id,
},
data: {
meta: {
...((post?.meta as any) || {}),
hates: totalViews._sum.views || 0, // Use 0 if no visits exist
},
},
});
}
} }

View File

@ -1,7 +1,6 @@
import { Card, Rate, Tag, Typography, Button } from "antd"; import { Card, Tag, Typography, Button } from "antd";
import { import {
UserOutlined, EyeOutlined,
ClockCircleOutlined,
PlayCircleOutlined, PlayCircleOutlined,
TeamOutlined, TeamOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
@ -16,9 +15,8 @@ export default function CourseCard({ course }: CourseCardProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const handleClick = (course: CourseDto) => { const handleClick = (course: CourseDto) => {
navigate(`/course/${course.id}/detail`); navigate(`/course/${course.id}/detail`);
window.scrollTo({top: 0,behavior: "smooth",}) window.scrollTo({ top: 0, behavior: "smooth", })
}; };
return ( return (
<Card <Card
onClick={() => handleClick(course)} onClick={() => handleClick(course)}
@ -38,32 +36,35 @@ export default function CourseCard({ course }: CourseCardProps) {
<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" /> <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>
}> }>
<div className="px-4"> <div className="px-4 ">
<div className="flex gap-2 mb-4"> <div className="overflow-hidden hover:overflow-auto">
{course?.terms?.map((term) => { <div className="flex gap-2 mb-4 whiteSpace-nowrap">
return ( {course?.terms?.map((term) => {
<Tag return (
key={term.id} <>
// color={term.taxonomy.slug===TaxonomySlug.CATEGORY? "blue" : "green"} <Tag
color={ key={term.id}
term?.taxonomy?.slug === color={
TaxonomySlug.CATEGORY term?.taxonomy?.slug ===
? "blue" TaxonomySlug.CATEGORY
: term?.taxonomy?.slug === ? "blue"
TaxonomySlug.LEVEL : term?.taxonomy?.slug ===
? "green" TaxonomySlug.LEVEL
: "orange" ? "green"
} : "orange"
className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0"> }
{term.name} className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0 ">
</Tag> {term.name}
); </Tag>
})} </>
);
})}
</div>
</div> </div>
<Title <Title
level={4} 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"> className="mb-4 mt-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> <button> {course.title}</button>
</Title> </Title>
@ -78,8 +79,8 @@ export default function CourseCard({ course }: CourseCardProps) {
{/* {course?.depts?.map((dept)=>{return dept.name})} */} {/* {course?.depts?.map((dept)=>{return dept.name})} */}
</Text> </Text>
</div> </div>
<span className="text-xs font-medium text-gray-500"> <span className="text-xs font-medium text-gray-500 flex items-center">
{course?.meta?.views <EyeOutlined className="mr-1" />{course?.meta?.views
? `观看次数 ${course?.meta?.views}` ? `观看次数 ${course?.meta?.views}`
: null} : null}
</span> </span>

View File

@ -44,10 +44,9 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
type: TaxonomySlug.CATEGORY, type: TaxonomySlug.CATEGORY,
}); });
return ( return (
<section className="relative py-16 overflow-hidden bg-gray-200"> <section className="relative py-16 overflow-hidden ">
<div className="absolute inset-0 bg-white max-w-screen-2xl mx-auto px-6"></div> <div className="max-w-screen-2xl mx-auto px-4 relative">
<div className="max-w-screen-2xl mx-auto px-6 relative"> <div className="flex justify-between items-end mb-12 ">
<div className="flex justify-between items-end mb-16 ">
<div> <div>
<Title <Title
level={2} level={2}

View File

@ -1,4 +1,10 @@
import React, { useRef, useCallback, useEffect, useMemo, useState } from "react"; import React, {
useRef,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { Carousel, Typography } from "antd"; import { Carousel, Typography } from "antd";
import { import {
TeamOutlined, TeamOutlined,
@ -30,13 +36,29 @@ interface PlatformStat {
const HeroSection = () => { const HeroSection = () => {
const carouselRef = useRef<CarouselRef>(null); const carouselRef = useRef<CarouselRef>(null);
const { statistics, slides } = useAppConfig(); const { statistics, slides } = useAppConfig();
const [countStatistics, setCountStatistics] = useState<number>(4) const [countStatistics, setCountStatistics] = useState<number>(4);
const platformStats: PlatformStat[] = useMemo(() => { const platformStats: PlatformStat[] = useMemo(() => {
return [ return [
{ icon: <TeamOutlined />, value: statistics.staffs, label: "注册学员" }, {
{ icon: <StarOutlined />, value: statistics.courses, label: "精品课程" }, icon: <TeamOutlined />,
{ icon: <BookOutlined />, value: statistics.lectures, label: '课程章节' }, value: statistics.staffs,
{ icon: <EyeOutlined />, value: statistics.reads, label: "观看次数" }, label: "注册学员",
},
{
icon: <StarOutlined />,
value: statistics.courses,
label: "精品课程",
},
{
icon: <BookOutlined />,
value: statistics.lectures,
label: "课程章节",
},
{
icon: <EyeOutlined />,
value: statistics.reads,
label: "观看次数",
},
]; ];
}, [statistics]); }, [statistics]);
const handlePrev = useCallback(() => { const handlePrev = useCallback(() => {
@ -48,7 +70,7 @@ const HeroSection = () => {
}, []); }, []);
const countNonZeroValues = (statistics: Record<string, number>): number => { const countNonZeroValues = (statistics: Record<string, number>): number => {
return Object.values(statistics).filter(value => value !== 0).length; return Object.values(statistics).filter((value) => value !== 0).length;
}; };
useEffect(() => { useEffect(() => {
@ -67,8 +89,8 @@ const HeroSection = () => {
dots={{ dots={{
className: "carousel-dots !bottom-32 !z-20", className: "carousel-dots !bottom-32 !z-20",
}}> }}>
{Array.isArray(slides) ? {Array.isArray(slides) ? (
(slides.map((item, index) => ( slides.map((item, index) => (
<div key={index} className="relative h-[600px]"> <div key={index} className="relative h-[600px]">
<div <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]" 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]"
@ -87,9 +109,9 @@ const HeroSection = () => {
<div className="relative h-full max-w-7xl mx-auto px-6 lg:px-8"></div> <div className="relative h-full max-w-7xl mx-auto px-6 lg:px-8"></div>
</div> </div>
)) ))
) : ( ) : (
<div></div> <div></div>
)} )}
</Carousel> </Carousel>
{/* Navigation Buttons */} {/* Navigation Buttons */}
@ -108,31 +130,30 @@ const HeroSection = () => {
</div> </div>
{/* Stats Container */} {/* Stats Container */}
{ {countStatistics > 1 && (
countStatistics > 1 && ( <div className="absolute -bottom-20 left-1/2 -translate-x-1/2 w-3/5 max-w-6xl px-4">
<div className="absolute -bottom-20 left-1/2 -translate-x-1/2 w-3/5 max-w-6xl px-4"> <div
<div className={`rounded-2xl grid grid-cols-${countStatistics} lg:grid-cols-${countStatistics} md:grid-cols-${countStatistics} 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]`}> className={`rounded-2xl grid grid-cols-${countStatistics} lg:grid-cols-${countStatistics} md:grid-cols-${countStatistics} 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) => {
return stat.value return stat.value ? (
? (<div <div
key={index} key={index}
className="text-center transform hover:-translate-y-1 hover:scale-105 transition-transform duration-300 ease-out"> className="text-center transform hover:-translate-y-1 hover:scale-105 transition-transform duration-300 ease-out">
<div className="inline-flex items-center justify-center w-16 h-16 mb-4 rounded-full bg-primary-50 text-primary-600 text-3xl transition-colors duration-300 group-hover:text-primary-700"> <div className="inline-flex items-center justify-center w-16 h-16 mb-4 rounded-full bg-primary-50 text-primary-600 text-3xl transition-colors duration-300 group-hover:text-primary-700">
{stat.icon} {stat.icon}
</div>
<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}
</div>
<div className="text-gray-600 font-medium">
{stat.label}
</div>
</div> </div>
) : null <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}
</div> </div>
<div className="text-gray-600 font-medium">
{stat.label}
</div>
</div>
) : null;
})}
</div> </div>
) </div>
} )}
</section> </section>
); );
}; };

View File

@ -9,9 +9,9 @@ const { Content } = Layout;
export function MainLayout() { export function MainLayout() {
return ( return (
<MainProvider> <MainProvider>
<Layout className="min-h-screen bg-gray-100"> <Layout className="min-h-screen">
<MainHeader /> <MainHeader />
<Content className="mt-16 bg-gray-200 "> <Content className="mt-16 bg-gray-50">
<Outlet /> <Outlet />
</Content> </Content>
<MainFooter /> <MainFooter />

View File

@ -86,6 +86,20 @@ export function UserMenu() {
setModalOpen(true); setModalOpen(true);
}, },
}, },
{
icon: <UserOutlined className="text-lg" />,
label: "我创建的课程",
action: () => {
setModalOpen(true);
},
},
{
icon: <UserOutlined className="text-lg" />,
label: "我学习的课程",
action: () => {
setModalOpen(true);
},
},
canManageAnyStaff && { canManageAnyStaff && {
icon: <SettingOutlined className="text-lg" />, icon: <SettingOutlined className="text-lg" />,
label: "设置", label: "设置",
@ -222,18 +236,20 @@ export function UserMenu() {
focus:ring-2 focus:ring-[#00538E]/20 focus:ring-2 focus:ring-[#00538E]/20
group relative overflow-hidden group relative overflow-hidden
active:scale-[0.99] active:scale-[0.99]
${item.label === "注销" ${
item.label === "注销"
? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700" ? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700"
: "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]" : "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]"
}`}> }`}>
<span <span
className={`w-5 h-5 flex items-center justify-center className={`w-5 h-5 flex items-center justify-center
transition-all duration-200 ease-in-out transition-all duration-200 ease-in-out
group-hover:scale-110 group-hover:rotate-6 group-hover:scale-110 group-hover:rotate-6
group-hover:translate-x-0.5 ${item.label === "注销" group-hover:translate-x-0.5 ${
? "group-hover:text-red-600" item.label === "注销"
: "group-hover:text-[#003F6A]" ? "group-hover:text-red-600"
}`}> : "group-hover:text-[#003F6A]"
}`}>
{item.icon} {item.icon}
</span> </span>
<span>{item.label}</span> <span>{item.label}</span>

View File

@ -0,0 +1,7 @@
export default function MyDutyPage() {
return <>
</>
}

View File

@ -0,0 +1,3 @@
export default function MyLearningPage() {
return <></>;
}

View File

@ -288,14 +288,6 @@ model Visit {
message Message? @relation(fields: [messageId], references: [id]) message Message? @relation(fields: [messageId], references: [id])
messageId String? @map("message_id") messageId String? @map("message_id")
lectureId String? @map("lecture_id") // 课时ID lectureId String? @map("lecture_id") // 课时ID
// 学习数据
// progress Float? @default(0) @map("progress") // 完成进度(0-100%)
// isCompleted Boolean? @default(false) @map("is_completed") // 是否完成
// lastPosition Int? @default(0) @map("last_position") // 视频播放位置(秒)
// totalWatchTime Int? @default(0) @map("total_watch_time") // 总观看时长(秒)
// // 时间记录
// lastWatchedAt DateTime? @map("last_watched_at") // 最后观看时间
createdAt DateTime @default(now()) @map("created_at") // 创建时间 createdAt DateTime @default(now()) @map("created_at") // 创建时间
updatedAt DateTime @updatedAt @map("updated_at") // 更新时间 updatedAt DateTime @updatedAt @map("updated_at") // 更新时间
deletedAt DateTime? @map("deleted_at") // 删除时间,可为空 deletedAt DateTime? @map("deleted_at") // 删除时间,可为空