add
This commit is contained in:
parent
ee9df61320
commit
49d3f613fc
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
|
@ -20,7 +20,7 @@ export function BasePostLayout({
|
|||
<div className="w-1/6">
|
||||
<FilterSection></FilterSection>
|
||||
</div>
|
||||
<div className="w-5/6 p-4">{children}</div>
|
||||
<div className="w-5/6 p-4 py-8">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -19,26 +19,27 @@ export function MainHeader() {
|
|||
const { searchValue, setSearchValue } = useMainContext();
|
||||
|
||||
return (
|
||||
<div className="select-none w-full flex items-center justify-start bg-white shadow-md border-b border-gray-100 fixed z-30 py-2">
|
||||
<div className="flex-1 px-4 md:px-6 mx-auto flex items-center justify-start h-full">
|
||||
<div className="flex items-center justify-start space-x-4">
|
||||
<img src="/logo.svg" className="h-12 w-12 " />
|
||||
<div
|
||||
onClick={() => navigate("/")}
|
||||
className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent hover:scale-105 transition-transform cursor-pointer">
|
||||
烽火慕课
|
||||
</div>
|
||||
<NavigationMenu />
|
||||
<div className="select-none w-full flex items-center justify-between bg-white shadow-md border-b border-gray-100 fixed z-30 py-2 px-4 md:px-6">
|
||||
{/* 左侧区域 - 设置为不收缩 */}
|
||||
<div className="flex items-center justify-start space-x-4 flex-shrink-0">
|
||||
<img src="/logo.svg" className="h-12 w-12" />
|
||||
<div
|
||||
onClick={() => navigate("/")}
|
||||
className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent hover:scale-105 transition-transform cursor-pointer whitespace-nowrap">
|
||||
烽火慕课
|
||||
</div>
|
||||
<NavigationMenu />
|
||||
</div>
|
||||
<div className=" flex ">
|
||||
|
||||
{/* 中间搜索区域 - 允许适当收缩但保持可用性 */}
|
||||
<div className="mx-4 flex-shrink md:flex-shrink-0 md:w-auto w-auto">
|
||||
<Input
|
||||
size="large"
|
||||
prefix={
|
||||
<SearchOutlined className="text-gray-400 group-hover:text-blue-500 transition-colors" />
|
||||
}
|
||||
placeholder="搜索课程"
|
||||
className="w-96 rounded-full"
|
||||
className="w-full md:w-96 rounded-full"
|
||||
value={searchValue}
|
||||
onClick={(e) => {
|
||||
if (!window.location.pathname.startsWith("/search")) {
|
||||
|
@ -61,8 +62,10 @@ export function MainHeader() {
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex justify-end gap-4 mr-2">
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
{/* 右侧区域 - 可以灵活收缩 */}
|
||||
<div className="flex justify-end gap-2 md:gap-4 flex-shrink">
|
||||
<div className="flex items-center gap-2 md:gap-4">
|
||||
{isAuthenticated && (
|
||||
<>
|
||||
<Button
|
||||
|
|
|
@ -11,8 +11,8 @@ export const NavigationMenu = () => {
|
|||
const menuItems = useMemo(() => {
|
||||
const baseItems = [
|
||||
{ key: "home", path: "/", label: "首页" },
|
||||
{ key: "courses", path: "/courses", label: "全部课程" },
|
||||
{ key: "path", path: "/path", label: "学习路径" },
|
||||
{ key: "courses", path: "/courses", label: "全部课程" },
|
||||
];
|
||||
|
||||
if (!isAuthenticated) {
|
||||
|
|
|
@ -2,10 +2,8 @@ import { Tag } from "antd";
|
|||
import { PostDto, TaxonomySlug } from "@nice/common";
|
||||
|
||||
const TermInfo = ({ post }: { post: PostDto }) => {
|
||||
console.log("xx", post?.terms);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{post?.terms && post?.terms?.length > 0 ? (
|
||||
<div className="flex gap-2 mb-4">
|
||||
{post?.terms?.map((term: any) => {
|
||||
|
@ -15,10 +13,10 @@ const TermInfo = ({ post }: { post: PostDto }) => {
|
|||
color={
|
||||
term?.taxonomy?.slug ===
|
||||
TaxonomySlug.CATEGORY
|
||||
? "blue"
|
||||
? "green"
|
||||
: term?.taxonomy?.slug ===
|
||||
TaxonomySlug.LEVEL
|
||||
? "green"
|
||||
? "blue"
|
||||
: "orange"
|
||||
}
|
||||
className="px-3 py-1 rounded-full border-0">
|
||||
|
@ -36,7 +34,7 @@ const TermInfo = ({ post }: { post: PostDto }) => {
|
|||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ const CollapsibleContent: React.FC<CollapsibleContentProps> = ({ content }) => {
|
|||
const contentWrapperRef = useRef(null);
|
||||
return (
|
||||
<div className=" text-base ">
|
||||
<div className=" flex flex-col gap-4 border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out rounded-xl p-6 ">
|
||||
<div className=" flex flex-col gap-4 transition-all duration-300 ease-in-out rounded-xl p-6 ">
|
||||
{/* 包装整个内容区域的容器 */}
|
||||
<div ref={contentWrapperRef}>
|
||||
{/* 内容区域 */}
|
||||
|
|
|
@ -35,7 +35,7 @@ export default function ResourcesShower({
|
|||
const imageResources = dealedResources.filter((res) => res.isImage);
|
||||
const fileResources = dealedResources.filter((res) => !res.isImage);
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
{imageResources.length > 0 && (
|
||||
<Row gutter={[16, 16]} className="mb-6">
|
||||
<Image.PreviewGroup>
|
||||
|
@ -82,6 +82,7 @@ export default function ResourcesShower({
|
|||
</Image.PreviewGroup>
|
||||
</Row>
|
||||
)}
|
||||
<div className=" text-sm px-2">附件:</div>
|
||||
{fileResources.length > 0 && (
|
||||
<div className="rounded-xl p-1 border border-gray-100 bg-white">
|
||||
<div className="flex flex-nowrap overflow-x-auto scrollbar-hide gap-1.5">
|
||||
|
|
|
@ -36,8 +36,12 @@ interface CourseFormProviderProps {
|
|||
editId?: string; // 添加 editId 参数
|
||||
}
|
||||
|
||||
export const CourseDetailContext =createContext<CourseDetailContextType | null>(null);
|
||||
export function CourseDetailProvider({children,editId}: CourseFormProviderProps) {
|
||||
export const CourseDetailContext =
|
||||
createContext<CourseDetailContextType | null>(null);
|
||||
export function CourseDetailProvider({
|
||||
children,
|
||||
editId,
|
||||
}: CourseFormProviderProps) {
|
||||
const navigate = useNavigate();
|
||||
const { read } = useVisitor();
|
||||
const { user, hasSomePermissions, isAuthenticated } = useAuth();
|
||||
|
@ -86,7 +90,9 @@ export function CourseDetailProvider({children,editId}: CourseFormProviderProps)
|
|||
}
|
||||
}, [course]);
|
||||
useEffect(() => {
|
||||
navigate(`/course/${editId}/detail/${selectedLectureId}`);
|
||||
if (lectureId !== selectedLectureId) {
|
||||
navigate(`/course/${editId}/detail/${selectedLectureId}`);
|
||||
}
|
||||
}, [selectedLectureId, editId]);
|
||||
const [isHeaderVisible, setIsHeaderVisible] = useState(true); // 新增
|
||||
return (
|
||||
|
|
|
@ -2,22 +2,25 @@ import { Course, TaxonomySlug } from "@nice/common";
|
|||
import React, { useContext, useMemo } from "react";
|
||||
import { Image, Typography, Skeleton, Tag } from "antd"; // 引入 antd 组件
|
||||
import { CourseDetailContext } from "./CourseDetailContext";
|
||||
import {
|
||||
BookOutlined,
|
||||
CalendarOutlined,
|
||||
EditTwoTone,
|
||||
EyeOutlined,
|
||||
PlayCircleOutlined,
|
||||
ReloadOutlined,
|
||||
TeamOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useStaff } from "@nice/client";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import TermInfo from "@web/src/app/main/path/components/TermInfo";
|
||||
import { PictureOutlined } from "@ant-design/icons";
|
||||
|
||||
export const CourseDetailDescription: React.FC = () => {
|
||||
const { course,canEdit, isLoading, selectedLectureId, setSelectedLectureId } =
|
||||
useContext(CourseDetailContext);
|
||||
const { Paragraph, Title } = Typography;
|
||||
const {
|
||||
course,
|
||||
canEdit,
|
||||
isLoading,
|
||||
selectedLectureId,
|
||||
setSelectedLectureId,
|
||||
userIsLearning,
|
||||
lecture = null,
|
||||
} = useContext(CourseDetailContext);
|
||||
const { Paragraph } = Typography;
|
||||
const { user } = useAuth();
|
||||
const { update } = useStaff();
|
||||
const firstLectureId = useMemo(() => {
|
||||
return course?.sections?.[0]?.lectures?.[0]?.id;
|
||||
}, [course]);
|
||||
|
@ -30,49 +33,44 @@ export const CourseDetailDescription: React.FC = () => {
|
|||
<Skeleton active paragraph={{ rows: 4 }} />
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{!selectedLectureId && course?.meta?.thumbnail && (
|
||||
<>
|
||||
<div className="relative mb-4 overflow-hidden flex justify-center items-center">
|
||||
{!selectedLectureId && (
|
||||
<div className="relative mb-4 overflow-hidden flex justify-center items-center">
|
||||
{
|
||||
<Image
|
||||
src={course?.meta?.thumbnail}
|
||||
src={course.meta.thumbnail}
|
||||
preview={false}
|
||||
className="w-full h-full object-cover z-0"
|
||||
fallback="/placeholder.webp"
|
||||
/>
|
||||
<div
|
||||
onClick={() => {
|
||||
setSelectedLectureId(firstLectureId);
|
||||
}}
|
||||
className="w-full h-full absolute top-0 z-10 bg-[rgba(0,0,0,0.3)] transition-all duration-300 ease-in-out hover:bg-[rgba(0,0,0,0.7)] cursor-pointer">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-4xl z-10">
|
||||
点击进入学习
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
onClick={async () => {
|
||||
setSelectedLectureId(firstLectureId);
|
||||
if (!userIsLearning) {
|
||||
await update.mutateAsync({
|
||||
where: { id: user?.id },
|
||||
data: {
|
||||
learningPosts: {
|
||||
connect: {
|
||||
id: course.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="w-full h-full absolute top-0 z-10 bg-[rgba(0,0,0,0.3)] transition-all duration-300 ease-in-out hover:bg-[rgba(0,0,0,0.7)] cursor-pointer group">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white text-4xl z-10 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
点击进入学习
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-lg font-bold">{"课程简介:"}</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2 flex-wrap items-center float-start">
|
||||
{course?.subTitle && <div>{course?.subTitle}</div>}
|
||||
{course.terms.map((term) => {
|
||||
return (
|
||||
<Tag
|
||||
key={term.id}
|
||||
// color={term.taxonomy.slug===TaxonomySlug.CATEGORY? "blue" : "green"}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
<TermInfo post={course}></TermInfo>
|
||||
</div>
|
||||
</div>
|
||||
<Paragraph
|
||||
|
|
|
@ -65,8 +65,8 @@ export const CourseDetailDisplayArea: React.FC = () => {
|
|||
{!lectureIsLoading &&
|
||||
selectedLectureId &&
|
||||
lecture?.meta?.type === LectureType.ARTICLE && (
|
||||
<div className="flex justify-center flex-col items-center gap-2 w-full my-2 px-4">
|
||||
<div className="w-full bg-white shadow-md rounded-lg border border-gray-200 p-6 ">
|
||||
<div className="flex justify-center flex-col items-center gap-2 w-full my-2 ">
|
||||
<div className="w-full rounded-lg ">
|
||||
<CollapsibleContent
|
||||
content={lecture?.content || ""}
|
||||
maxHeight={500} // Optional, defaults to 150
|
||||
|
|
|
@ -12,29 +12,34 @@ import dayjs from "dayjs";
|
|||
import CourseOperationBtns from "./JoinLearingButton";
|
||||
|
||||
export default function CourseDetailTitle() {
|
||||
const {
|
||||
course,
|
||||
isLoading,
|
||||
canEdit,
|
||||
lecture,
|
||||
lectureIsLoading,
|
||||
selectedLectureId,
|
||||
} = useContext(CourseDetailContext);
|
||||
const navigate = useNavigate();
|
||||
const { course } = useContext(CourseDetailContext);
|
||||
return (
|
||||
<div className="flex justify-center flex-col items-center gap-2 w-full my-2 px-6">
|
||||
<div className="flex justify-start w-full text-2xl font-bold">
|
||||
{course?.title}
|
||||
</div>
|
||||
<div className="text-gray-600 flex w-full justify-start items-center gap-5">
|
||||
{course?.author?.showname && (
|
||||
<div>
|
||||
发布者:
|
||||
{course?.author?.showname}
|
||||
</div>
|
||||
)}
|
||||
{course?.depts && course?.depts?.length > 0 && (
|
||||
<div>
|
||||
发布单位:
|
||||
{course?.depts?.map((dept) => dept.name)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-gray-600 flex w-full justify-start items-center gap-5">
|
||||
<div className="flex gap-1">
|
||||
<CalendarOutlined></CalendarOutlined>
|
||||
{"创建于:"}
|
||||
{"发布于:"}
|
||||
{dayjs(course?.createdAt).format("YYYY年M月D日")}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<ReloadOutlined spin></ReloadOutlined>
|
||||
{"更新于:"}
|
||||
{"最后更新:"}
|
||||
{dayjs(course?.updatedAt).format("YYYY年M月D日")}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Card, Typography, Button } from "antd";
|
||||
import { Card, Typography, Button, Empty } 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";
|
||||
import { PictureOutlined } from "@ant-design/icons";
|
||||
|
||||
interface PostCardProps {
|
||||
post?: PostDto;
|
||||
|
@ -20,18 +21,24 @@ export default function PostCard({ post, onClick }: PostCardProps) {
|
|||
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 className="relative h-56 bg-gradient-to-br from-gray-900 to-gray-800 overflow-hidden group">
|
||||
{post?.meta?.thumbnail ? (
|
||||
<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 className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-primary-500 to-primary-700">
|
||||
<PictureOutlined className="text-white text-6xl" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}>
|
||||
<div className="px-4 ">
|
||||
<div className="overflow-hidden hover:overflow-auto">
|
||||
<div className="flex gap-2 h-7 mb-4 whiteSpace-nowrap">
|
||||
<div className="flex gap-2 h-7 whiteSpace-nowrap">
|
||||
<TermInfo post={post}></TermInfo>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -40,20 +40,23 @@ export type PostDto = Post & {
|
|||
delete: boolean;
|
||||
// edit: boolean;
|
||||
};
|
||||
meta?: PostMeta;
|
||||
watchableDepts: Department[];
|
||||
watchableStaffs: Staff[];
|
||||
terms: TermDto[];
|
||||
depts: DepartmentDto[];
|
||||
meta?: {
|
||||
thumbnail?: string;
|
||||
views?: number;
|
||||
};
|
||||
|
||||
studentIds?: string[];
|
||||
};
|
||||
|
||||
export type LectureMeta = {
|
||||
type?: string;
|
||||
export type PostMeta = {
|
||||
thumbnail?: string;
|
||||
views?: number;
|
||||
likes?: number;
|
||||
hates?: number;
|
||||
};
|
||||
export type LectureMeta = PostMeta & {
|
||||
type?: string;
|
||||
|
||||
videoUrl?: string;
|
||||
videoThumbnail?: string;
|
||||
videoIds?: string[];
|
||||
|
@ -65,7 +68,7 @@ export type Lecture = Post & {
|
|||
meta?: LectureMeta;
|
||||
};
|
||||
|
||||
export type SectionMeta = {
|
||||
export type SectionMeta = PostMeta & {
|
||||
objectives?: string[];
|
||||
};
|
||||
export type Section = Post & {
|
||||
|
@ -74,13 +77,8 @@ export type Section = Post & {
|
|||
export type SectionDto = Section & {
|
||||
lectures: Lecture[];
|
||||
};
|
||||
export type CourseMeta = {
|
||||
thumbnail?: string;
|
||||
|
||||
export type CourseMeta = PostMeta & {
|
||||
objectives?: string[];
|
||||
views?: number;
|
||||
likes?: number;
|
||||
hates?: number;
|
||||
};
|
||||
export type Course = PostDto & {
|
||||
meta?: CourseMeta;
|
||||
|
@ -93,3 +91,45 @@ export type CourseDto = Course & {
|
|||
depts: Department[];
|
||||
studentIds: string[];
|
||||
};
|
||||
|
||||
export type Summary = {
|
||||
id: string;
|
||||
text: string;
|
||||
parent: string;
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
export type NodeObj = {
|
||||
topic: string;
|
||||
id: string;
|
||||
style?: {
|
||||
fontSize?: string;
|
||||
color?: string;
|
||||
background?: string;
|
||||
fontWeight?: string;
|
||||
};
|
||||
children?: NodeObj[];
|
||||
};
|
||||
export type Arrow = {
|
||||
id: string;
|
||||
label: string;
|
||||
from: string;
|
||||
to: string;
|
||||
delta1: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
delta2: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
export type PathMeta = PostMeta & {
|
||||
nodeData: NodeObj;
|
||||
arrows?: Arrow[];
|
||||
summaries?: Summary[];
|
||||
direction?: number;
|
||||
};
|
||||
export type PathDto = PostDto & {
|
||||
meta: PathMeta;
|
||||
};
|
||||
|
|
|
@ -6,6 +6,8 @@ export const postDetailSelect: Prisma.PostSelect = {
|
|||
title: true,
|
||||
content: true,
|
||||
resources: true,
|
||||
parent: true,
|
||||
parentId: true,
|
||||
// watchableDepts: true,
|
||||
// watchableStaffs: true,
|
||||
updatedAt: true,
|
||||
|
@ -18,9 +20,9 @@ export const postDetailSelect: Prisma.PostSelect = {
|
|||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
depts: true,
|
||||
author: {
|
||||
|
@ -42,12 +44,14 @@ export const postDetailSelect: Prisma.PostSelect = {
|
|||
},
|
||||
},
|
||||
},
|
||||
meta: true
|
||||
meta: true,
|
||||
};
|
||||
export const postUnDetailSelect: Prisma.PostSelect = {
|
||||
id: true,
|
||||
type: true,
|
||||
title: true,
|
||||
parent: true,
|
||||
parentId: true,
|
||||
content: true,
|
||||
resources: true,
|
||||
updatedAt: true,
|
||||
|
@ -85,6 +89,8 @@ export const courseDetailSelect: Prisma.PostSelect = {
|
|||
title: true,
|
||||
subTitle: true,
|
||||
type: true,
|
||||
author: true,
|
||||
authorId: true,
|
||||
content: true,
|
||||
depts: true,
|
||||
// isFeatured: true,
|
||||
|
|
Loading…
Reference in New Issue