erModel.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977
  1. import { useEffect, useMemo, useRef, useState } from "react";
  2. import { EventArgs, Graph, Shape } from "@antv/x6";
  3. import { Transform } from "@antv/x6-plugin-transform";
  4. import { Scroller } from "@antv/x6-plugin-scroller";
  5. import { Snapline } from "@antv/x6-plugin-snapline";
  6. import { Keyboard } from "@antv/x6-plugin-keyboard";
  7. import { Export } from "@antv/x6-plugin-export";
  8. import { Selection } from "@antv/x6-plugin-selection";
  9. import { SaveDataModel, UploadFile } from "@/api";
  10. import { useFullscreen, useSessionStorageState } from "ahooks";
  11. import { base64ToFile, createTable, uuid } from "@/utils";
  12. import dayjs from "dayjs";
  13. import type {
  14. ColumnItem,
  15. ColumnRelation,
  16. ProjectInfo,
  17. RemarkInfo,
  18. TableItemType,
  19. TopicAreaInfo,
  20. } from "@/type";
  21. import { RelationLineType } from "@/enum";
  22. import { render } from "./renderer";
  23. import { DEFAULT_SETTING } from "@/constants";
  24. import { initInfo } from "./initInfo";
  25. import "@/components/TableNode";
  26. import "@/components/TopicNode";
  27. import "@/components/NoticeNode";
  28. import { message } from "antd";
  29. export default function erModel() {
  30. const graphRef = useRef<Graph>();
  31. const [graph, setGraph] = useState<Graph>();
  32. const historyRef = useRef<ProjectInfo[]>([]);
  33. const activeIndex = useRef(0);
  34. const [_isFullscreen, { enterFullscreen, exitFullscreen }] = useFullscreen(
  35. document.body
  36. );
  37. const [playModeEnable, setPlayModeEnable] = useSessionStorageState(
  38. "playModeEnable",
  39. {
  40. defaultValue: false,
  41. listenStorageChange: true,
  42. }
  43. );
  44. const [saveTime, setSaveTime] = useState<string>();
  45. const [project, setProjectInfo] = useState<ProjectInfo>({
  46. id: "",
  47. name: "新建模型",
  48. directory: "",
  49. type: 3,
  50. description: "",
  51. isTemplate: false,
  52. industry: "",
  53. publishStatus: "",
  54. tables: [],
  55. relations: [],
  56. topicAreas: [],
  57. remarkInfos: [],
  58. todos: [],
  59. setting: {
  60. ...DEFAULT_SETTING,
  61. },
  62. });
  63. const [_tabActiveKey, setTabActiveKey] =
  64. useSessionStorageState("tabs-active-key");
  65. const [_relationActive, setRelationActive] =
  66. useSessionStorageState("relation-active");
  67. const [tableActive, setTableActive] = useSessionStorageState<string>('table-active', {
  68. defaultValue: "",
  69. listenStorageChange: true
  70. });
  71. const timer = useRef<any>();
  72. const saveData = (info: ProjectInfo) => {
  73. // 提交服务器
  74. // 清除定时器
  75. clearTimeout(timer.current);
  76. timer.current = setTimeout(() => {
  77. SaveDataModel(info);
  78. // 格式化当前时间
  79. setSaveTime(dayjs().format("YYYY-MM-DD HH:mm:ss"));
  80. }, 500);
  81. };
  82. /**
  83. * 统一修改数据
  84. * @param info 模型信息
  85. * @param ingoreHistory 忽略历史记录
  86. * @param isInit 初始化
  87. */
  88. const setProject = (
  89. info: ProjectInfo | ((state: ProjectInfo) => ProjectInfo),
  90. ingoreHistory?: boolean,
  91. isInit?: boolean,
  92. ingoreRender?: boolean
  93. ) => {
  94. if (isInit && typeof info === "object" && typeof info !== null) {
  95. historyRef.current = [];
  96. activeIndex.current = 0;
  97. initInfo(info);
  98. }
  99. if (info && typeof info === "function") {
  100. setProjectInfo((state) => {
  101. const result = info(state);
  102. if (!isInit) {
  103. saveData(result);
  104. }
  105. graphRef.current && !ingoreRender && render(graphRef.current, result);
  106. // 添加记录
  107. if (!ingoreHistory) {
  108. historyRef.current?.push(result);
  109. activeIndex.current = historyRef.current?.length - 1;
  110. if (historyRef.current?.length > 20) {
  111. historyRef.current?.shift();
  112. activeIndex.current -= 1;
  113. }
  114. }
  115. return result;
  116. });
  117. } else {
  118. setProjectInfo(info);
  119. graphRef.current && !ingoreRender && render(graphRef.current, info);
  120. // 添加记录
  121. if (!ingoreHistory) {
  122. historyRef.current?.push(info);
  123. activeIndex.current = historyRef.current?.length - 1;
  124. if (historyRef.current?.length > 20) {
  125. historyRef.current?.shift();
  126. activeIndex.current -= 1;
  127. }
  128. }
  129. if (!isInit) {
  130. saveData(info);
  131. }
  132. }
  133. };
  134. useEffect(() => {
  135. graphRef.current?.setGridSize(project.setting.showGrid ? 10 : 0);
  136. }, [project.setting.showGrid]);
  137. useEffect(() => {
  138. graphRef.current && render(graphRef.current, project);
  139. }, [project.setting.showRelation]);
  140. /**
  141. * 初始化画布
  142. * @param container
  143. */
  144. const initGraph = (
  145. container: HTMLElement,
  146. width?: number,
  147. height?: number,
  148. preview?: boolean
  149. ) => {
  150. graphRef.current?.dispose?.();
  151. const instance = new Graph({
  152. container,
  153. width: width || document.documentElement.clientWidth,
  154. height: height || document.documentElement.clientHeight,
  155. autoResize: true,
  156. async: false,
  157. mousewheel: {
  158. enabled: true,
  159. modifiers: "ctrl",
  160. minScale: 0.2,
  161. maxScale: 2,
  162. },
  163. highlighting: {
  164. nodeAvailable: {
  165. name: "stroke",
  166. args: {
  167. padding: 4,
  168. attrs: {
  169. "stroke-width": 2,
  170. stroke: "red",
  171. },
  172. },
  173. },
  174. },
  175. connecting: {
  176. allowBlank: false,
  177. allowEdge: false,
  178. allowLoop: false,
  179. router: {
  180. name: "normal",
  181. },
  182. createEdge() {
  183. return new Shape.Edge({
  184. attrs: {
  185. line: {
  186. stroke: "#ff0000",
  187. strokeWidth: 1,
  188. strokeDasharray: 5,
  189. targetMarker: null,
  190. },
  191. },
  192. data: {
  193. type: "refer",
  194. },
  195. });
  196. },
  197. },
  198. grid: {
  199. visible: true,
  200. size: 10,
  201. },
  202. background: {
  203. color: "#F2F7FA",
  204. },
  205. interacting: {
  206. nodeMovable: (view) => {
  207. const data = view.cell.getData<{
  208. ignoreDrag: boolean;
  209. lock: boolean;
  210. }>();
  211. // 禁止拖拽或锁节点
  212. if (data?.ignoreDrag || data?.lock) return false;
  213. return true;
  214. },
  215. },
  216. });
  217. if (project.id) {
  218. render(instance, project);
  219. }
  220. instance.use(new Snapline({ enabled: true }));
  221. instance.use(
  222. new Transform({
  223. resizing: {
  224. enabled: (node) => {
  225. return node.shape !== "table-node" && !preview;
  226. },
  227. },
  228. })
  229. );
  230. instance.use(new Scroller());
  231. instance.use(new Keyboard());
  232. instance.use(new Export());
  233. instance.use(
  234. new Selection({
  235. enabled: true,
  236. showNodeSelectionBox: true,
  237. multiple: false,
  238. })
  239. );
  240. setGraph(instance);
  241. graphRef.current = instance;
  242. instance.on(
  243. "node:change:add:relation",
  244. (args: EventArgs["cell:change:*"]) => {
  245. console.log("node:change:add:relation", args.current);
  246. const { source, target } = args.current;
  247. if (source && target) {
  248. addRelation(source, target);
  249. }
  250. }
  251. );
  252. instance.on("edge:dblclick", (args: EventArgs["edge:dblclick"]) => {
  253. console.log("edge:dblclick", args);
  254. setTabActiveKey("2");
  255. setRelationActive(args.cell.id);
  256. });
  257. instance.on(
  258. "node:change:update:remark",
  259. function (args: EventArgs["cell:change:*"]) {
  260. console.log("修改备注:", args.current);
  261. updateRemark(args.current);
  262. }
  263. );
  264. instance.on("node:moved", (args) => {
  265. const position = args.node.position();
  266. const data = args.node.data;
  267. if (data.isTable) {
  268. updateTable({
  269. ...data,
  270. table: {
  271. ...data.table,
  272. style: {
  273. ...data.table.style,
  274. x: position.x,
  275. y: position.y,
  276. },
  277. },
  278. });
  279. }
  280. if (data.isTopicArea) {
  281. updateTopicArea({
  282. ...data,
  283. style: {
  284. ...data.style,
  285. x: position.x,
  286. y: position.y,
  287. },
  288. });
  289. }
  290. if (data.isRemark) {
  291. updateRemark({
  292. ...data,
  293. style: {
  294. ...data.style,
  295. x: position.x,
  296. y: position.y,
  297. },
  298. });
  299. }
  300. });
  301. instance.on("node:resized", (args) => {
  302. console.log("node:resized", args);
  303. const size = args.node.getSize();
  304. const position = args.node.position();
  305. const data = args.node.data;
  306. if (data.isTopicArea) {
  307. updateTopicArea({
  308. ...data,
  309. style: {
  310. ...data.style,
  311. width: size.width,
  312. height: size.height,
  313. x: position.x,
  314. y: position.y,
  315. },
  316. });
  317. }
  318. if (data.isRemark) {
  319. updateRemark({
  320. ...data,
  321. style: {
  322. ...data.style,
  323. width: size.width,
  324. height: size.height,
  325. x: position.x,
  326. y: position.y,
  327. },
  328. });
  329. }
  330. });
  331. instance.bindKey("ctrl+z", onUndo);
  332. instance.bindKey("ctrl+y", onRedo);
  333. instance.bindKey("ctrl+c", onCopy);
  334. instance.bindKey("ctrl+x", onCut);
  335. instance.bindKey("ctrl+v", onPaste);
  336. instance.bindKey("delete", onDelete);
  337. instance.bindKey("ctrl+down", () => {
  338. const scale = instance.zoom() - 0.1;
  339. instance.zoomTo(scale < 0.2 ? 0.2 : scale);
  340. });
  341. instance.bindKey("ctrl+up", () => {
  342. const scale = instance.zoom() + 0.1;
  343. instance.zoomTo(scale > 2 ? 2 : scale);
  344. });
  345. instance.bindKey("ctrl+n", () => {
  346. // todo 新建
  347. });
  348. instance.bindKey("ctrl+s", () => {
  349. onSave();
  350. });
  351. };
  352. // 能否重做
  353. const canRedo = useMemo(() => {
  354. return (
  355. historyRef.current?.length > 1 &&
  356. activeIndex.current < historyRef.current?.length - 1
  357. );
  358. }, [historyRef.current, activeIndex.current]);
  359. // 能否撤销
  360. const canUndo = useMemo(() => {
  361. return activeIndex.current > 0 && historyRef.current?.length > 1;
  362. }, [historyRef.current, activeIndex.current]);
  363. // 撤销
  364. const onUndo = () => {
  365. const info = historyRef.current?.[activeIndex.current - 1];
  366. activeIndex.current -= 1;
  367. setProject(info, true);
  368. };
  369. // 重做
  370. const onRedo = () => {
  371. const info = historyRef.current?.[activeIndex.current + 1];
  372. activeIndex.current += 1;
  373. setProject(info, true);
  374. };
  375. /**
  376. * 添加表
  377. */
  378. const addTable = (parentId?: string) => {
  379. const area = graphRef.current?.getGraphArea();
  380. const x = area?.center.x || 300;
  381. const y = area?.center.y || 300;
  382. // 数据表类型动态路由传参
  383. const newTable = createTable(project.type || 3, project.id, parentId);
  384. newTable.table.style.x = x;
  385. newTable.table.style.y = y;
  386. // 子表插入到父表后面
  387. const list = [...project.tables];
  388. if (parentId) {
  389. const index = list.findIndex((item) => item.table.id === parentId);
  390. list.splice(index + 1, 0, newTable);
  391. } else {
  392. list.push(newTable);
  393. }
  394. setProject({
  395. ...project,
  396. tables: list,
  397. });
  398. setTabActiveKey("1");
  399. setTableActive(newTable.table.id);
  400. graphRef.current?.select(graphRef.current?.getCellById(newTable.table.id));
  401. };
  402. /**
  403. * 更新表
  404. * @param table
  405. */
  406. const updateTable = (table: TableItemType) => {
  407. setProject((project) => {
  408. return {
  409. ...project,
  410. tables: project.tables.map((item) => {
  411. if (item.table.id === table.table.id) {
  412. return table;
  413. }
  414. return item;
  415. }),
  416. };
  417. });
  418. };
  419. /**
  420. * 删除表及其子表
  421. * @param tableId
  422. */
  423. const deleteTable = (tableId: string) => {
  424. const childTableIds = project.tables
  425. .filter((item) => item.table.parentBusinessTableId === tableId)
  426. .map((item) => item.table.id);
  427. const newInfo = {
  428. ...project,
  429. tables: project.tables.filter(
  430. (item) =>
  431. item.table.id !== tableId &&
  432. item.table.parentBusinessTableId !== tableId
  433. ),
  434. // 对应关系
  435. relations: project.relations.filter(
  436. (item) =>
  437. item.primaryTable !== tableId &&
  438. item.foreignTable !== tableId &&
  439. !childTableIds.includes(item.primaryTable) &&
  440. !childTableIds.includes(item.foreignTable)
  441. ),
  442. };
  443. setProject(newInfo);
  444. };
  445. /**
  446. * 增加主题域
  447. */
  448. const addTopicArea = () => {
  449. const topicAreaId = uuid();
  450. const newTopicArea = {
  451. isTopicArea: true,
  452. id: topicAreaId,
  453. dataModelId: project.id,
  454. name: "主题域_" + (project.topicAreas.length + 1),
  455. style: {
  456. background: "#175e7a",
  457. x: 300,
  458. y: 300,
  459. width: 200,
  460. height: 200,
  461. },
  462. };
  463. setProject({
  464. ...project,
  465. topicAreas: [...project.topicAreas, newTopicArea],
  466. });
  467. setTabActiveKey("3");
  468. };
  469. /**
  470. * 修改主题域
  471. */
  472. const updateTopicArea = (topicArea: TopicAreaInfo) => {
  473. setProject((project) => {
  474. return {
  475. ...project,
  476. topicAreas: project.topicAreas.map((item) => {
  477. if (item.id === topicArea.id) {
  478. return topicArea;
  479. }
  480. return item;
  481. }),
  482. };
  483. });
  484. };
  485. /**
  486. * 删除主题域
  487. * @param topicAreaId
  488. */
  489. const deleteTopicArea = (topicAreaId: string) => {
  490. setProject({
  491. ...project,
  492. topicAreas: project.topicAreas.filter((item) => item.id !== topicAreaId),
  493. });
  494. };
  495. /**
  496. * 添加备注
  497. */
  498. const addRemark = () => {
  499. const remarkId = uuid();
  500. const newRemark = {
  501. isRemark: true,
  502. id: remarkId,
  503. name: "备注_" + (project.remarkInfos.length + 1),
  504. text: "",
  505. dataModelId: project.id,
  506. style: {
  507. x: 300,
  508. y: 300,
  509. width: 200,
  510. height: 200,
  511. background: "#fcf7ac",
  512. },
  513. };
  514. setProject({
  515. ...project,
  516. remarkInfos: [...project.remarkInfos, newRemark],
  517. });
  518. setTabActiveKey("4");
  519. };
  520. /**
  521. * 修改备注
  522. */
  523. const updateRemark = (remark: RemarkInfo) => {
  524. console.log(remark);
  525. setProject((state) => ({
  526. ...(state || {}),
  527. remarkInfos: state.remarkInfos.map((item) =>
  528. item.id === remark.id ? remark : item
  529. ),
  530. }));
  531. };
  532. /**
  533. * 删除备注
  534. * @param remarkId
  535. */
  536. const deleteRemark = (remarkId: string) => {
  537. setProject({
  538. ...project,
  539. remarkInfos: project.remarkInfos.filter((item) => item.id !== remarkId),
  540. });
  541. graphRef.current?.removeCell(remarkId);
  542. };
  543. const getRelations = (project: ProjectInfo, newRelation: ColumnRelation) => {
  544. let sourceColumn: ColumnItem | undefined;
  545. let targetColumn: ColumnItem | undefined;
  546. let sourceTable: TableItemType | undefined;
  547. let targetTable: TableItemType | undefined;
  548. project.tables.forEach((table) => {
  549. if (table.table.id === newRelation.primaryTable) {
  550. sourceTable = table;
  551. sourceColumn = table.tableColumnList.find(
  552. (item) => item.id === newRelation.primaryKey
  553. );
  554. }
  555. if (table.table.id === newRelation.foreignTable) {
  556. targetTable = table;
  557. targetColumn = table.tableColumnList.find(
  558. (item) => item.id === newRelation.foreignKey
  559. );
  560. }
  561. });
  562. if (!sourceColumn || !targetColumn) {
  563. return {
  564. relations: project.relations,
  565. canAdd: false,
  566. };
  567. }
  568. if (sourceColumn.type !== targetColumn.type) {
  569. message.warning("数据类型不一致");
  570. return {
  571. relations: project.relations,
  572. canAdd: false,
  573. };
  574. }
  575. return {
  576. relations: [
  577. ...project.relations,
  578. {
  579. ...newRelation,
  580. name: `${sourceTable?.table.schemaName}_${targetTable?.table.schemaName}_${sourceColumn.schemaName}`,
  581. },
  582. ],
  583. canAdd: true,
  584. };
  585. };
  586. /**
  587. * 添加关系
  588. */
  589. const addRelation = (
  590. source: {
  591. tableId: string;
  592. columnId: string;
  593. },
  594. target: {
  595. tableId: string;
  596. columnId: string;
  597. }
  598. ) => {
  599. const newRelation: ColumnRelation = {
  600. id: uuid(),
  601. name: "",
  602. primaryKey: source.columnId,
  603. primaryTable: source.tableId,
  604. foreignKey: target.columnId,
  605. foreignTable: target.tableId,
  606. relationType: 1,
  607. style: {
  608. color: "#333",
  609. lineType: RelationLineType.Solid,
  610. width: 1,
  611. },
  612. };
  613. setProject((state) => {
  614. const obj = getRelations(state, {
  615. ...newRelation,
  616. dataModelId: state.id,
  617. });
  618. if (obj.canAdd) {
  619. return {
  620. ...state,
  621. relations: obj.relations,
  622. };
  623. } else {
  624. return state;
  625. }
  626. });
  627. setTabActiveKey("2");
  628. };
  629. /**
  630. * 更新关系
  631. */
  632. const updateRelation = (relation: ColumnRelation) => {
  633. setProject((state) => {
  634. return {
  635. ...state,
  636. relations: state.relations.map((item) => {
  637. if (item.id === relation.id) {
  638. return relation;
  639. }
  640. return item;
  641. }),
  642. };
  643. });
  644. };
  645. /**
  646. * 删除关系
  647. */
  648. const deleteRelation = (relationId: string) => {
  649. setProject({
  650. ...project,
  651. relations: project.relations.filter((item) => item.id !== relationId),
  652. });
  653. };
  654. /**
  655. * 清空画布
  656. */
  657. const onClean = () => {
  658. setProject(
  659. {
  660. ...project,
  661. tables: [],
  662. relations: [],
  663. topicAreas: [],
  664. remarkInfos: [],
  665. },
  666. true,
  667. true
  668. );
  669. graph?.clearCells();
  670. };
  671. const [clipboardCache, setClipboardCache] = useState<any>(null);
  672. /**
  673. * 剪切
  674. */
  675. const onCut = () => {
  676. const cells = graphRef.current?.getSelectedCells();
  677. if (cells?.[0]?.isNode()) {
  678. const cell = cells[0];
  679. const data = cell.data;
  680. setClipboardCache(data);
  681. // 表
  682. if (data?.isTable) {
  683. const childTableIds = project.tables
  684. .filter((item) => item.table.parentBusinessTableId === cell.id)
  685. .map((item) => item.table.id);
  686. setProject({
  687. ...project,
  688. tables: project.tables.filter(
  689. (item) =>
  690. item.table.id !== cell.id &&
  691. !childTableIds.includes(item.table.id)
  692. ),
  693. relations: project.relations.filter(
  694. (item) =>
  695. item.primaryTable !== cell.id &&
  696. item.foreignTable !== cell.id &&
  697. !childTableIds.includes(item.primaryTable) &&
  698. !childTableIds.includes(item.foreignTable)
  699. ),
  700. });
  701. }
  702. // 主题区域
  703. if (data?.isTopicArea) {
  704. setProject({
  705. ...project,
  706. topicAreas: project.topicAreas.filter((item) => item.id !== cell.id),
  707. });
  708. }
  709. // 备注
  710. if (data?.isRemark) {
  711. setProject({
  712. ...project,
  713. remarkInfos: project.remarkInfos.filter(
  714. (item) => item.id !== cell.id
  715. ),
  716. });
  717. }
  718. }
  719. };
  720. /**
  721. * 复制
  722. */
  723. const onCopy = () => {
  724. const cells = graphRef.current?.getSelectedCells();
  725. if (cells?.[0]?.isNode()) {
  726. const cell = cells[0];
  727. const data = cell.data;
  728. setClipboardCache(data);
  729. }
  730. };
  731. /**
  732. * 粘贴
  733. */
  734. const onPaste = () => {
  735. if (clipboardCache) {
  736. const data = clipboardCache;
  737. // 表格
  738. if (data?.isTable) {
  739. const tableId = uuid();
  740. const newTable = {
  741. ...data,
  742. table: {
  743. ...data.table,
  744. id: tableId,
  745. style: {
  746. ...data.table.style,
  747. x: data.table.style.x + 20,
  748. y: data.table.style.y + 20,
  749. },
  750. },
  751. tableColumnList: data.tableColumnList.map((item: ColumnItem) => {
  752. return {
  753. ...item,
  754. id: uuid(),
  755. parentBusinessTableId: tableId,
  756. };
  757. }),
  758. };
  759. setProject((project) => ({
  760. ...project,
  761. tables: [...project.tables, newTable],
  762. }));
  763. }
  764. // 主题区域
  765. if (data?.isTopicArea) {
  766. const topicAreaId = uuid();
  767. const newTopicArea = {
  768. ...data,
  769. id: topicAreaId,
  770. style: {
  771. ...data.style,
  772. x: data.style.x + 20,
  773. y: data.style.y + 20,
  774. },
  775. };
  776. setProject((project) => ({
  777. ...project,
  778. topicAreas: [...project.topicAreas, newTopicArea],
  779. }));
  780. }
  781. // 注释节点
  782. if (data?.isRemark) {
  783. const remarkId = uuid();
  784. const newRemark = {
  785. ...data,
  786. id: remarkId,
  787. style: {
  788. ...data.style,
  789. x: data.style.x + 20,
  790. y: data.style.y + 20,
  791. },
  792. };
  793. setProject((project) => ({
  794. ...project,
  795. remarkInfos: [...project.remarkInfos, newRemark],
  796. }));
  797. }
  798. }
  799. };
  800. /**
  801. * 删除
  802. */
  803. const onDelete = () => {
  804. const cell = graphRef.current?.getSelectedCells();
  805. if (cell?.[0]?.isNode()) {
  806. const data = cell[0].data;
  807. if (data?.isTable) {
  808. setProject((project) => ({
  809. ...project,
  810. tables: project.tables.filter((item) => item.table.id !== cell[0].id),
  811. }));
  812. }
  813. if (data?.isTopicArea) {
  814. setProject((project) => ({
  815. ...project,
  816. topicAreas: project.topicAreas.filter(
  817. (item) => item.id !== cell[0].id
  818. ),
  819. }));
  820. }
  821. if (data?.isRemark) {
  822. setProject((project) => ({
  823. ...project,
  824. remarkInfos: project.remarkInfos.filter(
  825. (item) => item.id !== cell[0].id
  826. ),
  827. }));
  828. }
  829. }
  830. };
  831. /**
  832. * 演示模式
  833. */
  834. const enterPlayMode = () => {
  835. enterFullscreen();
  836. setPlayModeEnable(true);
  837. setTimeout(() => {
  838. graphRef.current?.centerContent();
  839. }, 100);
  840. };
  841. /**
  842. * 退出演示模式
  843. */
  844. const exitPlayMode = () => {
  845. exitFullscreen();
  846. graphRef.current?.enableKeyboard();
  847. setPlayModeEnable(false);
  848. };
  849. /**
  850. * 保存项目
  851. */
  852. const onSave = async () => {
  853. setProjectInfo((state) => {
  854. message.loading("保存中...", 0);
  855. graph?.toPNG(
  856. async (dataUri) => {
  857. const file = base64ToFile(
  858. dataUri,
  859. project?.id || "封面图",
  860. "image/png"
  861. );
  862. const formData = new FormData();
  863. formData.append("file", file);
  864. const res = await UploadFile(formData);
  865. await SaveDataModel({
  866. ...state,
  867. coverImage: res?.result?.[0]?.id,
  868. }).finally(() => {
  869. message.destroy();
  870. });
  871. setProjectInfo({
  872. ...state,
  873. coverImage: res?.result?.[0]?.id,
  874. })
  875. setSaveTime(dayjs().format("YYYY-MM-DD HH:mm:ss"));
  876. message.success("保存成功");
  877. },
  878. {
  879. width: 300,
  880. height: 150,
  881. quality: 0.2,
  882. copyStyles: true,
  883. }
  884. );
  885. return state;
  886. });
  887. };
  888. return {
  889. initGraph,
  890. graph,
  891. graphRef,
  892. project,
  893. setProject,
  894. addTable,
  895. updateTable,
  896. deleteTable,
  897. addTopicArea,
  898. updateTopicArea,
  899. deleteTopicArea,
  900. addRemark,
  901. updateRemark,
  902. deleteRemark,
  903. addRelation,
  904. updateRelation,
  905. deleteRelation,
  906. canRedo,
  907. canUndo,
  908. onRedo,
  909. onUndo,
  910. onClean,
  911. onCut,
  912. onCopy,
  913. onPaste,
  914. onDelete,
  915. enterPlayMode,
  916. playModeEnable,
  917. setPlayModeEnable,
  918. exitPlayMode,
  919. saveTime,
  920. onSave,
  921. tableActive,
  922. setTableActive
  923. };
  924. }