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 },
|
params: { staff?: UserProfile; tx?: Prisma.TransactionClient },
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const { courseDetail } = args;
|
const { courseDetail } = args;
|
||||||
// If no transaction is provided, create a new one
|
// If no transaction is provided, create a new one
|
||||||
if (!params.tx) {
|
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 { useMainContext } from "../../layout/MainProvider";
|
||||||
import { PostType, Prisma } from "@nice/common";
|
import { PostType, Prisma } from "@nice/common";
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
import PostList from "@web/src/components/models/course/list/PostList";
|
||||||
import { useMemo } from "react";
|
import CourseCard from "@web/src/components/models/post/SubPost/CourseCard";
|
||||||
import CourseCard from "./CourseCard";
|
|
||||||
import PostCard from "@web/src/components/models/course/card/PostCard";
|
|
||||||
|
|
||||||
export function CoursesContainer() {
|
export function CoursesContainer() {
|
||||||
const {searchCondition, termsCondition } = useMainContext();
|
const { searchCondition, termsCondition } = useMainContext();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PostList
|
<PostList
|
||||||
renderItem={(post) => <PostCard course={post}></PostCard>}
|
renderItem={(post) => <CourseCard post={post}></CourseCard>}
|
||||||
params={{
|
params={{
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -5,7 +5,8 @@ import { api } from "@nice/client";
|
||||||
import { CoursesSectionTag } from "./CoursesSectionTag";
|
import { CoursesSectionTag } from "./CoursesSectionTag";
|
||||||
import LookForMore from "./LookForMore";
|
import LookForMore from "./LookForMore";
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
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 {
|
interface GetTaxonomyProps {
|
||||||
categories: string[];
|
categories: string[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
@ -17,7 +18,7 @@ function useGetTaxonomy({ type }): GetTaxonomyProps {
|
||||||
taxonomy: {
|
taxonomy: {
|
||||||
slug: type,
|
slug: type,
|
||||||
},
|
},
|
||||||
parentId: null
|
parentId: null,
|
||||||
},
|
},
|
||||||
take: 11, // 只取前10个
|
take: 11, // 只取前10个
|
||||||
});
|
});
|
||||||
|
@ -82,7 +83,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<PostList
|
<PostList
|
||||||
renderItem={(post) => <CourseCard course={post} edit={false}></CourseCard>}
|
renderItem={(post) => <CourseCard post={post}></CourseCard>}
|
||||||
params={{
|
params={{
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: initialVisibleCoursesCount,
|
pageSize: initialVisibleCoursesCount,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PostType, Prisma } from "packages/common/dist";
|
import { PostType, Prisma } from "@nice/common";
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
import PostList from "@web/src/components/models/course/list/PostList";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
|
|
||||||
import CourseCard from "../../courses/components/CourseCard";
|
|
||||||
import { PostType } from "@nice/common";
|
import { PostType } from "@nice/common";
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
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() {
|
export default function MyDutyListContainer() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
@ -11,9 +11,7 @@ export default function MyDutyListContainer() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PostList
|
<PostList
|
||||||
renderItem={(post) => (
|
renderItem={(post) => <CourseCard post={post}></CourseCard>}
|
||||||
<CourseCard edit course={post}></CourseCard>
|
|
||||||
)}
|
|
||||||
params={{
|
params={{
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
import PostList from "@web/src/components/models/course/list/PostList";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
import { useMainContext } from "../../layout/MainProvider";
|
||||||
import CourseCard from "../../courses/components/CourseCard";
|
|
||||||
import { PostType } from "@nice/common";
|
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() {
|
export default function MyLearningListContainer() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
@ -10,9 +11,7 @@ export default function MyLearningListContainer() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PostList
|
<PostList
|
||||||
renderItem={(post) => (
|
renderItem={(post) => <CourseCard post={post}></CourseCard>}
|
||||||
<CourseCard edit={false} course={post}></CourseCard>
|
|
||||||
)}
|
|
||||||
params={{
|
params={{
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
import PostList from "@web/src/components/models/course/list/PostList";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
|
|
||||||
|
|
||||||
import { PostType } from "@nice/common";
|
import { PostType } from "@nice/common";
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
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() {
|
export default function MyPathListContainer() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
@ -12,7 +12,7 @@ export default function MyPathListContainer() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PostList
|
<PostList
|
||||||
renderItem={(post) => <PathCard path={post}></PathCard>}
|
renderItem={(post) => <PathCard post={post}></PathCard>}
|
||||||
params={{
|
params={{
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
import { TeamOutlined } from "@ant-design/icons";
|
import { BookOutlined, EyeOutlined, TeamOutlined } from "@ant-design/icons";
|
||||||
import { Typography } from "antd";
|
import { Typography } from "antd";
|
||||||
|
import { PostDto } from "@nice/common";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
const DeptInfo = ({ path }) => {
|
const DeptInfo = ({ post }: { post: PostDto }) => {
|
||||||
return (
|
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" />
|
<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]">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
|
<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>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 PostList from "@web/src/components/models/course/list/PostList";
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
import { useMainContext } from "../../layout/MainProvider";
|
||||||
import { PostType, Prisma } from "@nice/common";
|
import { PostType, Prisma } from "@nice/common";
|
||||||
import PathCard from "./PathCard";
|
import PostCard from "@web/src/components/models/post/PostCard";
|
||||||
import PostCard from "@web/src/components/models/course/card/PostCard";
|
import PathCard from "@web/src/components/models/post/SubPost/PathCard";
|
||||||
|
|
||||||
export function PathListContainer() {
|
export function PathListContainer() {
|
||||||
const { searchCondition, termsCondition } = useMainContext();
|
const { searchCondition, termsCondition } = useMainContext();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PostList
|
<PostList
|
||||||
renderItem={(post) => <PostCard path={post}></PostCard>}
|
renderItem={(post) => <PathCard post={post}></PathCard>}
|
||||||
params={{
|
params={{
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import { TaxonomySlug } from "@nice/common";
|
import { PostDto, TaxonomySlug } from "@nice/common";
|
||||||
|
|
||||||
const TermInfo = ({ path }) => {
|
const TermInfo = ({ post }: { post: PostDto }) => {
|
||||||
console.log('xx',path?.terms);
|
console.log("xx", post?.terms);
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
{path?.terms && path?.terms?.length > 0 ? (
|
<>
|
||||||
|
{post?.terms && post?.terms?.length > 0 ? (
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
{path?.terms?.map((term:any) => {
|
{post?.terms?.map((term: any) => {
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
key={term.id}
|
key={term.id}
|
||||||
|
@ -36,6 +37,7 @@ const TermInfo = ({ path }) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TermInfo;
|
export default TermInfo;
|
|
@ -1,14 +1,24 @@
|
||||||
import PostList from "@web/src/components/models/course/list/PostList";
|
import PostList from "@web/src/components/models/course/list/PostList";
|
||||||
import { useMainContext } from "../../layout/MainProvider";
|
import { useMainContext } from "../../layout/MainProvider";
|
||||||
import PathCard from "../../path/components/PathCard";
|
import PostCard from "@web/src/components/models/post/PostCard";
|
||||||
import { useEffect } from "react";
|
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() {
|
export default function SearchListContainer() {
|
||||||
const { searchCondition, termsCondition, searchMode } = useMainContext();
|
const { searchCondition, termsCondition, searchMode } = useMainContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PostList
|
<PostList
|
||||||
renderItem={(post) => <PathCard path={post}></PathCard>}
|
renderItem={(post) => {
|
||||||
|
const Component =
|
||||||
|
POST_TYPE_COMPONENTS[post.type] || PostCard;
|
||||||
|
return <Component post={post} />;
|
||||||
|
}}
|
||||||
params={{
|
params={{
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
where: {
|
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,
|
thumbnail: values?.meta?.thumbnail,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
terms: {
|
terms:
|
||||||
|
termIds?.length > 0
|
||||||
|
? {
|
||||||
set: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
set: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
||||||
},
|
}
|
||||||
depts: {
|
: undefined,
|
||||||
|
depts:
|
||||||
|
deptIds?.length > 0
|
||||||
|
? {
|
||||||
set: deptIds.map((id) => ({ id })),
|
set: deptIds.map((id) => ({ id })),
|
||||||
},
|
}
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
// 删除原始的 taxonomy 字段
|
// 删除原始的 taxonomy 字段
|
||||||
taxonomies.forEach((tax) => {
|
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 { VideoPlayer } from "@web/src/components/presentation/video-player/VideoPlayer";
|
||||||
import MultiAvatarUploader from "@web/src/components/common/uploader/MultiAvatarUploader";
|
import MultiAvatarUploader from "@web/src/components/common/uploader/MultiAvatarUploader";
|
||||||
import ResourcesShower from "@web/src/components/common/uploader/ResourceShower";
|
import ResourcesShower from "@web/src/components/common/uploader/ResourceShower";
|
||||||
|
import { set } from "idb-keyval";
|
||||||
|
|
||||||
interface SortableLectureProps {
|
interface SortableLectureProps {
|
||||||
field: Lecture;
|
field: Lecture;
|
||||||
|
@ -82,13 +83,16 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
||||||
? `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${videoUrlId}/stream/index.m3u8`
|
? `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${videoUrlId}/stream/index.m3u8`
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
resources: {
|
resources:
|
||||||
|
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
|
||||||
|
? {
|
||||||
connect: [videoUrlId, ...fileIds]
|
connect: [videoUrlId, ...fileIds]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((fileId) => ({
|
.map((fileId) => ({
|
||||||
fileId,
|
fileId,
|
||||||
})),
|
})),
|
||||||
},
|
}
|
||||||
|
: undefined,
|
||||||
content: values?.content,
|
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`
|
? `http://${env.SERVER_IP}:${env.FILE_PORT}/uploads/${videoUrlId}/stream/index.m3u8`
|
||||||
: undefined,
|
: undefined,
|
||||||
},
|
},
|
||||||
resources: {
|
resources:
|
||||||
|
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
|
||||||
|
? {
|
||||||
connect: [videoUrlId, ...fileIds]
|
connect: [videoUrlId, ...fileIds]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((fileId) => ({
|
.map((fileId) => ({
|
||||||
fileId,
|
fileId,
|
||||||
})),
|
})),
|
||||||
},
|
}
|
||||||
|
: undefined,
|
||||||
content: values?.content,
|
content: values?.content,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -199,13 +206,7 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["meta", "fileIds"]}
|
name={["meta", "fileIds"]}
|
||||||
className="mb-0 flex-1"
|
className="mb-0 flex-1">
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: "请传入文件",
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<TusUploader multiple={true} />
|
<TusUploader multiple={true} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -135,7 +135,7 @@ export const SortableSection: React.FC<SortableSectionProps> = ({
|
||||||
</Form>
|
</Form>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Space>
|
<Space className=" flex">
|
||||||
<DragOutlined
|
<DragOutlined
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
|
@ -143,17 +143,26 @@ export const SortableSection: React.FC<SortableSectionProps> = ({
|
||||||
/>
|
/>
|
||||||
<span>{field.title || "未命名章节"}</span>
|
<span>{field.title || "未命名章节"}</span>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
!editing && (
|
||||||
|
<Space onClick={(e) => e.stopPropagation()}>
|
||||||
<Button
|
<Button
|
||||||
|
size="small"
|
||||||
type="link"
|
type="link"
|
||||||
onClick={() => setEditing(true)}>
|
onClick={() => setEditing(true)}>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="link" danger onClick={remove}>
|
<Button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
onClick={remove}>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
key={field.id || "new"}>
|
key={field.id || "new"}>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Pagination, Empty, Skeleton } from "antd";
|
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 { api } from "@nice/client";
|
||||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
@ -12,7 +12,7 @@ interface PostListProps {
|
||||||
};
|
};
|
||||||
cols?: number;
|
cols?: number;
|
||||||
showPagination?: boolean;
|
showPagination?: boolean;
|
||||||
renderItem: (post: any) => React.ReactNode;
|
renderItem: (post: PostDto) => React.ReactNode;
|
||||||
}
|
}
|
||||||
interface PostPagnationProps {
|
interface PostPagnationProps {
|
||||||
data: {
|
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;
|
thumbnail?: string;
|
||||||
views?: number;
|
views?: number;
|
||||||
};
|
};
|
||||||
|
studentIds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LectureMeta = {
|
export type LectureMeta = {
|
||||||
|
|
Loading…
Reference in New Issue