This commit is contained in:
ditiqi 2025-02-21 13:14:47 +08:00
parent 7fd4bc59bc
commit 0597b4f5b3
14 changed files with 127 additions and 93 deletions

View File

@ -124,7 +124,10 @@ export async function updateCourseEnrollmentStats(courseId: string) {
},
});
}
export async function setCourseInfo({ data }: { data: Post }) {
// await db.term
if (data?.type === PostType.COURSE) {
const ancestries = await db.postAncestry.findMany({
where: {

View File

@ -2,7 +2,7 @@ import CourseDetail from "@web/src/components/models/course/detail/CourseDetail"
import { useParams } from "react-router-dom";
export function CourseDetailPage() {
const { id } = useParams();
const { id, lectureId } = useParams();
console.log("Course ID:", id);
return <CourseDetail id={id}></CourseDetail>;
return <CourseDetail id={id} lectureId={lectureId}></CourseDetail>;
}

View File

@ -1,6 +1,7 @@
import { Checkbox, Divider, Radio, Space } from "antd";
import { categories, levels } from "../mockData";
import { api } from "@nice/client";
import { TaxonomySlug } from "@nice/common";
interface FilterSectionProps {
selectedCategory: string;
@ -15,6 +16,13 @@ export default function FilterSection({
onCategoryChange,
onLevelChange,
}: FilterSectionProps) {
const { data: levels, isLoading } = api.term.findMany.useQuery({
where: {
taxonomy: {
slug: TaxonomySlug.LEVEL,
},
},
});
// const { data } = api.term;
return (
<div className="bg-white p-6 rounded-lg shadow-sm space-y-6">
@ -42,9 +50,9 @@ export default function FilterSection({
onChange={(e) => onLevelChange(e.target.value)}
className="flex flex-col space-y-3">
<Radio value=""></Radio>
{levels.map((level) => (
<Radio key={level} value={level}>
{level}
{levels?.map((level) => (
<Radio key={level.id} value={level.id}>
{level.name}
</Radio>
))}
</Radio.Group>

View File

@ -1,65 +1,75 @@
import { useState } from 'react';
import { Input, Layout, Avatar, Button, Dropdown } from 'antd';
import { SearchOutlined, UserOutlined } from '@ant-design/icons';
import { useAuth } from '@web/src/providers/auth-provider';
import { useNavigate } from 'react-router-dom';
import { UserMenu } from './UserMenu';
import { NavigationMenu } from './NavigationMenu';
import { useState } from "react";
import { Input, Layout, Avatar, Button, Dropdown } from "antd";
import { EditFilled, SearchOutlined, UserOutlined } from "@ant-design/icons";
import { useAuth } from "@web/src/providers/auth-provider";
import { useNavigate } from "react-router-dom";
import { UserMenu } from "./UserMenu";
import { NavigationMenu } from "./NavigationMenu";
const { Header } = Layout;
export function MainHeader() {
const [searchValue, setSearchValue] = useState('');
const { isAuthenticated, user } = useAuth();
const navigate = useNavigate();
const [searchValue, setSearchValue] = useState("");
const { isAuthenticated, user } = useAuth();
const navigate = useNavigate();
return (
<Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
<div className="w-full max-w-screen-2xl px-4 md:px-6 mx-auto flex items-center justify-between h-full">
<div className="flex items-center space-x-8">
<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>
<div className="flex items-center space-x-6">
<div className="group relative">
<Input
size="large"
prefix={<SearchOutlined className="text-gray-400 group-hover:text-blue-500 transition-colors" />}
placeholder="搜索课程"
className="w-72 rounded-full"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
</div>
{isAuthenticated ? (
<Dropdown
overlay={<UserMenu />}
trigger={['click']}
placement="bottomRight"
>
<Avatar
size="large"
className="cursor-pointer hover:scale-105 transition-all bg-gradient-to-r from-blue-500 to-blue-600 text-white font-semibold"
>
{(user?.showname || user?.username || '')[0]?.toUpperCase()}
</Avatar>
</Dropdown>
) : (
<Button
onClick={() => navigate('/login')}
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
icon={<UserOutlined />}
>
</Button>
)}
</div>
</div>
</Header>
);
return (
<Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
<div className="w-full max-w-screen-2xl px-4 md:px-6 mx-auto flex items-center justify-between h-full">
<div className="flex items-center space-x-8">
<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>
<div className="flex items-center space-x-6">
<div className="group relative">
<Input
size="large"
prefix={
<SearchOutlined className="text-gray-400 group-hover:text-blue-500 transition-colors" />
}
placeholder="搜索课程"
className="w-72 rounded-full"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
/>
</div>
{isAuthenticated && (
<>
<Button
onClick={() => navigate("/course/editor")}
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
icon={<EditFilled />}>
</Button>
</>
)}
{isAuthenticated ? (
<Dropdown
overlay={<UserMenu />}
trigger={["click"]}
placement="bottomRight">
<Avatar
size="large"
className="cursor-pointer hover:scale-105 transition-all bg-gradient-to-r from-blue-500 to-blue-600 text-white font-semibold">
{(user?.showname ||
user?.username ||
"")[0]?.toUpperCase()}
</Avatar>
</Dropdown>
) : (
<Button
onClick={() => navigate("/login")}
className="flex items-center space-x-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white hover:from-blue-600 hover:to-blue-700 border-none shadow-md hover:shadow-lg transition-all"
icon={<UserOutlined />}>
</Button>
)}
</div>
</div>
</Header>
);
}

View File

@ -1,7 +1,13 @@
import { CourseDetailProvider } from "./CourseDetailContext";
import CourseDetailLayout from "./CourseDetailLayout";
export default function CourseDetail({ id }: { id?: string }) {
export default function CourseDetail({
id,
lectureId,
}: {
id?: string;
lectureId?: string;
}) {
const iframeStyle = {
width: "50%",
height: "100vh",

View File

@ -21,12 +21,13 @@ export function CourseDetailProvider({
children,
editId,
}: CourseFormProviderProps) {
const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } =
(api.post as any).findFirst.useQuery(
{
where: { id: editId },
include: {
sections: { include: { lectures: true } },
// sections: { include: { lectures: true } },
enrollments: true,
},
},

View File

@ -7,11 +7,12 @@ import {
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import React, { useState, useRef, useContext } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { SectionDto } from "@nice/common";
import { SectionDto, TaxonomySlug } from "@nice/common";
import { SyllabusHeader } from "./SyllabusHeader";
import { SectionItem } from "./SectionItem";
import { CollapsedButton } from "./CollapsedButton";
import { CourseDetailContext } from "../CourseDetailContext";
import { api } from "@nice/client";
interface CourseSyllabusProps {
sections: SectionDto[];
@ -29,7 +30,11 @@ export const CourseSyllabus: React.FC<CourseSyllabusProps> = ({
const { isHeaderVisible } = useContext(CourseDetailContext);
const [expandedSections, setExpandedSections] = useState<string[]>([]);
const sectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
// api.term.findMany.useQuery({
// where: {
// taxonomy: { slug: TaxonomySlug.CATEGORY },
// },
// });
const toggleSection = (sectionId: string) => {
setExpandedSections((prev) =>
prev.includes(sectionId)

View File

@ -1,9 +1,8 @@
// components/CourseSyllabus/LectureItem.tsx
import { Lecture } from "@nice/common";
import { Lecture, LectureType } from "@nice/common";
import React from "react";
import { motion } from "framer-motion";
import { ClockIcon, PlayCircleIcon } from "@heroicons/react/24/outline";
import { ClockCircleOutlined, FileTextOutlined, PlayCircleOutlined } from "@ant-design/icons"; // 使用 Ant Design 图标
interface LectureItemProps {
lecture: Lecture;
@ -14,23 +13,24 @@ export const LectureItem: React.FC<LectureItemProps> = ({
lecture,
onClick,
}) => (
<motion.button
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="w-full flex items-center gap-4 p-4 hover:bg-gray-50 text-left transition-colors"
<div
className="w-full flex items-center gap-4 p-4 hover:bg-gray-50 text-left transition-colors cursor-pointer"
onClick={() => onClick(lecture.id)}>
<PlayCircleIcon className="w-5 h-5 text-blue-500 flex-shrink-0" />
{lecture.type === LectureType.VIDEO && (
<PlayCircleOutlined className="w-5 h-5 text-blue-500 flex-shrink-0" />
)}
{lecture.type === LectureType.ARTICLE && (
<FileTextOutlined className="w-5 h-5 text-blue-500 flex-shrink-0" /> // 为文章类型添加图标
)}
<div className="flex-grow">
<h4 className="font-medium text-gray-800">{lecture.title}</h4>
{lecture.description && (
<p className="text-sm text-gray-500 mt-1">
{lecture.description}
</p>
{lecture.subTitle && (
<p className="text-sm text-gray-500 mt-1">{lecture.subTitle}</p>
)}
</div>
<div className="flex items-center gap-1 text-sm text-gray-500">
<ClockIcon className="w-4 h-4" />
<ClockCircleOutlined className="w-4 h-4" />
<span>{lecture.duration}</span>
</div>
</motion.button>
</div>
);

View File

@ -33,8 +33,8 @@ export const SectionItem = React.forwardRef<HTMLDivElement, SectionItemProps>(
{section.title}
</h3>
<p className="text-sm text-gray-500">
{section.totalLectures} ·{" "}
{Math.floor(section.totalDuration / 60)}
{section?.lectures?.length} ·{" "}
{/* {Math.floor(section?.totalDuration / 60)}分钟 */}
</p>
</div>
</div>

View File

@ -96,7 +96,7 @@ export function CourseFormProvider({
};
// 删除原始的 taxonomy 字段
taxonomies.forEach((tax) => {
delete formattedValues[tax.name];
delete formattedValues[tax.id];
});
delete formattedValues.requirements;
delete formattedValues.objectives;

View File

@ -53,7 +53,9 @@ export function CourseBasicForm() {
label={tax.name}
name={tax.id}
key={index}>
<TermSelect taxonomyId={tax.id}></TermSelect>
<TermSelect
placeholder={`请选择${tax.name}`}
taxonomyId={tax.id}></TermSelect>
</Form.Item>
))}
{/* <Form.Item name="level" label="">

View File

@ -56,11 +56,10 @@ export const routes: CustomRouteObject[] = [
{
index: true,
element: <HomePage />,
},
{
path: "paths",
element: <PathsPage></PathsPage>
element: <PathsPage></PathsPage>,
},
{
path: "courses",
@ -125,7 +124,7 @@ export const routes: CustomRouteObject[] = [
],
},
{
path: ":id?/detail", // 使用 ? 表示 id 参数是可选的
path: ":id?/detail/:lectureId?", // 使用 ? 表示 id 参数是可选的
element: <CourseDetailPage />,
},
],

View File

@ -198,7 +198,8 @@ model Post {
terms Term[] @relation("post_term")
order Float? @default(0) @map("order")
duration Int?
rating Int? @default(0)
// 索引
// 日期时间类型字段
createdAt DateTime @default(now()) @map("created_at")
publishedAt DateTime? @map("published_at") // 发布时间
@ -242,7 +243,6 @@ model PostAncestry {
relDepth Int @map("rel_depth")
ancestor Post? @relation("AncestorPosts", fields: [ancestorId], references: [id])
descendant Post @relation("DescendantPosts", fields: [descendantId], references: [id])
// 复合索引优化
// 索引建议
@@index([ancestorId]) // 针对祖先的查询

View File

@ -6,8 +6,8 @@ export enum PostType {
POST_COMMENT = "post_comment",
COURSE_REVIEW = "course_review",
COURSE = "couse",
LECTURE = "lecture",
SECTION = "section",
LECTURE = "lecture",
}
export enum LectureType {
VIDEO = "video",