erModel.tsx 24 KB

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