123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977 |
- import { useEffect, useMemo, useRef, useState } from "react";
- import { EventArgs, Graph, Shape } from "@antv/x6";
- import { Transform } from "@antv/x6-plugin-transform";
- import { Scroller } from "@antv/x6-plugin-scroller";
- import { Snapline } from "@antv/x6-plugin-snapline";
- import { Keyboard } from "@antv/x6-plugin-keyboard";
- import { Export } from "@antv/x6-plugin-export";
- import { Selection } from "@antv/x6-plugin-selection";
- import { SaveDataModel, UploadFile } from "@/api";
- import { useFullscreen, useSessionStorageState } from "ahooks";
- import { base64ToFile, createTable, uuid } from "@/utils";
- import dayjs from "dayjs";
- import type {
- ColumnItem,
- ColumnRelation,
- ProjectInfo,
- RemarkInfo,
- TableItemType,
- TopicAreaInfo,
- } from "@/type";
- import { RelationLineType } from "@/enum";
- import { render } from "./renderer";
- import { DEFAULT_SETTING } from "@/constants";
- import { initInfo } from "./initInfo";
- import "@/components/TableNode";
- import "@/components/TopicNode";
- import "@/components/NoticeNode";
- import { message } from "antd";
- export default function erModel() {
- const graphRef = useRef<Graph>();
- const [graph, setGraph] = useState<Graph>();
- const historyRef = useRef<ProjectInfo[]>([]);
- const activeIndex = useRef(0);
- const [_isFullscreen, { enterFullscreen, exitFullscreen }] = useFullscreen(
- document.body
- );
- const [playModeEnable, setPlayModeEnable] = useSessionStorageState(
- "playModeEnable",
- {
- defaultValue: false,
- listenStorageChange: true,
- }
- );
- const [saveTime, setSaveTime] = useState<string>();
- const [project, setProjectInfo] = useState<ProjectInfo>({
- id: "",
- name: "新建模型",
- directory: "",
- type: 3,
- description: "",
- isTemplate: false,
- industry: "",
- publishStatus: "",
- tables: [],
- relations: [],
- topicAreas: [],
- remarkInfos: [],
- todos: [],
- setting: {
- ...DEFAULT_SETTING,
- },
- });
- const [_tabActiveKey, setTabActiveKey] =
- useSessionStorageState("tabs-active-key");
- const [_relationActive, setRelationActive] =
- useSessionStorageState("relation-active");
- const [tableActive, setTableActive] = useSessionStorageState<string>('table-active', {
- defaultValue: "",
- listenStorageChange: true
- });
- const timer = useRef<any>();
- const saveData = (info: ProjectInfo) => {
- // 提交服务器
- // 清除定时器
- clearTimeout(timer.current);
- timer.current = setTimeout(() => {
- SaveDataModel(info);
- // 格式化当前时间
- setSaveTime(dayjs().format("YYYY-MM-DD HH:mm:ss"));
- }, 500);
- };
- /**
- * 统一修改数据
- * @param info 模型信息
- * @param ingoreHistory 忽略历史记录
- * @param isInit 初始化
- */
- const setProject = (
- info: ProjectInfo | ((state: ProjectInfo) => ProjectInfo),
- ingoreHistory?: boolean,
- isInit?: boolean,
- ingoreRender?: boolean
- ) => {
- if (isInit && typeof info === "object" && typeof info !== null) {
- historyRef.current = [];
- activeIndex.current = 0;
- initInfo(info);
- }
- if (info && typeof info === "function") {
- setProjectInfo((state) => {
- const result = info(state);
- if (!isInit) {
- saveData(result);
- }
- graphRef.current && !ingoreRender && render(graphRef.current, result);
- // 添加记录
- if (!ingoreHistory) {
- historyRef.current?.push(result);
- activeIndex.current = historyRef.current?.length - 1;
- if (historyRef.current?.length > 20) {
- historyRef.current?.shift();
- activeIndex.current -= 1;
- }
- }
- return result;
- });
- } else {
- setProjectInfo(info);
- graphRef.current && !ingoreRender && render(graphRef.current, info);
- // 添加记录
- if (!ingoreHistory) {
- historyRef.current?.push(info);
- activeIndex.current = historyRef.current?.length - 1;
- if (historyRef.current?.length > 20) {
- historyRef.current?.shift();
- activeIndex.current -= 1;
- }
- }
- if (!isInit) {
- saveData(info);
- }
- }
- };
- useEffect(() => {
- graphRef.current?.setGridSize(project.setting.showGrid ? 10 : 0);
- }, [project.setting.showGrid]);
- useEffect(() => {
- graphRef.current && render(graphRef.current, project);
- }, [project.setting.showRelation]);
- /**
- * 初始化画布
- * @param container
- */
- const initGraph = (
- container: HTMLElement,
- width?: number,
- height?: number,
- preview?: boolean
- ) => {
- graphRef.current?.dispose?.();
- const instance = new Graph({
- container,
- width: width || document.documentElement.clientWidth,
- height: height || document.documentElement.clientHeight,
- autoResize: true,
- async: false,
- mousewheel: {
- enabled: true,
- modifiers: "ctrl",
- minScale: 0.2,
- maxScale: 2,
- },
- highlighting: {
- nodeAvailable: {
- name: "stroke",
- args: {
- padding: 4,
- attrs: {
- "stroke-width": 2,
- stroke: "red",
- },
- },
- },
- },
- connecting: {
- allowBlank: false,
- allowEdge: false,
- allowLoop: false,
- router: {
- name: "normal",
- },
- createEdge() {
- return new Shape.Edge({
- attrs: {
- line: {
- stroke: "#ff0000",
- strokeWidth: 1,
- strokeDasharray: 5,
- targetMarker: null,
- },
- },
- data: {
- type: "refer",
- },
- });
- },
- },
- grid: {
- visible: true,
- size: 10,
- },
- background: {
- color: "#F2F7FA",
- },
- interacting: {
- nodeMovable: (view) => {
- const data = view.cell.getData<{
- ignoreDrag: boolean;
- lock: boolean;
- }>();
- // 禁止拖拽或锁节点
- if (data?.ignoreDrag || data?.lock) return false;
- return true;
- },
- },
- });
- if (project.id) {
- render(instance, project);
- }
- instance.use(new Snapline({ enabled: true }));
- instance.use(
- new Transform({
- resizing: {
- enabled: (node) => {
- return node.shape !== "table-node" && !preview;
- },
- },
- })
- );
- instance.use(new Scroller());
- instance.use(new Keyboard());
- instance.use(new Export());
- instance.use(
- new Selection({
- enabled: true,
- showNodeSelectionBox: true,
- multiple: false,
- })
- );
- setGraph(instance);
- graphRef.current = instance;
- instance.on(
- "node:change:add:relation",
- (args: EventArgs["cell:change:*"]) => {
- console.log("node:change:add:relation", args.current);
- const { source, target } = args.current;
- if (source && target) {
- addRelation(source, target);
- }
- }
- );
- instance.on("edge:dblclick", (args: EventArgs["edge:dblclick"]) => {
- console.log("edge:dblclick", args);
- setTabActiveKey("2");
- setRelationActive(args.cell.id);
- });
- instance.on(
- "node:change:update:remark",
- function (args: EventArgs["cell:change:*"]) {
- console.log("修改备注:", args.current);
- updateRemark(args.current);
- }
- );
- instance.on("node:moved", (args) => {
- const position = args.node.position();
- const data = args.node.data;
- if (data.isTable) {
- updateTable({
- ...data,
- table: {
- ...data.table,
- style: {
- ...data.table.style,
- x: position.x,
- y: position.y,
- },
- },
- });
- }
- if (data.isTopicArea) {
- updateTopicArea({
- ...data,
- style: {
- ...data.style,
- x: position.x,
- y: position.y,
- },
- });
- }
- if (data.isRemark) {
- updateRemark({
- ...data,
- style: {
- ...data.style,
- x: position.x,
- y: position.y,
- },
- });
- }
- });
- instance.on("node:resized", (args) => {
- console.log("node:resized", args);
- const size = args.node.getSize();
- const position = args.node.position();
- const data = args.node.data;
- if (data.isTopicArea) {
- updateTopicArea({
- ...data,
- style: {
- ...data.style,
- width: size.width,
- height: size.height,
- x: position.x,
- y: position.y,
- },
- });
- }
- if (data.isRemark) {
- updateRemark({
- ...data,
- style: {
- ...data.style,
- width: size.width,
- height: size.height,
- x: position.x,
- y: position.y,
- },
- });
- }
- });
- instance.bindKey("ctrl+z", onUndo);
- instance.bindKey("ctrl+y", onRedo);
- instance.bindKey("ctrl+c", onCopy);
- instance.bindKey("ctrl+x", onCut);
- instance.bindKey("ctrl+v", onPaste);
- instance.bindKey("delete", onDelete);
- instance.bindKey("ctrl+down", () => {
- const scale = instance.zoom() - 0.1;
- instance.zoomTo(scale < 0.2 ? 0.2 : scale);
- });
- instance.bindKey("ctrl+up", () => {
- const scale = instance.zoom() + 0.1;
- instance.zoomTo(scale > 2 ? 2 : scale);
- });
- instance.bindKey("ctrl+n", () => {
- // todo 新建
- });
- instance.bindKey("ctrl+s", () => {
- onSave();
- });
- };
- // 能否重做
- const canRedo = useMemo(() => {
- return (
- historyRef.current?.length > 1 &&
- activeIndex.current < historyRef.current?.length - 1
- );
- }, [historyRef.current, activeIndex.current]);
- // 能否撤销
- const canUndo = useMemo(() => {
- return activeIndex.current > 0 && historyRef.current?.length > 1;
- }, [historyRef.current, activeIndex.current]);
- // 撤销
- const onUndo = () => {
- const info = historyRef.current?.[activeIndex.current - 1];
- activeIndex.current -= 1;
- setProject(info, true);
- };
- // 重做
- const onRedo = () => {
- const info = historyRef.current?.[activeIndex.current + 1];
- activeIndex.current += 1;
- setProject(info, true);
- };
- /**
- * 添加表
- */
- const addTable = (parentId?: string) => {
- const area = graphRef.current?.getGraphArea();
- const x = area?.center.x || 300;
- const y = area?.center.y || 300;
- // 数据表类型动态路由传参
- const newTable = createTable(project.type || 3, project.id, parentId);
- newTable.table.style.x = x;
- newTable.table.style.y = y;
- // 子表插入到父表后面
- const list = [...project.tables];
- if (parentId) {
- const index = list.findIndex((item) => item.table.id === parentId);
- list.splice(index + 1, 0, newTable);
- } else {
- list.push(newTable);
- }
- setProject({
- ...project,
- tables: list,
- });
- setTabActiveKey("1");
- setTableActive(newTable.table.id);
- graphRef.current?.select(graphRef.current?.getCellById(newTable.table.id));
- };
- /**
- * 更新表
- * @param table
- */
- const updateTable = (table: TableItemType) => {
- setProject((project) => {
- return {
- ...project,
- tables: project.tables.map((item) => {
- if (item.table.id === table.table.id) {
- return table;
- }
- return item;
- }),
- };
- });
- };
- /**
- * 删除表及其子表
- * @param tableId
- */
- const deleteTable = (tableId: string) => {
- const childTableIds = project.tables
- .filter((item) => item.table.parentBusinessTableId === tableId)
- .map((item) => item.table.id);
- const newInfo = {
- ...project,
- tables: project.tables.filter(
- (item) =>
- item.table.id !== tableId &&
- item.table.parentBusinessTableId !== tableId
- ),
- // 对应关系
- relations: project.relations.filter(
- (item) =>
- item.primaryTable !== tableId &&
- item.foreignTable !== tableId &&
- !childTableIds.includes(item.primaryTable) &&
- !childTableIds.includes(item.foreignTable)
- ),
- };
- setProject(newInfo);
- };
- /**
- * 增加主题域
- */
- const addTopicArea = () => {
- const topicAreaId = uuid();
- const newTopicArea = {
- isTopicArea: true,
- id: topicAreaId,
- dataModelId: project.id,
- name: "主题域_" + (project.topicAreas.length + 1),
- style: {
- background: "#175e7a",
- x: 300,
- y: 300,
- width: 200,
- height: 200,
- },
- };
- setProject({
- ...project,
- topicAreas: [...project.topicAreas, newTopicArea],
- });
- setTabActiveKey("3");
- };
- /**
- * 修改主题域
- */
- const updateTopicArea = (topicArea: TopicAreaInfo) => {
- setProject((project) => {
- return {
- ...project,
- topicAreas: project.topicAreas.map((item) => {
- if (item.id === topicArea.id) {
- return topicArea;
- }
- return item;
- }),
- };
- });
- };
- /**
- * 删除主题域
- * @param topicAreaId
- */
- const deleteTopicArea = (topicAreaId: string) => {
- setProject({
- ...project,
- topicAreas: project.topicAreas.filter((item) => item.id !== topicAreaId),
- });
- };
- /**
- * 添加备注
- */
- const addRemark = () => {
- const remarkId = uuid();
- const newRemark = {
- isRemark: true,
- id: remarkId,
- name: "备注_" + (project.remarkInfos.length + 1),
- text: "",
- dataModelId: project.id,
- style: {
- x: 300,
- y: 300,
- width: 200,
- height: 200,
- background: "#fcf7ac",
- },
- };
- setProject({
- ...project,
- remarkInfos: [...project.remarkInfos, newRemark],
- });
- setTabActiveKey("4");
- };
- /**
- * 修改备注
- */
- const updateRemark = (remark: RemarkInfo) => {
- console.log(remark);
- setProject((state) => ({
- ...(state || {}),
- remarkInfos: state.remarkInfos.map((item) =>
- item.id === remark.id ? remark : item
- ),
- }));
- };
- /**
- * 删除备注
- * @param remarkId
- */
- const deleteRemark = (remarkId: string) => {
- setProject({
- ...project,
- remarkInfos: project.remarkInfos.filter((item) => item.id !== remarkId),
- });
- graphRef.current?.removeCell(remarkId);
- };
- const getRelations = (project: ProjectInfo, newRelation: ColumnRelation) => {
- let sourceColumn: ColumnItem | undefined;
- let targetColumn: ColumnItem | undefined;
- let sourceTable: TableItemType | undefined;
- let targetTable: TableItemType | undefined;
- project.tables.forEach((table) => {
- if (table.table.id === newRelation.primaryTable) {
- sourceTable = table;
- sourceColumn = table.tableColumnList.find(
- (item) => item.id === newRelation.primaryKey
- );
- }
- if (table.table.id === newRelation.foreignTable) {
- targetTable = table;
- targetColumn = table.tableColumnList.find(
- (item) => item.id === newRelation.foreignKey
- );
- }
- });
- if (!sourceColumn || !targetColumn) {
- return {
- relations: project.relations,
- canAdd: false,
- };
- }
- if (sourceColumn.type !== targetColumn.type) {
- message.warning("数据类型不一致");
- return {
- relations: project.relations,
- canAdd: false,
- };
- }
- return {
- relations: [
- ...project.relations,
- {
- ...newRelation,
- name: `${sourceTable?.table.schemaName}_${targetTable?.table.schemaName}_${sourceColumn.schemaName}`,
- },
- ],
- canAdd: true,
- };
- };
- /**
- * 添加关系
- */
- const addRelation = (
- source: {
- tableId: string;
- columnId: string;
- },
- target: {
- tableId: string;
- columnId: string;
- }
- ) => {
- const newRelation: ColumnRelation = {
- id: uuid(),
- name: "",
- primaryKey: source.columnId,
- primaryTable: source.tableId,
- foreignKey: target.columnId,
- foreignTable: target.tableId,
- relationType: 1,
- style: {
- color: "#333",
- lineType: RelationLineType.Solid,
- width: 1,
- },
- };
- setProject((state) => {
- const obj = getRelations(state, {
- ...newRelation,
- dataModelId: state.id,
- });
- if (obj.canAdd) {
- return {
- ...state,
- relations: obj.relations,
- };
- } else {
- return state;
- }
- });
- setTabActiveKey("2");
- };
- /**
- * 更新关系
- */
- const updateRelation = (relation: ColumnRelation) => {
- setProject((state) => {
- return {
- ...state,
- relations: state.relations.map((item) => {
- if (item.id === relation.id) {
- return relation;
- }
- return item;
- }),
- };
- });
- };
- /**
- * 删除关系
- */
- const deleteRelation = (relationId: string) => {
- setProject({
- ...project,
- relations: project.relations.filter((item) => item.id !== relationId),
- });
- };
- /**
- * 清空画布
- */
- const onClean = () => {
- setProject(
- {
- ...project,
- tables: [],
- relations: [],
- topicAreas: [],
- remarkInfos: [],
- },
- true,
- true
- );
- graph?.clearCells();
- };
- const [clipboardCache, setClipboardCache] = useState<any>(null);
- /**
- * 剪切
- */
- const onCut = () => {
- const cells = graphRef.current?.getSelectedCells();
- if (cells?.[0]?.isNode()) {
- const cell = cells[0];
- const data = cell.data;
- setClipboardCache(data);
- // 表
- if (data?.isTable) {
- const childTableIds = project.tables
- .filter((item) => item.table.parentBusinessTableId === cell.id)
- .map((item) => item.table.id);
- setProject({
- ...project,
- tables: project.tables.filter(
- (item) =>
- item.table.id !== cell.id &&
- !childTableIds.includes(item.table.id)
- ),
- relations: project.relations.filter(
- (item) =>
- item.primaryTable !== cell.id &&
- item.foreignTable !== cell.id &&
- !childTableIds.includes(item.primaryTable) &&
- !childTableIds.includes(item.foreignTable)
- ),
- });
- }
- // 主题区域
- if (data?.isTopicArea) {
- setProject({
- ...project,
- topicAreas: project.topicAreas.filter((item) => item.id !== cell.id),
- });
- }
- // 备注
- if (data?.isRemark) {
- setProject({
- ...project,
- remarkInfos: project.remarkInfos.filter(
- (item) => item.id !== cell.id
- ),
- });
- }
- }
- };
- /**
- * 复制
- */
- const onCopy = () => {
- const cells = graphRef.current?.getSelectedCells();
- if (cells?.[0]?.isNode()) {
- const cell = cells[0];
- const data = cell.data;
- setClipboardCache(data);
- }
- };
- /**
- * 粘贴
- */
- const onPaste = () => {
- if (clipboardCache) {
- const data = clipboardCache;
- // 表格
- if (data?.isTable) {
- const tableId = uuid();
- const newTable = {
- ...data,
- table: {
- ...data.table,
- id: tableId,
- style: {
- ...data.table.style,
- x: data.table.style.x + 20,
- y: data.table.style.y + 20,
- },
- },
- tableColumnList: data.tableColumnList.map((item: ColumnItem) => {
- return {
- ...item,
- id: uuid(),
- parentBusinessTableId: tableId,
- };
- }),
- };
- setProject((project) => ({
- ...project,
- tables: [...project.tables, newTable],
- }));
- }
- // 主题区域
- if (data?.isTopicArea) {
- const topicAreaId = uuid();
- const newTopicArea = {
- ...data,
- id: topicAreaId,
- style: {
- ...data.style,
- x: data.style.x + 20,
- y: data.style.y + 20,
- },
- };
- setProject((project) => ({
- ...project,
- topicAreas: [...project.topicAreas, newTopicArea],
- }));
- }
- // 注释节点
- if (data?.isRemark) {
- const remarkId = uuid();
- const newRemark = {
- ...data,
- id: remarkId,
- style: {
- ...data.style,
- x: data.style.x + 20,
- y: data.style.y + 20,
- },
- };
- setProject((project) => ({
- ...project,
- remarkInfos: [...project.remarkInfos, newRemark],
- }));
- }
- }
- };
- /**
- * 删除
- */
- const onDelete = () => {
- const cell = graphRef.current?.getSelectedCells();
- if (cell?.[0]?.isNode()) {
- const data = cell[0].data;
- if (data?.isTable) {
- setProject((project) => ({
- ...project,
- tables: project.tables.filter((item) => item.table.id !== cell[0].id),
- }));
- }
- if (data?.isTopicArea) {
- setProject((project) => ({
- ...project,
- topicAreas: project.topicAreas.filter(
- (item) => item.id !== cell[0].id
- ),
- }));
- }
- if (data?.isRemark) {
- setProject((project) => ({
- ...project,
- remarkInfos: project.remarkInfos.filter(
- (item) => item.id !== cell[0].id
- ),
- }));
- }
- }
- };
- /**
- * 演示模式
- */
- const enterPlayMode = () => {
- enterFullscreen();
- setPlayModeEnable(true);
- setTimeout(() => {
- graphRef.current?.centerContent();
- }, 100);
- };
- /**
- * 退出演示模式
- */
- const exitPlayMode = () => {
- exitFullscreen();
- graphRef.current?.enableKeyboard();
- setPlayModeEnable(false);
- };
- /**
- * 保存项目
- */
- const onSave = async () => {
- setProjectInfo((state) => {
- message.loading("保存中...", 0);
- graph?.toPNG(
- async (dataUri) => {
- const file = base64ToFile(
- dataUri,
- project?.id || "封面图",
- "image/png"
- );
- const formData = new FormData();
- formData.append("file", file);
- const res = await UploadFile(formData);
- await SaveDataModel({
- ...state,
- coverImage: res?.result?.[0]?.id,
- }).finally(() => {
- message.destroy();
- });
- setProjectInfo({
- ...state,
- coverImage: res?.result?.[0]?.id,
- })
- setSaveTime(dayjs().format("YYYY-MM-DD HH:mm:ss"));
- message.success("保存成功");
- },
- {
- width: 300,
- height: 150,
- quality: 0.2,
- copyStyles: true,
- }
- );
- return state;
- });
- };
- return {
- initGraph,
- graph,
- graphRef,
- project,
- setProject,
- addTable,
- updateTable,
- deleteTable,
- addTopicArea,
- updateTopicArea,
- deleteTopicArea,
- addRemark,
- updateRemark,
- deleteRemark,
- addRelation,
- updateRelation,
- deleteRelation,
- canRedo,
- canUndo,
- onRedo,
- onUndo,
- onClean,
- onCut,
- onCopy,
- onPaste,
- onDelete,
- enterPlayMode,
- playModeEnable,
- setPlayModeEnable,
- exitPlayMode,
- saveTime,
- onSave,
- tableActive,
- setTableActive
- };
- }
|