collect-system/apps/web/src/components/common/editor/MindEditor.tsx

291 lines
8.1 KiB
TypeScript
Raw Normal View History

2025-02-27 23:07:57 +08:00
import { Button, Empty, Form, Spin } from "antd";
2025-02-27 09:47:46 +08:00
import NodeMenu from "./NodeMenu";
2025-02-27 22:58:42 +08:00
import { api, usePost, useVisitor } from "@nice/client";
2025-02-27 09:47:46 +08:00
import {
ObjectType,
2025-02-27 22:06:26 +08:00
PathDto,
2025-02-27 09:47:46 +08:00
postDetailSelect,
PostType,
Prisma,
2025-02-27 16:11:53 +08:00
RolePerms,
2025-02-27 22:58:42 +08:00
VisitType,
2025-02-27 09:47:46 +08:00
} from "@nice/common";
import TermSelect from "../../models/term/term-select";
import DepartmentSelect from "../../models/department/department-select";
2025-03-02 11:49:46 +08:00
import { useContext, useEffect, useMemo, useRef, useState } from "react";
2025-02-27 09:47:46 +08:00
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";
2025-02-27 16:11:53 +08:00
import { useAuth } from "@web/src/providers/auth-provider";
2025-02-27 22:06:26 +08:00
import { MIND_OPTIONS } from "./constant";
2025-03-05 21:12:45 +08:00
import { LinkOutlined, SaveOutlined } from "@ant-design/icons";
2025-03-02 11:49:46 +08:00
import JoinButton from "../../models/course/detail/CourseOperationBtns/JoinButton";
import { CourseDetailContext } from "../../models/course/detail/PostDetailContext";
2025-03-05 16:50:47 +08:00
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
2025-02-26 23:18:14 +08:00
export default function MindEditor({ id }: { id?: string }) {
const containerRef = useRef<HTMLDivElement>(null);
2025-03-02 11:49:46 +08:00
const {
post,
isLoading,
// userIsLearning,
// setUserIsLearning,
} = useContext(CourseDetailContext);
2025-02-26 23:18:14 +08:00
const [instance, setInstance] = useState<MindElixirInstance | null>(null);
2025-02-27 16:11:53 +08:00
const { isAuthenticated, user, hasSomePermissions } = useAuth();
2025-02-28 15:17:56 +08:00
const { read } = useVisitor();
2025-03-02 11:49:46 +08:00
// const { data: post, isLoading }: { data: PathDto; isLoading: boolean } =
// api.post.findFirst.useQuery(
// {
// where: {
// id,
// },
// select: postDetailSelect,
// },
// { enabled: Boolean(id) }
// );
2025-03-02 22:25:39 +08:00
2025-02-27 16:11:53 +08:00
const canEdit: boolean = useMemo(() => {
2025-02-27 22:06:26 +08:00
const isAuth = isAuthenticated && user?.id === post?.author?.id;
2025-03-06 22:26:59 +08:00
return (
isAuthenticated &&
(!id || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST))
);
2025-02-27 22:06:26 +08:00
}, [user]);
2025-03-02 22:25:39 +08:00
2025-02-27 09:47:46 +08:00
const navigate = useNavigate();
2025-02-26 23:18:14 +08:00
const { create, update } = usePost();
const { data: taxonomies } = api.taxonomy.getAll.useQuery({
type: ObjectType.COURSE,
});
2025-02-27 09:47:46 +08:00
const { handleFileUpload } = useTusUpload();
const [form] = Form.useForm();
2025-03-07 18:01:07 +08:00
const handleIcon = () => {
const hyperLinkElement =document.querySelectorAll(".hyper-link");
console.log("hyperLinkElement", hyperLinkElement);
hyperLinkElement.forEach((item) => {
const hyperLinkDom = createRoot(item);
hyperLinkDom.render(<LinkOutlined />);
2025-03-05 16:50:47 +08:00
});
2025-03-07 18:01:07 +08:00
}
const CustomLinkIconPlugin = (mind) => {
mind.bus.addListener("operation", handleIcon)
2025-03-05 16:50:47 +08:00
};
2025-02-27 22:58:42 +08:00
useEffect(() => {
if (post?.id && id) {
2025-02-27 22:58:42 +08:00
read.mutateAsync({
data: {
visitorId: user?.id || null,
postId: post?.id,
type: VisitType.READED,
},
});
}
}, [post]);
2025-02-21 16:57:22 +08:00
useEffect(() => {
2025-02-26 23:18:14 +08:00
if (post && form && instance && id) {
2025-02-27 09:47:46 +08:00
instance.refresh((post as any).meta);
2025-02-26 23:18:14 +08:00
const deptIds = (post?.depts || [])?.map((dept) => dept.id);
const formData = {
title: post.title,
deptIds: deptIds,
};
2025-03-06 22:26:59 +08:00
// post.terms?.forEach((term) => {
// formData[term.taxonomyId] = term.id; // 假设 taxonomyName是您在 Form.Item 中使用的name
// });
// 按 taxonomyId 分组所有 terms
const termsByTaxonomy = {};
2025-02-26 23:18:14 +08:00
post.terms?.forEach((term) => {
2025-03-06 22:26:59 +08:00
if (!termsByTaxonomy[term.taxonomyId]) {
termsByTaxonomy[term.taxonomyId] = [];
}
termsByTaxonomy[term.taxonomyId].push(term.id);
});
// 将分组后的 terms 设置到 formData
Object.entries(termsByTaxonomy).forEach(([taxonomyId, termIds]) => {
formData[taxonomyId] = termIds;
2025-02-28 15:17:56 +08:00
});
2025-02-26 23:18:14 +08:00
form.setFieldsValue(formData);
}
}, [post, form, instance, id]);
useEffect(() => {
if (!containerRef.current) return;
const mind = new MindElixir({
...MIND_OPTIONS,
el: containerRef.current,
2025-02-27 22:37:09 +08:00
before: {
beginEdit() {
2025-02-27 22:06:26 +08:00
return canEdit;
},
2025-02-27 16:11:53 +08:00
},
draggable: canEdit, // 禁用拖拽
contextMenu: canEdit, // 禁用右键菜单
toolBar: canEdit, // 禁用工具栏
nodeMenu: canEdit, // 禁用节点右键菜单
keypress: canEdit, // 禁用键盘快捷键
2025-02-21 16:57:22 +08:00
});
2025-03-05 16:50:47 +08:00
mind.install(CustomLinkIconPlugin);
2025-02-27 22:37:09 +08:00
mind.init(MindElixir.new("新思维导图"));
2025-02-26 23:18:14 +08:00
containerRef.current.hidden = true;
2025-02-27 22:39:31 +08:00
//挂载实例
2025-02-26 23:18:14 +08:00
setInstance(mind);
2025-03-07 18:01:07 +08:00
2025-02-27 16:11:53 +08:00
}, [canEdit]);
2025-03-07 18:01:07 +08:00
useEffect(() => {
handleIcon()
});
2025-02-26 23:18:14 +08:00
useEffect(() => {
if ((!id || post) && instance) {
2025-02-27 09:47:46 +08:00
containerRef.current.hidden = false;
instance.toCenter();
2025-03-02 11:49:46 +08:00
if ((post as any as PathDto)?.meta?.nodeData) {
instance.refresh((post as any as PathDto)?.meta);
2025-02-27 22:06:26 +08:00
}
2025-02-26 23:18:14 +08:00
}
2025-02-27 09:47:46 +08:00
}, [id, post, instance]);
2025-02-27 22:39:31 +08:00
//保存 按钮 函数
2025-02-26 23:18:14 +08:00
const handleSave = async () => {
if (!instance) return;
2025-02-27 09:47:46 +08:00
const values = form.getFieldsValue();
2025-02-28 15:43:32 +08:00
//以图片格式导出思维导图以作为思维导图封面
2025-02-27 09:47:46 +08:00
const imgBlob = await instance?.exportPng();
handleFileUpload(
imgBlob,
async (result) => {
const termIds = taxonomies
2025-03-06 22:26:59 +08:00
.flatMap((tax) => values[tax.id] || []) // 获取每个 taxonomy 对应的选中值并展平
.filter((id) => id); // 过滤掉空值
2025-02-27 09:47:46 +08:00
const deptIds = (values?.deptIds || []) as string[];
const { theme, ...data } = instance.getData();
try {
if (post && id) {
const params: Prisma.PostUpdateArgs = {
where: {
id,
},
data: {
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,
2025-02-26 23:18:14 +08:00
title: data.nodeData.topic,
meta: { ...data, thumbnail: result.compressedUrl },
terms: {
2025-02-27 09:47:46 +08:00
connect: termIds.map((id) => ({ id })),
2025-02-26 23:18:14 +08:00
},
depts: {
2025-02-27 09:47:46 +08:00
connect: deptIds.map((id) => ({ id })),
2025-02-26 23:18:14 +08:00
},
2025-02-27 09:47:46 +08:00
updatedAt: new Date(),
};
2025-02-26 23:18:14 +08:00
2025-02-27 09:47:46 +08:00
const res = await create.mutateAsync({ data: params });
navigate(`/path/editor/${res.id}`, { replace: true });
toast.success("创建成功");
}
} catch (error) {
toast.error("保存失败");
throw error;
2025-02-26 23:18:14 +08:00
}
2025-02-27 09:47:46 +08:00
console.log(result);
},
2025-03-07 18:01:07 +08:00
(error) => { },
2025-02-27 09:47:46 +08:00
`mind-thumb-${new Date().toString()}`
);
2025-02-26 23:18:14 +08:00
};
2025-02-27 22:37:09 +08:00
useEffect(() => {
2025-02-27 23:50:23 +08:00
containerRef.current.style.height = `${Math.floor(window.innerHeight - 271)}px`;
}, []);
2025-02-21 16:57:22 +08:00
return (
2025-02-27 23:56:13 +08:00
<div className={` flex-col flex `}>
2025-03-02 22:25:39 +08:00
{taxonomies && (
2025-02-28 15:17:56 +08:00
<Form form={form} className=" bg-white p-4 border-b">
2025-02-27 23:56:13 +08:00
<div className="flex items-center justify-between gap-4">
2025-02-27 09:47:46 +08:00
<div className="flex items-center gap-4">
2025-02-26 23:18:14 +08:00
{taxonomies.map((tax, index) => (
<Form.Item
key={tax.id}
name={tax.id}
2025-02-27 09:47:46 +08:00
// rules={[{ required: true }]}
noStyle>
2025-02-26 23:18:14 +08:00
<TermSelect
2025-03-06 22:26:59 +08:00
maxTagCount={1}
multiple
2025-02-27 23:05:12 +08:00
disabled={!canEdit}
2025-03-06 22:26:59 +08:00
className=" w-56"
2025-02-26 23:18:14 +08:00
placeholder={`请选择${tax.name}`}
taxonomyId={tax.id}
/>
</Form.Item>
))}
2025-02-27 09:47:46 +08:00
<Form.Item
// rules={[{ required: true }]}
name="deptIds"
noStyle>
<DepartmentSelect
2025-02-27 23:05:12 +08:00
disabled={!canEdit}
2025-02-27 09:47:46 +08:00
className="w-96"
placeholder="请选择制作单位"
multiple
/>
2025-02-26 23:18:14 +08:00
</Form.Item>
2025-03-05 09:37:45 +08:00
{post && id ? <JoinButton></JoinButton> : <></>}
2025-03-02 11:49:46 +08:00
</div>
<div>
{canEdit && (
<Button
ghost
type="primary"
icon={<SaveOutlined></SaveOutlined>}
onSubmit={(e) => e.preventDefault()}
onClick={handleSave}>
{id ? "更新" : "保存"}
</Button>
)}
2025-02-26 23:18:14 +08:00
</div>
</div>
</Form>
)}
2025-02-27 22:06:26 +08:00
<div
ref={containerRef}
className="w-full"
2025-02-27 22:06:26 +08:00
onContextMenu={(e) => e.preventDefault()}
/>
2025-02-27 16:11:53 +08:00
{canEdit && instance && <NodeMenu mind={instance} />}
2025-02-28 15:17:56 +08:00
{isLoading && (
<div
className="py-64 justify-center flex"
style={{ height: "calc(100vh - 271px)" }}>
<Spin size="large"></Spin>
</div>
)}
{!post && id && !isLoading && (
<div
className="py-64"
style={{ height: "calc(100vh - 271px)" }}>
<Empty></Empty>
</div>
)}
</div>
2025-02-21 16:57:22 +08:00
);
2025-03-06 22:26:59 +08:00
}