diff --git a/Dockerfile b/Dockerfile index 3d15a41..ff297ad 100755 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/apps/server/src/models/post/post.router.ts b/apps/server/src/models/post/post.router.ts index bcde094..9ee4f9b 100755 --- a/apps/server/src/models/post/post.router.ts +++ b/apps/server/src/models/post/post.router.ts @@ -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) + }) }); } diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index 0f3e8cd..5d4db4c 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -101,6 +101,7 @@ export class PostService extends BaseTreeService { }, 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 { 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 + } } diff --git a/apps/web/src/components/common/editor/MindEditor.tsx b/apps/web/src/components/common/editor/MindEditor.tsx index 0b1dff6..0aba7cf 100755 --- a/apps/web/src/components/common/editor/MindEditor.tsx +++ b/apps/web/src/components/common/editor/MindEditor.tsx @@ -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(null); const { @@ -36,6 +38,7 @@ export default function MindEditor({ id }: { id?: string }) { const [instance, setInstance] = useState(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 ( @@ -63,7 +70,7 @@ export default function MindEditor({ id }: { id?: string }) { const { handleFileUpload } = useTusUpload(); const [form] = Form.useForm(); const handleIcon = () => { - const hyperLinkElement =document.querySelectorAll(".hyper-link"); + const hyperLinkElement = document.querySelectorAll(".hyper-link"); console.log("hyperLinkElement", hyperLinkElement); hyperLinkElement.forEach((item) => { const hyperLinkDom = createRoot(item); @@ -133,7 +140,7 @@ export default function MindEditor({ id }: { id?: string }) { containerRef.current.hidden = true; //挂载实例 setInstance(mind); - + }, [canEdit]); useEffect(() => { handleIcon() @@ -212,6 +219,12 @@ export default function MindEditor({ id }: { id?: string }) { `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`; }, []); @@ -252,14 +265,28 @@ export default function MindEditor({ id }: { id?: string }) {
{canEdit && ( - + <> + { + id && ( + + ) + } + + )}
diff --git a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx index fc9ed18..cc6303a 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx @@ -41,7 +41,7 @@ export const CourseDetailDisplayArea: React.FC = () => { }} className="w-full bg-black rounded-lg ">
- { onError={(error) => { console.log(error); }} - /> - {/* */} + /> */} +
diff --git a/apps/web/src/components/models/course/detail/CourseOperationBtns/CourseOperationBtns.tsx b/apps/web/src/components/models/course/detail/CourseOperationBtns/CourseOperationBtns.tsx index aa7124c..912386d 100644 --- a/apps/web/src/components/models/course/detail/CourseOperationBtns/CourseOperationBtns.tsx +++ b/apps/web/src/components/models/course/detail/CourseOperationBtns/CourseOperationBtns.tsx @@ -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 && ( -
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 ? ( - - ) : ( - - ) - ) : ( - - )} - - {userIsLearning - ? isHovered - ? "退出学习" - : "正在学习" - : "加入学习"} - -
- )} */} {canEdit && ( -
{ - const url = post?.id - ? `/course/${post?.id}/editor` - : "/course/editor"; - navigate(url); - }}> - - {"编辑课程"} -
+ <> +
{ + const url = post?.id + ? `/course/${post?.id}/editor` + : "/course/editor"; + navigate(url); + }}> + + {"编辑课程"} +
+ + + )} ); 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 f623660..c5c30b9 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -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; + handleDeleteCourse: () => Promise; editId?: string; course?: CourseDto; taxonomies?: Taxonomy[]; // 根据实际类型调整 @@ -45,6 +48,7 @@ export function CourseFormProvider({ }: CourseFormProviderProps) { const [form] = Form.useForm(); 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 }}>
{ + 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 (
@@ -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); 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 a3197f8..c294d54 100755 --- a/apps/web/src/components/models/course/editor/layout/CourseEditorHeader.tsx +++ b/apps/web/src/components/models/course/editor/layout/CourseEditorHeader.tsx @@ -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 = { 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: , + content: '', + okText: '删除', + okType: 'danger', + cancelText: '取消', + async onOk() { + console.log('OK'); + console.log(editId) + await handleDeleteCourse() + toast.success('课程已删除') + navigate("/courses"); + }, + onCancel() { + console.log('Cancel'); + }, + }); + }; return (
@@ -70,16 +90,27 @@ export default function CourseEditorHeader() { )} */}
- + } + + > + 保存 + + ); diff --git a/apps/web/src/components/models/course/list/PostList.tsx b/apps/web/src/components/models/course/list/PostList.tsx index a276060..b0e69f5 100755 --- a/apps/web/src/components/models/course/list/PostList.tsx +++ b/apps/web/src/components/models/course/list/PostList.tsx @@ -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]); diff --git a/apps/web/web-dist.zip b/apps/web/web-dist.zip new file mode 100644 index 0000000..2e31d07 Binary files /dev/null and b/apps/web/web-dist.zip differ diff --git a/config/nginx/entrypoint.sh b/config/nginx/entrypoint.sh index 136c830..7a546eb 100755 --- a/config/nginx/entrypoint.sh +++ b/config/nginx/entrypoint.sh @@ -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 diff --git a/packages/common/src/models/select.ts b/packages/common/src/models/select.ts index f21f1a6..ba35ec2 100755 --- a/packages/common/src/models/select.ts +++ b/packages/common/src/models/select.ts @@ -100,6 +100,7 @@ export const courseDetailSelect: Prisma.PostSelect = { // isFeatured: true, createdAt: true, updatedAt: true, + deletedAt: true, // 关联表选择 terms: { select: {