mirror of
https://gitee.com/antv/g6.git
synced 2024-11-30 10:48:24 +08:00
502 lines
15 KiB
TypeScript
502 lines
15 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useTranslation } from 'react-i18next';
|
||
import {
|
||
EyeOutlined,
|
||
EyeInvisibleOutlined,
|
||
NodeIndexOutlined,
|
||
DisconnectOutlined,
|
||
TagOutlined,
|
||
SearchOutlined,
|
||
HighlightOutlined
|
||
} from '@ant-design/icons';
|
||
|
||
const isBrowser = typeof window !== 'undefined';
|
||
const G6 = isBrowser ? require('../../dist/g6.min.js') : null;
|
||
const insertCss = isBrowser ? require('insert-css') : null;
|
||
const modifyCSS = isBrowser ? require('@antv/dom-util/lib/modify-css').default : null;
|
||
|
||
if (isBrowser) {
|
||
insertCss(`
|
||
#canvas-menu {
|
||
position: absolute;
|
||
z-index: 2;
|
||
left: 16px;
|
||
top: 80px;
|
||
width: fit-content;
|
||
padding: 8px 16px;
|
||
background-color: rgba(54, 59, 64, 0);
|
||
border-radius: 24px;
|
||
box-shadow: 0 5px 18px 0 rgba(0, 0, 0, 0);
|
||
font-family: PingFangSC-Semibold;
|
||
transition: all 0.2s linear;
|
||
}
|
||
#canvas-menu:hover {
|
||
background-color: rgba(54, 59, 64, 1);
|
||
box-shadow: 0 5px 18px 0 rgba(0, 0, 0, 0.6);
|
||
}
|
||
.icon-span {
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
cursor: pointer;
|
||
}
|
||
#search-node-input {
|
||
background-color: rgba(60, 60, 60, 0.95);
|
||
border-radius: 21px;
|
||
width: 100px;
|
||
border-color: rgba(80, 80, 80, 0.95);
|
||
border-style: solid;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
}
|
||
#submit-button {
|
||
background-color: rgba(82, 115, 224, 0.2);
|
||
border-radius: 21px;
|
||
border-color: rgb(82, 115, 224);
|
||
border-style: solid;
|
||
color: rgba(152, 165, 254, 1);
|
||
margin-left: 4px;
|
||
}
|
||
.menu-tip {
|
||
position: absolute;
|
||
right: calc(30% + 32px);
|
||
width: fit-content;
|
||
height: 40px;
|
||
line-height: 40px;
|
||
top: 80px;
|
||
padding-left: 16px;
|
||
padding-right: 16px;
|
||
background-color: rgba(54, 59, 64, 0.5);
|
||
color: rgba(255, 255, 255, 0.65);
|
||
border-radius: 8px;
|
||
transition: all 0.2s linear;
|
||
font-family: PingFangSC-Semibold;
|
||
}
|
||
#g6-canavs-menu-item-tip {
|
||
position: absolute;
|
||
background-color: rgba(0,0,0, 0.65);
|
||
padding: 10px;
|
||
box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 10px;
|
||
width: fit-content;
|
||
color: #fff;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
height: fit-content;
|
||
font-family: PingFangSC-Semibold;
|
||
transition: all 0.2s linear;
|
||
}
|
||
`);
|
||
}
|
||
|
||
let fishEye = null;
|
||
|
||
const CanvasMenu: React.FC<{
|
||
graph: any;
|
||
clickFisheyeIcon: (onlyDisable?: boolean) => void;
|
||
clickLassoIcon: (onlyDisable?: boolean) => void;
|
||
fisheyeEnabled: boolean;
|
||
lassoEnabled: boolean;
|
||
edgeLabelVisible: boolean;
|
||
setEdgeLabelVisible: (vis: boolean) => void;
|
||
searchNode: (id: string) => boolean;
|
||
handleFindPath: () => void;
|
||
stopLayout: () => void;
|
||
}> = ({
|
||
graph,
|
||
clickFisheyeIcon,
|
||
clickLassoIcon,
|
||
fisheyeEnabled,
|
||
lassoEnabled,
|
||
stopLayout,
|
||
edgeLabelVisible,
|
||
setEdgeLabelVisible,
|
||
searchNode,
|
||
handleFindPath
|
||
}) => {
|
||
const { t } = useTranslation();
|
||
|
||
// menu tip, 例如 “按下 ESC 退出鱼眼”
|
||
const [menuTip, setMenuTip] = useState({
|
||
text: '',
|
||
display: 'none',
|
||
opacity: 0
|
||
});
|
||
|
||
// menu item tip
|
||
const [menuItemTip, setMenuItemTip] = useState({
|
||
text: '',
|
||
display: 'none',
|
||
opacity: 0
|
||
});
|
||
|
||
const [enableSearch, setEnableSearch] = useState(false);
|
||
|
||
const [enableSelectPathEnd, setEnableSelectPathEnd] = useState(false);
|
||
|
||
const clickEdgeLabelController = () => {
|
||
setEdgeLabelVisible(!edgeLabelVisible);
|
||
}
|
||
|
||
const handleEnableSearch = () => {
|
||
// 关闭 lasso 框选
|
||
if (lassoEnabled) clickLassoIcon(true)
|
||
// 关闭选择路径端点
|
||
if (enableSelectPathEnd) setEnableSelectPathEnd(false);
|
||
// 关闭搜索节点框
|
||
if (enableSearch) setEnableSearch(false);
|
||
// 关闭 fisheye
|
||
if (fisheyeEnabled && fishEye) {
|
||
graph.removePlugin(fishEye);
|
||
clickFisheyeIcon(true);
|
||
}
|
||
setEnableSearch(old => {
|
||
if (old) {
|
||
// 设置 menuTip
|
||
setMenuTip({
|
||
text: '',
|
||
display: 'none',
|
||
opacity: 0
|
||
});
|
||
return false;
|
||
}
|
||
|
||
// 设置 menuTip
|
||
setMenuTip({
|
||
text: t('输入需要搜索的节点 ID,并点击 Submit 按钮'),
|
||
display: 'block',
|
||
opacity: 1
|
||
});
|
||
return true;
|
||
});
|
||
}
|
||
// 放大
|
||
const handleZoomOut = () => {
|
||
if (!graph || graph.destroyed) return;
|
||
const current = graph.getZoom();
|
||
const canvas = graph.get('canvas');
|
||
const point = canvas.getPointByClient(canvas.get('width') / 2, canvas.get('height') / 2);
|
||
const pixelRatio = canvas.get('pixelRatio') || 1;
|
||
const ratio = 1 + 0.05 * 5;
|
||
if (ratio * current > 5) {
|
||
return;
|
||
}
|
||
graph.zoom(ratio, { x: point.x / pixelRatio, y: point.y / pixelRatio });
|
||
};
|
||
|
||
// 缩小
|
||
const handleZoomIn = () => {
|
||
if (!graph || graph.destroyed) return;
|
||
|
||
const current = graph.getZoom();
|
||
const canvas = graph.get('canvas');
|
||
const point = canvas.getPointByClient(canvas.get('width') / 2, canvas.get('height') / 2);
|
||
const pixelRatio = canvas.get('pixelRatio') || 1;
|
||
const ratio = 1 - 0.05 * 5;
|
||
if (ratio * current < 0.3) {
|
||
return;
|
||
}
|
||
graph.zoom(ratio, { x: point.x / pixelRatio, y: point.y / pixelRatio });
|
||
};
|
||
|
||
const handleFitViw = () => {
|
||
if (!graph || graph.destroyed) return;
|
||
graph.fitView(16);
|
||
}
|
||
|
||
const handleSearchNode = () => {
|
||
const value = (document.getElementById('search-node-input') as HTMLInputElement).value;
|
||
const found = searchNode(value);
|
||
if (!found) setMenuTip({
|
||
text: t('没有找到该节点'),
|
||
display: 'block',
|
||
opacity: 1
|
||
});
|
||
}
|
||
|
||
const showItemTip = (e, text) => {
|
||
const { clientX: x, clientY: y } = e;
|
||
setMenuItemTip({
|
||
text,
|
||
display: 'block',
|
||
opacity: 1
|
||
});
|
||
const tipDom = document.getElementById('g6-canavs-menu-item-tip')
|
||
modifyCSS(tipDom, {
|
||
top: `${124}px`,
|
||
left: `${x - 20}px`,
|
||
zIndex: 100,
|
||
});
|
||
};
|
||
|
||
const hideItemTip = () => {
|
||
setMenuItemTip({
|
||
text: '',
|
||
display: 'none',
|
||
opacity: 0
|
||
});
|
||
const tipDom = document.getElementById('g6-canavs-menu-item-tip')
|
||
modifyCSS(tipDom, {
|
||
zIndex: -100,
|
||
});
|
||
}
|
||
/**
|
||
* 打开或关闭鱼眼放大功能
|
||
*/
|
||
const toggleFishEye = () => {
|
||
|
||
if (!graph || graph.destroyed) return;
|
||
|
||
// 设置鼠标样式为默认
|
||
graph.get('canvas').setCursor('default');
|
||
|
||
// 关闭 FishEye
|
||
if (fisheyeEnabled && fishEye) {
|
||
graph.removePlugin(fishEye);
|
||
graph.setMode('default')
|
||
|
||
// 设置 menuTip
|
||
setMenuTip({
|
||
text: t('按下 Esc 键退出鱼眼放大镜'),
|
||
display: 'none',
|
||
opacity: 0
|
||
});
|
||
} else {
|
||
// 停止布局
|
||
stopLayout();
|
||
|
||
// 关闭 lasso 框选
|
||
if (lassoEnabled) clickLassoIcon(true)
|
||
// 关闭选择路径端点
|
||
if (enableSelectPathEnd) setEnableSelectPathEnd(false);
|
||
// 关闭搜索节点框
|
||
if (enableSearch) setEnableSearch(false);
|
||
|
||
// 设置 menuTip
|
||
setMenuTip({
|
||
text: t('按下 Esc 键退出鱼眼放大镜'),
|
||
display: 'block',
|
||
opacity: 1
|
||
});
|
||
|
||
// 将交互模式切换到鱼眼只读模式,即不能缩放画布、框选节点、拉索选择节点等可能和鱼眼有冲突的操作
|
||
graph.setMode('fisheyeMode');
|
||
// 开启 FishEye
|
||
fishEye = new G6.Fisheye({
|
||
r: 249,
|
||
scaleRByWheel: true,
|
||
minR: 100,
|
||
maxR: 500
|
||
// showLabel: true,
|
||
});
|
||
|
||
graph.addPlugin(fishEye);
|
||
}
|
||
clickFisheyeIcon();
|
||
};
|
||
|
||
// 开启 lasso select 功能
|
||
const enabledLassoSelect = () => {
|
||
if (!graph || graph.destroyed) return;
|
||
clickLassoIcon();
|
||
if (!lassoEnabled) {
|
||
graph.setMode('lassoSelect');
|
||
|
||
// 设置鼠标样式为十字形
|
||
graph.get('canvas').setCursor('crosshair');
|
||
|
||
// 关闭 fisheye
|
||
if (fisheyeEnabled && fishEye) {
|
||
graph.removePlugin(fishEye);
|
||
clickFisheyeIcon(true);
|
||
}
|
||
// 关闭选择路径端点
|
||
if (enableSelectPathEnd) setEnableSelectPathEnd(false);
|
||
// 关闭搜索节点框
|
||
if (enableSearch) setEnableSearch(false);
|
||
|
||
// 设置 menuTip
|
||
setMenuTip({
|
||
text: t('按下 Esc 键退出拉索选择模式'),
|
||
display: 'block',
|
||
opacity: 1
|
||
});
|
||
|
||
} else {
|
||
graph.setMode('default');
|
||
|
||
// 设置鼠标样式为默认
|
||
graph.get('canvas').setCursor('default');
|
||
|
||
// 设置 menuTip
|
||
setMenuTip({
|
||
text: t('按下 Esc 键退出拉索选择模式'),
|
||
display: 'none',
|
||
opacity: 0
|
||
});
|
||
}
|
||
}
|
||
|
||
// 开启选择两个节点显示最短路径
|
||
const handleEnableSelectPathEnd = () => {
|
||
setEnableSelectPathEnd(old => {
|
||
if (!old) {
|
||
graph.setMode('default');
|
||
// 关闭 fisheye
|
||
if (fishEye) {
|
||
graph.removePlugin(fishEye);
|
||
clickFisheyeIcon(true);
|
||
}
|
||
// 关闭 lasso 框选
|
||
if (lassoEnabled) clickLassoIcon(true)
|
||
|
||
// 关闭搜索节点框
|
||
if (enableSearch) setEnableSearch(false);
|
||
|
||
// 设置 menuTip
|
||
setMenuTip({
|
||
text: t('按住 SHIFT 键并点选两个节点作为路径起终点'),
|
||
display: 'block',
|
||
opacity: 1
|
||
});
|
||
return true;
|
||
}
|
||
// 设置 menuTip
|
||
setMenuTip({
|
||
text: '',
|
||
display: 'none',
|
||
opacity: 0
|
||
});
|
||
return false;
|
||
})
|
||
}
|
||
|
||
const escListener = e => {
|
||
if (!graph || graph.destroyed) return;
|
||
if (e.key !== 'Escape') return;
|
||
if (fishEye) {
|
||
graph.removePlugin(fishEye);
|
||
clickFisheyeIcon(true);
|
||
}
|
||
// 关闭 lasso 框选
|
||
graph.setMode('default');
|
||
// 关闭搜索节点框
|
||
setEnableSearch(false);
|
||
// 关闭选择路径端点
|
||
setEnableSelectPathEnd(false);
|
||
|
||
// 设置鼠标样式为默认
|
||
graph.get('canvas').setCursor('default');
|
||
clickLassoIcon(true);
|
||
|
||
// 设置 menuTip
|
||
setMenuTip({
|
||
text: t('按下 Esc 键退出当前模式'),
|
||
display: 'none',
|
||
opacity: 0
|
||
});
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (typeof window !== 'undefined') {
|
||
window.addEventListener("keydown", escListener.bind(this));
|
||
return window.removeEventListener("keydown", escListener.bind(this));
|
||
}
|
||
}, []);
|
||
|
||
const iconStyle = {
|
||
disable: { color: 'rgba(255, 255, 255, 0.85)' },
|
||
enable: { color: 'rgba(82, 115, 224, 1)' },
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<div id='canvas-menu'>
|
||
<div className='icon-container'>
|
||
<span className='icon-span'
|
||
style={{ backgroundColor: 'rgba(0, 0, 0, 0)' }}
|
||
onClick={clickEdgeLabelController}
|
||
onMouseEnter={e => showItemTip(e, edgeLabelVisible ? t('隐藏边标签') : t('显示边标签'))}
|
||
onMouseLeave={hideItemTip}
|
||
>
|
||
{edgeLabelVisible ? <DisconnectOutlined style={iconStyle.enable} /> : <TagOutlined style={iconStyle.disable} />}
|
||
</span>
|
||
<span className='icon-span'
|
||
onClick={toggleFishEye}
|
||
onMouseEnter={e => showItemTip(e, fisheyeEnabled ? t('关闭鱼眼放大镜') : t('打开鱼眼放大镜'))}
|
||
onMouseLeave={hideItemTip}
|
||
>
|
||
{fisheyeEnabled ? <EyeInvisibleOutlined style={iconStyle.enable} /> : <EyeOutlined style={iconStyle.disable} />}
|
||
</span>
|
||
<span className='icon-span'
|
||
onClick={enabledLassoSelect}
|
||
onMouseEnter={e => showItemTip(e, lassoEnabled ? t('关闭拉索选择模式') : t('打开拉索选择模式'))}
|
||
onMouseLeave={hideItemTip}
|
||
>
|
||
<HighlightOutlined style={lassoEnabled ? iconStyle.enable : iconStyle.disable} />
|
||
</span>
|
||
<span className='icon-span'
|
||
onClick={handleEnableSelectPathEnd}
|
||
onMouseEnter={e => showItemTip(e, t('搜索最短路径'))}
|
||
onMouseLeave={hideItemTip}
|
||
>
|
||
<NodeIndexOutlined style={enableSelectPathEnd ? iconStyle.enable : iconStyle.disable} />
|
||
</span>
|
||
<span className='icon-span' style={{ width: 'fit-content', ...iconStyle.disable }} >
|
||
<span className='zoom-icon'
|
||
onClick={handleZoomIn}
|
||
onMouseEnter={e => showItemTip(e, t('缩小'))}
|
||
onMouseLeave={hideItemTip}
|
||
>
|
||
-
|
||
</span>
|
||
<span className='zoom-icon'
|
||
onClick={handleFitViw}
|
||
onMouseEnter={e => showItemTip(e, t('图内容适配容器,快捷键:ctrl + 1'))}
|
||
onMouseLeave={hideItemTip}
|
||
style={{ paddingLeft: '8px', paddingRight: '8px' }}
|
||
>
|
||
FIT
|
||
</span>
|
||
<span className='zoom-icon'
|
||
onClick={handleZoomOut}
|
||
onMouseEnter={e => showItemTip(e, t('放大'))}
|
||
onMouseLeave={hideItemTip}
|
||
>
|
||
+
|
||
</span>
|
||
</span>
|
||
<span className='icon-span'
|
||
onClick={handleEnableSearch}
|
||
onMouseEnter={e => showItemTip(e, t('输入 ID 搜索节点'))}
|
||
onMouseLeave={hideItemTip}
|
||
>
|
||
<SearchOutlined style={enableSearch ? iconStyle.enable : iconStyle.disable} />
|
||
</span>
|
||
{enableSearch &&
|
||
<span
|
||
onMouseEnter={e => showItemTip(e, t('输入需要搜索的节点 ID,并点击 Submit 按钮'))}
|
||
onMouseLeave={hideItemTip}
|
||
>
|
||
<input type="text" id="search-node-input" />
|
||
<button id="submit-button" onClick={handleSearchNode}>Submit</button>
|
||
</span>
|
||
}
|
||
{enableSelectPathEnd &&
|
||
<span
|
||
onMouseEnter={e => showItemTip(e, t('选择有且仅有两个节点作为端点,并点击 Find Path 按钮'))}
|
||
onMouseLeave={hideItemTip}
|
||
>
|
||
<button id="submit-button" onClick={handleFindPath}>Find Path</button>
|
||
</span>}
|
||
</div>
|
||
</div>
|
||
<div className='menu-tip' style={{ opacity: menuTip.opacity }}>
|
||
{menuTip.text}
|
||
</div>
|
||
<div id='g6-canavs-menu-item-tip' style={{ opacity: menuItemTip.opacity }}>
|
||
{menuItemTip.text}
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default CanvasMenu;
|