From c9199a77094131317576536bf6f4ef11dde59d54 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 11:39:53 +0800 Subject: [PATCH 01/24] add --- apps/web/src/components/layout/breadcrumb.tsx | 70 ++++++++++--------- .../course/detail/CourseDetailContext.tsx | 27 +++++-- .../CourseDetailHeader/CourseDetailHeader.tsx | 9 ++- .../course/detail/CourseDetailLayout.tsx | 9 +-- apps/web/src/routes/index.tsx | 16 ----- 5 files changed, 66 insertions(+), 65 deletions(-) diff --git a/apps/web/src/components/layout/breadcrumb.tsx b/apps/web/src/components/layout/breadcrumb.tsx index 1804dab..bffbb65 100755 --- a/apps/web/src/components/layout/breadcrumb.tsx +++ b/apps/web/src/components/layout/breadcrumb.tsx @@ -1,38 +1,42 @@ -import React from 'react'; -import { useLocation, Link, useMatches } from 'react-router-dom'; -import { theme } from 'antd'; -import { RightOutlined } from '@ant-design/icons'; +import React from "react"; +import { useLocation, Link, useMatches } from "react-router-dom"; +import { theme } from "antd"; +import { RightOutlined } from "@ant-design/icons"; export default function Breadcrumb() { - let matches = useMatches(); - const { token } = theme.useToken() + const matches = useMatches(); + const { token } = theme.useToken(); - let crumbs = matches - // first get rid of any matches that don't have handle and crumb - .filter((match) => Boolean((match.handle as any)?.crumb)) - // now map them into an array of elements, passing the loader - // data to each one - .map((match) => (match.handle as any).crumb(match.data)); + const crumbs = matches + // first get rid of any matches that don't have handle and crumb + .filter((match) => Boolean((match.handle as any)?.crumb)) + // now map them into an array of elements, passing the loader + // data to each one + .map((match) => (match.handle as any).crumb(match.data)); - return ( -
    - {crumbs.map((crumb, index) => ( - -
  1. - {crumb} -
  2. - {index < crumbs.length - 1 && ( -
  3. - -
  4. - )} -
    - ))} -
- ); + return ( +
    + {crumbs.map((crumb, index) => ( + +
  1. + {crumb} +
  2. + {index < crumbs.length - 1 && ( +
  3. + +
  4. + )} +
    + ))} +
+ ); } diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 6787e13..2f702e7 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -3,10 +3,17 @@ import { courseDetailSelect, CourseDto, Lecture, + RolePerms, VisitType, } from "@nice/common"; import { useAuth } from "@web/src/providers/auth-provider"; -import React, { createContext, ReactNode, useEffect, useState } from "react"; +import React, { + createContext, + ReactNode, + useEffect, + useMemo, + useState, +} from "react"; import { useNavigate, useParams } from "react-router-dom"; interface CourseDetailContextType { @@ -19,11 +26,14 @@ interface CourseDetailContextType { lectureIsLoading?: boolean; isHeaderVisible: boolean; // 新增 setIsHeaderVisible: (visible: boolean) => void; // 新增 + canEdit?: boolean; } + interface CourseFormProviderProps { children: ReactNode; editId?: string; // 添加 editId 参数 } + export const CourseDetailContext = createContext(null); export function CourseDetailProvider({ @@ -32,8 +42,9 @@ export function CourseDetailProvider({ }: CourseFormProviderProps) { const navigate = useNavigate(); const { read } = useVisitor(); - const { user } = useAuth(); + const { user, hasSomePermissions } = useAuth(); const { lectureId } = useParams(); + const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = (api.post as any).findFirst.useQuery( { @@ -45,7 +56,14 @@ export function CourseDetailProvider({ }, { enabled: Boolean(editId) } ); - + const canEdit = useMemo(() => { + const isAuthor = user?.id === course?.authorId; + const isDept = course?.depts + ?.map((dept) => dept.id) + .includes(user?.deptId); + const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST); + return isAuthor || isDept || isRoot; + }, [user, course]); const [selectedLectureId, setSelectedLectureId] = useState< string | undefined >(lectureId || undefined); @@ -57,9 +75,9 @@ export function CourseDetailProvider({ }, { enabled: Boolean(editId) } ); + useEffect(() => { if (course) { - console.log("read"); read.mutateAsync({ data: { visitorId: user?.id || null, @@ -85,6 +103,7 @@ export function CourseDetailProvider({ lectureIsLoading, isHeaderVisible, setIsHeaderVisible, + canEdit, }}> {children} 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 36f7c71..433ef3b 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -10,7 +10,7 @@ import { useAuth } from "@web/src/providers/auth-provider"; import { useNavigate, useParams } from "react-router-dom"; import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu"; import { CourseDetailContext } from "../CourseDetailContext"; -import { Department, RolePerms } from "@nice/common"; + const { Header } = Layout; @@ -20,9 +20,8 @@ export function CourseDetailHeader() { const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } = useAuth(); const navigate = useNavigate(); - const { course } = useContext(CourseDetailContext); - hasSomePermissions(RolePerms.MANAGE_ANY_POST, RolePerms.MANAGE_DOM_POST); - hasEveryPermissions(RolePerms.MANAGE_ANY_POST, RolePerms.MANAGE_DOM_POST); + const { course, canEdit } = useContext(CourseDetailContext); + return (
@@ -52,7 +51,7 @@ export function CourseDetailHeader() { onChange={(e) => setSearchValue(e.target.value)} />
- {isAuthenticated && ( + {canEdit && ( <> - )} - {/* PostResources 组件 */} - {/* */} ); diff --git a/apps/web/src/components/common/editor/quill/constants.ts b/apps/web/src/components/common/editor/quill/constants.ts index d47f27b..fd92bd1 100755 --- a/apps/web/src/components/common/editor/quill/constants.ts +++ b/apps/web/src/components/common/editor/quill/constants.ts @@ -1,11 +1,11 @@ export const defaultModules = { - toolbar: [ - [{ 'header': [1, 2, 3, 4, 5, 6, false] }], - ['bold', 'italic', 'underline', 'strike'], - [{ 'list': 'ordered' }, { 'list': 'bullet' }], - [{ 'color': [] }, { 'background': [] }], - [{ 'align': [] }], - ['link', 'image'], - ['clean'] - ] -}; \ No newline at end of file + toolbar: [ + [{ header: [1, 2, 3, 4, 5, 6, false] }], + ["bold", "italic", "underline", "strike"], + [{ list: "ordered" }, { list: "bullet" }], + [{ color: [] }, { background: [] }], + [{ align: [] }], + ["link"], + ["clean"], + ], +}; diff --git a/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx b/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx index 16f389d..bf324ea 100644 --- a/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx +++ b/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx @@ -1,5 +1,4 @@ -import { useEffect, useState } from "react"; -// import UncoverAvatarUploader from "../uploader/UncoverAvatarUploader "; +import React, { useEffect, useState } from "react"; import { Upload, Progress, Button, Image, Form } from "antd"; import { DeleteOutlined } from "@ant-design/icons"; import AvatarUploader from "./AvatarUploader"; @@ -8,11 +7,17 @@ import { isEqual } from "lodash"; interface MultiAvatarUploaderProps { value?: string[]; onChange?: (value: string[]) => void; + className?: string; + placeholder?: string; + style?: React.CSSProperties; } export function MultiAvatarUploader({ value, onChange, + className, + style, + placeholder = "点击上传", }: MultiAvatarUploaderProps) { const [imageList, setImageList] = useState(value || []); const [previewImage, setPreviewImage] = useState(""); @@ -30,12 +35,21 @@ export function MultiAvatarUploader({ {(imageList || [])?.map((image, index) => { return (
+ style={{ + width: "100px", + height: "100px", + ...style, + }}>
{ console.log(value); diff --git a/apps/web/src/components/common/uploader/ResourceShower.tsx b/apps/web/src/components/common/uploader/ResourceShower.tsx new file mode 100644 index 0000000..98d14d4 --- /dev/null +++ b/apps/web/src/components/common/uploader/ResourceShower.tsx @@ -0,0 +1,135 @@ +import React, { useMemo } from "react"; +import { Image, Button, Row, Col, Tooltip } from "antd"; +import { ResourceDto } from "@nice/common"; +import { env } from "@web/src/env"; +import { getFileIcon } from "./utils"; +import { formatFileSize, getCompressedImageUrl } from "@nice/utils"; + +export default function ResourcesShower({ + resources = [], +}: { + resources: ResourceDto[]; +}) { + const { resources: dealedResources } = useMemo(() => { + if (!resources) return { resources: [] }; + + const isImage = (url: string) => + /\.(png|jpg|jpeg|gif|webp)$/i.test(url); + + const sortedResources = resources + .map((resource) => { + const original = `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${resource.url}`; + const isImg = isImage(resource.url); + return { + ...resource, + url: isImg ? getCompressedImageUrl(original) : original, + originalUrl: original, + isImage: isImg, + }; + }) + .sort((a, b) => (a.isImage === b.isImage ? 0 : a.isImage ? -1 : 1)); + + return { resources: sortedResources }; + }, [resources]); + + const imageResources = dealedResources.filter((res) => res.isImage); + const fileResources = dealedResources.filter((res) => !res.isImage); + return ( +
+ {imageResources.length > 0 && ( + + + {imageResources.map((resource) => ( + +
+
+ {resource.title} + 点击预览 +
+ ), + }} + style={{ + position: "absolute", + inset: 0, + width: "100%", + height: "100%", + objectFit: "cover", + }} + rootClassName="w-full h-full" + /> +
+ {resource.title && ( +
+ {resource.title} +
+ )} +
+ + ))} + + + )} + {fileResources.length > 0 && ( + + )} +
+ ); +} diff --git a/apps/web/src/components/common/uploader/utils.tsx b/apps/web/src/components/common/uploader/utils.tsx new file mode 100644 index 0000000..8437211 --- /dev/null +++ b/apps/web/src/components/common/uploader/utils.tsx @@ -0,0 +1,48 @@ +import { + FilePdfOutlined, + FileWordOutlined, + FileExcelOutlined, + FilePptOutlined, + FileTextOutlined, + FileZipOutlined, + FileImageOutlined, + FileUnknownOutlined, +} from "@ant-design/icons"; + +export const isContentEmpty = (html: string) => { + // 创建一个临时 div 来解析 HTML 内容 + const temp = document.createElement("div"); + temp.innerHTML = html; + // 获取纯文本内容并检查是否为空 + return !temp.textContent?.trim(); +}; +export const getFileIcon = (filename: string) => { + const extension = filename.split(".").pop()?.toLowerCase(); + switch (extension) { + case "pdf": + return ; + case "doc": + case "docx": + return ; + case "xls": + case "xlsx": + return ; + case "ppt": + case "pptx": + return ; + case "txt": + return ; + case "zip": + case "rar": + case "7z": + return ; + case "png": + case "jpg": + case "jpeg": + case "gif": + case "webp": + return ; + default: + return ; + } +}; diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 2f702e7..9ba1283 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -3,6 +3,7 @@ import { courseDetailSelect, CourseDto, Lecture, + lectureDetailSelect, RolePerms, VisitType, } from "@nice/common"; @@ -72,16 +73,17 @@ export function CourseDetailProvider({ ).findFirst.useQuery( { where: { id: selectedLectureId }, + select: lectureDetailSelect, }, { enabled: Boolean(editId) } ); useEffect(() => { - if (course) { + if (lecture?.id) { read.mutateAsync({ data: { visitorId: user?.id || null, - postId: course.id, + postId: lecture?.id, type: VisitType.READED, }, }); diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription/Description/Overview.tsx b/apps/web/src/components/models/course/detail/CourseDetailDescription/Description/Overview.tsx deleted file mode 100755 index 984f266..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetailDescription/Description/Overview.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// import { useContext } from "react"; -// import { CourseDetailContext } from "../../CourseDetailContext"; -// import { CheckCircleIcon } from "@heroicons/react/24/solid"; - -// export function Overview() { -// const { course } = useContext(CourseDetailContext); -// return ( -// <> -//
-// {/* 课程描述 */} -//
-//

{course?.description}

-//
- -// {/* 学习目标 */} -//
-//

学习目标

-//
-// {course?.objectives.map((objective, index) => ( -//
-// -// {objective} -//
-// ))} -//
-//
- -// {/* 适合人群 */} -//
-//

适合人群

-//
-// {course?.audiences.map((audience, index) => ( -//
-// -// {audience} -//
-// ))} -//
-//
- -// {/* 课程要求 */} -//
-//

课程要求

-//
    -// {course?.requirements.map((requirement, index) => ( -//
  • {requirement}
  • -// ))} -//
-//
- -// {/* 可获得技能 */} -//
-//

可获得技能

-//
-// {course?.skills.map((skill, index) => ( -// -// {skill} -// -// ))} -//
-//
-//
-// -// ); -// } diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription/Description/index.ts b/apps/web/src/components/models/course/detail/CourseDetailDescription/Description/index.ts deleted file mode 100755 index e69de29..0000000 diff --git a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx index 447ecda..bcef62a 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx @@ -8,6 +8,7 @@ import { CourseDetailContext } from "./CourseDetailContext"; import CollapsibleContent from "@web/src/components/common/container/CollapsibleContent"; import { Skeleton } from "antd"; import { CoursePreview } from "./CoursePreview/CoursePreview"; +import ResourcesShower from "@web/src/components/common/uploader/ResourceShower"; // interface CourseDetailDisplayAreaProps { // // course: Course; @@ -53,6 +54,12 @@ export const CourseDetailDisplayArea: React.FC = () => { content={lecture?.content || ""} maxHeight={500} // Optional, defaults to 150 /> +
+ +
)} 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 b495edd..138fc0f 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 @@ -33,7 +33,12 @@ import { useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; -import { Lecture, LectureType, PostType } from "@nice/common"; +import { + Lecture, + lectureDetailSelect, + LectureType, + PostType, +} from "@nice/common"; import { useCourseEditor } from "../../context/CourseEditorContext"; import { usePost } from "@nice/client"; import { LectureData, SectionData } from "./interface"; @@ -62,6 +67,7 @@ export const LectureList: React.FC = ({ orderBy: { order: "asc", }, + select: lectureDetailSelect, }, { enabled: !!sectionId, diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx index a3ed362..26282d2 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx @@ -15,6 +15,7 @@ import { LectureType, LessonTypeLabel, PostType, + ResourceStatus, videoMimeTypes, } from "@nice/common"; import { usePost } from "@nice/client"; @@ -23,6 +24,8 @@ import toast from "react-hot-toast"; import { env } from "@web/src/env"; import CollapsibleContent from "@web/src/components/common/container/CollapsibleContent"; import { VideoPlayer } from "@web/src/components/presentation/video-player/VideoPlayer"; +import MultiAvatarUploader from "@web/src/components/common/uploader/MultiAvatarUploader"; +import ResourcesShower from "@web/src/components/common/uploader/ResourceShower"; interface SortableLectureProps { field: Lecture; @@ -58,6 +61,7 @@ export const SortableLecture: React.FC = ({ setLoading(true); const values = await form.validateFields(); let result; + const fileIds = values?.meta?.fileIds || []; const videoUrlId = Array.isArray(values?.meta?.videoIds) ? values?.meta?.videoIds[0] : typeof values?.meta?.videoIds === "string" @@ -72,13 +76,14 @@ export const SortableLecture: React.FC = ({ title: values?.title, meta: { type: values?.meta?.type, + fileIds: fileIds, videoIds: videoUrlId ? [videoUrlId] : [], videoUrl: videoUrlId ? `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${videoUrlId}/stream/index.m3u8` : undefined, }, resources: { - connect: [videoUrlId] + connect: [videoUrlId, ...fileIds] .filter(Boolean) .map((fileId) => ({ fileId, @@ -97,13 +102,14 @@ export const SortableLecture: React.FC = ({ title: values?.title, meta: { type: values?.meta?.type, + fileIds: fileIds, videoIds: videoUrlId ? [videoUrlId] : [], videoUrl: videoUrlId ? `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${videoUrlId}/stream/index.m3u8` : undefined, }, resources: { - connect: [videoUrlId] + connect: [videoUrlId, ...fileIds] .filter(Boolean) .map((fileId) => ({ fileId, @@ -113,6 +119,7 @@ export const SortableLecture: React.FC = ({ }, }); } + setIsContentVisible(false); toast.success("课时已更新"); field.id = result.id; setEditing(false); @@ -178,14 +185,30 @@ export const SortableLecture: React.FC = ({ /> ) : ( - - - +
+ + + + + + +
)} @@ -237,10 +260,16 @@ export const SortableLecture: React.FC = ({ {isContentVisible && !editing && // Conditionally render content based on type (field?.meta?.type === LectureType.ARTICLE ? ( - +
+ +
+ +
+
) : ( ))} diff --git a/apps/web/src/components/models/term/term-select.tsx b/apps/web/src/components/models/term/term-select.tsx index 95990dd..3896d8e 100755 --- a/apps/web/src/components/models/term/term-select.tsx +++ b/apps/web/src/components/models/term/term-select.tsx @@ -48,7 +48,7 @@ export default function TermSelect({ const [listTreeData, setListTreeData] = useState< Omit[] >([]); - + const fetchParentTerms = useCallback( async (termIds: string | string[], taxonomyId?: string) => { const idsArray = Array.isArray(termIds) diff --git a/apps/web/src/components/presentation/video-player/VideoControls.tsx b/apps/web/src/components/presentation/video-player/VideoControls.tsx index 93fc540..4200476 100755 --- a/apps/web/src/components/presentation/video-player/VideoControls.tsx +++ b/apps/web/src/components/presentation/video-player/VideoControls.tsx @@ -96,7 +96,6 @@ export const Controls = () => {
{/* 播放/暂停按钮 */} - {/* 时间显示 */} {duration && ( @@ -114,7 +113,7 @@ export const Controls = () => { {/* 倍速控制 */} {/* 设置按钮 */} - + {/* */} {/* 全屏按钮 */} diff --git a/apps/web/src/components/presentation/video-player/VideoDisplay.tsx b/apps/web/src/components/presentation/video-player/VideoDisplay.tsx index b786739..5b48209 100755 --- a/apps/web/src/components/presentation/video-player/VideoDisplay.tsx +++ b/apps/web/src/components/presentation/video-player/VideoDisplay.tsx @@ -14,7 +14,6 @@ export const VideoDisplay: React.FC = ({ onError, videoRef, setIsReady, - isPlaying, setIsPlaying, setError, setBufferingState, @@ -26,8 +25,6 @@ export const VideoDisplay: React.FC = ({ isDragging, setIsDragging, progressRef, - resolution, - setResolutions, } = useContext(VideoPlayerContext); // 处理进度条拖拽 @@ -40,7 +37,6 @@ export const VideoDisplay: React.FC = ({ ); videoRef.current.currentTime = percent * videoRef.current.duration; }; - // 添加拖拽事件监听 useEffect(() => { const handleMouseUp = () => setIsDragging(false); @@ -66,15 +62,12 @@ export const VideoDisplay: React.FC = ({ setError(null); setLoadingProgress(0); setBufferingState(false); - // Check for native HLS support (Safari) if (videoRef.current.canPlayType("application/vnd.apple.mpegurl")) { videoRef.current.src = src; setIsReady(true); - // 设置视频时长 setDuration(videoRef.current.duration); - if (autoPlay) { try { await videoRef.current.play(); @@ -85,14 +78,12 @@ export const VideoDisplay: React.FC = ({ } return; } - if (!Hls.isSupported()) { const errorMessage = "您的浏览器不支持 HLS 视频播放"; setError(errorMessage); onError?.(errorMessage); return; } - hls = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 600, @@ -102,13 +93,10 @@ export const VideoDisplay: React.FC = ({ hls.loadSource(src); hls.attachMedia(videoRef.current); - hls.on(Hls.Events.MANIFEST_PARSED, async () => { setIsReady(true); - // 设置视频时长 setDuration(videoRef.current?.duration || 0); - if (autoPlay && videoRef.current) { try { await videoRef.current.play(); @@ -118,7 +106,6 @@ export const VideoDisplay: React.FC = ({ } } }); - hls.on(Hls.Events.BUFFER_APPENDING, () => { setBufferingState(true); }); diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 9a60fd1..e1ae965 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -71,7 +71,7 @@ export const routes: CustomRouteObject[] = [ { path: ":id?/editor", element: ( - + ), diff --git a/packages/common/src/models/index.ts b/packages/common/src/models/index.ts index 2716f11..6273433 100755 --- a/packages/common/src/models/index.ts +++ b/packages/common/src/models/index.ts @@ -1,7 +1,8 @@ -export * from "./department" -export * from "./message" -export * from "./staff" -export * from "./term" -export * from "./post" -export * from "./rbac" -export * from "./select" \ No newline at end of file +export * from "./department"; +export * from "./message"; +export * from "./staff"; +export * from "./term"; +export * from "./post"; +export * from "./rbac"; +export * from "./select"; +export * from "./resource"; diff --git a/packages/common/src/models/post.ts b/packages/common/src/models/post.ts index 0d2bf07..254f8eb 100755 --- a/packages/common/src/models/post.ts +++ b/packages/common/src/models/post.ts @@ -8,6 +8,7 @@ import { } from "@prisma/client"; import { StaffDto } from "./staff"; import { TermDto } from "./term"; +import { ResourceDto } from "./resource"; export type PostComment = { id: string; @@ -51,6 +52,7 @@ export type LectureMeta = { }; export type Lecture = Post & { + resources?: ResourceDto[]; meta?: LectureMeta; }; @@ -79,5 +81,5 @@ export type CourseDto = Course & { sections?: SectionDto[]; terms: TermDto[]; lectureCount?: number; - depts:Department[] + depts: Department[]; }; diff --git a/packages/common/src/models/resource.ts b/packages/common/src/models/resource.ts new file mode 100644 index 0000000..e732d88 --- /dev/null +++ b/packages/common/src/models/resource.ts @@ -0,0 +1,52 @@ +import { Resource } from "@prisma/client"; + +export interface BaseMetadata { + size: number; + filetype: string; + filename: string; + extension: string; + modifiedAt: Date; +} +/** + * 图片特有元数据接口 + */ +export interface ImageMetadata { + width: number; // 图片宽度(px) + height: number; // 图片高度(px) + compressedUrl?: string; + orientation?: number; // EXIF方向信息 + space?: string; // 色彩空间 (如: RGB, CMYK) + hasAlpha?: boolean; // 是否包含透明通道 +} + +/** + * 视频特有元数据接口 + */ +export interface VideoMetadata { + width?: number; + height?: number; + duration?: number; + videoCodec?: string; + audioCodec?: string; + coverUrl?: string; +} + +/** + * 音频特有元数据接口 + */ +export interface AudioMetadata { + duration: number; // 音频时长(秒) + bitrate?: number; // 比特率(bps) + sampleRate?: number; // 采样率(Hz) + channels?: number; // 声道数 + codec?: string; // 音频编码格式 +} + +export type FileMetadata = ImageMetadata & + VideoMetadata & + AudioMetadata & + BaseMetadata; + +export type ResourceDto = Resource & { + meta: FileMetadata; +}; diff --git a/packages/common/src/models/section.ts b/packages/common/src/models/section.ts deleted file mode 100755 index 0824441..0000000 --- a/packages/common/src/models/section.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Lecture, Section } from "@prisma/client"; - -export type SectionDto = Section & { - lectures: Lecture[]; -}; \ No newline at end of file diff --git a/packages/common/src/models/select.ts b/packages/common/src/models/select.ts index a591134..f7fdaf5 100755 --- a/packages/common/src/models/select.ts +++ b/packages/common/src/models/select.ts @@ -79,6 +79,7 @@ export const courseDetailSelect: Prisma.PostSelect = { select: { id: true, name: true, + taxonomyId: true, taxonomy: { select: { id: true, @@ -95,3 +96,15 @@ export const courseDetailSelect: Prisma.PostSelect = { meta: true, rating: true, }; +export const lectureDetailSelect: Prisma.PostSelect = { + id: true, + title: true, + subTitle: true, + content: true, + resources: true, + createdAt: true, + updatedAt: true, + // 关联表选择 + + meta: true, +}; From baa65e0e3f1f061ea84fcf93be72e4301f5fe548 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 16:15:56 +0800 Subject: [PATCH 06/24] add --- apps/server/src/models/post/post.service.ts | 2 +- apps/server/src/models/post/utils.ts | 3 ++- .../models/course/editor/context/CourseEditorContext.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 675dda4..6a9310e 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -163,7 +163,7 @@ export class PostService extends BaseTreeService { await this.setPerms(result, staff); await setCourseInfo({ data: result }); } - + // console.log(result); return result; }, ); diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 811a75f..daa564b 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -128,7 +128,7 @@ export async function updateCourseEnrollmentStats(courseId: string) { export async function setCourseInfo({ data }: { data: Post }) { // await db.term - + console.log(12314243342); if (data?.type === PostType.COURSE) { const ancestries = await db.postAncestry.findMany({ where: { @@ -169,6 +169,7 @@ export async function setCourseInfo({ data }: { data: Post }) { (lecture) => lecture.parentId === section.id, ) as any as Lecture[]; }); + console.log(sections); Object.assign(data, { sections, lectureCount }); } } 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 c8171da..0c8becc 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -111,7 +111,7 @@ export function CourseFormProvider({ delete formattedValues.sections; delete formattedValues.deptIds; - console.log(course?.meta); + console.log(course.meta); console.log(formattedValues?.meta); try { if (editId) { From 317df03dede5308125f59b1f203040543cdd3a68 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 16:23:43 +0800 Subject: [PATCH 07/24] qee --- apps/server/src/models/post/post.service.ts | 2 -- apps/server/src/models/post/utils.ts | 1 + .../models/course/editor/context/CourseEditorContext.tsx | 5 ++--- packages/common/src/enum.ts | 3 +-- packages/common/src/models/select.ts | 1 + 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 6a9310e..1e4c961 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -112,8 +112,6 @@ export class PostService extends BaseTreeService { return await db.$transaction(async (tx) => { const courseParams = { ...params, tx }; // Create the course first - console.log(courseParams?.staff?.id); - console.log('courseDetail', courseDetail); const createdCourse = await this.create(courseDetail, courseParams); // If sections are provided, create them return createdCourse; diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index daa564b..ea05ce6 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -129,6 +129,7 @@ export async function updateCourseEnrollmentStats(courseId: string) { export async function setCourseInfo({ data }: { data: Post }) { // await db.term console.log(12314243342); + console.log(data?.type); if (data?.type === PostType.COURSE) { const ancestries = await db.postAncestry.findMany({ where: { 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 0c8becc..0fd8040 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -91,6 +91,7 @@ export function CourseFormProvider({ const formattedValues = { ...values, + type: PostType.COURSE, meta: { ...((course?.meta as CourseMeta) || {}), ...(values?.meta?.thumbnail !== undefined && { @@ -111,8 +112,6 @@ export function CourseFormProvider({ delete formattedValues.sections; delete formattedValues.deptIds; - console.log(course.meta); - console.log(formattedValues?.meta); try { if (editId) { const result = await update.mutateAsync({ @@ -125,7 +124,7 @@ export function CourseFormProvider({ const result = await createCourse.mutateAsync({ courseDetail: { data: { - title: formattedValues.title || "12345", + title: formattedValues.title, // state: CourseStatus.DRAFT, type: PostType.COURSE, diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 1bcd87c..309ef6d 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -5,7 +5,7 @@ export enum PostType { POST = "post", POST_COMMENT = "post_comment", COURSE_REVIEW = "course_review", - COURSE = "couse", + COURSE = "course", SECTION = "section", LECTURE = "lecture", PATH = "path", @@ -101,7 +101,6 @@ export enum RolePerms { } export enum AppConfigSlug { BASE_SETTING = "base_setting", - } // 资源类型的枚举,定义了不同类型的资源,以字符串值表示 export enum ResourceType { diff --git a/packages/common/src/models/select.ts b/packages/common/src/models/select.ts index f7fdaf5..2e96315 100755 --- a/packages/common/src/models/select.ts +++ b/packages/common/src/models/select.ts @@ -69,6 +69,7 @@ export const courseDetailSelect: Prisma.PostSelect = { id: true, title: true, subTitle: true, + type: true, content: true, depts: true, // isFeatured: true, From 6d2d3ff745b57a9daf01551d5500ed9209914630 Mon Sep 17 00:00:00 2001 From: Rao <1227431568@qq.com> Date: Wed, 26 Feb 2025 16:26:34 +0800 Subject: [PATCH 08/24] rht02261626 --- .../detail/CourseDetailHeader/CourseDetailHeader.tsx | 12 ------------ 1 file changed, 12 deletions(-) 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 f780ddd..401484a 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -39,18 +39,6 @@ export function CourseDetailHeader() { {/* */}
-
- - } - placeholder="搜索课程" - className="w-72 rounded-full" - value={searchValue} - onChange={(e) => setSearchValue(e.target.value)} - /> -
{canEdit && ( <>
}> -
-
- {course?.terms?.map((term) => { - return ( - - {term.name} - - ); - })} +
+
+
+ {course?.terms?.map((term) => { + return ( + <> + + {term.name} + + + ); + })} +
+ 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> @@ -78,8 +79,8 @@ export default function CourseCard({ course }: CourseCardProps) { {/* {course?.depts?.map((dept)=>{return dept.name})} */}
- - {course?.meta?.views + + {course?.meta?.views ? `观看次数 ${course?.meta?.views}` : null} From 754aa9e4963511be586325bd36b01febf6974edb Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 26 Feb 2025 17:04:20 +0800 Subject: [PATCH 11/24] add --- apps/server/src/queue/models/post/utils.ts | 90 ++++++++++++---------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 690a3b0..51d2b04 100644 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -64,9 +64,50 @@ export async function updateTotalCourseViewCount(type: VisitType) { export async function updatePostViewCount(id: string, type: VisitType) { const post = await db.post.findFirst({ 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({ _sum: { views: true, @@ -76,42 +117,13 @@ export async function updatePostViewCount(id: string, type: VisitType) { type: type, }, }); - if (type === VisitType.READED) { - await db.post.update({ - where: { - id: id, + await db.post.update({ + where: { id }, + data: { + 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 - }, - }, - }); - } + }, + }); } From dda7bb6b996d77d073937bee30d11a397a6b2fdf Mon Sep 17 00:00:00 2001 From: Li1304553726 <1304553726@qq.com> Date: Wed, 26 Feb 2025 19:42:48 +0800 Subject: [PATCH 12/24] main Li --- apps/web/src/app/main/courses/components/CourseCard.tsx | 2 +- apps/web/src/app/main/home/components/CoursesSection.tsx | 7 +++---- apps/web/src/app/main/layout/MainLayout.tsx | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index f81c3a3..5de3a9d 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -38,7 +38,7 @@ export default function CourseCard({ course }: CourseCardProps) { }>
-
+
{course?.terms?.map((term) => { return ( <> diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index 23cd420..999e931 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -44,10 +44,9 @@ const CoursesSection: React.FC = ({ type: TaxonomySlug.CATEGORY, }); return ( -
-
-
-
+
+
+
- <Layout className="min-h-screen bg-gray-100"> + <Layout className="min-h-screen"> <MainHeader /> - <Content className="mt-16 bg-gray-200 "> + <Content className="mt-16 bg-gray-50"> <Outlet /> </Content> <MainFooter /> From 57f486ca6eee676e68d6c94352c203591cbfd7fb Mon Sep 17 00:00:00 2001 From: ditiqi <ditiqi@163.com> Date: Wed, 26 Feb 2025 19:49:50 +0800 Subject: [PATCH 13/24] add --- .../app/main/home/components/HeroSection.tsx | 91 ++++++++++++------- .../src/app/main/layout/UserMenu/UserMenu.tsx | 28 ++++-- apps/web/src/app/main/my-duty/page.tsx | 7 ++ apps/web/src/app/main/my-learning/page.tsx | 3 + packages/common/prisma/schema.prisma | 8 -- 5 files changed, 88 insertions(+), 49 deletions(-) create mode 100644 apps/web/src/app/main/my-duty/page.tsx create mode 100644 apps/web/src/app/main/my-learning/page.tsx diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx index eca460d..4073109 100755 --- a/apps/web/src/app/main/home/components/HeroSection.tsx +++ b/apps/web/src/app/main/home/components/HeroSection.tsx @@ -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 { TeamOutlined, @@ -30,13 +36,29 @@ interface PlatformStat { const HeroSection = () => { const carouselRef = useRef<CarouselRef>(null); const { statistics, slides } = useAppConfig(); - const [countStatistics, setCountStatistics] = useState<number>(4) + const [countStatistics, setCountStatistics] = useState<number>(4); const platformStats: PlatformStat[] = useMemo(() => { return [ - { icon: <TeamOutlined />, value: statistics.staffs, label: "注册学员" }, - { icon: <StarOutlined />, value: statistics.courses, label: "精品课程" }, - { icon: <BookOutlined />, value: statistics.lectures, label: '课程章节' }, - { icon: <EyeOutlined />, value: statistics.reads, label: "观看次数" }, + { + icon: <TeamOutlined />, + value: statistics.staffs, + label: "注册学员", + }, + { + icon: <StarOutlined />, + value: statistics.courses, + label: "精品课程", + }, + { + icon: <BookOutlined />, + value: statistics.lectures, + label: "课程章节", + }, + { + icon: <EyeOutlined />, + value: statistics.reads, + label: "观看次数", + }, ]; }, [statistics]); const handlePrev = useCallback(() => { @@ -48,7 +70,7 @@ const HeroSection = () => { }, []); 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(() => { @@ -67,8 +89,8 @@ const HeroSection = () => { dots={{ className: "carousel-dots !bottom-32 !z-20", }}> - {Array.isArray(slides) ? - (slides.map((item, index) => ( + {Array.isArray(slides) ? ( + slides.map((item, index) => ( <div key={index} className="relative h-[600px]"> <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]" @@ -87,9 +109,9 @@ const HeroSection = () => { <div className="relative h-full max-w-7xl mx-auto px-6 lg:px-8"></div> </div> )) - ) : ( - <div></div> - )} + ) : ( + <div></div> + )} </Carousel> {/* Navigation Buttons */} @@ -108,31 +130,30 @@ const HeroSection = () => { </div> {/* Stats Container */} - { - countStatistics > 1 && ( - <div className="absolute -bottom-20 left-1/2 -translate-x-1/2 w-3/5 max-w-6xl px-4"> - <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]`}> - {platformStats.map((stat, index) => { - return stat.value - ? (<div - key={index} - 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"> - {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> + {countStatistics > 1 && ( + <div className="absolute -bottom-20 left-1/2 -translate-x-1/2 w-3/5 max-w-6xl px-4"> + <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]`}> + {platformStats.map((stat, index) => { + return stat.value ? ( + <div + key={index} + 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"> + {stat.icon} </div> - ) : null - })} - </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> + ) : null; + })} </div> - ) - } + </div> + )} </section> ); }; diff --git a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx index ea23902..41da474 100755 --- a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx @@ -86,6 +86,20 @@ export function UserMenu() { setModalOpen(true); }, }, + { + icon: <UserOutlined className="text-lg" />, + label: "我创建的课程", + action: () => { + setModalOpen(true); + }, + }, + { + icon: <UserOutlined className="text-lg" />, + label: "我学习的课程", + action: () => { + setModalOpen(true); + }, + }, canManageAnyStaff && { icon: <SettingOutlined className="text-lg" />, label: "设置", @@ -222,18 +236,20 @@ export function UserMenu() { focus:ring-2 focus:ring-[#00538E]/20 group relative overflow-hidden active:scale-[0.99] - ${item.label === "注销" + ${ + item.label === "注销" ? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700" : "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]" - }`}> + }`}> <span className={`w-5 h-5 flex items-center justify-center transition-all duration-200 ease-in-out group-hover:scale-110 group-hover:rotate-6 - group-hover:translate-x-0.5 ${item.label === "注销" - ? "group-hover:text-red-600" - : "group-hover:text-[#003F6A]" - }`}> + group-hover:translate-x-0.5 ${ + item.label === "注销" + ? "group-hover:text-red-600" + : "group-hover:text-[#003F6A]" + }`}> {item.icon} </span> <span>{item.label}</span> diff --git a/apps/web/src/app/main/my-duty/page.tsx b/apps/web/src/app/main/my-duty/page.tsx new file mode 100644 index 0000000..7871969 --- /dev/null +++ b/apps/web/src/app/main/my-duty/page.tsx @@ -0,0 +1,7 @@ +export default function MyDutyPage() { + + + + return <> + </> +} diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx new file mode 100644 index 0000000..503600c --- /dev/null +++ b/apps/web/src/app/main/my-learning/page.tsx @@ -0,0 +1,3 @@ +export default function MyLearningPage() { + return <></>; +} diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 1c245e2..236c4ab 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -288,14 +288,6 @@ model Visit { message Message? @relation(fields: [messageId], references: [id]) messageId String? @map("message_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") // 创建时间 updatedAt DateTime @updatedAt @map("updated_at") // 更新时间 deletedAt DateTime? @map("deleted_at") // 删除时间,可为空 From 54f2ed407fc406c32850ac592f67be72c820b85d Mon Sep 17 00:00:00 2001 From: Rao <1227431568@qq.com> Date: Wed, 26 Feb 2025 20:00:15 +0800 Subject: [PATCH 14/24] rht02262000 --- .../course/detail/CourseSyllabus/CourseSyllabus.tsx | 1 - .../course/detail/CourseSyllabus/LectureItem.tsx | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) 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 9e6cf84..10b756a 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx @@ -45,7 +45,6 @@ export const CourseSyllabus: React.FC<CourseSyllabusProps> = ({ block: "start", }); }; - return ( <> {/* 收起按钮直接显示 */} 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 ffc0a49..6bdc9b3 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -4,6 +4,7 @@ import { Lecture, LectureType, LessonTypeLabel } from "@nice/common"; import React, { useMemo } from "react"; import { ClockCircleOutlined, + EyeOutlined, FileTextOutlined, PlayCircleOutlined, } from "@ant-design/icons"; // 使用 Ant Design 图标 @@ -43,13 +44,17 @@ export const LectureItem: React.FC<LectureItemProps> = ({ <span>{LessonTypeLabel[lecture?.meta?.type]}</span> </div> )} - <div className="flex-grow"> + <div className="flex-grow flex justify-between items-center w-2/3 realative"> <h4 className="font-medium text-gray-800">{lecture.title}</h4> {lecture.subTitle && ( - <p className="text-sm text-gray-500 mt-1"> + <span className="text-sm text-gray-500 mt-1"> {lecture.subTitle} - </p> + </span> )} + <div className="text-gray-500 whitespace-normal"> + <EyeOutlined></EyeOutlined> + <span className="ml-2">{lecture?.meta?.views}</span> + </div> </div> </div> ); From 4a6957f1814f4125800baf6b6bfed4f508b40da3 Mon Sep 17 00:00:00 2001 From: ditiqi <ditiqi@163.com> Date: Wed, 26 Feb 2025 21:08:38 +0800 Subject: [PATCH 15/24] addd --- apps/server/src/models/post/post.service.ts | 60 +++++++++---------- apps/server/src/queue/models/post/utils.ts | 2 +- .../main/courses/components/CourseCard.tsx | 22 ++++--- .../src/app/main/layout/NavigationMenu.tsx | 27 +++++++-- .../src/app/main/layout/UserMenu/UserMenu.tsx | 4 +- apps/web/src/app/main/my-duty/page.tsx | 24 ++++++-- apps/web/src/app/main/my-learning/page.tsx | 20 ++++++- .../models/course/list/CourseList.tsx | 14 ++++- apps/web/src/routes/index.tsx | 21 ++++++- packages/common/prisma/schema.prisma | 13 ++-- packages/common/src/models/select.ts | 4 +- 11 files changed, 147 insertions(+), 64 deletions(-) diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 6964696..8f90a85 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -306,37 +306,37 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> { staff?.id && { authorId: staff.id, }, - staff?.id && { - watchableStaffs: { - some: { - id: staff.id, - }, - }, - }, - deptId && { - watchableDepts: { - some: { - id: { - in: parentDeptIds, - }, - }, - }, - }, + // staff?.id && { + // watchableStaffs: { + // some: { + // id: staff.id, + // }, + // }, + // }, + // deptId && { + // watchableDepts: { + // some: { + // id: { + // in: parentDeptIds, + // }, + // }, + // }, + // }, - { - AND: [ - { - watchableStaffs: { - none: {}, // 匹配 watchableStaffs 为空 - }, - }, - { - watchableDepts: { - none: {}, // 匹配 watchableDepts 为空 - }, - }, - ], - }, + // { + // AND: [ + // { + // watchableStaffs: { + // none: {}, // 匹配 watchableStaffs 为空 + // }, + // }, + // { + // watchableDepts: { + // none: {}, // 匹配 watchableDepts 为空 + // }, + // }, + // ], + // }, ].filter(Boolean); if (orCondition?.length > 0) return orCondition; diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 51d2b04..51bffca 100644 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -23,7 +23,7 @@ export async function updateTotalCourseViewCount(type: VisitType) { views: true, }, where: { - postId: { in: lectures.map((lecture) => lecture.id) }, + postId: { in: posts.map((post) => post.id) }, type: type, }, }); diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 5de3a9d..3dd5032 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -9,13 +9,18 @@ import { useNavigate } from "react-router-dom"; interface CourseCardProps { course: CourseDto; + edit?: boolean; } const { Title, Text } = Typography; -export default function CourseCard({ course }: CourseCardProps) { +export default function CourseCard({ course, edit = false }: CourseCardProps) { const navigate = useNavigate(); const handleClick = (course: CourseDto) => { - navigate(`/course/${course.id}/detail`); - window.scrollTo({ top: 0, behavior: "smooth", }) + if (!edit) { + navigate(`/course/${course.id}/detail`); + } else { + navigate(`/course/${course.id}/editor`); + } + window.scrollTo({ top: 0, behavior: "smooth" }); }; return ( <Card @@ -46,10 +51,10 @@ export default function CourseCard({ course }: CourseCardProps) { key={term.id} color={ term?.taxonomy?.slug === - TaxonomySlug.CATEGORY + TaxonomySlug.CATEGORY ? "blue" : term?.taxonomy?.slug === - TaxonomySlug.LEVEL + TaxonomySlug.LEVEL ? "green" : "orange" } @@ -80,9 +85,8 @@ export default function CourseCard({ course }: CourseCardProps) { </Text> </div> <span className="text-xs font-medium text-gray-500 flex items-center"> - <EyeOutlined className="mr-1" />{course?.meta?.views - ? `观看次数 ${course?.meta?.views}` - : null} + <EyeOutlined className="mr-1" /> + {`观看次数 ${course?.meta?.views || 0}`} </span> </div> <div className="pt-4 border-t border-gray-100 text-center"> @@ -91,7 +95,7 @@ export default function CourseCard({ course }: CourseCardProps) { size="large" className="w-full shadow-[0_8px_20px_-6px_rgba(59,130,246,0.5)] hover:shadow-[0_12px_24px_-6px_rgba(59,130,246,0.6)] transform hover:translate-y-[-2px] transition-all duration-500 ease-out"> - 立即学习 + {edit ? "进行编辑" : "立即学习"} </Button> </div> </div> diff --git a/apps/web/src/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx index 53efdb3..6f0f5de 100755 --- a/apps/web/src/app/main/layout/NavigationMenu.tsx +++ b/apps/web/src/app/main/layout/NavigationMenu.tsx @@ -1,15 +1,30 @@ +import { useAuth } from "@web/src/providers/auth-provider"; import { Menu } from "antd"; +import { useMemo } from "react"; import { useNavigate, useLocation } from "react-router-dom"; -const menuItems = [ - { key: "home", path: "/", label: "首页" }, - { key: "courses", path: "/courses", label: "全部课程" }, - { key: "paths", path: "/paths", label: "学习路径" }, -]; - export const NavigationMenu = () => { const navigate = useNavigate(); + const { isAuthenticated } = useAuth(); const { pathname } = useLocation(); + + const menuItems = useMemo(() => { + const baseItems = [ + { key: "home", path: "/", label: "首页" }, + { key: "courses", path: "/courses", label: "全部课程" }, + { key: "paths", path: "/paths", label: "学习路径" }, + ]; + if (!isAuthenticated) { + return baseItems; + } else { + return [ + ...baseItems, + { key: "my-duty", path: "/my-duty", label: "我创建的" }, + { key: "my-learning", path: "/my-learning", label: "我学习的" }, + ]; + } + }, [isAuthenticated]); + const selectedKey = menuItems.find((item) => item.path === pathname)?.key || ""; return ( diff --git a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx index 41da474..e3ef21f 100755 --- a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx @@ -90,14 +90,14 @@ export function UserMenu() { icon: <UserOutlined className="text-lg" />, label: "我创建的课程", action: () => { - setModalOpen(true); + navigate("/my/duty"); }, }, { icon: <UserOutlined className="text-lg" />, label: "我学习的课程", action: () => { - setModalOpen(true); + navigate("/my/learning"); }, }, canManageAnyStaff && { diff --git a/apps/web/src/app/main/my-duty/page.tsx b/apps/web/src/app/main/my-duty/page.tsx index 7871969..fd33761 100644 --- a/apps/web/src/app/main/my-duty/page.tsx +++ b/apps/web/src/app/main/my-duty/page.tsx @@ -1,7 +1,21 @@ +import CourseList from "@web/src/components/models/course/list/CourseList"; +import { useAuth } from "@web/src/providers/auth-provider"; + export default function MyDutyPage() { - - - - return <> - </> + const { user } = useAuth(); + return ( + <> + <div className="p-4"> + <CourseList + edit + params={{ + pageSize: 12, + where: { + authorId: user.id, + }, + }} + cols={4}></CourseList> + </div> + </> + ); } diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx index 503600c..0216515 100644 --- a/apps/web/src/app/main/my-learning/page.tsx +++ b/apps/web/src/app/main/my-learning/page.tsx @@ -1,3 +1,21 @@ +import CourseList from "@web/src/components/models/course/list/CourseList"; +import { useAuth } from "@web/src/providers/auth-provider"; + export default function MyLearningPage() { - return <></>; + const { user } = useAuth(); + return ( + <> + <div className="p-4"> + <CourseList + edit + params={{ + pageSize: 12, + where: { + authorId: user.id, + }, + }} + cols={4}></CourseList> + </div> + </> + ); } diff --git a/apps/web/src/components/models/course/list/CourseList.tsx b/apps/web/src/components/models/course/list/CourseList.tsx index 3ba3eaf..2111a3a 100755 --- a/apps/web/src/components/models/course/list/CourseList.tsx +++ b/apps/web/src/components/models/course/list/CourseList.tsx @@ -13,6 +13,7 @@ interface CourseListProps { }; cols?: number; showPagination?: boolean; + edit?: boolean; } interface CoursesPagnationProps { data: { @@ -25,6 +26,7 @@ export default function CourseList({ params, cols = 3, showPagination = true, + edit = false, }: CourseListProps) { const [currentPage, setCurrentPage] = useState<number>(params?.page || 1); const { data, isLoading }: CoursesPagnationProps = @@ -55,7 +57,11 @@ export default function CourseList({ window.scrollTo({ top: 0, behavior: "smooth" }); } if (isLoading) { - return <Skeleton paragraph={{ rows: 10 }}></Skeleton>; + return ( + <div className="space-y-6"> + <Skeleton paragraph={{ rows: 10 }}></Skeleton> + </div> + ); } return ( <div className="space-y-6"> @@ -66,7 +72,11 @@ export default function CourseList({ <Skeleton paragraph={{ rows: 5 }}></Skeleton> ) : ( courses.map((course) => ( - <CourseCard key={course.id} course={course} /> + <CourseCard + edit={edit} + key={course.id} + course={course} + /> )) )} </div> diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index e1ae965..1ba39aa 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -18,6 +18,8 @@ import CoursesPage from "../app/main/courses/page"; import PathsPage from "../app/main/paths/page"; import { adminRoute } from "./admin-route"; import { CoursePreview } from "../app/main/course/preview/page"; +import MyLearningPage from "../app/main/my-learning/page"; +import MyDutyPage from "../app/main/my-duty/page"; interface CustomIndexRouteObject extends IndexRouteObject { name?: string; breadcrumb?: string; @@ -63,15 +65,32 @@ export const routes: CustomRouteObject[] = [ path: "courses", element: <CoursesPage></CoursesPage>, }, + { + path: "my-duty", + element: ( + <WithAuth> + <MyDutyPage></MyDutyPage> + </WithAuth> + ), + }, + { + path: "my-learning", + element: ( + <WithAuth> + <MyLearningPage></MyLearningPage> + </WithAuth> + ), + }, ], }, + { path: "course", children: [ { path: ":id?/editor", element: ( - <WithAuth > + <WithAuth> <CourseEditorLayout></CourseEditorLayout> </WithAuth> ), diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 236c4ab..75e8be6 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -88,9 +88,12 @@ model Staff { deletedAt DateTime? @map("deleted_at") officerId String? @map("officer_id") - watchedPost Post[] @relation("post_watch_staff") + // watchedPost Post[] @relation("post_watch_staff") visits Visit[] posts Post[] + + + learningPost Post[] @relation("post_student") sentMsgs Message[] @relation("message_sender") receivedMsgs Message[] @relation("message_receiver") registerToken String? @@ -124,7 +127,7 @@ model Department { deptStaffs Staff[] @relation("DeptStaff") terms Term[] @relation("department_term") - watchedPost Post[] @relation("post_watch_dept") + // watchedPost Post[] @relation("post_watch_dept") hasChildren Boolean? @default(false) @map("has_children") @@index([parentId]) @@ -201,7 +204,7 @@ model Post { order Float? @default(0) @map("order") duration Int? rating Int? @default(0) - + students Staff[] @relation("post_student") depts Department[] @relation("post_dept") // 索引 // 日期时间类型字段 @@ -223,8 +226,8 @@ model Post { ancestors PostAncestry[] @relation("DescendantPosts") descendants PostAncestry[] @relation("AncestorPosts") resources Resource[] // 附件列表 - watchableStaffs Staff[] @relation("post_watch_staff") // 可观看的员工列表,关联 Staff 模型 - watchableDepts Department[] @relation("post_watch_dept") // 可观看的部门列表,关联 Department 模型 + // watchableStaffs Staff[] @relation("post_watch_staff") + // watchableDepts Department[] @relation("post_watch_dept") // 可观看的部门列表,关联 Department 模型 meta Json? // 封面url 视频url objectives具体的学习目标 rating评分Int // 索引 diff --git a/packages/common/src/models/select.ts b/packages/common/src/models/select.ts index 2e96315..38fd3bf 100755 --- a/packages/common/src/models/select.ts +++ b/packages/common/src/models/select.ts @@ -6,8 +6,8 @@ export const postDetailSelect: Prisma.PostSelect = { title: true, content: true, resources: true, - watchableDepts: true, - watchableStaffs: true, + // watchableDepts: true, + // watchableStaffs: true, updatedAt: true, author: { select: { From 5f140e7e33d6af7347e2ec77504a9ca749ab22bc Mon Sep 17 00:00:00 2001 From: Rao <1227431568@qq.com> Date: Wed, 26 Feb 2025 21:08:57 +0800 Subject: [PATCH 16/24] rht --- apps/web/src/app/main/courses/components/FilterSection.tsx | 2 +- .../models/course/detail/CourseSyllabus/LectureItem.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx index 01ffba6..d594d4c 100755 --- a/apps/web/src/app/main/courses/components/FilterSection.tsx +++ b/apps/web/src/app/main/courses/components/FilterSection.tsx @@ -19,7 +19,7 @@ export default function FilterSection() { }); }; return ( - <div className="bg-white p-6 rounded-lg shadow-sm space-y-6 h-full"> + <div className="bg-white p-6 rounded-lg shadow-sm w-1/6 space-y-6 h-full fixed"> {taxonomies?.map((tax, index) => { const items = Object.entries(selectedTerms).find( ([key, items]) => key === tax.slug 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 6bdc9b3..72393db 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -53,7 +53,7 @@ export const LectureItem: React.FC<LectureItemProps> = ({ )} <div className="text-gray-500 whitespace-normal"> <EyeOutlined></EyeOutlined> - <span className="ml-2">{lecture?.meta?.views}</span> + <span className="ml-2">{lecture?.meta?.views ? lecture?.meta?.views : 0}</span> </div> </div> </div> From 5872f4b7280ccc91ad47067d2cd3239d8f6ac11e Mon Sep 17 00:00:00 2001 From: ditiqi <ditiqi@163.com> Date: Wed, 26 Feb 2025 22:03:07 +0800 Subject: [PATCH 17/24] add --- apps/server/src/models/post/post.service.ts | 15 ++------------- apps/server/src/models/post/utils.ts | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 8f90a85..947ecfc 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -166,19 +166,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> { ); return transDto; } - // async findMany(args: Prisma.PostFindManyArgs, staff?: UserProfile) { - // if (!args.where) args.where = {}; - // args.where.OR = await this.preFilter(args.where.OR, staff); - // return this.wrapResult(super.findMany(args), async (result) => { - // await Promise.all( - // result.map(async (item) => { - // await setPostRelation({ data: item, staff }); - // await this.setPerms(item, staff); - // }), - // ); - // return { ...result }; - // }); - // } + async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) { if (!args.where) args.where = {}; args.where.OR = await this.preFilter(args.where.OR, staff); @@ -255,6 +243,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> { // 批量执行更新 return updates.length > 0 ? await db.$transaction(updates) : []; } + 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 0e1f835..57b0fd0 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -168,6 +168,21 @@ export async function setCourseInfo({ data }: { data: Post }) { (lecture) => lecture.parentId === section.id, ) as any as Lecture[]; }); - Object.assign(data, { sections, lectureCount }); + + const students = await db.staff.findMany({ + where: { + learningPosts: { + some: { + id: data.id, + }, + }, + }, + select: { + id: true, + }, + }); + + const studentIds = (students || []).map((student) => student?.id); + Object.assign(data, { sections, lectureCount, studentIds }); } } From 4ec92966ab09bc3f32c6665ce39df763fa8d0d69 Mon Sep 17 00:00:00 2001 From: ditiqi <ditiqi@163.com> Date: Wed, 26 Feb 2025 22:03:18 +0800 Subject: [PATCH 18/24] add --- packages/common/prisma/schema.prisma | 2 +- packages/common/src/models/post.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index 75e8be6..db764d2 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -93,7 +93,7 @@ model Staff { posts Post[] - learningPost Post[] @relation("post_student") + learningPosts Post[] @relation("post_student") sentMsgs Message[] @relation("message_sender") receivedMsgs Message[] @relation("message_receiver") registerToken String? diff --git a/packages/common/src/models/post.ts b/packages/common/src/models/post.ts index a10fcf5..63a38b0 100755 --- a/packages/common/src/models/post.ts +++ b/packages/common/src/models/post.ts @@ -83,4 +83,5 @@ export type CourseDto = Course & { terms: TermDto[]; lectureCount?: number; depts: Department[]; + studentIds: string[]; }; From 4c89a43197605f4c2324a130ba1337a2cc57d495 Mon Sep 17 00:00:00 2001 From: ditiqi <ditiqi@163.com> Date: Wed, 26 Feb 2025 22:03:25 +0800 Subject: [PATCH 19/24] add --- packages/client/src/api/hooks/useStaff.ts | 67 +++++++++++++---------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/packages/client/src/api/hooks/useStaff.ts b/packages/client/src/api/hooks/useStaff.ts index fbdf571..aed3aa7 100755 --- a/packages/client/src/api/hooks/useStaff.ts +++ b/packages/client/src/api/hooks/useStaff.ts @@ -5,39 +5,48 @@ import { ObjectType, Staff } from "@nice/common"; import { findQueryData } from "../utils"; import { CrudOperation, emitDataChange } from "../../event"; export function useStaff() { - const queryClient = useQueryClient(); - const queryKey = getQueryKey(api.staff); + const queryClient = useQueryClient(); + const queryKey = getQueryKey(api.staff); - const create = api.staff.create.useMutation({ - onSuccess: (result) => { - queryClient.invalidateQueries({ queryKey }); - emitDataChange(ObjectType.STAFF, result as any, CrudOperation.CREATED) - }, - }); - const updateUserDomain = api.staff.updateUserDomain.useMutation({ - onSuccess: async (result) => { - queryClient.invalidateQueries({ queryKey }); - }, - }); - const update = api.staff.update.useMutation({ - onSuccess: (result) => { - queryClient.invalidateQueries({ queryKey }); - emitDataChange(ObjectType.STAFF, result as any, CrudOperation.UPDATED) - }, - }); + const create = api.staff.create.useMutation({ + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey }); + emitDataChange( + ObjectType.STAFF, + result as any, + CrudOperation.CREATED + ); + }, + }); + const updateUserDomain = api.staff.updateUserDomain.useMutation({ + onSuccess: async (result) => { + queryClient.invalidateQueries({ queryKey }); + }, + }); + const update = api.staff.update.useMutation({ + onSuccess: (result) => { + queryClient.invalidateQueries({ queryKey }); + queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) }); + emitDataChange( + ObjectType.STAFF, + result as any, + CrudOperation.UPDATED + ); + }, + }); const softDeleteByIds = api.staff.softDeleteByIds.useMutation({ onSuccess: (result, variables) => { queryClient.invalidateQueries({ queryKey }); }, }); - const getStaff = (key: string) => { - return findQueryData<Staff>(queryClient, api.staff, key); - }; - return { - create, - update, - softDeleteByIds, - getStaff, - updateUserDomain - }; + const getStaff = (key: string) => { + return findQueryData<Staff>(queryClient, api.staff, key); + }; + return { + create, + update, + softDeleteByIds, + getStaff, + updateUserDomain, + }; } From a7bbc88e994c515c67ecb8d60bb3903472129df0 Mon Sep 17 00:00:00 2001 From: ditiqi <ditiqi@163.com> Date: Wed, 26 Feb 2025 22:03:29 +0800 Subject: [PATCH 20/24] add --- apps/web/src/app/main/my-learning/page.tsx | 7 +- .../course/detail/CourseDetailContext.tsx | 23 +++--- .../CourseDetailHeader/CourseDetailHeader.tsx | 61 +++++++++++---- .../CourseDetailHeader_BACKUP.tsx | 77 ------------------- .../editor/context/CourseEditorContext.tsx | 4 +- .../CourseContentForm/SortableLecture.tsx | 4 +- 6 files changed, 64 insertions(+), 112 deletions(-) delete mode 100755 apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx diff --git a/apps/web/src/app/main/my-learning/page.tsx b/apps/web/src/app/main/my-learning/page.tsx index 0216515..8807119 100644 --- a/apps/web/src/app/main/my-learning/page.tsx +++ b/apps/web/src/app/main/my-learning/page.tsx @@ -7,11 +7,14 @@ export default function MyLearningPage() { <> <div className="p-4"> <CourseList - edit params={{ pageSize: 12, where: { - authorId: user.id, + students: { + some: { + id: user?.id, + }, + }, }, }} cols={4}></CourseList> diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index b722f3c..0da1702 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -28,6 +28,7 @@ interface CourseDetailContextType { isHeaderVisible: boolean; // 新增 setIsHeaderVisible: (visible: boolean) => void; // 新增 canEdit?: boolean; + userIsLearning?: boolean; } interface CourseFormProviderProps { @@ -43,30 +44,25 @@ export function CourseDetailProvider({ }: CourseFormProviderProps) { const navigate = useNavigate(); const { read } = useVisitor(); - const { user, hasSomePermissions } = useAuth(); + const { user, hasSomePermissions, isAuthenticated } = useAuth(); const { lectureId } = useParams(); const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = (api.post as any).findFirst.useQuery( { where: { id: editId }, - // include: { - // // sections: { include: { lectures: true } }, - // enrollments: true, - // terms:true - // }, - - select:courseDetailSelect + select: courseDetailSelect, }, { enabled: Boolean(editId) } ); + + const userIsLearning = useMemo(() => { + return (course?.studentIds || []).includes(user?.id); + }, [user, course, isLoading]); const canEdit = useMemo(() => { - const isAuthor = user?.id === course?.authorId; - const isDept = course?.depts - ?.map((dept) => dept.id) - .includes(user?.deptId); + const isAuthor = isAuthenticated && user?.id === course?.authorId; const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST); - return isAuthor || isDept || isRoot; + return isAuthor || isRoot; }, [user, course]); const [selectedLectureId, setSelectedLectureId] = useState< string | undefined @@ -109,6 +105,7 @@ export function CourseDetailProvider({ isHeaderVisible, setIsHeaderVisible, canEdit, + userIsLearning, }}> {children} </CourseDetailContext.Provider> 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 401484a..e07ad48 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -10,17 +10,18 @@ import { useAuth } from "@web/src/providers/auth-provider"; import { useNavigate, useParams } from "react-router-dom"; import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu"; import { CourseDetailContext } from "../CourseDetailContext"; - +import { usePost, useStaff } from "@nice/client"; +import toast from "react-hot-toast"; const { Header } = Layout; export function CourseDetailHeader() { - const [searchValue, setSearchValue] = useState(""); const { id } = useParams(); const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } = useAuth(); const navigate = useNavigate(); - const { course, canEdit } = useContext(CourseDetailContext); + const { course, canEdit, userIsLearning } = useContext(CourseDetailContext); + const { update } = useStaff(); return ( <Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30"> @@ -39,20 +40,48 @@ export function CourseDetailHeader() { {/* <NavigationMenu /> */} </div> <div className="flex items-center space-x-6"> + {isAuthenticated && ( + <Button + onClick={async () => { + if (!userIsLearning) { + await update.mutateAsync({ + where: { id: user?.id }, + data: { + learningPosts: { + connect: { id: course.id }, + }, + }, + }); + } else { + await update.mutateAsync({ + where: { id: user?.id }, + data: { + learningPosts: { + disconnect: { + id: course.id, + }, + }, + }, + }); + } + }} + className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all" + icon={<EditFilled />}> + {userIsLearning ? "退出学习" : "加入学习"} + </Button> + )} {canEdit && ( - <> - <Button - onClick={() => { - const url = id - ? `/course/${id}/editor` - : "/course/editor"; - navigate(url); - }} - className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all" - icon={<EditFilled />}> - {"编辑课程"} - </Button> - </> + <Button + onClick={() => { + const url = id + ? `/course/${id}/editor` + : "/course/editor"; + navigate(url); + }} + className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all" + icon={<EditFilled />}> + {"编辑课程"} + </Button> )} {isAuthenticated ? ( <UserMenu /> diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx deleted file mode 100755 index 0fc9815..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx +++ /dev/null @@ -1,77 +0,0 @@ -// // components/Header.tsx -// import { motion, useScroll, useTransform } from "framer-motion"; -// import { useContext, useEffect, useState } from "react"; -// import { CourseDetailContext } from "../CourseDetailContext"; -// import { Avatar, Button, Dropdown } from "antd"; -// import { UserOutlined } from "@ant-design/icons"; -// import { UserMenu } from "@web/src/app/main/layout/UserMenu"; -// import { useAuth } from "@web/src/providers/auth-provider"; - -// export const CourseDetailHeader = () => { -// const { scrollY } = useScroll(); -// const { user, isAuthenticated } = useAuth(); -// const [lastScrollY, setLastScrollY] = useState(0); -// const { course, isHeaderVisible, setIsHeaderVisible, lecture } = -// useContext(CourseDetailContext); -// useEffect(() => { -// const updateHeader = () => { -// const current = scrollY.get(); -// const direction = current > lastScrollY ? "down" : "up"; - -// if (direction === "down" && current > 100) { -// setIsHeaderVisible(false); -// } else if (direction === "up") { -// setIsHeaderVisible(true); -// } - -// setLastScrollY(current); -// }; - -// // 使用 requestAnimationFrame 来优化性能 -// const unsubscribe = scrollY.on("change", () => { -// requestAnimationFrame(updateHeader); -// }); - -// return () => { -// unsubscribe(); -// }; -// }, [lastScrollY, scrollY, setIsHeaderVisible]); - -// return ( -// <motion.header -// initial={{ y: 0 }} -// animate={{ y: isHeaderVisible ? 0 : -100 }} -// transition={{ type: "spring", stiffness: 300, damping: 30 }} -// className="fixed top-0 left-0 w-full h-16 bg-slate-900 backdrop-blur-sm z-50 shadow-sm"> -// <div className="w-full mx-auto px-4 h-full flex items-center justify-between"> -// <div className="flex items-center space-x-4"> -// <h1 className="text-white text-xl ">{course?.title}</h1> -// </div> - -// {isAuthenticated ? ( -// <Dropdown -// overlay={<UserMenu />} -// trigger={["click"]} -// placement="bottomRight"> -// <Avatar -// size="large" -// className="cursor-pointer hover:scale-105 transition-all bg-gradient-to-r from-blue-500 to-blue-600 text-white font-semibold"> -// {(user?.showname || -// user?.username || -// "")[0]?.toUpperCase()} -// </Avatar> -// </Dropdown> -// ) : ( -// <Button -// onClick={() => navigator("/login")} -// className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all" -// icon={<UserOutlined />}> -// 登录 -// </Button> -// )} -// </div> -// </motion.header> -// ); -// }; - -// export default CourseDetailHeader; 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 0fd8040..88571a3 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -99,10 +99,10 @@ export function CourseFormProvider({ }), }, terms: { - connect: termIds.map((id) => ({ id })), // 转换成 connect 格式 + set: termIds.map((id) => ({ id })), // 转换成 connect 格式 }, depts: { - connect: deptIds.map((id) => ({ id })), + set: deptIds.map((id) => ({ id })), }, }; // 删除原始的 taxonomy 字段 diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx index 26282d2..a153137 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx @@ -83,7 +83,7 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({ : undefined, }, resources: { - connect: [videoUrlId, ...fileIds] + set: [videoUrlId, ...fileIds] .filter(Boolean) .map((fileId) => ({ fileId, @@ -109,7 +109,7 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({ : undefined, }, resources: { - connect: [videoUrlId, ...fileIds] + set: [videoUrlId, ...fileIds] .filter(Boolean) .map((fileId) => ({ fileId, From ed85a700a4d5213c040dec39116b1d418c2a98b6 Mon Sep 17 00:00:00 2001 From: ditiqi <ditiqi@163.com> Date: Wed, 26 Feb 2025 22:38:28 +0800 Subject: [PATCH 21/24] add --- .../main/courses/components/CourseCard.tsx | 13 +++- .../course/detail/CourseDetailDescription.tsx | 74 +++++++++++-------- .../course/editor/form/CourseBasicForm.tsx | 2 +- .../CourseContentForm/SortableLecture.tsx | 4 +- 4 files changed, 55 insertions(+), 38 deletions(-) diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 3dd5032..acb4e9d 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -1,5 +1,6 @@ import { Card, Tag, Typography, Button } from "antd"; import { + BookOutlined, EyeOutlined, PlayCircleOutlined, TeamOutlined, @@ -73,10 +74,10 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) { <button> {course.title}</button> -
+
- + {course?.depts?.length > 1 ? `${course.depts[0].name}等` : course?.depts?.[0]?.name} @@ -84,10 +85,16 @@ export default function CourseCard({ course, edit = false }: CourseCardProps) { {/* {course?.depts?.map((dept)=>{return dept.name})} */}
+
+
- + {`观看次数 ${course?.meta?.views || 0}`} + + + {`学习人数 ${course?.studentIds?.length || 0}`} +