This commit is contained in:
ditiqi 2025-02-27 21:45:40 +08:00
parent ee9df61320
commit 49d3f613fc
15 changed files with 179 additions and 115 deletions

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

View File

@ -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>
</>

View File

@ -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

View File

@ -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) {

View File

@ -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>
);
};

View File

@ -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}>
{/* 内容区域 */}

View File

@ -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">

View File

@ -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();
@ -60,7 +64,7 @@ export function CourseDetailProvider({children,editId}: CourseFormProviderProps)
const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST);
return isAuthor || isRoot;
}, [user, course]);
const [selectedLectureId, setSelectedLectureId] = useState<
string | undefined
>(lectureId || undefined);
@ -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 (

View File

@ -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

View File

@ -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

View File

@ -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">

View File

@ -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>

View File

@ -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;
};

View File

@ -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,