Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add group demo #434

Merged
merged 1 commit into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/basic/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const routes = [
{ path: '/dag', component: '@/pages/dag' },
{ path: '/diff', component: '@/pages/diff' },
{ path: '/flow', component: '@/pages/flow' },
{ path: '/group', component: '@/pages/group' },
];
15 changes: 9 additions & 6 deletions apps/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@
"dependencies": {
"@antv/xflow": "workspace:*",
"@antv/xflow-diff": "workspace:*",
"@antv/layout": "^0.3.2",
"@antv/x6-react-components": "^2.0.8",
"highlight.js": "^10.7.3",
"umi": "^4.0.64"
"umi": "^4.0.64",
"@ant-design/icons": "^5.2.6",
"classnames": "^2.3.2",
"lodash": "^4.17.15",
"@types/lodash": "4.14.186"
},
"devDependencies": {
"@ant-design/icons": "^5.2.6",
"@types/highlight.js": "^9.12.4",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"antd": "^5.0.0",
"classnames": "^2.3.2"
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0"
},
"peerDependencies": {
"antd": "^5.0.0",
Expand Down
3 changes: 3 additions & 0 deletions apps/basic/src/pages/group/ContextMenu/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.context-menu-wrapper {
position: absolute;
}
143 changes: 143 additions & 0 deletions apps/basic/src/pages/group/ContextMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* @description 节点的右键菜单
*/

import { Menu } from '@antv/x6-react-components';
import type { Cell } from '@antv/xflow';
// 不引入组件样式的话,节点右键菜单样式会有问题
import '@antv/x6-react-components/es/menu/style/index.css';

import { useGraphEvent } from '@antv/xflow';
import React, { useState, useMemo, useCallback } from 'react';
import ReactDOM from 'react-dom';

import { Utils } from '../utils';

import styles from './index.less';

// https://g6.antv.antgroup.com/manual/advanced/coordinate-system
// 获取到位置后将 dom 节点挂到 body 上定位,否则可能 dom 展示位位置和鼠标点击的位置有偏差
const contextMenuRoot = document.createElement('div');
document.body.appendChild(contextMenuRoot);

interface IProps {
// 重命名群组
renameCombineGroup: (
cell: Cell,
confirmPosition: { confirmX: number; confirmY: number },
) => void;
// 解散群组
removeCombineGroup: (
cell: Cell,
confirmPosition: { confirmX: number; confirmY: number },
) => void;
}
const ContextMenu = (props: IProps) => {
const { renameCombineGroup, removeCombineGroup } = props;
const [contextMenuPosition, setContextMenuPosition] = useState<{
contextMenuX: number;
contextMenuY: number;
} | null>(null);
// 当前操作的 cell
const [currentCell, setCurrnetCell] = useState<Cell | null>();

// 取消右键菜单
const cancelCellContextMenu = () => {
setCurrnetCell(null);
setContextMenuPosition(null);
};

useGraphEvent('cell:contextmenu', ({ e, x, y, cell, view }) => {
if (cell.isNode()) {
const nodeType = cell?.getData()?.originData?.conceptType;
// 除组合节点内部的普通子节点外,其它节点右键都会有操作
if (!(cell.getParent() && !Utils.isCombineGroup(nodeType))) {
setCurrnetCell(cell);
setContextMenuPosition({ contextMenuX: e.pageX, contextMenuY: e.pageY });
}
} else {
setCurrnetCell(cell);
setContextMenuPosition({ contextMenuX: e.pageX, contextMenuY: e.pageY });
}
});

// 取消右键菜单
useGraphEvent('blank:click', () => {
setCurrnetCell(null);
setContextMenuPosition(null);
});

useGraphEvent('cell:click', () => {
setCurrnetCell(null);
setContextMenuPosition(null);
});

// 解散组
const handleRemoveCombineGroup = useCallback(() => {
removeCombineGroup(currentCell as Cell, {
confirmX: contextMenuPosition?.contextMenuX || 0,
confirmY: contextMenuPosition?.contextMenuY || 0,
});
cancelCellContextMenu();
}, [currentCell, contextMenuPosition, removeCombineGroup]);

// 重命名组
const handleRenameCombineGroup = useCallback(() => {
renameCombineGroup(currentCell as Cell, {
confirmX: contextMenuPosition?.contextMenuX || 0,
confirmY: contextMenuPosition?.contextMenuY || 0,
});
cancelCellContextMenu();
}, [currentCell, contextMenuPosition, renameCombineGroup]);

// 获取右键菜单操作
const rightMenuItem = useMemo(() => {
let menuItem: {
key: string;
dom: React.ReactNode;
}[] = [];
if (
currentCell?.isNode() &&
Utils.isCombineGroup(currentCell?.getData()?.nodeType)
) {
menuItem = [
{
key: 'removeCombineGroup',
dom: (
<Menu.Item key="removeCombineGroup" onClick={handleRemoveCombineGroup}>
解除组
</Menu.Item>
),
},
{
key: 'renameCombineGroup',
dom: (
<Menu.Item key="renameCombineGroup" onClick={handleRenameCombineGroup}>
重命名组
</Menu.Item>
),
},
];
}
return menuItem;
}, [currentCell, handleRemoveCombineGroup, handleRenameCombineGroup]);

return ReactDOM.createPortal(
currentCell?.isNode() &&
Utils.isCombineGroup(currentCell?.getData()?.nodeType) &&
contextMenuPosition ? (
<div
className={styles['context-menu-wrapper']}
style={{
left: contextMenuPosition?.contextMenuX,
top: contextMenuPosition?.contextMenuY,
}}
>
<Menu>{rightMenuItem.map((item) => item.dom)}</Menu>
</div>
) : null,
contextMenuRoot,
);
};

export default ContextMenu;
70 changes: 70 additions & 0 deletions apps/basic/src/pages/group/GroupNode/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
.group-wrap {
.fold-group-wrap {
display: flex;
flex-direction: column;
align-items: center;
}

.group-name {
margin-top: 6px;
text-align: center;
cursor: pointer;
}

.group-name-fold {
width: 100px;
}

.fold-icon {
position: absolute;
top: 98%;
left: 50%;
width: 16px;
height: 16px;
background-color: rgb(245, 247, 251);
transform: translate(-50%, -50%);
cursor: pointer;
}

.fold-style {
position: relative;
width: 36px;
height: 36px;
border: 2px solid #227eff;
border-radius: 2px;
background-color: #fff;
}

.children-number {
position: absolute;
top: -30%;
left: 80%;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border: 1px solid #fff;
border-radius: 50%;
color: #227eff;
background-color: #dde7f9;
}

.unfold-style {
position: relative;
padding: 12px;
border: 1px solid rgba(#227eff, 0.4);
border-radius: 8px;
background-color: rgba(#227eff, 0.08);

.unfold-icon {
position: absolute;
top: 98%;
left: 50%;
width: 16px;
height: 16px;
background-color: rgb(245, 247, 251);
transform: translate(-50%, -50%);
cursor: pointer;
}
}
}
62 changes: 62 additions & 0 deletions apps/basic/src/pages/group/GroupNode/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
import type { Node } from '@antv/xflow';
import cx from 'classnames';

import type { INodeData } from '../type';

import styles from './index.less';

const GroupNode = ({ node }: { node: Node }) => {
const { width, height } = node.getBBox();
const data: INodeData = node.getData();
const isFold = node.getProp('isFold');
const { label } = data || {};
const descendantsNumber = node.getProp('descendantsNumber') || 0;

const renderNodes = () => {
const foldElment = (
<div className={styles['fold-group-wrap']}>
<div className={styles['fold-style']}>
<div className={styles['children-number']}>{descendantsNumber}</div>
{/* event 自定义事件 */}
<div
className={styles['fold-icon']}
event="group:node:collapse"
onClick={(e) => e.stopPropagation()}
>
<PlusCircleOutlined />
</div>
</div>
<div className={cx(styles['group-name'], [styles['group-name-fold']])}>
{label}
</div>
</div>
);

const unfoldElment = (
<>
<div
className={styles['unfold-style']}
style={{ height: height < 18 ? height : height - 18 }}
>
<div
className={styles['unfold-icon']}
// event 自定义事件
event="group:node:collapse"
onClick={(e) => e.stopPropagation()}
>
<MinusCircleOutlined />
</div>
</div>
<div className={styles['group-name']}>{label}</div>
</>
);
return (
<div className={styles['group-wrap']}>{isFold ? foldElment : unfoldElment}</div>
);
};

return <div style={{ width, height }}>{renderNodes()}</div>;
};

export default GroupNode;
21 changes: 21 additions & 0 deletions apps/basic/src/pages/group/NormalNode/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.graph-normal-node {
height: 100%;

.node-wrap {
display: flex;
flex-direction: column;
align-items: center;
}

.node-styl {
position: relative;
background-color: #227eff;
border-radius: 2px;
}

.node-text {
width: 100px;
margin-top: 6px;
text-align: center;
}
}
21 changes: 21 additions & 0 deletions apps/basic/src/pages/group/NormalNode/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Node } from '@antv/xflow';

import type { INodeData } from '../type';

import styles from './index.less';

const NormalNode = ({ node }: { node: Node }) => {
const data: INodeData = node.getData();
const { width, height } = node.getBBox();

return (
<div className={styles['graph-normal-node']}>
<div className={styles['node-wrap']}>
<div className={styles['node-styl']} style={{ width, height }} />
<div className={styles['node-text']}>{data?.label}</div>
</div>
</div>
);
};

export default NormalNode;
14 changes: 14 additions & 0 deletions apps/basic/src/pages/group/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 群组节点默认的 size:调整大小或影响 Ports 的位置
export const defaultGroupSize = {
width: 36,
height: 36,
};

// 群组节点的 padding
export const groupNodePadding = 24;

// 非群组节点的 size:调整大小或影响 Ports 的位置
export const notGroupNodeSize = {
width: 36,
height: 36,
};
25 changes: 25 additions & 0 deletions apps/basic/src/pages/group/dagreLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @description @antv/layout DagreLayout
*/
import { DagreLayout } from '@antv/layout';
import * as _ from 'lodash';

import type { IGraph } from './type';

export const layoutDagre = (graphData: IGraph, options?: Record<string, any>) => {
const antvDagreLayout = new DagreLayout({
type: 'dagre',
nodesep: 50,
// 布局方向和文档说明不一样:https://g6.antv.antgroup.com/manual/middle/layout/graph-layout#dagre
// LR 是从左到有布局,但是配置之后变成从上到下的布局展示
// TB 是从上到下布局,但是配置之后变成从左到右布局
rankdir: 'TB',
begin: options?.begin || [250, 250],
});
antvDagreLayout.layout(graphData);

return {
edges: graphData.edges,
nodes: graphData.nodes,
};
};
Loading