import { Button, Empty, Form, Modal, Spin } from "antd"; import NodeMenu from "./NodeMenu"; import { api, usePost, useVisitor } from "@nice/client"; import { ObjectType, PathDto, postDetailSelect, PostType, Prisma, RolePerms, VisitType, } from "@nice/common"; import TermSelect from "../../models/term/term-select"; import DepartmentSelect from "../../models/department/department-select"; import { useContext, useEffect, useMemo, useRef, useState } from "react"; import toast from "react-hot-toast"; import { MindElixirInstance } from "mind-elixir"; import MindElixir from "mind-elixir"; import { useTusUpload } from "@web/src/hooks/useTusUpload"; import { useNavigate } from "react-router-dom"; import { useAuth } from "@web/src/providers/auth-provider"; import { MIND_OPTIONS } from "./constant"; import { ExclamationCircleFilled, LinkOutlined, SaveOutlined } from "@ant-design/icons"; import JoinButton from "../../models/course/detail/CourseOperationBtns/JoinButton"; 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 { confirm } = Modal; const { post, isLoading, // userIsLearning, // setUserIsLearning, } = useContext(CourseDetailContext); const [instance, setInstance] = useState(null); const { isAuthenticated, user, hasSomePermissions } = useAuth(); const { read } = useVisitor(); const queryClient = useQueryClient(); useEffect(() => { console.log("post", post) console.log("user", user) console.log(canEdit) }) // const { data: post, isLoading }: { data: PathDto; isLoading: boolean } = // api.post.findFirst.useQuery( // { // where: { // id, // }, // select: postDetailSelect, // }, // { 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 ( isAuthenticated && (!id || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST)) ); }, [user, post]); const navigate = useNavigate(); const { create, update } = usePost(); const { data: taxonomies } = api.taxonomy.getAll.useQuery({ type: ObjectType.COURSE, }); const { handleFileUpload } = useTusUpload(); const [form] = Form.useForm(); const handleIcon = () => { const hyperLinkElement = document.querySelectorAll(".hyper-link"); console.log("hyperLinkElement", hyperLinkElement); hyperLinkElement.forEach((item) => { const hyperLinkDom = createRoot(item); hyperLinkDom.render(); }); } const CustomLinkIconPlugin = (mind) => { mind.bus.addListener("operation", handleIcon) }; useEffect(() => { if (post?.id && id) { read.mutateAsync({ data: { visitorId: user?.id || null, postId: post?.id, type: VisitType.READED, }, }); } }, [post]); useEffect(() => { if (post && form && instance && id) { instance.refresh((post as any).meta); const deptIds = (post?.depts || [])?.map((dept) => dept.id); const formData = { title: post.title, deptIds: deptIds, }; // post.terms?.forEach((term) => { // formData[term.taxonomyId] = term.id; // 假设 taxonomyName是您在 Form.Item 中使用的name // }); // 按 taxonomyId 分组所有 terms const termsByTaxonomy = {}; post.terms?.forEach((term) => { if (!termsByTaxonomy[term.taxonomyId]) { termsByTaxonomy[term.taxonomyId] = []; } termsByTaxonomy[term.taxonomyId].push(term.id); }); // 将分组后的 terms 设置到 formData Object.entries(termsByTaxonomy).forEach(([taxonomyId, termIds]) => { formData[taxonomyId] = termIds; }); form.setFieldsValue(formData); } }, [post, form, instance, id]); useEffect(() => { if (!containerRef.current) return; const mind = new MindElixir({ ...MIND_OPTIONS, el: containerRef.current, before: { beginEdit() { return canEdit; }, }, draggable: canEdit, // 禁用拖拽 contextMenu: canEdit, // 禁用右键菜单 toolBar: canEdit, // 禁用工具栏 nodeMenu: canEdit, // 禁用节点右键菜单 keypress: canEdit, // 禁用键盘快捷键 }); mind.install(CustomLinkIconPlugin); mind.init(MindElixir.new("新思维导图")); containerRef.current.hidden = true; //挂载实例 setInstance(mind); }, [canEdit, post]); useEffect(() => { handleIcon() }); useEffect(() => { if ((!id || post) && instance) { containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`; containerRef.current.style.width = `100%`; containerRef.current.hidden = false; instance.toCenter(); if ((post as any as PathDto)?.meta?.nodeData) { instance.refresh((post as any as PathDto)?.meta); } } }, [id, post, instance]); //保存 按钮 函数 const handleSave = async () => { if (!instance) return; const values = form.getFieldsValue(); //以图片格式导出思维导图以作为思维导图封面 const imgBlob = await instance?.exportPng(); handleFileUpload( imgBlob, async (result) => { const termIds = taxonomies .flatMap((tax) => values[tax.id] || []) // 获取每个 taxonomy 对应的选中值并展平 .filter((id) => id); // 过滤掉空值 const deptIds = (values?.deptIds || []) as string[]; const { theme, ...data } = instance.getData(); try { if (post && id) { const params: Prisma.PostUpdateArgs = { where: { id, }, data: { //authorId: post.authorId, title: data.nodeData.topic, meta: { ...data, thumbnail: result.compressedUrl, }, terms: { set: termIds.map((id) => ({ id })), }, depts: { set: deptIds.map((id) => ({ id })), }, updatedAt: new Date(), }, }; await update.mutateAsync(params); toast.success("更新成功"); } else { const params: Prisma.PostCreateInput = { type: PostType.PATH, title: data.nodeData.topic, meta: { ...data, thumbnail: result.compressedUrl }, terms: { connect: termIds.map((id) => ({ id })), }, depts: { connect: deptIds.map((id) => ({ id })), }, updatedAt: new Date(), }; const res = await create.mutateAsync({ data: params }); navigate(`/path/editor/${res.id}`, { replace: true }); toast.success("创建成功"); } } catch (error) { toast.error("保存失败"); throw error; } console.log(result); }, (error) => { }, `mind-thumb-${new Date().toString()}` ); }; const handleDelete = async () => { await softDeletePostDescendant.mutateAsync({ ancestorId: id, }); navigate("/path"); } const showDeleteConfirm = () => { confirm({ title: '确定删除该思维导图吗', icon: , content: '', okText: '删除', okType: 'danger', cancelText: '取消', async onOk() { console.log('OK'); await handleDelete() toast.success('思维导图已删除') }, onCancel() { console.log('Cancel'); }, }); }; useEffect(() => { containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`; }, []); return (
{taxonomies && (
{taxonomies.map((tax, index) => ( ))} {post && id ? : <>}
{canEdit && ( <> { id && ( ) } )}
)}
e.preventDefault()} /> {canEdit && instance && } {isLoading && (
)} {!post && id && !isLoading && (
)}
); }