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/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/hooks/useLocalSetting.ts b/apps/web/src/hooks/useLocalSetting.ts index cd78328..7cb5c21 100755 --- a/apps/web/src/hooks/useLocalSetting.ts +++ b/apps/web/src/hooks/useLocalSetting.ts @@ -1,4 +1,3 @@ - import { useCallback, useMemo } from "react"; import { env } from "../env"; export function useLocalSettings() { @@ -14,4 +13,4 @@ export function useLocalSettings() { return { apiUrl, websocketUrl, checkIsTusUrl, tusUrl } -} \ No newline at end of file +} diff --git a/apps/web/src/utils/axios-client.ts b/apps/web/src/utils/axios-client.ts index a42ae46..5b6a119 100755 --- a/apps/web/src/utils/axios-client.ts +++ b/apps/web/src/utils/axios-client.ts @@ -2,19 +2,19 @@ import axios from 'axios'; import { env } from '../env'; 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/config/nginx/conf.d/web.template b/config/nginx/conf.d/web.template index 7f564bb..779c9b2 100755 --- a/config/nginx/conf.d/web.template +++ b/config/nginx/conf.d/web.template @@ -101,6 +101,7 @@ server { internal; # 代理到认证服务 proxy_pass http://${SERVER_IP}:${SERVER_PORT}/auth/file; + # 请求优化:不传递请求体 proxy_pass_request_body off; proxy_set_header Content-Length ""; 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;