This commit is contained in:
ditiqi 2025-02-27 09:47:46 +08:00
parent 4fda9e30ac
commit 46db980b0c
1 changed files with 148 additions and 119 deletions

View File

@ -1,15 +1,22 @@
import { Button, Card, Empty, Form, Space, Spin, message, theme } from 'antd'; import { Button, Card, Empty, Form, Space, Spin, message, theme } from "antd";
import NodeMenu from './NodeMenu'; import NodeMenu from "./NodeMenu";
import { useEntity, api, usePost } from '@nice/client'; import { api, usePost } from "@nice/client";
import { ObjectType, postDetailSelect, PostDto, PostType, Prisma, Taxonomy } from '@nice/common'; import {
import TermSelect from '../../models/term/term-select'; ObjectType,
import DepartmentSelect from '../../models/department/department-select'; postDetailSelect,
import { useEffect, useRef, useState } from 'react'; PostDto,
import toast from 'react-hot-toast'; PostType,
import { MindElixirInstance } from 'mind-elixir'; Prisma,
import MindElixir from 'mind-elixir'; Taxonomy,
import { useTusUpload } from '@web/src/hooks/useTusUpload'; } from "@nice/common";
import { useNavigate } from 'react-router-dom'; import TermSelect from "../../models/term/term-select";
import DepartmentSelect from "../../models/department/department-select";
import { useEffect, 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";
const MIND_OPTIONS = { const MIND_OPTIONS = {
direction: MindElixir.SIDE, direction: MindElixir.SIDE,
draggable: true, draggable: true,
@ -17,53 +24,54 @@ const MIND_OPTIONS = {
toolBar: true, toolBar: true,
nodeMenu: true, nodeMenu: true,
keypress: true, keypress: true,
locale: 'zh_CN' as const, locale: "zh_CN" as const,
theme: { theme: {
name: 'Latte', name: "Latte",
palette: [ palette: [
'#dd7878', "#dd7878",
'#ea76cb', "#ea76cb",
'#8839ef', "#8839ef",
'#e64553', "#e64553",
'#fe640b', "#fe640b",
'#df8e1d', "#df8e1d",
'#40a02b', "#40a02b",
'#209fb5', "#209fb5",
'#1e66f5', "#1e66f5",
'#7287fd', "#7287fd",
], ],
cssVar: { cssVar: {
'--main-color': '#444446', "--main-color": "#444446",
'--main-bgcolor': '#ffffff', "--main-bgcolor": "#ffffff",
'--color': '#777777', "--color": "#777777",
'--bgcolor': '#f6f6f6', "--bgcolor": "#f6f6f6",
'--panel-color': '#444446', "--panel-color": "#444446",
'--panel-bgcolor': '#ffffff', "--panel-bgcolor": "#ffffff",
'--panel-border-color': '#eaeaea', "--panel-border-color": "#eaeaea",
}, },
} },
}; };
export default function MindEditor({ id }: { id?: string }) { export default function MindEditor({ id }: { id?: string }) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [instance, setInstance] = useState<MindElixirInstance | null>(null); const [instance, setInstance] = useState<MindElixirInstance | null>(null);
const { data: post, isLoading }: { data: PostDto, isLoading: boolean } = api.post.findFirst.useQuery({ const { data: post, isLoading }: { data: PostDto; isLoading: boolean } =
where: { api.post.findFirst.useQuery({
id where: {
}, id,
select: postDetailSelect },
}) select: postDetailSelect,
const navigate = useNavigate() });
const navigate = useNavigate();
const { create, update } = usePost(); const { create, update } = usePost();
const { data: taxonomies } = api.taxonomy.getAll.useQuery({ const { data: taxonomies } = api.taxonomy.getAll.useQuery({
type: ObjectType.COURSE, type: ObjectType.COURSE,
}); });
const { handleFileUpload } = useTusUpload() const { handleFileUpload } = useTusUpload();
const [form] = Form.useForm() const [form] = Form.useForm();
useEffect(() => { useEffect(() => {
if (post && form && instance && id) { if (post && form && instance && id) {
console.log(post) console.log(post);
instance.refresh((post as any).meta) instance.refresh((post as any).meta);
const deptIds = (post?.depts || [])?.map((dept) => dept.id); const deptIds = (post?.depts || [])?.map((dept) => dept.id);
const formData = { const formData = {
title: post.title, title: post.title,
@ -82,116 +90,137 @@ export default function MindEditor({ id }: { id?: string }) {
...MIND_OPTIONS, ...MIND_OPTIONS,
el: containerRef.current, el: containerRef.current,
}); });
mind.init(MindElixir.new('新学习路径')); mind.init(MindElixir.new("新学习路径"));
containerRef.current.hidden = true; containerRef.current.hidden = true;
setInstance(mind); setInstance(mind);
}, []); }, []);
useEffect(() => { useEffect(() => {
if ((!id || post) && instance) { if ((!id || post) && instance) {
containerRef.current.hidden = false containerRef.current.hidden = false;
instance.toCenter() instance.toCenter();
instance.refresh((post as any)?.meta) instance.refresh((post as any)?.meta);
} }
}, [id, post, instance]) }, [id, post, instance]);
const handleSave = async () => { const handleSave = async () => {
if (!instance) return; if (!instance) return;
const values = form.getFieldsValue() const values = form.getFieldsValue();
const imgBlob = await instance?.exportPng() const imgBlob = await instance?.exportPng();
handleFileUpload(imgBlob, async (result) => { handleFileUpload(
const termIds = taxonomies.map((tax) => values[tax.id]).filter((id) => id); imgBlob,
const deptIds = (values?.deptIds || []) as string[]; async (result) => {
const { theme, ...data } = instance.getData(); const termIds = taxonomies
try { .map((tax) => values[tax.id])
if (post && id) { .filter((id) => id);
const params: Prisma.PostUpdateArgs = { const deptIds = (values?.deptIds || []) as string[];
where: { const { theme, ...data } = instance.getData();
id try {
}, if (post && id) {
data: { 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, title: data.nodeData.topic,
meta: { ...data, thumbnail: result.compressedUrl }, meta: { ...data, thumbnail: result.compressedUrl },
terms: { terms: {
set: termIds.map((id) => ({ id })) connect: termIds.map((id) => ({ id })),
}, },
depts: { depts: {
set: deptIds.map((id) => ({ id })), connect: deptIds.map((id) => ({ id })),
}, },
updatedAt: new Date() updatedAt: new Date(),
} };
const res = await create.mutateAsync({ data: params });
navigate(`/path/editor/${res.id}`, { replace: true });
toast.success("创建成功");
} }
await update.mutateAsync(params); } catch (error) {
toast.success('更新成功'); toast.error("保存失败");
} else { throw error;
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) { console.log(result);
toast.error('保存失败'); },
throw error; (error) => {},
} `mind-thumb-${new Date().toString()}`
console.log(result) );
}, (error) => { }, `mind-thumb-${new Date().toString()}`)
}; };
return ( return (
<div className=' flex flex-col border rounded-lg overflow-hidden'> <div className=" flex flex-col border rounded-lg overflow-hidden">
{taxonomies && ( {taxonomies && (
<Form onFinish={(values) => { <Form
console.log(values) onFinish={(values) => {
}} form={form} className=' bg-white p-2 '> console.log(values);
<div className='flex items-center justify-between gap-4'> }}
<div className='flex items-center gap-4'> form={form}
className=" bg-white p-2 ">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4">
{taxonomies.map((tax, index) => ( {taxonomies.map((tax, index) => (
<Form.Item <Form.Item
key={tax.id} key={tax.id}
name={tax.id} name={tax.id}
rules={[{ required: false }]} // rules={[{ required: true }]}
noStyle noStyle>
>
<TermSelect <TermSelect
className=' w-48' className=" w-48"
placeholder={`请选择${tax.name}`} placeholder={`请选择${tax.name}`}
taxonomyId={tax.id} taxonomyId={tax.id}
/> />
</Form.Item> </Form.Item>
))} ))}
<Form.Item name="deptIds" noStyle> <Form.Item
<DepartmentSelect className='w-96' placeholder='请选择制作单位' multiple /> // rules={[{ required: true }]}
name="deptIds"
noStyle>
<DepartmentSelect
className="w-96"
placeholder="请选择制作单位"
multiple
/>
</Form.Item> </Form.Item>
</div> </div>
<Button ghost type='primary' onClick={handleSave} >{id ? '更新' : '保存'}</Button> <Button ghost type="primary" onClick={handleSave}>
{id ? "更新" : "保存"}
</Button>
</div> </div>
</Form> </Form>
)
}
<div
ref={containerRef}
className='mind-editor'
/>
{instance && (<NodeMenu mind={instance} />
)} )}
{isLoading && <div className='py-64 justify-center flex' style={{ height: "calc(100vh - 287px)" }}> <div ref={containerRef} className="mind-editor min-h-screen" />
<Spin size='large'></Spin> {instance && <NodeMenu mind={instance} />}
</div>} {isLoading && (
{!post && id && !isLoading && <div className='py-64' style={{ height: "calc(100vh - 287px)" }}> <div
<Empty></Empty> className="py-64 justify-center flex"
</div>} 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> </div>
); );
} }