diff --git a/.gitignore b/.gitignore index fcd12c1..2e6bac7 100755 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,5 @@ yarn-error.log* # Ignore .idea files in the Expo monorepo **/.idea/ uploads -packages/mind-elixir-core \ No newline at end of file +packages/mind-elixir-core +config/nginx/conf.d/web.conf \ No newline at end of file diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index fc3db64..d830c4d 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -60,24 +60,6 @@ export default function CourseCard({ course }: CourseCardProps) { ); })} - {/* - {course.terms?.[0].name} - - - - - {course.terms?.[1].name} - */} </div> - <div className="pt-4 border-t border-gray-100 text-center"> <Button type="primary" diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx index f3ed9ec..40043a3 100755 --- a/apps/web/src/app/main/home/components/CategorySection.tsx +++ b/apps/web/src/app/main/home/components/CategorySection.tsx @@ -41,7 +41,7 @@ const CategorySection = () => { window.scrollTo({top: 0,behavior: "smooth",}) },[]); return ( - <section className="py-32 relative overflow-hidden"> + <section className="py-8 relative overflow-hidden"> <div className="max-w-screen-2xl mx-auto px-4 relative"> <div className="text-center mb-24"> <Title diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index 9f45863..902e979 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -6,37 +6,37 @@ import { CoursesSectionTag } from "./CoursesSectionTag"; import CourseList from "@web/src/components/models/course/list/CourseList"; import LookForMore from "./LookForMore"; interface GetTaxonomyProps { - categories: string[]; - isLoading: boolean; + categories: string[]; + isLoading: boolean; } function useGetTaxonomy({ type }): GetTaxonomyProps { - const { data, isLoading }: { data: TermDto[]; isLoading: boolean } = - api.term.findMany.useQuery({ - where: { - taxonomy: { - slug: type, - }, - }, - take: 10, // 只取前10个 - }); - const categories = useMemo(() => { - const allCategories = isLoading - ? [] - : data?.map((course) => course.name); - return [...Array.from(new Set(allCategories))]; - }, [data]); - return { categories, isLoading }; + const { data, isLoading }: { data: TermDto[]; isLoading: boolean } = + api.term.findMany.useQuery({ + where: { + taxonomy: { + slug: type, + }, + }, + take: 10, // 只取前10个 + }); + const categories = useMemo(() => { + const allCategories = isLoading + ? [] + : data?.map((course) => course.name); + return [...Array.from(new Set(allCategories))]; + }, [data]); + return { categories, isLoading }; } const { Title, Text } = Typography; interface CoursesSectionProps { - title: string; - description: string; - initialVisibleCoursesCount?: number; + title: string; + description: string; + initialVisibleCoursesCount?: number; } const CoursesSection: React.FC<CoursesSectionProps> = ({ - title, - description, - initialVisibleCoursesCount = 8, + title, + description, + initialVisibleCoursesCount = 8, }) => { const [selectedCategory, setSelectedCategory] = useState<string>("全部"); const gateGory: GetTaxonomyProps = useGetTaxonomy({ diff --git a/apps/web/src/app/main/layout/MainLayout.tsx b/apps/web/src/app/main/layout/MainLayout.tsx index 0a0074b..b573e2f 100755 --- a/apps/web/src/app/main/layout/MainLayout.tsx +++ b/apps/web/src/app/main/layout/MainLayout.tsx @@ -9,9 +9,9 @@ const { Content } = Layout; export function MainLayout() { return ( <MainProvider> - <Layout className="min-h-screen"> + <Layout className="min-h-screen bg-gray-100"> <MainHeader /> - <Content className="mt-16 bg-gray-50"> + <Content className="mt-16 bg-gray-50 "> <Outlet /> </Content> <MainFooter /> diff --git a/apps/web/src/app/main/paths/page.tsx b/apps/web/src/app/main/paths/page.tsx index e1283a0..9624011 100755 --- a/apps/web/src/app/main/paths/page.tsx +++ b/apps/web/src/app/main/paths/page.tsx @@ -1,5 +1,6 @@ import MindEditor from "@web/src/components/common/editor/MindEditor"; export default function PathsPage() { - return <MindEditor></MindEditor>; + // return <MindEditor></MindEditor>; + return <>123</> } diff --git a/apps/web/src/components/layout/breadcrumb.tsx b/apps/web/src/components/layout/breadcrumb.tsx index 1804dab..bffbb65 100755 --- a/apps/web/src/components/layout/breadcrumb.tsx +++ b/apps/web/src/components/layout/breadcrumb.tsx @@ -1,38 +1,42 @@ -import React from 'react'; -import { useLocation, Link, useMatches } from 'react-router-dom'; -import { theme } from 'antd'; -import { RightOutlined } from '@ant-design/icons'; +import React from "react"; +import { useLocation, Link, useMatches } from "react-router-dom"; +import { theme } from "antd"; +import { RightOutlined } from "@ant-design/icons"; export default function Breadcrumb() { - let matches = useMatches(); - const { token } = theme.useToken() + const matches = useMatches(); + const { token } = theme.useToken(); - let crumbs = matches - // first get rid of any matches that don't have handle and crumb - .filter((match) => Boolean((match.handle as any)?.crumb)) - // now map them into an array of elements, passing the loader - // data to each one - .map((match) => (match.handle as any).crumb(match.data)); + const crumbs = matches + // first get rid of any matches that don't have handle and crumb + .filter((match) => Boolean((match.handle as any)?.crumb)) + // now map them into an array of elements, passing the loader + // data to each one + .map((match) => (match.handle as any).crumb(match.data)); - return ( - <ol className='flex items-center space-x-2 text-gray-600'> - {crumbs.map((crumb, index) => ( - <React.Fragment key={index}> - <li className={`inline-flex items-center `} - style={{ - color: (index === crumbs.length - 1) ? token.colorPrimaryText : token.colorTextSecondary, - fontWeight: (index === crumbs.length - 1) ? "bold" : "normal", - }} - > - {crumb} - </li> - {index < crumbs.length - 1 && ( - <li className='mx-2'> - <RightOutlined></RightOutlined> - </li> - )} - </React.Fragment> - ))} - </ol> - ); + return ( + <ol className="flex items-center space-x-2 text-gray-600"> + {crumbs.map((crumb, index) => ( + <React.Fragment key={index}> + <li + className={`inline-flex items-center `} + style={{ + color: + index === crumbs.length - 1 + ? token.colorPrimaryText + : token.colorTextSecondary, + fontWeight: + index === crumbs.length - 1 ? "bold" : "normal", + }}> + {crumb} + </li> + {index < crumbs.length - 1 && ( + <li className="mx-2"> + <RightOutlined></RightOutlined> + </li> + )} + </React.Fragment> + ))} + </ol> + ); } diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 6787e13..2f702e7 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -3,10 +3,17 @@ import { courseDetailSelect, CourseDto, Lecture, + RolePerms, VisitType, } from "@nice/common"; import { useAuth } from "@web/src/providers/auth-provider"; -import React, { createContext, ReactNode, useEffect, useState } from "react"; +import React, { + createContext, + ReactNode, + useEffect, + useMemo, + useState, +} from "react"; import { useNavigate, useParams } from "react-router-dom"; interface CourseDetailContextType { @@ -19,11 +26,14 @@ interface CourseDetailContextType { lectureIsLoading?: boolean; isHeaderVisible: boolean; // 新增 setIsHeaderVisible: (visible: boolean) => void; // 新增 + canEdit?: boolean; } + interface CourseFormProviderProps { children: ReactNode; editId?: string; // 添加 editId 参数 } + export const CourseDetailContext = createContext<CourseDetailContextType | null>(null); export function CourseDetailProvider({ @@ -32,8 +42,9 @@ export function CourseDetailProvider({ }: CourseFormProviderProps) { const navigate = useNavigate(); const { read } = useVisitor(); - const { user } = useAuth(); + const { user, hasSomePermissions } = useAuth(); const { lectureId } = useParams(); + const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = (api.post as any).findFirst.useQuery( { @@ -45,7 +56,14 @@ export function CourseDetailProvider({ }, { enabled: Boolean(editId) } ); - + const canEdit = useMemo(() => { + const isAuthor = user?.id === course?.authorId; + const isDept = course?.depts + ?.map((dept) => dept.id) + .includes(user?.deptId); + const isRoot = hasSomePermissions(RolePerms?.MANAGE_ANY_POST); + return isAuthor || isDept || isRoot; + }, [user, course]); const [selectedLectureId, setSelectedLectureId] = useState< string | undefined >(lectureId || undefined); @@ -57,9 +75,9 @@ export function CourseDetailProvider({ }, { enabled: Boolean(editId) } ); + useEffect(() => { if (course) { - console.log("read"); read.mutateAsync({ data: { visitorId: user?.id || null, @@ -85,6 +103,7 @@ export function CourseDetailProvider({ lectureIsLoading, isHeaderVisible, setIsHeaderVisible, + canEdit, }}> {children} </CourseDetailContext.Provider> diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx index acb8d70..433ef3b 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -11,14 +11,16 @@ import { useNavigate, useParams } from "react-router-dom"; import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu"; import { CourseDetailContext } from "../CourseDetailContext"; + const { Header } = Layout; export function CourseDetailHeader() { const [searchValue, setSearchValue] = useState(""); const { id } = useParams(); - const { isAuthenticated, user, hasSomePermissions } = useAuth(); + const { isAuthenticated, user, hasSomePermissions, hasEveryPermissions } = + useAuth(); const navigate = useNavigate(); - const { course } = useContext(CourseDetailContext); + const { course, canEdit } = useContext(CourseDetailContext); return ( <Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30"> @@ -49,7 +51,7 @@ export function CourseDetailHeader() { onChange={(e) => setSearchValue(e.target.value)} /> </div> - {isAuthenticated && ( + {canEdit && ( <> <Button onClick={() => { @@ -65,18 +67,7 @@ export function CourseDetailHeader() { </> )} {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> + <UserMenu /> ) : ( <Button onClick={() => navigate("/login")} diff --git a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx index d2cffe6..42b3d55 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx @@ -9,9 +9,7 @@ import { CourseDetailHeader } from "./CourseDetailHeader/CourseDetailHeader"; export default function CourseDetailLayout() { const { course, - selectedLectureId, - lecture, - isLoading, + setSelectedLectureId, } = useContext(CourseDetailContext); @@ -38,10 +36,7 @@ export default function CourseDetailLayout() { }} transition={{ type: "spring", stiffness: 300, damping: 30 }} className="relative"> - <CourseDetailDisplayArea - // course={course} - // isLoading={isLoading} - /> + <CourseDetailDisplayArea /> </motion.div> {/* 课程大纲侧边栏 */} <CourseSyllabus diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx index f4276bf..ffc0a49 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -24,7 +24,12 @@ export const LectureItem: React.FC<LectureItemProps> = ({ }, [lectureId, lecture]); return ( <div - className="w-full flex items-center gap-4 p-4 hover:bg-gray-200 text-left transition-colors cursor-pointer" + className={`w-full flex items-center gap-4 p-4 text-left transition-colors cursor-pointer + ${ + isReading + ? "bg-blue-50 border-l-4 border-blue-500 hover:bg-blue-50" + : "hover:bg-gray-200" + }`} onClick={() => onClick(lecture.id)}> {lecture?.meta?.type === LectureType.VIDEO && ( <div className="text-blue-500 flex items-center"> diff --git a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx index 6b61854..0c8becc 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -1,6 +1,7 @@ import { createContext, useContext, ReactNode, useEffect } from "react"; import { Form, FormInstance, message } from "antd"; import { + courseDetailSelect, CourseDto, CourseMeta, CourseStatus, @@ -48,9 +49,7 @@ export function CourseFormProvider({ const { data: course }: { data: CourseDto } = api.post.findFirst.useQuery( { where: { id: editId }, - include: { - terms: true, - }, + select: courseDetailSelect, }, { enabled: Boolean(editId) } ); @@ -65,10 +64,12 @@ export function CourseFormProvider({ useEffect(() => { if (course) { + const deptIds = (course?.depts || [])?.map((dept) => dept.id); const formData = { title: course.title, subTitle: course.subTitle, content: course.content, + deptIds: deptIds, meta: { thumbnail: course?.meta?.thumbnail, }, @@ -91,7 +92,10 @@ export function CourseFormProvider({ const formattedValues = { ...values, meta: { - thumbnail: values?.meta?.thumbnail, + ...((course?.meta as CourseMeta) || {}), + ...(values?.meta?.thumbnail !== undefined && { + thumbnail: values?.meta?.thumbnail, + }), }, terms: { connect: termIds.map((id) => ({ id })), // 转换成 connect 格式 @@ -106,12 +110,9 @@ export function CourseFormProvider({ }); delete formattedValues.sections; delete formattedValues.deptIds; - if (course) { - formattedValues.meta = { - ...(course?.meta as CourseMeta), - thumbnail: values?.meta?.thumbnail, - }; - } + + console.log(course.meta); + console.log(formattedValues?.meta); try { if (editId) { const result = await update.mutateAsync({ diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableSection.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableSection.tsx index aea9b29..bf6af5b 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableSection.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableSection.tsx @@ -69,6 +69,9 @@ export const SortableSection: React.FC<SortableSectionProps> = ({ }); } else { result = await update.mutateAsync({ + where: { + id: field?.id, + }, data: { title: values?.title, }, diff --git a/apps/web/src/components/models/course/editor/layout/CourseEditorHeader.tsx b/apps/web/src/components/models/course/editor/layout/CourseEditorHeader.tsx index 8c23fa7..a3197f8 100755 --- a/apps/web/src/components/models/course/editor/layout/CourseEditorHeader.tsx +++ b/apps/web/src/components/models/course/editor/layout/CourseEditorHeader.tsx @@ -4,6 +4,7 @@ import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { CourseStatus, CourseStatusLabel } from "@nice/common"; import { useCourseEditor } from "../context/CourseEditorContext"; +import { useAuth } from "@web/src/providers/auth-provider"; const { Title } = Typography; @@ -16,6 +17,8 @@ const courseStatusVariant: Record<CourseStatus, string> = { export default function CourseEditorHeader() { const navigate = useNavigate(); + const { user, hasSomePermissions } = useAuth(); + const { onSubmit, course, form } = useCourseEditor(); const handleSave = () => { @@ -34,7 +37,13 @@ export default function CourseEditorHeader() { <div className="flex items-center space-x-3"> <Button icon={<ArrowLeftOutlined />} - onClick={() => navigate(-1)} + onClick={() => { + if (course?.id) { + navigate(`/course/${course?.id}/detail`); + } else { + navigate("/"); + } + }} type="text" /> <div className="flex items-center space-x-2"> diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 7592489..9a60fd1 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -63,16 +63,6 @@ export const routes: CustomRouteObject[] = [ path: "courses", element: <CoursesPage></CoursesPage>, }, - - { - path: "profiles", - }, - - // // 课程预览页面 - // { - // path: "coursePreview/:id?", - // element: <CoursePreview></CoursePreview>, - // }, ], }, { @@ -103,12 +93,6 @@ export const routes: CustomRouteObject[] = [ </WithAuth> ), }, - // { - // path: "setting", - // element: ( - // <CourseSettingForm></CourseSettingForm> - // ), - // }, ], }, { diff --git a/config/nginx/conf.d/web.conf b/config/nginx/conf.d/web.conf index 67302b8..e0ac769 100755 --- a/config/nginx/conf.d/web.conf +++ b/config/nginx/conf.d/web.conf @@ -100,7 +100,7 @@ server { # 仅供内部使用 internal; # 代理到认证服务 - proxy_pass http://host.docker.internal:3000/auth/file; + proxy_pass http://host.docker.internal:/auth/file; # 请求优化:不传递请求体 proxy_pass_request_body off; diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 6264c9b..1bcd87c 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -8,6 +8,7 @@ export enum PostType { COURSE = "couse", SECTION = "section", LECTURE = "lecture", + PATH = "path", } export enum LectureType { VIDEO = "video", @@ -100,7 +101,7 @@ export enum RolePerms { } export enum AppConfigSlug { BASE_SETTING = "base_setting", - + } // 资源类型的枚举,定义了不同类型的资源,以字符串值表示 export enum ResourceType { diff --git a/packages/mind-elixir-core b/packages/mind-elixir-core deleted file mode 160000 index b911b4b..0000000 --- a/packages/mind-elixir-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b911b4ba7629da9d6c622abe241fd25299baf1a5