Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
17633bcbd3
|
@ -1,5 +1,5 @@
|
|||
# 基础镜像
|
||||
FROM node:18.17-alpine as base
|
||||
FROM node:18-alpine as base
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
|
||||
echo "https://mirrors.aliyun.com/alpine/v3.18/community" >> /etc/apk/repositories
|
||||
|
||||
|
|
|
@ -125,5 +125,14 @@ export class PostRouter {
|
|||
const { staff } = ctx;
|
||||
return await this.postService.updateOrderByIds(input.ids);
|
||||
}),
|
||||
softDeletePostDescendant:this.trpc.protectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
ancestorId:z.string()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input })=>{
|
||||
return await this.postService.softDeletePostDescendant(input)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
@ -101,6 +101,7 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
|||
},
|
||||
params: { staff?: UserProfile; tx?: Prisma.TransactionClient },
|
||||
) {
|
||||
|
||||
const { courseDetail } = args;
|
||||
// If no transaction is provided, create a new one
|
||||
if (!params.tx) {
|
||||
|
@ -295,40 +296,33 @@ export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
|||
staff?.id && {
|
||||
authorId: staff.id,
|
||||
},
|
||||
// staff?.id && {
|
||||
// watchableStaffs: {
|
||||
// some: {
|
||||
// id: staff.id,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// deptId && {
|
||||
// watchableDepts: {
|
||||
// some: {
|
||||
// id: {
|
||||
// in: parentDeptIds,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
|
||||
// {
|
||||
// AND: [
|
||||
// {
|
||||
// watchableStaffs: {
|
||||
// none: {}, // 匹配 watchableStaffs 为空
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// watchableDepts: {
|
||||
// none: {}, // 匹配 watchableDepts 为空
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
].filter(Boolean);
|
||||
|
||||
if (orCondition?.length > 0) return orCondition;
|
||||
return undefined;
|
||||
}
|
||||
async softDeletePostDescendant(args:{ancestorId?:string}){
|
||||
const { ancestorId } = args
|
||||
const descendantIds = []
|
||||
await db.postAncestry.findMany({
|
||||
where:{
|
||||
ancestorId,
|
||||
},
|
||||
select:{
|
||||
descendantId:true
|
||||
}
|
||||
}).then(res=>{
|
||||
res.forEach(item=>{
|
||||
descendantIds.push(item.descendantId)
|
||||
})
|
||||
})
|
||||
console.log(descendantIds)
|
||||
const result = super.softDeleteByIds([...descendantIds,ancestorId])
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.POST,
|
||||
operation: CrudOperation.DELETED,
|
||||
data: result,
|
||||
});
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-player": "^2.16.0",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.24.1",
|
||||
"superjson": "^2.2.1",
|
||||
|
|
|
@ -6,7 +6,7 @@ export default function PathEditorPage() {
|
|||
const { id } = useParams();
|
||||
return (
|
||||
<PostDetailProvider editId={id}>
|
||||
<MindEditor id={id}></MindEditor>;
|
||||
<MindEditor id={id}></MindEditor>
|
||||
</PostDetailProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ import JoinButton from "../../models/course/detail/CourseOperationBtns/JoinButto
|
|||
import { CourseDetailContext } from "../../models/course/detail/PostDetailContext";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getQueryKey } from "@trpc/react-query";
|
||||
export default function MindEditor({ id }: { id?: string }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
|
@ -36,6 +38,7 @@ export default function MindEditor({ id }: { id?: string }) {
|
|||
const [instance, setInstance] = useState<MindElixirInstance | null>(null);
|
||||
const { isAuthenticated, user, hasSomePermissions } = useAuth();
|
||||
const { read } = useVisitor();
|
||||
const queryClient = useQueryClient();
|
||||
// const { data: post, isLoading }: { data: PathDto; isLoading: boolean } =
|
||||
// api.post.findFirst.useQuery(
|
||||
// {
|
||||
|
@ -46,7 +49,11 @@ export default function MindEditor({ id }: { id?: string }) {
|
|||
// },
|
||||
// { enabled: Boolean(id) }
|
||||
// );
|
||||
|
||||
const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({
|
||||
onSuccess:()=>{
|
||||
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
|
||||
}
|
||||
})
|
||||
const canEdit: boolean = useMemo(() => {
|
||||
const isAuth = isAuthenticated && user?.id === post?.author?.id;
|
||||
return (
|
||||
|
@ -62,16 +69,16 @@ export default function MindEditor({ id }: { id?: string }) {
|
|||
});
|
||||
const { handleFileUpload } = useTusUpload();
|
||||
const [form] = Form.useForm();
|
||||
const CustomLinkIconPlugin = (mind) => {
|
||||
mind.bus.addListener("operation", async () => {
|
||||
const hyperLinkElement =
|
||||
await document.querySelectorAll(".hyper-link");
|
||||
console.log("hyperLinkElement", hyperLinkElement);
|
||||
hyperLinkElement.forEach((item) => {
|
||||
const hyperLinkDom = createRoot(item);
|
||||
hyperLinkDom.render(<LinkOutlined />);
|
||||
});
|
||||
const handleIcon = () => {
|
||||
const hyperLinkElement = document.querySelectorAll(".hyper-link");
|
||||
console.log("hyperLinkElement", hyperLinkElement);
|
||||
hyperLinkElement.forEach((item) => {
|
||||
const hyperLinkDom = createRoot(item);
|
||||
hyperLinkDom.render(<LinkOutlined />);
|
||||
});
|
||||
}
|
||||
const CustomLinkIconPlugin = (mind) => {
|
||||
mind.bus.addListener("operation", handleIcon)
|
||||
};
|
||||
useEffect(() => {
|
||||
if (post?.id && id) {
|
||||
|
@ -133,7 +140,11 @@ export default function MindEditor({ id }: { id?: string }) {
|
|||
containerRef.current.hidden = true;
|
||||
//挂载实例
|
||||
setInstance(mind);
|
||||
|
||||
}, [canEdit]);
|
||||
useEffect(() => {
|
||||
handleIcon()
|
||||
});
|
||||
useEffect(() => {
|
||||
if ((!id || post) && instance) {
|
||||
containerRef.current.hidden = false;
|
||||
|
@ -204,10 +215,16 @@ export default function MindEditor({ id }: { id?: string }) {
|
|||
}
|
||||
console.log(result);
|
||||
},
|
||||
(error) => {},
|
||||
(error) => { },
|
||||
`mind-thumb-${new Date().toString()}`
|
||||
);
|
||||
};
|
||||
const handleDelete = async () => {
|
||||
await softDeletePostDescendant.mutateAsync({
|
||||
ancestorId: id,
|
||||
});
|
||||
navigate("/path");
|
||||
}
|
||||
useEffect(() => {
|
||||
containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`;
|
||||
}, []);
|
||||
|
@ -248,14 +265,28 @@ export default function MindEditor({ id }: { id?: string }) {
|
|||
</div>
|
||||
<div>
|
||||
{canEdit && (
|
||||
<Button
|
||||
ghost
|
||||
type="primary"
|
||||
icon={<SaveOutlined></SaveOutlined>}
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
onClick={handleSave}>
|
||||
{id ? "更新" : "保存"}
|
||||
</Button>
|
||||
<>
|
||||
{
|
||||
id && (
|
||||
<Button
|
||||
danger
|
||||
icon={<SaveOutlined></SaveOutlined>}
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
onClick={handleDelete}>
|
||||
删除
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
className="ml-4"
|
||||
ghost
|
||||
type="primary"
|
||||
icon={<SaveOutlined></SaveOutlined>}
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
onClick={handleSave}>
|
||||
{id ? "更新" : "保存"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@ import PostSelect from "../../models/post/PostSelect/PostSelect";
|
|||
import { Lecture, PostType } from "@nice/common";
|
||||
import { xmindColorPresets } from "./constant";
|
||||
import { api } from "@nice/client";
|
||||
import { env } from "@web/src/env";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
|
||||
interface NodeMenuProps {
|
||||
mind: MindElixirInstance;
|
||||
|
@ -20,12 +20,13 @@ interface NodeMenuProps {
|
|||
|
||||
//管理节点样式状态
|
||||
const NodeMenu: React.FC<NodeMenuProps> = ({ mind }) => {
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selectedFontColor, setSelectedFontColor] = useState<string>("");
|
||||
const [selectedBgColor, setSelectedBgColor] = useState<string>("");
|
||||
const [selectedSize, setSelectedSize] = useState<string>("");
|
||||
const [isBold, setIsBold] = useState(false);
|
||||
|
||||
const { user} = useAuth();
|
||||
const [urlMode, setUrlMode] = useState<"URL" | "POSTURL">("POSTURL");
|
||||
const [url, setUrl] = useState<string>("");
|
||||
const [postId, setPostId] = useState<string>("");
|
||||
|
@ -238,13 +239,15 @@ const NodeMenu: React.FC<NodeMenuProps> = ({ mind }) => {
|
|||
{urlMode === "POSTURL" ? (
|
||||
<PostSelect
|
||||
onChange={(value) => {
|
||||
if (typeof value === "string") {
|
||||
if (typeof value === "string" ) {
|
||||
setPostId(value);
|
||||
}
|
||||
}}
|
||||
params={{
|
||||
where: {
|
||||
type: PostType.LECTURE,
|
||||
deletedAt: null,
|
||||
authorId: user?.id,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -7,8 +7,10 @@ import { formatFileSize, getCompressedImageUrl } from "@nice/utils";
|
|||
|
||||
export default function ResourcesShower({
|
||||
resources = [],
|
||||
isShowImage
|
||||
}: {
|
||||
resources: ResourceDto[];
|
||||
isShowImage?: boolean;
|
||||
}) {
|
||||
const { resources: dealedResources } = useMemo(() => {
|
||||
if (!resources) return { resources: [] };
|
||||
|
@ -36,7 +38,7 @@ export default function ResourcesShower({
|
|||
const fileResources = dealedResources.filter((res) => !res.isImage);
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{imageResources.length > 0 && (
|
||||
{imageResources.length > 0 && isShowImage && (
|
||||
<Row gutter={[16, 16]} className="mb-6">
|
||||
<Image.PreviewGroup>
|
||||
{imageResources.map((resource) => (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import { ReactNode, useCallback, useState } from "react";
|
||||
import {
|
||||
UploadOutlined,
|
||||
CheckCircleOutlined,
|
||||
|
@ -12,6 +12,9 @@ export interface TusUploaderProps {
|
|||
onChange?: (value: string[]) => void;
|
||||
multiple?: boolean;
|
||||
allowTypes?: string[];
|
||||
style?:string
|
||||
icon?:ReactNode,
|
||||
description?:string
|
||||
}
|
||||
|
||||
interface UploadingFile {
|
||||
|
@ -27,6 +30,9 @@ export const TusUploader = ({
|
|||
onChange,
|
||||
multiple = true,
|
||||
allowTypes = undefined,
|
||||
style="",
|
||||
icon = <UploadOutlined />,
|
||||
description = "点击或拖拽文件到此区域进行上传",
|
||||
}: TusUploaderProps) => {
|
||||
const { handleFileUpload, uploadProgress } = useTusUpload();
|
||||
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
|
||||
|
@ -137,7 +143,7 @@ export const TusUploader = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className={`space-y-1 ${style}`}>
|
||||
<Upload.Dragger
|
||||
accept={allowTypes?.join(",")}
|
||||
name="files"
|
||||
|
@ -145,18 +151,18 @@ export const TusUploader = ({
|
|||
showUploadList={false}
|
||||
beforeUpload={handleBeforeUpload}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<UploadOutlined />
|
||||
{icon}
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
点击或拖拽文件到此区域进行上传
|
||||
{description}
|
||||
</p>
|
||||
<p className="ant-upload-hint">
|
||||
{multiple ? "支持单个或批量上传文件" : "仅支持上传单个文件"}
|
||||
{allowTypes && (
|
||||
{/* {allowTypes && (
|
||||
<span className="block text-xs text-gray-500">
|
||||
允许类型: {allowTypes.join(", ")}
|
||||
</span>
|
||||
)}
|
||||
)} */}
|
||||
</p>
|
||||
|
||||
<div className="px-2 py-0 rounded mt-1">
|
||||
|
|
|
@ -10,7 +10,7 @@ import { Skeleton } from "antd";
|
|||
import ResourcesShower from "@web/src/components/common/uploader/ResourceShower";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import CourseDetailTitle from "./CourseDetailTitle";
|
||||
|
||||
import ReactPlayer from "react-player";
|
||||
export const CourseDetailDisplayArea: React.FC = () => {
|
||||
// 创建滚动动画效果
|
||||
const {
|
||||
|
@ -41,6 +41,15 @@ export const CourseDetailDisplayArea: React.FC = () => {
|
|||
}}
|
||||
className="w-full bg-black rounded-lg ">
|
||||
<div className=" w-full cursor-pointer">
|
||||
{/* <ReactPlayer
|
||||
url={lecture?.meta?.videoUrl}
|
||||
controls={true}
|
||||
width="100%"
|
||||
height="100%"
|
||||
onError={(error) => {
|
||||
console.log(error);
|
||||
}}
|
||||
/> */}
|
||||
<VideoPlayer src={lecture?.meta?.videoUrl} />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
@ -48,18 +57,22 @@ 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 ">
|
||||
<div className="w-full rounded-lg ">
|
||||
<CollapsibleContent
|
||||
content={lecture?.content || ""}
|
||||
maxHeight={500} // Optional, defaults to 150
|
||||
/>
|
||||
{lecture?.meta?.type === LectureType.ARTICLE && (
|
||||
<CollapsibleContent
|
||||
content={lecture?.content || ""}
|
||||
maxHeight={500} // Optional, defaults to 150
|
||||
/>
|
||||
)}
|
||||
<div className="px-6">
|
||||
<ResourcesShower
|
||||
resources={
|
||||
lecture?.resources
|
||||
}></ResourcesShower>
|
||||
}
|
||||
isShowImage = {lecture?.meta?.type === LectureType.ARTICLE}
|
||||
></ResourcesShower>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -23,15 +23,9 @@ export default function CourseDetailTitle() {
|
|||
{!selectedLectureId ? course?.title : lecture?.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>
|
||||
)}
|
||||
|
|
|
@ -1,97 +1,40 @@
|
|||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { useContext, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { CourseDetailContext } from "../PostDetailContext";
|
||||
import { useStaff } from "@nice/client";
|
||||
import { api } from "@nice/client";
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
DeleteTwoTone,
|
||||
EditTwoTone,
|
||||
LoginOutlined,
|
||||
ExclamationCircleFilled,
|
||||
} from "@ant-design/icons";
|
||||
import toast from "react-hot-toast";
|
||||
import JoinButton from "./JoinButton";
|
||||
import { Modal } from "antd";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getQueryKey } from "@trpc/react-query";
|
||||
|
||||
export default function CourseOperationBtns() {
|
||||
// const { isAuthenticated, user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const { post, canEdit, userIsLearning, setUserIsLearning } =
|
||||
useContext(CourseDetailContext);
|
||||
// const { update } = useStaff();
|
||||
// const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
// const toggleLearning = async () => {
|
||||
// if (!userIsLearning) {
|
||||
// await update.mutateAsync({
|
||||
// where: { id: user?.id },
|
||||
// data: {
|
||||
// learningPosts: {
|
||||
// connect: { id: course.id },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// setUserIsLearning(true);
|
||||
// toast.success("加入学习成功");
|
||||
// } else {
|
||||
// await update.mutateAsync({
|
||||
// where: { id: user?.id },
|
||||
// data: {
|
||||
// learningPosts: {
|
||||
// disconnect: {
|
||||
// id: course.id,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// toast.success("退出学习成功");
|
||||
// setUserIsLearning(false);
|
||||
// }
|
||||
// };
|
||||
const { post, canEdit } = useContext(CourseDetailContext);
|
||||
return (
|
||||
<>
|
||||
{/* {isAuthenticated && (
|
||||
<div
|
||||
onClick={toggleLearning}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={`flex px-1 py-0.5 gap-1 hover:cursor-pointer transition-all ${
|
||||
userIsLearning
|
||||
? isHovered
|
||||
? "text-red-500 border-red-500 rounded-md "
|
||||
: "text-green-500 "
|
||||
: "text-primary "
|
||||
}`}>
|
||||
{userIsLearning ? (
|
||||
isHovered ? (
|
||||
<CloseCircleOutlined />
|
||||
) : (
|
||||
<CheckCircleOutlined />
|
||||
)
|
||||
) : (
|
||||
<LoginOutlined />
|
||||
)}
|
||||
<span>
|
||||
{userIsLearning
|
||||
? isHovered
|
||||
? "退出学习"
|
||||
: "正在学习"
|
||||
: "加入学习"}
|
||||
</span>
|
||||
</div>
|
||||
)} */}
|
||||
<JoinButton></JoinButton>
|
||||
{canEdit && (
|
||||
<div
|
||||
className="flex gap-1 px-1 py-0.5 text-primary hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
const url = post?.id
|
||||
? `/course/${post?.id}/editor`
|
||||
: "/course/editor";
|
||||
navigate(url);
|
||||
}}>
|
||||
<EditTwoTone></EditTwoTone>
|
||||
{"编辑课程"}
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className="flex gap-1 px-1 py-0.5 text-primary hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
const url = post?.id
|
||||
? `/course/${post?.id}/editor`
|
||||
: "/course/editor";
|
||||
navigate(url);
|
||||
}}>
|
||||
<EditTwoTone></EditTwoTone>
|
||||
{"编辑课程"}
|
||||
</div>
|
||||
</>
|
||||
|
||||
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -13,6 +13,8 @@ import { api, usePost } from "@nice/client";
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { z } from "zod";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { getQueryKey } from "@trpc/react-query";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
export type CourseFormData = {
|
||||
title: string;
|
||||
|
@ -26,6 +28,7 @@ export type CourseFormData = {
|
|||
|
||||
interface CourseEditorContextType {
|
||||
onSubmit: (values: CourseFormData) => Promise<void>;
|
||||
handleDeleteCourse: () => Promise<void>;
|
||||
editId?: string;
|
||||
course?: CourseDto;
|
||||
taxonomies?: Taxonomy[]; // 根据实际类型调整
|
||||
|
@ -45,6 +48,7 @@ export function CourseFormProvider({
|
|||
}: CourseFormProviderProps) {
|
||||
const [form] = Form.useForm<CourseFormData>();
|
||||
const { create, update, createCourse } = usePost();
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useAuth();
|
||||
const { data: course }: { data: CourseDto } = api.post.findFirst.useQuery(
|
||||
{
|
||||
|
@ -60,6 +64,11 @@ export function CourseFormProvider({
|
|||
} = api.taxonomy.getAll.useQuery({
|
||||
type: ObjectType.COURSE,
|
||||
});
|
||||
const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({
|
||||
onSuccess:()=>{
|
||||
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
|
||||
}
|
||||
})
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -92,7 +101,15 @@ export function CourseFormProvider({
|
|||
form.setFieldsValue(formData);
|
||||
}
|
||||
}, [course, form]);
|
||||
const handleDeleteCourse = async () => {
|
||||
if(editId){
|
||||
await softDeletePostDescendant.mutateAsync({
|
||||
ancestorId: editId,
|
||||
});
|
||||
|
||||
navigate("/courses");
|
||||
}
|
||||
}
|
||||
const onSubmit = async (values: any) => {
|
||||
const sections = values?.sections || [];
|
||||
const deptIds = values?.deptIds || [];
|
||||
|
@ -172,6 +189,7 @@ export function CourseFormProvider({
|
|||
course,
|
||||
taxonomies,
|
||||
form,
|
||||
handleDeleteCourse
|
||||
}}>
|
||||
<Form
|
||||
// requiredMark="optional"
|
||||
|
|
|
@ -25,8 +25,11 @@ import { CourseSectionEmpty } from "./CourseSectionEmpty";
|
|||
import { SortableSection } from "./SortableSection";
|
||||
import { LectureList } from "./LectureList";
|
||||
import toast from "react-hot-toast";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { getQueryKey } from "@trpc/react-query";
|
||||
|
||||
const CourseContentForm: React.FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { editId } = useCourseEditor();
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
|
@ -71,7 +74,11 @@ const CourseContentForm: React.FC = () => {
|
|||
ids: newItems.map((item) => item.id),
|
||||
});
|
||||
};
|
||||
|
||||
const softDeletePostDescendant = api.post.softDeletePostDescendant.useMutation({
|
||||
onSuccess:()=>{
|
||||
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<CourseContentFormHeader />
|
||||
|
@ -93,8 +100,8 @@ const CourseContentForm: React.FC = () => {
|
|||
field={section}
|
||||
remove={async () => {
|
||||
if (section?.id) {
|
||||
await softDeleteByIds.mutateAsync({
|
||||
ids: [section.id],
|
||||
await softDeletePostDescendant.mutateAsync({
|
||||
ancestorId: section.id,
|
||||
});
|
||||
}
|
||||
setItems(sections);
|
||||
|
|
|
@ -3,6 +3,8 @@ import {
|
|||
CaretRightOutlined,
|
||||
SaveOutlined,
|
||||
CaretDownOutlined,
|
||||
PaperClipOutlined,
|
||||
PlaySquareOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Form, Button, Input, Select, Space } from "antd";
|
||||
import React, { useState } from "react";
|
||||
|
@ -86,12 +88,12 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
|||
resources:
|
||||
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
|
||||
? {
|
||||
connect: [videoUrlId, ...fileIds]
|
||||
.filter(Boolean)
|
||||
.map((fileId) => ({
|
||||
fileId,
|
||||
})),
|
||||
}
|
||||
connect: [videoUrlId, ...fileIds]
|
||||
.filter(Boolean)
|
||||
.map((fileId) => ({
|
||||
fileId,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
content: values?.content,
|
||||
},
|
||||
|
@ -115,12 +117,12 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
|||
resources:
|
||||
[videoUrlId, ...fileIds].filter(Boolean)?.length > 0
|
||||
? {
|
||||
set: [videoUrlId, ...fileIds]
|
||||
.filter(Boolean)
|
||||
.map((fileId) => ({
|
||||
fileId,
|
||||
})),
|
||||
}
|
||||
set: [videoUrlId, ...fileIds]
|
||||
.filter(Boolean)
|
||||
.map((fileId) => ({
|
||||
fileId,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
content: values?.content,
|
||||
},
|
||||
|
@ -175,22 +177,42 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
|||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-1 ">
|
||||
<div className="mt-4 flex flex-2 flex-row gap-x-5 ">
|
||||
{lectureType === LectureType.VIDEO ? (
|
||||
<Form.Item
|
||||
name={["meta", "videoIds"]}
|
||||
className="mb-0 flex-1"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请传入视频",
|
||||
},
|
||||
]}>
|
||||
<TusUploader
|
||||
allowTypes={videoMimeTypes}
|
||||
multiple={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
<>
|
||||
<div className="mb-0 flex-1">
|
||||
{/* <span className="inline-block w-full h-7 my-1 rounded-lg bg-slate-100 text-center leading-7">添加视频</span> */}
|
||||
<Form.Item
|
||||
name={["meta", "videoIds"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "请传入视频",
|
||||
},
|
||||
]}>
|
||||
<TusUploader
|
||||
allowTypes={videoMimeTypes}
|
||||
multiple={false}
|
||||
style={"h-64"}
|
||||
icon={<PlaySquareOutlined />}
|
||||
description="点击或拖拽视频到此区域进行上传"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="mb-0 flex-1">
|
||||
{/* <span className="inline-block w-full h-7 my-1 rounded-lg bg-slate-100 text-center leading-7">添加附件</span> */}
|
||||
<Form.Item
|
||||
name={["meta", "fileIds"]}>
|
||||
<TusUploader
|
||||
style={"h-64"}
|
||||
multiple={true}
|
||||
icon={<PaperClipOutlined />}
|
||||
description="点击或拖拽附件到此区域进行上传"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>
|
||||
|
||||
) : (
|
||||
<div>
|
||||
<Form.Item
|
||||
|
@ -204,7 +226,7 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
|||
]}>
|
||||
<QuillEditor
|
||||
style={{
|
||||
width:"700px",
|
||||
width: "700px",
|
||||
}}
|
||||
></QuillEditor>
|
||||
</Form.Item>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { ArrowLeftOutlined, ClockCircleOutlined } from "@ant-design/icons";
|
||||
import { Button, Tag, Typography } from "antd";
|
||||
import { ArrowLeftOutlined, ClockCircleOutlined, ExclamationCircleFilled } from "@ant-design/icons";
|
||||
import { Button, Modal, Tag, Typography } from "antd";
|
||||
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";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
|
@ -18,8 +19,8 @@ const courseStatusVariant: Record<CourseStatus, string> = {
|
|||
export default function CourseEditorHeader() {
|
||||
const navigate = useNavigate();
|
||||
const { user, hasSomePermissions } = useAuth();
|
||||
|
||||
const { onSubmit, course, form } = useCourseEditor();
|
||||
const { confirm } = Modal;
|
||||
const { onSubmit, course, form, handleDeleteCourse, editId } = useCourseEditor();
|
||||
|
||||
const handleSave = () => {
|
||||
try {
|
||||
|
@ -30,7 +31,26 @@ export default function CourseEditorHeader() {
|
|||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
const showDeleteConfirm = () => {
|
||||
confirm({
|
||||
title: '确定删除该课程吗',
|
||||
icon: <ExclamationCircleFilled />,
|
||||
content: '',
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
async onOk() {
|
||||
console.log('OK');
|
||||
console.log(editId)
|
||||
await handleDeleteCourse()
|
||||
toast.success('课程已删除')
|
||||
navigate("/courses");
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 h-16 bg-white border-b border-gray-200 z-10">
|
||||
<div className="h-full flex items-center justify-between px-3 md:px-4">
|
||||
|
@ -70,16 +90,27 @@ export default function CourseEditorHeader() {
|
|||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
// size="small"
|
||||
onClick={handleSave}
|
||||
<div>
|
||||
{editId &&
|
||||
<Button
|
||||
danger
|
||||
onClick={showDeleteConfirm}
|
||||
>
|
||||
删除课程
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
className="ml-4"
|
||||
type="primary"
|
||||
// size="small"
|
||||
onClick={handleSave}
|
||||
// disabled={form
|
||||
// .getFieldsError()
|
||||
// .some(({ errors }) => errors.length)}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
@ -44,7 +44,8 @@ export default function PostList({
|
|||
|
||||
const posts = useMemo(() => {
|
||||
if (data && !isLoading) {
|
||||
return data?.items;
|
||||
console.log(data?.items)
|
||||
return data?.items.filter(item=>item.deletedAt === null);
|
||||
}
|
||||
return [];
|
||||
}, [data, isLoading]);
|
||||
|
|
|
@ -13,6 +13,7 @@ export default function PostSelect({
|
|||
placeholder = "请选择课时",
|
||||
params = { where: {}, select: {} },
|
||||
className,
|
||||
createdById,
|
||||
}: {
|
||||
value?: string | string[];
|
||||
onChange?: (value: string | string[]) => void;
|
||||
|
@ -22,6 +23,7 @@ export default function PostSelect({
|
|||
select?: Prisma.PostSelect<DefaultArgs>;
|
||||
};
|
||||
className?: string;
|
||||
createdById?: string;
|
||||
}) {
|
||||
const [searchValue, setSearch] = useState("");
|
||||
const searchCondition: Prisma.PostWhereInput = useMemo(() => {
|
||||
|
|
Binary file not shown.
|
@ -15,7 +15,7 @@ done
|
|||
if [ -f "/usr/share/nginx/html/index.html" ]; then
|
||||
# Use envsubst to replace environment variable placeholders
|
||||
echo "Processing /usr/share/nginx/html/index.html"
|
||||
envsubst < /usr/share/nginx/html/index.temp > /usr/share/nginx/html/index.html.tmp
|
||||
envsubst < /usr/share/nginx/html/index.template > /usr/share/nginx/html/index.html.tmp
|
||||
mv /usr/share/nginx/html/index.html.tmp /usr/share/nginx/html/index.html
|
||||
echo "Processed content:"
|
||||
cat /usr/share/nginx/html/index.html
|
||||
|
|
|
@ -100,6 +100,7 @@ export const courseDetailSelect: Prisma.PostSelect = {
|
|||
// isFeatured: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
deletedAt: true,
|
||||
// 关联表选择
|
||||
terms: {
|
||||
select: {
|
||||
|
|
|
@ -386,6 +386,9 @@ importers:
|
|||
react-hot-toast:
|
||||
specifier: ^2.4.1
|
||||
version: 2.5.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react-player:
|
||||
specifier: ^2.16.0
|
||||
version: 2.16.0(react@18.2.0)
|
||||
react-resizable:
|
||||
specifier: ^3.0.5
|
||||
version: 3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
|
@ -5603,6 +5606,9 @@ packages:
|
|||
enquirer:
|
||||
optional: true
|
||||
|
||||
load-script@1.0.0:
|
||||
resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==}
|
||||
|
||||
load-tsconfig@0.2.5:
|
||||
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
@ -5818,6 +5824,9 @@ packages:
|
|||
resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
||||
memoize-one@5.2.1:
|
||||
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||
|
||||
meow@8.1.2:
|
||||
resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -6645,6 +6654,9 @@ packages:
|
|||
react: '>= 16.3.0'
|
||||
react-dom: '>= 16.3.0'
|
||||
|
||||
react-fast-compare@3.2.2:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
|
||||
react-hook-form@7.54.2:
|
||||
resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
@ -6664,6 +6676,11 @@ packages:
|
|||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
react-player@2.16.0:
|
||||
resolution: {integrity: sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.6.0'
|
||||
|
||||
react-refresh@0.14.2:
|
||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -13723,6 +13740,8 @@ snapshots:
|
|||
rfdc: 1.4.1
|
||||
wrap-ansi: 8.1.0
|
||||
|
||||
load-script@1.0.0: {}
|
||||
|
||||
load-tsconfig@0.2.5: {}
|
||||
|
||||
loader-runner@4.3.0: {}
|
||||
|
@ -13899,6 +13918,8 @@ snapshots:
|
|||
dependencies:
|
||||
fs-monkey: 1.0.6
|
||||
|
||||
memoize-one@5.2.1: {}
|
||||
|
||||
meow@8.1.2:
|
||||
dependencies:
|
||||
'@types/minimist': 1.2.5
|
||||
|
@ -14774,6 +14795,8 @@ snapshots:
|
|||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
react-fast-compare@3.2.2: {}
|
||||
|
||||
react-hook-form@7.54.2(react@18.2.0):
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
|
@ -14789,6 +14812,15 @@ snapshots:
|
|||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
react-player@2.16.0(react@18.2.0):
|
||||
dependencies:
|
||||
deepmerge: 4.3.1
|
||||
load-script: 1.0.0
|
||||
memoize-one: 5.2.1
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-fast-compare: 3.2.2
|
||||
|
||||
react-refresh@0.14.2: {}
|
||||
|
||||
react-resizable@3.0.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
|
|
Loading…
Reference in New Issue