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

247 lines
6.6 KiB
TypeScript
Executable File

import { Button, Card, Empty, Form, Space, Spin, message, theme } from "antd";
import NodeMenu from "./NodeMenu";
import { api, usePost } from "@nice/client";
import {
ObjectType,
postDetailSelect,
PostDto,
PostType,
Prisma,
RolePerms,
Taxonomy,
} 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";
const MIND_OPTIONS = {
direction: MindElixir.SIDE,
draggable: true,
contextMenu: true,
toolBar: true,
nodeMenu: true,
keypress: true,
locale: "zh_CN" as const,
theme: {
name: "Latte",
palette: [
"#dd7878",
"#ea76cb",
"#8839ef",
"#e64553",
"#fe640b",
"#df8e1d",
"#40a02b",
"#209fb5",
"#1e66f5",
"#7287fd",
],
cssVar: {
"--main-color": "#444446",
"--main-bgcolor": "#ffffff",
"--color": "#777777",
"--bgcolor": "#f6f6f6",
"--panel-color": "#444446",
"--panel-bgcolor": "#ffffff",
"--panel-border-color": "#eaeaea",
},
},
};
export default function MindEditor({ id }: { id?: string }) {
const containerRef = useRef<HTMLDivElement>(null);
const [instance, setInstance] = useState<MindElixirInstance | null>(null);
const { isAuthenticated, user, hasSomePermissions } = useAuth();
const { data: post, isLoading }: { data: PostDto; isLoading: boolean } =
api.post.findFirst.useQuery({
where: {
id,
},
select: postDetailSelect,
});
const canEdit: boolean = useMemo(() => {
//登录了且是作者、超管、无id新建模式
const isAuth = isAuthenticated && user?.id == post?.author.id
return !Boolean(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 && form && instance && id) {
console.log(post);
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();
instance.refresh((post as any)?.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>
);
}