This commit is contained in:
ditiqi 2025-02-27 13:32:33 +08:00
parent 78fc40a719
commit 669525fb61
23 changed files with 256 additions and 462 deletions

View File

@ -101,7 +101,6 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
},
params: { staff?: UserProfile; tx?: Prisma.TransactionClient },
) {
const { courseDetail } = args;
// If no transaction is provided, create a new one
if (!params.tx) {
@ -124,7 +123,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
) {
args.data.authorId = params?.staff?.id;
args.data.updatedAt = dayjs().toDate();
const result = await super.create(args);
EventBus.emit('dataChanged', {
type: ObjectType.POST,

View File

@ -1,111 +0,0 @@
import { Card, Tag, Typography, Button } from "antd";
import {
BookOutlined,
EyeOutlined,
PlayCircleOutlined,
TeamOutlined,
} from "@ant-design/icons";
import { CourseDto, TaxonomySlug } from "@nice/common";
import { useNavigate } from "react-router-dom";
interface CourseCardProps {
course: CourseDto;
edit?: boolean;
}
const { Title, Text } = Typography;
export default function CourseCard({ course, edit = false }: CourseCardProps) {
const navigate = useNavigate();
const handleClick = (course: CourseDto) => {
if (!edit) {
navigate(`/course/${course.id}/detail`);
} else {
navigate(`/course/${course.id}/editor`);
}
window.scrollTo({ top: 0, behavior: "smooth" });
};
return (
<Card
onClick={() => handleClick(course)}
key={course.id}
hoverable
className="group overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2"
cover={
<div className="relative h-56 bg-gradient-to-br from-gray-900 to-gray-800 overflow-hidden">
<div
className="absolute inset-0 bg-cover bg-center transform transition-all duration-700 ease-out group-hover:scale-110"
style={{
backgroundImage: `url(${course?.meta?.thumbnail})`,
}}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-80 group-hover:opacity-60 transition-opacity duration-300" />
<PlayCircleOutlined className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-6xl text-white/90 opacity-0 group-hover:opacity-100 transition-all duration-300" />
</div>
}>
<div className="px-4 ">
<div className="overflow-hidden hover:overflow-auto">
<div className="flex gap-2 h-7 mb-4 whiteSpace-nowrap">
{course?.terms?.map((term) => {
return (
<>
<Tag
key={term.id}
color={
term?.taxonomy?.slug ===
TaxonomySlug.CATEGORY
? "blue"
: term?.taxonomy?.slug ===
TaxonomySlug.LEVEL
? "green"
: "orange"
}
className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0 ">
{term.name}
</Tag>
</>
);
})}
</div>
</div>
<Title
level={4}
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>
</Title>
<div className="flex items-center mb-4 rounded-lg transition-all duration-300 hover:bg-blue-50 group">
<TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" />
<div className="ml-2 flex items-center flex-grow">
<Text className="font-medium text-blue-500 transition-colors duration-300 truncate max-w-[120px]">
{course?.depts?.length > 1
? `${course.depts[0].name}`
: course?.depts?.[0]?.name}
{/* {course?.depts?.map((dept) => {return dept.name.length > 1 ?`${dept.name.slice}等`: dept.name})} */}
{/* {course?.depts?.map((dept)=>{return dept.name})} */}
</Text>
</div>
</div>
<div className="flex items-center gap-2">
<span className="gap-1 text-xs font-medium text-gray-500 flex items-center">
<EyeOutlined />
{`观看次数 ${course?.meta?.views || 0}`}
</span>
<span className="gap-1 text-xs font-medium text-gray-500 flex items-center">
<BookOutlined />
{`学习人数 ${course?.studentIds?.length || 0}`}
</span>
</div>
<div className="pt-4 border-t border-gray-100 text-center">
<Button
type="primary"
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>
</Card>
);
}

View File

@ -1,16 +1,14 @@
import { useMainContext } from "../../layout/MainProvider";
import { PostType, Prisma } from "@nice/common";
import PostList from "@web/src/components/models/course/list/PostList";
import { useMemo } from "react";
import CourseCard from "./CourseCard";
import PostCard from "@web/src/components/models/course/card/PostCard";
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
export function CoursesContainer() {
const {searchCondition, termsCondition } = useMainContext();
const { searchCondition, termsCondition } = useMainContext();
return (
<>
<PostList
renderItem={(post) => <PostCard course={post}></PostCard>}
renderItem={(post) => <CourseCard post={post}></CourseCard>}
params={{
pageSize: 12,
where: {

View File

@ -5,7 +5,8 @@ import { api } from "@nice/client";
import { CoursesSectionTag } from "./CoursesSectionTag";
import LookForMore from "./LookForMore";
import PostList from "@web/src/components/models/course/list/PostList";
import CourseCard from "../../courses/components/CourseCard";
import PostCard from "@web/src/components/models/post/PostCard";
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
interface GetTaxonomyProps {
categories: string[];
isLoading: boolean;
@ -17,7 +18,7 @@ function useGetTaxonomy({ type }): GetTaxonomyProps {
taxonomy: {
slug: type,
},
parentId: null
parentId: null,
},
take: 11, // 只取前10个
});
@ -82,17 +83,17 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
)}
</div>
<PostList
renderItem={(post) => <CourseCard course={post} edit={false}></CourseCard>}
renderItem={(post) => <CourseCard post={post}></CourseCard>}
params={{
page: 1,
pageSize: initialVisibleCoursesCount,
where: {
terms: !(selectedCategory === "全部")
? {
some: {
name: selectedCategory,
},
}
some: {
name: selectedCategory,
},
}
: {},
},
}}

View File

@ -1,4 +1,4 @@
import { PostType, Prisma } from "packages/common/dist";
import { PostType, Prisma } from "@nice/common";
import React, {
createContext,
ReactNode,

View File

@ -1,9 +1,9 @@
import PostList from "@web/src/components/models/course/list/PostList";
import { useAuth } from "@web/src/providers/auth-provider";
import CourseCard from "../../courses/components/CourseCard";
import { PostType } from "@nice/common";
import { useMainContext } from "../../layout/MainProvider";
import PostCard from "@web/src/components/models/post/PostCard";
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
export default function MyDutyListContainer() {
const { user } = useAuth();
@ -11,9 +11,7 @@ export default function MyDutyListContainer() {
return (
<>
<PostList
renderItem={(post) => (
<CourseCard edit course={post}></CourseCard>
)}
renderItem={(post) => <CourseCard post={post}></CourseCard>}
params={{
pageSize: 12,
where: {

View File

@ -1,8 +1,9 @@
import PostList from "@web/src/components/models/course/list/PostList";
import { useAuth } from "@web/src/providers/auth-provider";
import { useMainContext } from "../../layout/MainProvider";
import CourseCard from "../../courses/components/CourseCard";
import { PostType } from "@nice/common";
import PostCard from "@web/src/components/models/post/PostCard";
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
export default function MyLearningListContainer() {
const { user } = useAuth();
@ -10,9 +11,7 @@ export default function MyLearningListContainer() {
return (
<>
<PostList
renderItem={(post) => (
<CourseCard edit={false} course={post}></CourseCard>
)}
renderItem={(post) => <CourseCard post={post}></CourseCard>}
params={{
pageSize: 12,
where: {

View File

@ -1,10 +1,10 @@
import PostList from "@web/src/components/models/course/list/PostList";
import { useAuth } from "@web/src/providers/auth-provider";
import { PostType } from "@nice/common";
import { useMainContext } from "../../layout/MainProvider";
import PathCard from "../../path/components/PathCard";
import PostCard from "@web/src/components/models/post/PostCard";
import PathCard from "@web/src/components/models/post/SubPost/PathCard";
export default function MyPathListContainer() {
const { user } = useAuth();
@ -12,7 +12,7 @@ export default function MyPathListContainer() {
return (
<>
<PostList
renderItem={(post) => <PathCard path={post}></PathCard>}
renderItem={(post) => <PathCard post={post}></PathCard>}
params={{
pageSize: 12,
where: {

View File

@ -1,22 +1,41 @@
import { TeamOutlined } from "@ant-design/icons";
import { BookOutlined, EyeOutlined, TeamOutlined } from "@ant-design/icons";
import { Typography } from "antd";
import { PostDto } from "@nice/common";
const { Title, Text } = Typography;
const DeptInfo = ({ path }) => {
return (
<div className="gap-1 flex items-center flex-grow">
<TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" />
{path?.depts && path?.depts?.length > 0 ? (
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
{path?.depts?.length > 1 ? `${path.depts[0].name}` : path?.depts?.[0]?.name}
</Text>
) : (
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
</Text>
)}
</div>
);
const DeptInfo = ({ post }: { post: PostDto }) => {
return (
<div className="gap-1 flex items-center justify-between flex-grow">
<div className=" flex justify-start gap-1 items-center">
<TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" />
{post?.depts && post?.depts?.length > 0 ? (
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
{post?.depts?.length > 1
? `${post.depts[0].name}`
: post?.depts?.[0]?.name}
</Text>
) : (
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
</Text>
)}
</div>
{post && (
<div className="flex items-center gap-2">
<span className="gap-1 text-xs font-medium text-gray-500 flex items-center">
<EyeOutlined />
{`${post?.meta?.views || 0}`}
</span>
{post?.studentIds && post?.studentIds?.length > 0 && (
<span className="gap-1 text-xs font-medium text-gray-500 flex items-center">
<BookOutlined />
{`${post?.studentIds?.length || 0}`}
</span>
)}
</div>
)}
</div>
);
};
export default DeptInfo;
export default DeptInfo;

View File

@ -1,65 +0,0 @@
import { Card, Tag, Typography, Button } from "antd";
import {
EyeOutlined
} from "@ant-design/icons";
import { PostDto, TaxonomySlug } from "@nice/common";
import { useNavigate } from "react-router-dom";
import DeptInfo from "./DeptInfo";
import TermInfo from "./TermInfo";
interface pathCardProps {
path: PostDto;
}
const { Title, Text } = Typography;
export default function PathCard({ path }: pathCardProps) {
const navigate = useNavigate();
const handleClick = (path: PostDto) => {
navigate(`/path/editor/${path.id}`);
window.scrollTo({ top: 0, behavior: "smooth" });
};
return (
<Card
onClick={() => handleClick(path)}
key={path.id}
hoverable
className=" group overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2"
cover={
<div className="relative h-56 bg-gradient-to-br from-gray-900 to-gray-800 overflow-hidden">
<div
className="absolute inset-0 bg-cover bg-center transform transition-all duration-700 ease-out group-hover:scale-110"
style={{
backgroundImage: `url(${path?.meta?.thumbnail})`,
}}
/>
{/* <div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-80 group-hover:opacity-60 transition-opacity duration-300" /> */}
</div>
}>
<div className="px-4">
<TermInfo path={path} />
<Title
level={4}
className="mb-4 line-clamp-2 font-bold leading-snug text-gray-800 hover:text-blue-600 transition-colors duration-300 group-hover:scale-[1.02] transform origin-left">
<button> {path.title}</button>
</Title>
<div className="flex items-center mb-4 p-2 rounded-lg transition-all duration-300 hover:bg-blue-50 group">
<DeptInfo path={path} />
<span className="flex text-xs font-medium text-gray-500">
<EyeOutlined className="mr-2"></EyeOutlined>
{path?.meta?.views
? `观看次数 ${path?.meta?.views}`
: 0}
</span>
</div>
<div className="pt-4 border-t border-gray-100 text-center">
<Button
type="primary"
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">
</Button>
</div>
</div>
</Card>
);
}

View File

@ -1,15 +1,15 @@
import PostList from "@web/src/components/models/course/list/PostList";
import { useMainContext } from "../../layout/MainProvider";
import { PostType, Prisma } from "@nice/common";
import PathCard from "./PathCard";
import PostCard from "@web/src/components/models/course/card/PostCard";
import PostCard from "@web/src/components/models/post/PostCard";
import PathCard from "@web/src/components/models/post/SubPost/PathCard";
export function PathListContainer() {
const { searchCondition, termsCondition } = useMainContext();
return (
<>
<PostList
renderItem={(post) => <PostCard path={post}></PostCard>}
renderItem={(post) => <PathCard post={post}></PathCard>}
params={{
pageSize: 12,
where: {

View File

@ -1,41 +1,43 @@
import { Tag } from "antd";
import { TaxonomySlug } from "@nice/common";
import { PostDto, TaxonomySlug } from "@nice/common";
const TermInfo = ({ path }) => {
console.log('xx',path?.terms);
return <>
{path?.terms && path?.terms?.length > 0 ? (
<div className="flex gap-2 mb-4">
{path?.terms?.map((term:any) => {
return (
<Tag
key={term.id}
color={
term?.taxonomy?.slug ===
TaxonomySlug.CATEGORY
? "blue"
: term?.taxonomy?.slug ===
TaxonomySlug.LEVEL
? "green"
: "orange"
}
className="px-3 py-1 rounded-full border-0">
{term.name}
</Tag>
);
})}
</div>
) : (
<div className="flex gap-2 mb-4">
<Tag
color={"orange"}
className="px-3 py-1 rounded-full border-0">
{"未设置分类"}
</Tag>
</div>
)}
</>
const TermInfo = ({ post }: { post: PostDto }) => {
console.log("xx", post?.terms);
return (
<>
{post?.terms && post?.terms?.length > 0 ? (
<div className="flex gap-2 mb-4">
{post?.terms?.map((term: any) => {
return (
<Tag
key={term.id}
color={
term?.taxonomy?.slug ===
TaxonomySlug.CATEGORY
? "blue"
: term?.taxonomy?.slug ===
TaxonomySlug.LEVEL
? "green"
: "orange"
}
className="px-3 py-1 rounded-full border-0">
{term.name}
</Tag>
);
})}
</div>
) : (
<div className="flex gap-2 mb-4">
<Tag
color={"orange"}
className="px-3 py-1 rounded-full border-0">
{"未设置分类"}
</Tag>
</div>
)}
</>
);
};
export default TermInfo;
export default TermInfo;

View File

@ -1,14 +1,24 @@
import PostList from "@web/src/components/models/course/list/PostList";
import { useMainContext } from "../../layout/MainProvider";
import PathCard from "../../path/components/PathCard";
import { useEffect } from "react";
import PostCard from "@web/src/components/models/post/PostCard";
import { PostType } from "@nice/common";
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
import PathCard from "@web/src/components/models/post/SubPost/PathCard";
const POST_TYPE_COMPONENTS = {
[PostType.COURSE]: CourseCard,
[PostType.PATH]: PathCard,
};
export default function SearchListContainer() {
const { searchCondition, termsCondition, searchMode } = useMainContext();
return (
<>
<PostList
renderItem={(post) => <PathCard path={post}></PathCard>}
renderItem={(post) => {
const Component =
POST_TYPE_COMPONENTS[post.type] || PostCard;
return <Component post={post} />;
}}
params={{
pageSize: 12,
where: {

View File

@ -1,32 +0,0 @@
import { CourseDto } from "@nice/common";
import { Card } from "@web/src/components/common/container/Card";
import { CourseHeader } from "./CourseHeader";
import { CourseStats } from "./CourseStats";
import { Popover } from "@web/src/components/presentation/popover";
import { useState } from "react";
interface CourseCardProps {
course: CourseDto;
onClick?: () => void;
}
export const CourseCard = ({ course, onClick }: CourseCardProps) => {
return (
<Card onClick={onClick} className="w-full max-w-sm">
<CourseHeader
title={course.title}
subTitle={course.subTitle}
thumbnail={course.thumbnail}
level={course.level}
numberOfStudents={course.numberOfStudents}
publishedAt={course.publishedAt}
/>
<CourseStats
averageRating={course.averageRating}
numberOfReviews={course.numberOfReviews}
completionRate={course.completionRate}
totalDuration={course.totalDuration}
/>
</Card>
);
};

View File

@ -1,127 +0,0 @@
import { Card, Tag, Typography, Button } from "antd";
import {
BookOutlined,
EyeOutlined,
PlayCircleOutlined,
TeamOutlined,
} from "@ant-design/icons";
import { CourseDto, PostDto, TaxonomySlug } from "@nice/common";
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
interface PostCardProps {
course?: CourseDto;
path?: PostDto
}
const { Title, Text } = Typography;
export default function PostCard({ course = null, path = null }: PostCardProps) {
const navigate = useNavigate();
const handleClick = (course: CourseDto) => {
if (course) {
navigate(`/course/${course.id}/detail`);
} else if (path) {
navigate(`/path/editor/${path.id}`);
}
window.scrollTo({ top: 0, behavior: "smooth", })
};
return (
<Card
onClick={() => handleClick(course)}
key={course?.id}
hoverable
className="group overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2"
cover={
<div className="relative h-56 bg-gradient-to-br from-gray-900 to-gray-800 overflow-hidden">
<div
className="absolute inset-0 bg-cover bg-center transform transition-all duration-700 ease-out group-hover:scale-110"
style={{
backgroundImage: `url(${course?.meta?.thumbnail})`,
}}
/>
{course && (
<>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-80 group-hover:opacity-60 transition-opacity duration-300" />
<PlayCircleOutlined className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-6xl text-white/90 opacity-0 group-hover:opacity-100 transition-all duration-300" />
</>
)}
</div>
}>
<div className="px-4 ">
<div className="overflow-hidden hover:overflow-auto">
<div className="flex gap-2 h-7 mb-4 whiteSpace-nowrap">
{course?.terms?.map((term) => {
return (
<>
<Tag
key={term.id}
color={
term?.taxonomy?.slug ===
TaxonomySlug.CATEGORY
? "blue"
: term?.taxonomy?.slug ===
TaxonomySlug.LEVEL
? "green"
: "orange"
}
className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0 ">
{term.name}
</Tag>
</>
);
})}
</div>
</div>
<Title
level={4}
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>
</Title>
<div className="flex items-center mb-4 rounded-lg transition-all duration-300 hover:bg-blue-50 group">
<TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" />
<div className="ml-2 flex items-center flex-grow">
<Text className="font-medium text-blue-500 transition-colors duration-300 truncate max-w-[120px]">
{course?.depts?.length > 1
? `${course.depts[0].name}`
: course?.depts?.[0]?.name}
{/* {course?.depts?.map((dept) => {return dept.name.length > 1 ?`${dept.name.slice}等`: dept.name})} */}
{/* {course?.depts?.map((dept)=>{return dept.name})} */}
</Text>
</div>
{!course && (
<>
<span className="text-xs font-medium text-gray-500">
{path?.meta?.views
? `观看次数 ${path?.meta?.views}`
: null}
</span>
</>
)}
</div>
{course && (
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-gray-500 flex items-center">
<EyeOutlined />
{`观看次数 ${course?.meta?.views || 0}`}
</span>
<span className="text-xs font-medium text-gray-500 flex items-center">
<BookOutlined />
{`学习人数 ${course?.studentIds?.length || 0}`}
</span>
</div>
)}
<div className="pt-4 border-t border-gray-100 text-center">
<Button
type="primary"
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">
{"立即学习"}
</Button>
</div>
</div>
</Card>
);
}

View File

@ -98,12 +98,18 @@ export function CourseFormProvider({
thumbnail: values?.meta?.thumbnail,
}),
},
terms: {
set: termIds.map((id) => ({ id })), // 转换成 connect 格式
},
depts: {
set: deptIds.map((id) => ({ id })),
},
terms:
termIds?.length > 0
? {
set: termIds.map((id) => ({ id })), // 转换成 connect 格式
}
: undefined,
depts:
deptIds?.length > 0
? {
set: deptIds.map((id) => ({ id })),
}
: undefined,
};
// 删除原始的 taxonomy 字段
taxonomies.forEach((tax) => {

View File

@ -26,6 +26,7 @@ import CollapsibleContent from "@web/src/components/common/container/Collapsible
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";
import { set } from "idb-keyval";
interface SortableLectureProps {
field: Lecture;
@ -82,13 +83,16 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
? `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${videoUrlId}/stream/index.m3u8`
: undefined,
},
resources: {
connect: [videoUrlId, ...fileIds]
.filter(Boolean)
.map((fileId) => ({
fileId,
})),
},
resources:
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
? {
connect: [videoUrlId, ...fileIds]
.filter(Boolean)
.map((fileId) => ({
fileId,
})),
}
: undefined,
content: values?.content,
},
});
@ -108,13 +112,16 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
? `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${videoUrlId}/stream/index.m3u8`
: undefined,
},
resources: {
connect: [videoUrlId, ...fileIds]
.filter(Boolean)
.map((fileId) => ({
fileId,
})),
},
resources:
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
? {
connect: [videoUrlId, ...fileIds]
.filter(Boolean)
.map((fileId) => ({
fileId,
})),
}
: undefined,
content: values?.content,
},
});
@ -199,13 +206,7 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
</Form.Item>
<Form.Item
name={["meta", "fileIds"]}
className="mb-0 flex-1"
rules={[
{
required: true,
message: "请传入文件",
},
]}>
className="mb-0 flex-1">
<TusUploader multiple={true} />
</Form.Item>
</div>

View File

@ -135,7 +135,7 @@ export const SortableSection: React.FC<SortableSectionProps> = ({
</Form>
) : (
<div className="flex items-center justify-between">
<Space>
<Space className=" flex">
<DragOutlined
{...attributes}
{...listeners}
@ -143,19 +143,28 @@ export const SortableSection: React.FC<SortableSectionProps> = ({
/>
<span>{field.title || "未命名章节"}</span>
</Space>
<Space>
<Button
type="link"
onClick={() => setEditing(true)}>
</Button>
<Button type="link" danger onClick={remove}>
</Button>
</Space>
</div>
)
}
extra={
!editing && (
<Space onClick={(e) => e.stopPropagation()}>
<Button
size="small"
type="link"
onClick={() => setEditing(true)}>
</Button>
<Button
size="small"
type="link"
danger
onClick={remove}>
</Button>
</Space>
)
}
key={field.id || "new"}>
{children}
</Collapse.Panel>

View File

@ -1,5 +1,5 @@
import { Pagination, Empty, Skeleton } from "antd";
import { courseDetailSelect, CourseDto, Prisma } from "@nice/common";
import { courseDetailSelect, CourseDto, PostDto, Prisma } from "@nice/common";
import { api } from "@nice/client";
import { DefaultArgs } from "@prisma/client/runtime/library";
import React, { useEffect, useMemo, useState } from "react";
@ -12,7 +12,7 @@ interface PostListProps {
};
cols?: number;
showPagination?: boolean;
renderItem: (post: any) => React.ReactNode;
renderItem: (post: PostDto) => React.ReactNode;
}
interface PostPagnationProps {
data: {

View File

@ -0,0 +1,59 @@
import { Card, Typography, Button } from "antd";
import { PostDto } from "@nice/common";
import DeptInfo from "@web/src/app/main/path/components/DeptInfo";
import TermInfo from "@web/src/app/main/path/components/TermInfo";
interface PostCardProps {
post?: PostDto;
onClick?: (post?: PostDto) => void;
}
const { Title } = Typography;
export default function PostCard({ post, onClick }: PostCardProps) {
const handleClick = (post: PostDto) => {
onClick?.(post);
};
return (
<Card
onClick={() => handleClick(post)}
key={post?.id}
hoverable
className="group overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2"
cover={
<div className="relative h-56 bg-gradient-to-br from-gray-900 to-gray-800 overflow-hidden">
<div
className="absolute inset-0 bg-cover bg-center transform transition-all duration-700 ease-out group-hover:scale-110"
style={{
backgroundImage: `url(${post?.meta?.thumbnail})`,
}}
/>
</div>
}>
<div className="px-4 ">
<div className="overflow-hidden hover:overflow-auto">
<div className="flex gap-2 h-7 mb-4 whiteSpace-nowrap">
<TermInfo post={post}></TermInfo>
</div>
</div>
<Title
level={4}
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> {post?.title}</button>
</Title>
<div className="flex items-center mb-4 rounded-lg transition-all duration-300 hover:bg-blue-50 group">
<DeptInfo post={post}></DeptInfo>
</div>
<div className="pt-4 border-t border-gray-100 text-center">
<Button
type="primary"
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">
{"立即学习"}
</Button>
</div>
</div>
</Card>
);
}

View File

@ -0,0 +1,13 @@
import { CourseDto, PostDto } from "@nice/common";
import { useNavigate } from "react-router-dom";
import PostCard from "../PostCard";
export default function CourseCard({ post }: { post: PostDto }) {
const navigate = useNavigate();
return (
<PostCard
post={post}
onClick={() => {
navigate(`/course/${post?.id}/detail`);
}}></PostCard>
);
}

View File

@ -0,0 +1,14 @@
import { PostDto } from "@nice/common";
import { useNavigate } from "react-router-dom";
import PostCard from "../PostCard";
export default function PathCard({ post }: { post: PostDto }) {
const navigate = useNavigate();
return (
<PostCard
post={post}
onClick={() => {
navigate(`/path/editor/${post?.id}`);
}}></PostCard>
);
}

View File

@ -48,6 +48,7 @@ export type PostDto = Post & {
thumbnail?: string;
views?: number;
};
studentIds?: string[];
};
export type LectureMeta = {