diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts index f7f679c..49d25c1 100755 --- a/apps/server/src/auth/auth.service.ts +++ b/apps/server/src/auth/auth.service.ts @@ -32,7 +32,7 @@ export class AuthService { return { isValid: false, error: FileValidationErrorType.INVALID_URI }; } const fileId = extractFileIdFromNginxUrl(params.originalUri); - console.log(params.originalUri, fileId); + // console.log(params.originalUri, fileId); const resource = await db.resource.findFirst({ where: { fileId } }); // 资源验证 diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 675dda4..6964696 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -112,15 +112,12 @@ 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; }); } // If transaction is provided, use it directly - console.log('courseDetail', courseDetail); const createdCourse = await this.create(courseDetail, params); // If sections are provided, create them return createdCourse; @@ -163,7 +160,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 6ca6139..0e1f835 100755 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -1,6 +1,7 @@ import { db, EnrollmentStatus, + Lecture, Post, PostType, SectionDto, @@ -127,7 +128,6 @@ export async function updateCourseEnrollmentStats(courseId: string) { export async function setCourseInfo({ data }: { data: Post }) { // await db.term - if (data?.type === PostType.COURSE) { const ancestries = await db.postAncestry.findMany({ where: { @@ -144,28 +144,29 @@ export async function setCourseInfo({ data }: { data: Post }) { }, }); const descendants = ancestries.map((ancestry) => ancestry.descendant); - const sections: SectionDto[] = descendants - .filter((descendant) => { + const sections: SectionDto[] = ( + descendants.filter((descendant) => { return ( descendant.type === PostType.SECTION && descendant.parentId === data.id ); - }) - .map((section) => ({ - ...section, - lectures: [], - })); + }) as any + ).map((section) => ({ + ...section, + lectures: [], + })); const lectures = descendants.filter((descendant) => { return ( descendant.type === PostType.LECTURE && sections.map((section) => section.id).includes(descendant.parentId) ); }); + const lectureCount = lectures?.length || 0; sections.forEach((section) => { section.lectures = lectures.filter( (lecture) => lecture.parentId === section.id, - ); + ) as any as Lecture[]; }); Object.assign(data, { sections, lectureCount }); } diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 0b25170..690a3b0 100644 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -3,7 +3,6 @@ import { BaseSetting, db, PostType, - TaxonomySlug, VisitType, } from '@nice/common'; export async function updateTotalCourseViewCount(type: VisitType) { @@ -24,7 +23,7 @@ export async function updateTotalCourseViewCount(type: VisitType) { views: true, }, where: { - postId: { in: courseIds }, + postId: { in: lectures.map((lecture) => lecture.id) }, type: type, }, }); @@ -67,6 +66,7 @@ export async function updatePostViewCount(id: string, type: VisitType) { where: { id }, select: { id: true, meta: true }, }); + const totalViews = await db.visit.aggregate({ _sum: { views: true, diff --git a/apps/server/src/queue/worker/file.processor.ts b/apps/server/src/queue/worker/file.processor.ts index b416af4..1012a8c 100755 --- a/apps/server/src/queue/worker/file.processor.ts +++ b/apps/server/src/queue/worker/file.processor.ts @@ -10,7 +10,7 @@ const pipeline = new ResourceProcessingPipeline() .addProcessor(new VideoProcessor()); export default async function processJob(job: Job) { if (job.name === QueueJobType.FILE_PROCESS) { - console.log('job', job); + // console.log('job', job); const { resource } = job.data; if (!resource) { throw new Error('No resource provided in job data'); diff --git a/apps/server/src/upload/tus.service.ts b/apps/server/src/upload/tus.service.ts index 8844b97..6de4e53 100755 --- a/apps/server/src/upload/tus.service.ts +++ b/apps/server/src/upload/tus.service.ts @@ -89,8 +89,8 @@ export class TusService implements OnModuleInit { upload: Upload, ) { try { - console.log('upload.id', upload.id); - console.log('fileId', this.getFileId(upload.id)); + // console.log('upload.id', upload.id); + // console.log('fileId', this.getFileId(upload.id)); const resource = await this.resourceService.update({ where: { fileId: this.getFileId(upload.id) }, data: { status: ResourceStatus.UPLOADED }, diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index e890b62..23cd420 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -16,8 +16,9 @@ function useGetTaxonomy({ type }): GetTaxonomyProps { taxonomy: { slug: type, }, + parentId : null }, - take: 10, // 只取前10个 + take: 11, // 只取前10个 }); const categories = useMemo(() => { const allCategories = isLoading diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx index 4718835..eca460d 100755 --- a/apps/web/src/app/main/home/components/HeroSection.tsx +++ b/apps/web/src/app/main/home/components/HeroSection.tsx @@ -30,7 +30,7 @@ interface PlatformStat { const HeroSection = () => { const carouselRef = useRef(null); const { statistics, slides } = useAppConfig(); - const [countStatistics, setCountStatistics] = useState(0) + const [countStatistics, setCountStatistics] = useState(4) const platformStats: PlatformStat[] = useMemo(() => { return [ { icon: , value: statistics.staffs, label: "注册学员" }, @@ -53,7 +53,7 @@ const HeroSection = () => { useEffect(() => { const count = countNonZeroValues(statistics); - //console.log(count); + console.log(count); setCountStatistics(count); }, [statistics]); return ( @@ -111,7 +111,7 @@ const HeroSection = () => { { countStatistics > 1 && (
-
+
{platformStats.map((stat, index) => { return stat.value ? (
= ({ - content, - maxHeight = 150, -}) => { +const CollapsibleContent: React.FC = ({ content }) => { const contentWrapperRef = useRef(null); - const [isExpanded, setIsExpanded] = useState(false); - const [shouldCollapse, setShouldCollapse] = useState(false); - - useEffect(() => { - if (contentWrapperRef.current) { - const shouldCollapse = - contentWrapperRef.current.scrollHeight > maxHeight; - setShouldCollapse(shouldCollapse); - } - }, [content]); - return (
{/* 包装整个内容区域的容器 */} -
+
{/* 内容区域 */}
= ({ __html: content || "", }} /> - - {/* 渐变遮罩 */} - {shouldCollapse && !isExpanded && ( -
- )}
- - {/* 展开/收起按钮 */} - {shouldCollapse && ( - - )} - {/* 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..b722f3c 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"; @@ -49,10 +50,13 @@ export function CourseDetailProvider({ (api.post as any).findFirst.useQuery( { where: { id: editId }, - include: { - // sections: { include: { lectures: true } }, - enrollments: true, - }, + // include: { + // // sections: { include: { lectures: true } }, + // enrollments: true, + // terms:true + // }, + + select:courseDetailSelect }, { enabled: Boolean(editId) } ); @@ -72,16 +76,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.tsx b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx index 6573753..39204fb 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx @@ -1,14 +1,16 @@ -import { Course } from "@nice/common"; +import { Course, TaxonomySlug } from "@nice/common"; import React, { useContext, useMemo } from "react"; -import { Image, Typography, Skeleton } from "antd"; // 引入 antd 组件 +import { Image, Typography, Skeleton, Tag } from "antd"; // 引入 antd 组件 import { CourseDetailContext } from "./CourseDetailContext"; import { CalendarOutlined, + EditTwoTone, EyeOutlined, PlayCircleOutlined, + ReloadOutlined, } from "@ant-design/icons"; import dayjs from "dayjs"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; export const CourseDetailDescription: React.FC = () => { const { course, isLoading, selectedLectureId, setSelectedLectureId } = @@ -18,12 +20,14 @@ export const CourseDetailDescription: React.FC = () => { return course?.sections?.[0]?.lectures?.[0]?.id; }, [course]); const navigate = useNavigate(); + const { canEdit } = useContext(CourseDetailContext); + const { id } = useParams(); return ( -
+
{isLoading || !course ? ( ) : ( -
+
{!selectedLectureId && ( <>
@@ -43,16 +47,61 @@ export const CourseDetailDescription: React.FC = () => { )}
{"课程简介:"}
-
+
{course?.subTitle}
-
- -
{course?.meta?.views || 0}
-
+ { + course.terms.map((term) => { + return ( + + {term.name} + + ) + }) + } +
+
+ +
{dayjs(course?.createdAt).format("YYYY年M月D日")}
+
+ + {dayjs(course?.updatedAt).format("YYYY年M月D日")} +
+
+ +
{course?.meta?.views || 0}
+
+ { + canEdit && ( +
{ + const url = id + ? `/course/${id}/editor` + : "/course/editor"; + navigate(url); + }} + > + + {"点击编辑课程"} +
+ ) + }
-//
-// {/* 课程描述 */} -//
-//

{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/detail/CourseDetailHeader/CourseDetailHeader.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx index 433ef3b..401484a 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -24,8 +24,8 @@ export function CourseDetailHeader() { return (
-
-
+
+
{ navigate("/"); @@ -33,24 +33,12 @@ export function CourseDetailHeader() { className="text-2xl text-primary-500 hover:scale-105 cursor-pointer" /> -
+
{course?.title}
{/* */}
-
- - } - placeholder="搜索课程" - className="w-72 rounded-full" - value={searchValue} - onChange={(e) => setSearchValue(e.target.value)} - /> -
{canEdit && ( <>
@@ -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/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/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..a10fcf5 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; @@ -44,6 +45,7 @@ export type PostDto = Post & { export type LectureMeta = { type?: string; + views?: number; videoUrl?: string; videoThumbnail?: string; videoIds?: string[]; @@ -51,6 +53,7 @@ export type LectureMeta = { }; export type Lecture = Post & { + resources?: ResourceDto[]; meta?: LectureMeta; }; @@ -79,5 +82,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..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, @@ -79,6 +80,7 @@ export const courseDetailSelect: Prisma.PostSelect = { select: { id: true, name: true, + taxonomyId: true, taxonomy: { select: { id: true, @@ -95,3 +97,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, +};