238 lines
6.5 KiB
TypeScript
Executable File
238 lines
6.5 KiB
TypeScript
Executable File
import { Button, Card, Empty, Form, Space, Spin, message, theme } 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 { 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";
|
|
export default function MindEditor({ id }: { id?: string }) {
|
|
//containerRef 容器ref instance 实例
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const [instance, setInstance] = useState<MindElixirInstance | null>(null);
|
|
const { isAuthenticated, user, hasSomePermissions } = useAuth();
|
|
const { read } = useVisitor()
|
|
const { data: post, isLoading }: { data: PathDto; isLoading: boolean } =
|
|
api.post.findFirst.useQuery({
|
|
where: {
|
|
id,
|
|
},
|
|
select: postDetailSelect,
|
|
}, { enabled: Boolean(id) });
|
|
const canEdit: boolean = useMemo(() => {
|
|
//登录了且是作者、超管、无id新建模式
|
|
const isAuth = isAuthenticated && user?.id === post?.author?.id;
|
|
return !!id || isAuth || hasSomePermissions(RolePerms.MANAGE_ANY_POST);
|
|
}, [user]);
|
|
const navigate = useNavigate();
|
|
const { create, update } = usePost();
|
|
const { data: taxonomies } = api.taxonomy.getAll.useQuery({
|
|
type: ObjectType.COURSE,
|
|
});
|
|
const { handleFileUpload } = useTusUpload();
|
|
const [form] = Form.useForm();
|
|
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
|
|
})
|
|
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.init(MindElixir.new("新思维导图"));
|
|
containerRef.current.hidden = true;
|
|
//挂载实例
|
|
setInstance(mind);
|
|
}, [canEdit]);
|
|
useEffect(() => {
|
|
if ((!id || post) && instance) {
|
|
containerRef.current.hidden = false;
|
|
instance.toCenter();
|
|
if (post?.meta?.nodeData) {
|
|
instance.refresh(post?.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
|
|
.map((tax) => values[tax.id])
|
|
.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: {
|
|
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()}`
|
|
);
|
|
};
|
|
useEffect(() => {
|
|
containerRef.current.style.height = `${Math.floor(window.innerHeight / 1.25)}px`;
|
|
}, []);
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 flex-col w-[90vw] my-5 h-[80vh] border rounded-lg mx-auto">
|
|
{canEdit && taxonomies && (
|
|
<Form form={form} className=" bg-white p-4 ">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex items-center gap-4">
|
|
{taxonomies.map((tax, index) => (
|
|
<Form.Item
|
|
key={tax.id}
|
|
name={tax.id}
|
|
// rules={[{ required: true }]}
|
|
noStyle>
|
|
<TermSelect
|
|
className=" w-48"
|
|
placeholder={`请选择${tax.name}`}
|
|
taxonomyId={tax.id}
|
|
/>
|
|
</Form.Item>
|
|
))}
|
|
<Form.Item
|
|
// rules={[{ required: true }]}
|
|
name="deptIds"
|
|
noStyle>
|
|
<DepartmentSelect
|
|
className="w-96"
|
|
placeholder="请选择制作单位"
|
|
multiple
|
|
/>
|
|
</Form.Item>
|
|
</div>
|
|
<Button
|
|
ghost
|
|
type="primary"
|
|
onSubmit={(e) => e.preventDefault()}
|
|
onClick={handleSave}>
|
|
{id ? "更新" : "保存"}
|
|
</Button>
|
|
</div>
|
|
</Form>
|
|
)}
|
|
<div
|
|
ref={containerRef}
|
|
className="w-full"
|
|
onContextMenu={(e) => e.preventDefault()}
|
|
/>
|
|
{canEdit && instance && <NodeMenu mind={instance} />}
|
|
{
|
|
isLoading && (
|
|
<div
|
|
className="py-64 justify-center flex"
|
|
style={{ height: "calc(100vh - 287px)" }}>
|
|
<Spin size="large"></Spin>
|
|
</div>
|
|
)
|
|
}
|
|
{
|
|
!post && id && !isLoading && (
|
|
<div
|
|
className="py-64"
|
|
style={{ height: "calc(100vh - 287px)" }}>
|
|
<Empty></Empty>
|
|
</div>
|
|
)
|
|
}
|
|
</div >
|
|
);
|
|
}
|