Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
49464054dc
|
@ -69,3 +69,4 @@ yarn-error.log*
|
|||
**/.idea/
|
||||
uploads
|
||||
packages/mind-elixir-core
|
||||
config/nginx/conf.d/web.conf
|
|
@ -60,24 +60,6 @@ export default function CourseCard({ course }: CourseCardProps) {
|
|||
</Tag>
|
||||
);
|
||||
})}
|
||||
{/* <Tag
|
||||
color="blue"
|
||||
className="px-3 py-1 rounded-full bg-blue-100 text-blue-600 border-0">
|
||||
{course.terms?.[0].name}
|
||||
|
||||
</Tag>
|
||||
|
||||
<Tag
|
||||
color={
|
||||
course.terms?.[1].name === "入门"
|
||||
? "green"
|
||||
: course.terms?.[1].name === "中级"
|
||||
? "blue"
|
||||
: "purple"
|
||||
}
|
||||
className="px-3 py-1 rounded-full border-0">
|
||||
{course.terms?.[1].name}
|
||||
</Tag> */}
|
||||
</div>
|
||||
|
||||
<Title
|
||||
|
@ -103,7 +85,6 @@ export default function CourseCard({ course }: CourseCardProps) {
|
|||
: null}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-gray-100 text-center">
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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</>
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
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
|
||||
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
|
||||
|
@ -15,19 +15,23 @@ export default function Breadcrumb() {
|
|||
.map((match) => (match.handle as any).crumb(match.data));
|
||||
|
||||
return (
|
||||
<ol className='flex items-center space-x-2 text-gray-600'>
|
||||
<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 `}
|
||||
<li
|
||||
className={`inline-flex items-center `}
|
||||
style={{
|
||||
color: (index === crumbs.length - 1) ? token.colorPrimaryText : token.colorTextSecondary,
|
||||
fontWeight: (index === crumbs.length - 1) ? "bold" : "normal",
|
||||
}}
|
||||
>
|
||||
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'>
|
||||
<li className="mx-2">
|
||||
<RightOutlined></RightOutlined>
|
||||
</li>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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: {
|
||||
...((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({
|
||||
|
|
|
@ -69,6 +69,9 @@ export const SortableSection: React.FC<SortableSectionProps> = ({
|
|||
});
|
||||
} else {
|
||||
result = await update.mutateAsync({
|
||||
where: {
|
||||
id: field?.id,
|
||||
},
|
||||
data: {
|
||||
title: values?.title,
|
||||
},
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
// ),
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -8,6 +8,7 @@ export enum PostType {
|
|||
COURSE = "couse",
|
||||
SECTION = "section",
|
||||
LECTURE = "lecture",
|
||||
PATH = "path",
|
||||
}
|
||||
export enum LectureType {
|
||||
VIDEO = "video",
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Subproject commit b911b4ba7629da9d6c622abe241fd25299baf1a5
|
Loading…
Reference in New Issue