diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 386668a..8b82ed1 100755 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -15,7 +15,7 @@ async function bootstrap() { const trpc = app.get(TrpcRouter); trpc.applyMiddleware(app); - const port = process.env.SERVER_PORT || 3001; + const port = process.env.SERVER_PORT || 3000; await app.listen(port); } diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index df99714..30d56e8 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -19,6 +19,7 @@ import { setCourseInfo, setPostRelation } from './utils'; import EventBus, { CrudOperation } from '@server/utils/event-bus'; import { BaseTreeService } from '../base/base.tree.service'; import { z } from 'zod'; +import { DefaultArgs } from '@prisma/client/runtime/library'; @Injectable() export class PostService extends BaseTreeService { @@ -48,7 +49,7 @@ export class PostService extends BaseTreeService { meta: { type: type, }, - }, + } as any, }, { tx }, ); @@ -70,7 +71,7 @@ export class PostService extends BaseTreeService { parentId: courseId, title: title, authorId: staff?.id, - }, + } as any, }, { tx }, ); @@ -215,7 +216,38 @@ export class PostService extends BaseTreeService { return { ...result, items }; }); } - + async findManyWithPagination(args: { + page?: number; + pageSize?: number; + where?: Prisma.PostWhereInput; + select?: Prisma.PostSelect; + }): 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) { if (!staff) return; const perms: ResPerm = { diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 20bbe59..c257663 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -137,6 +137,11 @@ export async function setCourseInfo({ data }: { data: Post }) { id: true, descendant: true, }, + orderBy: { + descendant: { + order: 'asc', + }, + }, }); const descendants = ancestries.map((ancestry) => ancestry.descendant); const sections: SectionDto[] = descendants diff --git a/apps/server/src/queue/models/post/post.queue.service.ts b/apps/server/src/queue/models/post/post.queue.service.ts index f7370df..452076c 100644 --- a/apps/server/src/queue/models/post/post.queue.service.ts +++ b/apps/server/src/queue/models/post/post.queue.service.ts @@ -37,4 +37,5 @@ export class PostQueueService implements OnModuleInit { debounce: { id: `${QueueJobType.UPDATE_POST_STATE}_${data.id}` }, }); } + } diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 8546c66..98eb43c 100644 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -1,5 +1,9 @@ import { db, VisitType } from '@nice/common'; export async function updatePostViewCount(id: string, type: VisitType) { + const post = await db.post.findFirst({ + where: { id }, + select: { id: true, meta: true }, + }); const totalViews = await db.visit.aggregate({ _sum: { views: true, @@ -16,10 +20,12 @@ export async function updatePostViewCount(id: string, type: VisitType) { }, 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: { @@ -27,6 +33,7 @@ export async function updatePostViewCount(id: string, type: VisitType) { }, data: { meta: { + ...((post?.meta as any) || {}), likes: totalViews._sum.views || 0, // Use 0 if no visits exist }, }, @@ -38,6 +45,7 @@ export async function updatePostViewCount(id: string, type: VisitType) { }, data: { meta: { + ...((post?.meta as any) || {}), hates: totalViews._sum.views || 0, // Use 0 if no visits exist }, }, diff --git a/apps/server/src/queue/worker/processor.ts b/apps/server/src/queue/worker/processor.ts index 7273ecf..a968179 100755 --- a/apps/server/src/queue/worker/processor.ts +++ b/apps/server/src/queue/worker/processor.ts @@ -11,6 +11,7 @@ import { updateCourseReviewStats, updateParentLectureStats, } from '@server/models/post/utils'; +import { updatePostViewCount } from '../models/post/utils'; const logger = new Logger('QueueWorker'); export default async function processJob(job: Job) { try { @@ -44,6 +45,12 @@ export default async function processJob(job: Job) { `Updated course stats for courseId: ${courseId}, type: ${type}`, ); } + if (job.name === QueueJobType.UPDATE_POST_VISIT_COUNT) { + await updatePostViewCount(job.data.id, job.data.type); + } + if (job.name === QueueJobType.UPDATE_POST_STATE) { + await updatePostViewCount(job.data.id, job.data.type); + } } catch (error: any) { logger.error( `Error processing stats update job: ${error.message}`, diff --git a/apps/web/src/app/main/course/preview/components/coursePreviewAllmsg.tsx b/apps/web/src/app/main/course/preview/components/coursePreviewAllmsg.tsx index f56466d..73ed1f9 100644 --- a/apps/web/src/app/main/course/preview/components/coursePreviewAllmsg.tsx +++ b/apps/web/src/app/main/course/preview/components/coursePreviewAllmsg.tsx @@ -1,51 +1,73 @@ -import { useEffect } from 'react'; -import { CoursePreviewMsg } from '@web/src/app/main/course/preview/type.ts'; -import { Button , Tabs , Image, Skeleton } from 'antd'; -import type { TabsProps } from 'antd'; +import { useEffect } from "react"; +import { CoursePreviewMsg } from "@web/src/app/main/course/preview/type.ts"; +import { Button, Tabs, Image, Skeleton } from "antd"; +import type { TabsProps } from "antd"; import { PlayCircleOutlined } from "@ant-design/icons"; -export function CoursePreviewAllmsg({previewMsg,items,isLoading}: {previewMsg?:CoursePreviewMsg,items:TabsProps['items'],isLoading:Boolean}){ - useEffect(() => { - console.log(previewMsg) - }) - const TapOnChange = (key: string) => { - console.log(key); - }; - return ( -
-
-
- example -
- -
- -
-
- { - isLoading ? - :( - <> - {previewMsg.Title} - {previewMsg.SubTitle} - {previewMsg.Description} - - ) - } - - -
-
-
- -
-
- ) -} \ No newline at end of file +export function CoursePreviewAllmsg({ + previewMsg, + items, + isLoading, +}: { + previewMsg?: CoursePreviewMsg; + items: TabsProps["items"]; + isLoading: boolean; +}) { + useEffect(() => { + console.log(previewMsg); + }); + const TapOnChange = (key: string) => { + console.log(key); + }; + return ( +
+
+
+ example +
+ +
+
+
+ {isLoading ? ( + + ) : ( + <> + + {previewMsg.Title} + + + {previewMsg.SubTitle} + + + {previewMsg.Description} + + + )} + + +
+
+
+ +
+
+ ); +} diff --git a/apps/web/src/app/main/course/preview/type.ts b/apps/web/src/app/main/course/preview/type.ts index 3119ae7..fe3a2fd 100644 --- a/apps/web/src/app/main/course/preview/type.ts +++ b/apps/web/src/app/main/course/preview/type.ts @@ -1,8 +1,8 @@ -export interface CoursePreviewMsg{ - videoPreview: string; - Title: string; - SubTitle:string; - Description:string; - ToCourseUrl:string; - isLoading:Boolean +export interface CoursePreviewMsg { + videoPreview: string; + Title: string; + SubTitle: string; + Description: string; + ToCourseUrl: string; + isLoading: boolean; } diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index 32f41ba..6de4312 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -2,8 +2,9 @@ import { Checkbox, Divider, Radio, Space , Spin} from 'antd'; import { categories, levels } from '../mockData'; import { TaxonomySlug, TermDto } from '@nice/common'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { api } from '@nice/client'; +import { useSearchParams } from 'react-router-dom'; interface FilterSectionProps { selectedCategory: string; @@ -53,6 +54,12 @@ export default function FilterSection({ const levels : GetTaxonomyProps = useGetTaxonomy({ type: TaxonomySlug.LEVEL, }) + + const [searchParams,setSearchParams] = useSearchParams() + useEffect(() => { + if(searchParams.get('category')) onCategoryChange(searchParams.get('category')) + },[searchParams.get('category')]) + return (
diff --git a/apps/web/src/app/main/courses/page.tsx b/apps/web/src/app/main/courses/page.tsx index 30cd309..cfdd484 100755 --- a/apps/web/src/app/main/courses/page.tsx +++ b/apps/web/src/app/main/courses/page.tsx @@ -66,6 +66,7 @@ export default function CoursesPage() { selectedCategory={selectedCategory} selectedLevel={selectedLevel} onCategoryChange={(category) => { + console.log(category); setSelectedCategory(category); setCurrentPage(1); }} diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index 02fa591..967336f 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -1,6 +1,9 @@ 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 { api,} from '@nice/client'; +import { ControlOutlined } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; const { Title, Text } = Typography; @@ -10,45 +13,45 @@ interface CourseCategory { description: string; } -const courseCategories: CourseCategory[] = [ - { - name: '计算机基础', - count: 120, - description: '计算机组成原理、操作系统、网络等基础知识' - }, - { - name: '编程语言', - count: 85, - description: 'Python、Java、JavaScript等主流编程语言' - }, - { - name: '人工智能', - count: 65, - description: '机器学习、深度学习、自然语言处理等前沿技术' - }, - { - name: '数据科学', - count: 45, - description: '数据分析、数据可视化、商业智能等' - }, - { - name: '云计算', - count: 38, - description: '云服务、容器化、微服务架构等' - }, - { - name: '网络安全', - count: 42, - description: '网络安全基础、渗透测试、安全防护等' - } -]; +// const courseCategories: CourseCategory[] = [ +// { +// name: '计算机基础', +// count: 120, +// description: '计算机组成原理、操作系统、网络等基础知识' +// }, +// { +// name: '编程语言', +// count: 85, +// description: 'Python、Java、JavaScript等主流编程语言' +// }, +// { +// name: '人工智能', +// count: 65, +// description: '机器学习、深度学习、自然语言处理等前沿技术' +// }, +// { +// name: '数据科学', +// count: 45, +// description: '数据分析、数据可视化、商业智能等' +// }, +// { +// name: '云计算', +// count: 38, +// description: '云服务、容器化、微服务架构等' +// }, +// { +// name: '网络安全', +// count: 42, +// description: '网络安全基础、渗透测试、安全防护等' +// } +// ]; const CategorySection = () => { const [hoveredIndex, setHoveredIndex] = useState(null); 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:{ taxonomy: { slug:TaxonomySlug.CATEGORY @@ -56,16 +59,32 @@ const CategorySection = () => { }, include:{ children :true - } + }, + orderBy: { + createdAt: 'desc', // 按创建时间降序排列 + }, + take:10 }) - const courseCategories: CourseCategory[] = useMemo(() => { - return data?.map((term) => ({ - name: term.name, - count: term.hasChildren ? term.children.length : 0, - description: term.description - })) || []; - },[data]) - */ + // 分类展示 + const [displayedCategories,setDisplayedCategories] = useState([]) + useEffect(() => { + console.log(courseCategoriesData); + if(!isLoading){ + if(showAll){ + 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) => { @@ -76,9 +95,7 @@ const CategorySection = () => { setHoveredIndex(null); }, []); - const displayedCategories = showAll - ? courseCategories - : courseCategories.slice(0, 8); + const navigate = useNavigate() return (
@@ -92,78 +109,86 @@ const CategorySection = () => {
- {displayedCategories.map((category, index) => { - const categoryColor = stringToColor(category.name); - const isHovered = hoveredIndex === index; - - return ( -
handleMouseEnter(index)} - onMouseLeave={handleMouseLeave} - role="button" - tabIndex={0} - aria-label={`查看${category.name}课程类别`} - > -
+ { + isLoading ? : + (displayedCategories.map((category, index) => { + const categoryColor = stringToColor(category.name); + const isHovered = hoveredIndex === index; + + return (
-
-
-
-
- - {category.name} - - - {category.count} 门课程 - -
- - {category.description} - + key={index} + 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)} + onMouseLeave={handleMouseLeave} + role="button" + tabIndex={0} + aria-label={`查看${category.name}课程类别`} + onClick={()=>{ + console.log(category.name) + navigate(`/courses?category=${category.name}`) + }} + > +
- 了解更多 - +
+
+
+
+ + {category.name} + + {/* + {category.children.length} 门课程 + */} +
+ + {category.description} + +
- → - + 了解更多 + + → + +
-
- ); - })} + ); + })) + } +
- {courseCategories.length > 8 && ( + {!isLoading && courseCategoriesData.length > 8 && (
{/* Stats Container */} -
-
+
+
{platformStats.map((stat, index) => (
{
{stat.value}
-
{stat.label}
+
+ {stat.label} +
))}
diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index 453fa17..8f59ac7 100755 --- a/apps/web/src/app/main/home/page.tsx +++ b/apps/web/src/app/main/home/page.tsx @@ -2,8 +2,8 @@ import HeroSection from './components/HeroSection'; import CategorySection from './components/CategorySection'; import CoursesSection from './components/CoursesSection'; import FeaturedTeachersSection from './components/FeaturedTeachersSection'; +import { api } from '@nice/client'; import { useEffect } from 'react'; -import { api } from '@nice/client' const HomePage = () => { const mockCourses = [ { diff --git a/apps/web/src/components/common/uploader/AvatarUploader.tsx b/apps/web/src/components/common/uploader/AvatarUploader.tsx index d4077f3..1ca2e20 100755 --- a/apps/web/src/components/common/uploader/AvatarUploader.tsx +++ b/apps/web/src/components/common/uploader/AvatarUploader.tsx @@ -44,7 +44,9 @@ const AvatarUploader: React.FC = ({ // 在组件中定义 key 状态 const [avatarKey, setAvatarKey] = useState(0); const { token } = theme.useToken(); - + useEffect(() => { + setPreviewUrl(value || ""); + }, [value]); const handleChange = async (event: React.ChangeEvent) => { const selectedFile = event.target.files?.[0]; if (!selectedFile) return; diff --git a/apps/web/src/components/layout/element/usermenu/usermenu.tsx b/apps/web/src/components/layout/element/usermenu/usermenu.tsx index e00965b..d2611dd 100755 --- a/apps/web/src/components/layout/element/usermenu/usermenu.tsx +++ b/apps/web/src/components/layout/element/usermenu/usermenu.tsx @@ -185,13 +185,13 @@ export function UserMenu() { id="user-menu" aria-orientation="vertical" aria-labelledby="user-menu-button" - style={{ zIndex: 100 }} + style={{ zIndex: 1000 }} className="absolute right-0 mt-3 w-64 origin-top-right bg-white rounded-xl overflow-hidden shadow-lg border border-[#E5EDF5]"> {/* User Profile Section */}
= () => { - const { course, isLoading } = useContext(CourseDetailContext); +export const CourseDetailDescription: React.FC = () => { + const { course, isLoading, selectedLectureId, setSelectedLectureId } = + useContext(CourseDetailContext); const { Paragraph, Title } = Typography; - + const firstLectureId = useMemo(() => { + return course?.sections?.[0]?.lectures?.[0]?.id; + }, [course]); + const navigate = useNavigate(); return (
{isLoading || !course ? ( ) : (
-
{"课程简介"}
+ {!selectedLectureId && ( + <> +
+ +
{ + setSelectedLectureId(firstLectureId); + }} + className="w-full h-full absolute top-0 z-10 bg-black opacity-30 transition-opacity duration-300 ease-in-out hover:opacity-70 cursor-pointer"> + +
+
+ + )} +
{"课程简介:"}
+
+
{course?.subTitle}
+
+ +
{course?.meta?.views}
+
+
+ + {dayjs(course?.createdAt).format("YYYY年M月D日")} +
+
= () => { onExpand: () => console.log("展开"), // collapseText: "收起", }}> - {course.content} + {course?.content}
)} diff --git a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx index f6f4913..142abde 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx @@ -7,6 +7,7 @@ import { Course, LectureType, PostType } from "@nice/common"; import { CourseDetailContext } from "./CourseDetailContext"; import CollapsibleContent from "@web/src/components/common/container/CollapsibleContent"; import { Skeleton } from "antd"; +import { CoursePreview } from "./CoursePreview/CoursePreview"; // interface CourseDetailDisplayAreaProps { // // course: Course; @@ -17,7 +18,7 @@ import { Skeleton } from "antd"; export const CourseDetailDisplayArea: React.FC = () => { // 创建滚动动画效果 - const { course, isLoading, lecture, lectureIsLoading } = + const { course, isLoading, lecture, lectureIsLoading, selectedLectureId } = useContext(CourseDetailContext); const { scrollY } = useScroll(); const videoOpacity = useTransform(scrollY, [0, 200], [1, 0.8]); @@ -27,20 +28,24 @@ export const CourseDetailDisplayArea: React.FC = () => { {lectureIsLoading && ( )} - {!lectureIsLoading && lecture?.meta?.type === LectureType.VIDEO && ( -
- -
- -
-
{" "} -
- )} + + {selectedLectureId && + !lectureIsLoading && + lecture?.meta?.type === LectureType.VIDEO && ( +
+ +
+ +
+
+
+ )} {!lectureIsLoading && + selectedLectureId && lecture?.meta?.type === LectureType.ARTICLE && (
@@ -52,10 +57,7 @@ export const CourseDetailDisplayArea: React.FC = () => {
)}
- +
{/* 课程内容区域 */}
diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx index 88657a2..71ec0e0 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -8,7 +8,7 @@ import { } from "@ant-design/icons"; import { useAuth } from "@web/src/providers/auth-provider"; import { useNavigate } from "react-router-dom"; -import { UserMenu } from "@web/src/components/layout/element/usermenu/usermenu"; +import { UserMenu } from "@web/src/app/main/layout/UserMenu"; import { CourseDetailContext } from "../CourseDetailContext"; const { Header } = Layout; @@ -18,7 +18,7 @@ export function CourseDetailHeader() { const { isAuthenticated, user } = useAuth(); const navigate = useNavigate(); const { course } = useContext(CourseDetailContext); - + return (
diff --git a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx index 02316bb..d2cffe6 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx @@ -18,7 +18,7 @@ export default function CourseDetailLayout() { const handleLectureClick = (lectureId: string) => { setSelectedLectureId(lectureId); }; - const [isSyllabusOpen, setIsSyllabusOpen] = useState(false); + const [isSyllabusOpen, setIsSyllabusOpen] = useState(true); return (
@@ -30,6 +30,9 @@ export default function CourseDetailLayout() { {" "} {/* 添加这个包装 div */} +
+
+ example +
+ +
+
+
+ {isLoading ? ( + + ) : ( + <> + + {course.title} + + + {course.subTitle} + + + {course.content} + + + )} + + +
+
+
+ ); +} diff --git a/apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx b/apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx new file mode 100644 index 0000000..8f32fce --- /dev/null +++ b/apps/web/src/components/models/course/detail/CoursePreview/couresPreviewTabmsg.tsx @@ -0,0 +1,25 @@ +import { Checkbox, List } from 'antd'; +import React from 'react'; + +export function CoursePreviewTabmsg({data}){ + + + const renderItem = (item) => ( + + + + ); + + return( +
+ +
+ ) +} \ No newline at end of file diff --git a/apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx b/apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx new file mode 100644 index 0000000..b13e87d --- /dev/null +++ b/apps/web/src/components/models/course/detail/CoursePreview/courseCatalog.tsx @@ -0,0 +1,11 @@ +import type { MenuProps } from 'antd'; +import { Menu } from 'antd'; + +type MenuItem = Required['items'][number]; + +export function CourseCatalog(){ + return ( + <> + + ) +} \ No newline at end of file diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx index 3b04395..9e6cf84 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx @@ -6,7 +6,6 @@ import { } from "@heroicons/react/24/outline"; import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; import React, { useState, useRef, useContext } from "react"; -import { motion, AnimatePresence } from "framer-motion"; import { SectionDto, TaxonomySlug } from "@nice/common"; import { SyllabusHeader } from "./SyllabusHeader"; import { SectionItem } from "./SectionItem"; @@ -28,13 +27,11 @@ export const CourseSyllabus: React.FC = ({ onToggle, }) => { const { isHeaderVisible } = useContext(CourseDetailContext); - const [expandedSections, setExpandedSections] = useState([]); + const [expandedSections, setExpandedSections] = useState( + sections.map((section) => section.id) // 默认展开所有章节 + ); const sectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); - // api.term.findMany.useQuery({ - // where: { - // taxonomy: { slug: TaxonomySlug.CATEGORY }, - // }, - // }); + const toggleSection = (sectionId: string) => { setExpandedSections((prev) => prev.includes(sectionId) @@ -42,70 +39,56 @@ export const CourseSyllabus: React.FC = ({ : [...prev, sectionId] ); - setTimeout(() => { - sectionRefs.current[sectionId]?.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - }, 100); + // 直接滚动,无需延迟 + sectionRefs.current[sectionId]?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); }; return ( <> - - {/* 收起时的悬浮按钮 */} - {!isOpen && ( - - - - )} - - + +
+ )} + +
- - {isOpen && ( - - + {isOpen && ( +
+ -
-
- {sections.map((section, index) => ( - - (sectionRefs.current[ - section.id - ] = el) - } - index={index + 1} - section={section} - isExpanded={expandedSections.includes( - section.id - )} - onToggle={toggleSection} - onLectureClick={onLectureClick} - /> - ))} -
+
+
+ {sections.map((section, index) => ( + + (sectionRefs.current[section.id] = + el) + } + index={index + 1} + section={section} + isExpanded={expandedSections.includes( + section.id + )} + onToggle={toggleSection} + onLectureClick={onLectureClick} + /> + ))}
- - )} - - +
+
+ )} +
); }; diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx index a79b16e..ed73745 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -7,6 +7,7 @@ import { FileTextOutlined, PlayCircleOutlined, } from "@ant-design/icons"; // 使用 Ant Design 图标 +import { useParams } from "react-router-dom"; interface LectureItemProps { lecture: Lecture; @@ -16,25 +17,30 @@ interface LectureItemProps { export const LectureItem: React.FC = ({ lecture, onClick, -}) => ( -
onClick(lecture.id)}> - {lecture.type === LectureType.VIDEO && ( - - )} - {lecture.type === LectureType.ARTICLE && ( - // 为文章类型添加图标 - )} -
-

{lecture.title}

- {lecture.subTitle && ( -

{lecture.subTitle}

+}) => { + const { lectureId } = useParams(); + return ( +
onClick(lecture.id)}> + {lecture.type === LectureType.VIDEO && ( + )} -
- {/*
+ {lecture.type === LectureType.ARTICLE && ( + // 为文章类型添加图标 + )} +
+

{lecture.title}

+ {lecture.subTitle && ( +

+ {lecture.subTitle} +

+ )} +
+ {/*
{lecture.duration}分钟
*/} -
-); +
+ ); +}; diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx index d2a4fe6..6cd1627 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx @@ -16,11 +16,11 @@ interface SectionItemProps { export const SectionItem = React.forwardRef( ({ section, index, isExpanded, onToggle, onLectureClick }, ref) => ( -
) ); diff --git a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx index 5ee54b3..913bf34 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -2,6 +2,7 @@ import { createContext, useContext, ReactNode, useEffect } from "react"; import { Form, FormInstance, message } from "antd"; import { CourseDto, + CourseMeta, CourseStatus, ObjectType, PostType, @@ -10,6 +11,7 @@ import { import { api, usePost } from "@nice/client"; import { useNavigate } from "react-router-dom"; import { z } from "zod"; +import { useAuth } from "@web/src/providers/auth-provider"; export type CourseFormData = { title: string; @@ -42,6 +44,7 @@ export function CourseFormProvider({ }: CourseFormProviderProps) { const [form] = Form.useForm(); const { create, update, createCourse } = usePost(); + const { user } = useAuth(); const { data: course }: { data: CourseDto } = api.post.findFirst.useQuery( { where: { id: editId }, @@ -77,7 +80,7 @@ export function CourseFormProvider({ } }, [course, form]); - const onSubmit = async (values: CourseFormData) => { + const onSubmit = async (values: any) => { console.log(values); const sections = values?.sections || []; const termIds = taxonomies @@ -87,7 +90,7 @@ export function CourseFormProvider({ const formattedValues = { ...values, meta: { - thumbnail: values.thumbnail, + thumbnail: values?.meta?.thumbnail, }, terms: { connect: termIds.map((id) => ({ id })), // 转换成 connect 格式 @@ -98,6 +101,12 @@ export function CourseFormProvider({ delete formattedValues[tax.id]; }); delete formattedValues.sections; + if (course) { + formattedValues.meta = { + ...(course?.meta as CourseMeta), + thumbnail: values?.meta?.thumbnail, + }; + } try { if (editId) { await update.mutateAsync({ @@ -110,6 +119,7 @@ export function CourseFormProvider({ courseDetail: { data: { title: formattedValues.title || "12345", + // state: CourseStatus.DRAFT, type: PostType.COURSE, ...formattedValues, diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx index ee0916a..4e4d641 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/CourseContentForm.tsx @@ -41,6 +41,9 @@ const CourseContentForm: React.FC = () => { type: PostType.SECTION, deletedAt: null, }, + orderBy: { + order: "asc", + }, }, { enabled: !!editId, diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx index 1bbc610..dac0143 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/LectureList.tsx @@ -58,6 +58,9 @@ export const LectureList: React.FC = ({ type: PostType.LECTURE, deletedAt: null, }, + orderBy: { + order: "asc", + }, }, { enabled: !!sectionId, diff --git a/apps/web/src/hooks/useLocalSetting.ts b/apps/web/src/hooks/useLocalSetting.ts index 6d21f29..7cb5c21 100755 --- a/apps/web/src/hooks/useLocalSetting.ts +++ b/apps/web/src/hooks/useLocalSetting.ts @@ -1,25 +1,16 @@ import { useCallback, useMemo } from "react"; import { env } from "../env"; export function useLocalSettings() { - const getBaseUrl = useCallback((protocol: string, port: number) => { - return `${protocol}://${env.SERVER_IP}:${port}`; - }, []); - const tusUrl = useMemo(() => getBaseUrl("http", 8080), [getBaseUrl]); - const apiUrl = useMemo( - () => getBaseUrl("http", parseInt(env.SERVER_PORT)), - [getBaseUrl] - ); - const websocketUrl = useMemo(() => parseInt(env.SERVER_PORT), [getBaseUrl]); - const checkIsTusUrl = useCallback( - (url: string) => { - return url.startsWith(tusUrl); - }, - [tusUrl] - ); - return { - apiUrl, - websocketUrl, - checkIsTusUrl, - tusUrl, - }; + const getBaseUrl = useCallback((protocol: string, port: number) => { + return `${protocol}://${env.SERVER_IP}:${port}`; + }, []); + const tusUrl = useMemo(() => getBaseUrl('http', 8080), [getBaseUrl]); + const apiUrl = useMemo(() => getBaseUrl('http', parseInt(env.SERVER_PORT)), [getBaseUrl]); + const websocketUrl = useMemo(() => getBaseUrl('ws', parseInt(env.SERVER_PORT)), [getBaseUrl]); + const checkIsTusUrl = useCallback((url: string) => { + return url.startsWith(tusUrl) + }, [tusUrl]) + return { + apiUrl, websocketUrl, checkIsTusUrl, tusUrl + } } diff --git a/apps/web/src/utils/axios-client.ts b/apps/web/src/utils/axios-client.ts index f75302f..5b6a119 100755 --- a/apps/web/src/utils/axios-client.ts +++ b/apps/web/src/utils/axios-client.ts @@ -1,20 +1,20 @@ import axios from 'axios'; import { env } from '../env'; -const BASE_URL = `http://${env.SERVER_IP}:${env.SERVER_PORT}` +const BASE_URL = `http://${env.SERVER_IP}:${env?.SERVER_PORT}` const apiClient = axios.create({ - baseURL: BASE_URL, - // withCredentials: true, + baseURL: BASE_URL, + // withCredentials: true, }); // Add a request interceptor to attach the access token apiClient.interceptors.request.use( - (config) => { - const accessToken = localStorage.getItem('access_token'); - if (accessToken) { - config.headers['Authorization'] = `Bearer ${accessToken}`; - } - return config; - }, - (error) => Promise.reject(error) + (config) => { + const accessToken = localStorage.getItem("access_token"); + if (accessToken) { + config.headers["Authorization"] = `Bearer ${accessToken}`; + } + return config; + }, + (error) => Promise.reject(error) ); export default apiClient; diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 89768ea..e27fc2a 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -204,7 +204,7 @@ model Post { // 日期时间类型字段 createdAt DateTime @default(now()) @map("created_at") publishedAt DateTime? @map("published_at") // 发布时间 - updatedAt DateTime @updatedAt @map("updated_at") + updatedAt DateTime @map("updated_at") deletedAt DateTime? @map("deleted_at") // 删除时间,可为空 instructors PostInstructor[] // 关系类型字段 diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 0a41f6e..fbc63cd 100755 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -58,6 +58,7 @@ export const InitTaxonomies: { { name: "分类", slug: TaxonomySlug.CATEGORY, + objectType: [ObjectType.COURSE], }, { name: "难度等级", diff --git a/packages/common/src/models/post.ts b/packages/common/src/models/post.ts index 0961cab..fa030b6 100755 --- a/packages/common/src/models/post.ts +++ b/packages/common/src/models/post.ts @@ -67,6 +67,9 @@ export type CourseMeta = { thumbnail?: string; objectives?: string[]; + views?: number; + likes?: number; + hates?: number; }; export type Course = Post & { meta?: CourseMeta;