add
This commit is contained in:
parent
78fc40a719
commit
669525fb61
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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,7 +83,7 @@ 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,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PostType, Prisma } from "packages/common/dist";
|
||||
import { PostType, Prisma } from "@nice/common";
|
||||
import React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
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 }) => {
|
||||
const DeptInfo = ({ post }: { post: PostDto }) => {
|
||||
return (
|
||||
<div className="gap-1 flex items-center flex-grow">
|
||||
<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" />
|
||||
{path?.depts && path?.depts?.length > 0 ? (
|
||||
{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]">
|
||||
{path?.depts?.length > 1 ? `${path.depts[0].name}等` : path?.depts?.[0]?.name}
|
||||
{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]">
|
||||
|
@ -16,6 +20,21 @@ const DeptInfo = ({ path }) => {
|
|||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { Tag } from "antd";
|
||||
import { TaxonomySlug } from "@nice/common";
|
||||
import { PostDto, TaxonomySlug } from "@nice/common";
|
||||
|
||||
const TermInfo = ({ path }) => {
|
||||
console.log('xx',path?.terms);
|
||||
const TermInfo = ({ post }: { post: PostDto }) => {
|
||||
console.log("xx", post?.terms);
|
||||
|
||||
return <>
|
||||
{path?.terms && path?.terms?.length > 0 ? (
|
||||
return (
|
||||
<>
|
||||
{post?.terms && post?.terms?.length > 0 ? (
|
||||
<div className="flex gap-2 mb-4">
|
||||
{path?.terms?.map((term:any) => {
|
||||
{post?.terms?.map((term: any) => {
|
||||
return (
|
||||
<Tag
|
||||
key={term.id}
|
||||
|
@ -36,6 +37,7 @@ const TermInfo = ({ path }) => {
|
|||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermInfo;
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -98,12 +98,18 @@ export function CourseFormProvider({
|
|||
thumbnail: values?.meta?.thumbnail,
|
||||
}),
|
||||
},
|
||||
terms: {
|
||||
terms:
|
||||
termIds?.length > 0
|
||||
? {
|
||||
set: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
||||
},
|
||||
depts: {
|
||||
}
|
||||
: undefined,
|
||||
depts:
|
||||
deptIds?.length > 0
|
||||
? {
|
||||
set: deptIds.map((id) => ({ id })),
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
// 删除原始的 taxonomy 字段
|
||||
taxonomies.forEach((tax) => {
|
||||
|
|
|
@ -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: {
|
||||
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: {
|
||||
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>
|
||||
|
|
|
@ -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,17 +143,26 @@ export const SortableSection: React.FC<SortableSectionProps> = ({
|
|||
/>
|
||||
<span>{field.title || "未命名章节"}</span>
|
||||
</Space>
|
||||
<Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
extra={
|
||||
!editing && (
|
||||
<Space onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
onClick={() => setEditing(true)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" danger onClick={remove}>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
onClick={remove}>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
key={field.id || "new"}>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -48,6 +48,7 @@ export type PostDto = Post & {
|
|||
thumbnail?: string;
|
||||
views?: number;
|
||||
};
|
||||
studentIds?: string[];
|
||||
};
|
||||
|
||||
export type LectureMeta = {
|
||||
|
|
Loading…
Reference in New Issue