feat: 拓扑编辑demo

This commit is contained in:
TsMask
2023-12-26 10:10:59 +08:00
parent b54027bf0f
commit 3aea520289

View File

@@ -4,9 +4,9 @@ import { PageContainer } from 'antdv-pro-layout';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { listNe, stateNe } from '@/api/ne/ne';
import message from 'ant-design-vue/lib/message';
import { parseDateToStr } from '@/utils/date-utils';
import { Graph, Util } from '@antv/g6';
import { Graph, Menu, Tooltip, Util, registerBehavior } from '@antv/g6';
import { message, Modal, Form, notification } from 'ant-design-vue/lib';
const { t } = useI18n();
/**图DOM节点实例对象 */
@@ -15,6 +15,12 @@ const graphG6Dom = ref<HTMLElement | undefined>(undefined);
/**图实例对象 */
const graphG6 = ref<any>(null);
/**图实例状态 */
const graphG6State = reactive<Record<string, any>>({
mode: 'default',
editEdge: {},
});
/**图数据 */
const graphG6Data = reactive<Record<string, any>>({
nodes: [
@@ -309,18 +315,18 @@ function graphEvent(graph: Graph) {
// 该边的结束点
const target = edge.getTarget();
// 先将边提前,再将端点提前。这样该边两个端点还是在该边上层,较符合常规。
edge.toFront();
source.toFront();
target.toFront();
// edge.toFront();
// source.toFront();
// target.toFront();
});
graph.on('edge:mouseleave', (ev: any) => {
// 获得图上所有边实例
const edges = graph.getEdges();
// 遍历边,将所有边的层级放置在后方,以恢复原样
edges.forEach(edge => {
edge.toBack();
});
// edges.forEach(edge => {
// edge.toBack();
// });
});
graph.on('node:mouseenter', (ev: any) => {
@@ -329,36 +335,122 @@ function graphEvent(graph: Graph) {
// 获取该节点的所有相关边
const edges = node.getEdges();
// 遍历相关边,将所有相关边提前,再将相关边的两个端点提前,以保证相关边的端点在边的上方常规效果
edges.forEach((edge: any) => {
edge.toFront();
edge.getSource().toFront();
edge.getTarget().toFront();
});
// edges.forEach((edge: any) => {
// edge.toFront();
// edge.getSource().toFront();
// edge.getTarget().toFront();
// });
});
graph.on('node:mouseleave', (ev: any) => {
// 获得图上所有边实例
const edges = graph.getEdges();
// 遍历边,将所有边的层级放置在后方,以恢复原样
edges.forEach(edge => {
edge.toBack();
});
});
// 使用内置交互 create-edge创建边之后触发
graph.on('aftercreateedge', e => {
console.log(JSON.parse(JSON.stringify(graph.save())));
const edges = graph.save().edges as any;
// Util.processParallelEdges(edges);
graph.getEdges().forEach((edge, i) => {
graph.updateItem(edge, {
curveOffset: edges[i].curveOffset,
curvePosition: edges[i].curvePosition,
});
});
// edges.forEach(edge => {
// edge.toBack();
// });
});
}
/**图节点右击菜单 */
const graphNodeMenu = new Menu({
offsetX: 6,
offseY: 10,
itemTypes: ['node'],
getContent(e) {
const outDiv = document.createElement('div');
outDiv.style.width = '180px';
outDiv.innerHTML = `<ul>
<li>测试01</li>
<li>测试01</li>
<li>测试01</li>
<li>测试01</li>
<li>测试01</li>
</ul>`;
return outDiv;
},
handleMenuClick(target, item) {
console.log(target, item);
},
});
/**图节点展示 */
const graphNodeTooltip = new Tooltip({
offsetX: 10,
offsetY: 20,
getContent(e: any) {
const outDiv = document.createElement('div');
outDiv.style.width = '180px';
outDiv.innerHTML = `
<h4>自定义tooltip</h4>
<ul>
<li>Label: ${e.item.getModel().label || e.item.getModel().id}</li>
</ul>`;
return outDiv;
},
itemTypes: ['node'],
});
/**图边右击菜单 */
const graphEdgeMenu = new Menu({
offsetX: 6,
offseY: 10,
itemTypes: ['edge'],
getContent(evt) {
return `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
background: #e6f7ff;
"
>
<div id="edit" style="cursor: pointer; margin-bottom: 2px">
1. 编辑
</div>
<div id="hide" style="cursor: pointer; margin-bottom: 2px">
2. 隐藏
</div>
</div>
`;
},
handleMenuClick(target, item) {
console.log(target, item);
const targetId = target.id;
switch (targetId) {
case 'edit':
const info = item.getModel();
console.log(Object.assign({}, info));
modalState.title = '边信息编辑';
modalState.formEdge = Object.assign(modalState.formEdge, info);
modalState.visibleByEdge = true;
break;
case 'hide':
graphG6.value.hideItem(item);
break;
}
},
});
/**图边展示 */
const graphEdgeTooltip = new Tooltip({
offsetX: 10,
offsetY: 20,
getContent(e: any) {
const outDiv = document.createElement('div');
outDiv.style.width = '180px';
outDiv.innerHTML = `
<h4>graphEdgeTooltip</h4>
<ul>
<li>Label: ${e.item.getModel().label || e.item.getModel().id}</li>
</ul>`;
return outDiv;
},
itemTypes: ['edge'],
});
/**查询全部网元数据列表 */
function fnRanderGraph() {
if (!graphG6Dom.value) return;
@@ -390,16 +482,20 @@ function fnRanderGraph() {
'drag-canvas',
'zoom-canvas',
'collapse-expand-combo',
{
type: 'create-edge',
trigger: 'click', // 'click' by default. options: 'drag', 'click'
},
],
edit: [
{
type: 'create-edge',
trigger: 'click', // 'click' by default. options: 'drag', 'click'
type: 'click-select',
selectEdge: true,
},
{
type: 'drag-node',
onlyChangeComboSize: true,
shouldEnd: (e: any) => {
return true;
},
},
{ type: 'create-edge', key: 'alt' },
],
},
groupByTypes: false,
@@ -426,6 +522,9 @@ function fnRanderGraph() {
lineWidth: 1,
},
},
// defaultEdge: {
// type: 'line',
// },
// 全局框节点 矩形
defaultCombo: {
type: 'rect', // Combo 类型
@@ -434,10 +533,12 @@ function fnRanderGraph() {
fillOpacity: 0.1,
},
},
plugins: [graphNodeMenu, graphNodeTooltip, graphEdgeMenu, graphEdgeTooltip],
});
graph.data(graphG6Data);
graph.render();
// 图绑定事件
graphEvent(graph);
graphG6.value = graph;
@@ -568,6 +669,185 @@ onMounted(() => {
// fnGetList();
fnRanderGraph();
});
/**改变图模式 */
function fnChangeMode(value: any) {
console.log(value, JSON.parse(JSON.stringify(graphG6.value.save())));
graphG6.value.setMode(value);
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**图边框是否显示 */
visibleByEdge: boolean;
/**标题 */
title: string;
/**表单数据 */
form: Record<string, any>;
/**图边表单数据 */
formEdge: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByEdge: false,
title: '图信息',
form: {
id: '',
msisdn: '',
},
formEdge: {
id: '',
source: '',
target: '',
type: '',
style: {},
label: '',
labelCfg: {
refX: 0,
refY: 0,
position: 'middle',
autoRotate: false,
style: {},
},
},
confirmLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateForm = Form.useForm(
modalState.form,
reactive({
imsi: [{ required: true, message: 'IMSI' + t('common.unableNull') }],
msisdn: [{ required: true, message: 'MSISDN' + t('common.unableNull') }],
staticIp: [
{ required: true, message: 'static ip' + t('common.unableNull') },
],
smData: [
{
required: true,
message: 'Subscribed SM Data' + t('common.unableNull'),
},
],
})
);
/**对话框内表单属性和校验规则 */
const modalStateFormEdge = Form.useForm(
modalState.formEdge,
reactive({
id: [{ required: true, message: '边唯一 ID' }],
source: [{ required: true, message: '起始点 id' }],
target: [{ required: true, message: '结束点 id' }],
type: [{ required: true, message: 'line' }],
})
);
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByEdge = false;
modalStateForm.resetFields();
modalStateFormEdge.resetFields();
}
/**内置边类型 */
const edgeType = [
{
type: 'line',
description: '连接两个节点的直线',
},
{
type: 'polyline',
description: '多段线段构成的折线,连接两个端点',
},
{
type: 'arc',
description: '连接两个节点的一段圆弧',
},
{
type: 'quadratic',
description: '只有一个控制点的曲线',
},
{
type: 'cubic',
description: '有两个控制点的曲线',
},
{
type: 'cubic-vertical',
description: '垂直方向的三阶贝塞尔曲线',
},
{
type: 'cubic-horizontal',
description: '水平方向的三阶贝塞尔曲线',
},
{
type: 'loop',
description: '自环',
},
];
/**边新增 */
function fnModalOkEdge() {
const model = {
id: '2-190',
source: '2',
target: '190',
type: 'line',
style: {
stroke: 'steelblue',
lineWidth: 5,
},
label: '边新增',
labelCfg: {
position: 'end',
refY: 20,
},
};
graphG6.value.addItem('edge', model);
console.log(JSON.parse(JSON.stringify(graphG6.value.save())));
}
/**边新增 */
function fnAddEdge() {
const model = {
id: '2-190',
source: '2',
target: '190',
type: 'line',
style: {
stroke: 'steelblue',
lineWidth: 5,
},
label: '边新增',
labelCfg: {
position: 'end',
refY: 20,
},
};
graphG6.value.addItem('edge', model);
console.log(JSON.parse(JSON.stringify(graphG6.value.save())));
}
/**保存图数据 */
function fnGraphSave() {
sessionStorage.setItem('graph', JSON.stringify(graphG6.value.save()));
console.log(JSON.parse(JSON.stringify(graphG6.value.save())));
}
/**保存图数据 */
function fnGraphLoad() {
const data = sessionStorage.getItem('graph') || '{}';
graphG6.value.read(JSON.parse(data));
console.log(JSON.parse(JSON.stringify(graphG6.value.save())));
}
</script>
<template>
@@ -580,56 +860,37 @@ onMounted(() => {
<!-- 插槽-卡片左侧侧 -->
<template #title>
<div class="button-container" style="margin-bottom: -12px">
<a-select size="small" value="view">
<a-select-option value="view" key="view"> 查看 </a-select-option>
<a-select
size="small"
v-model:value="graphG6State.mode"
@change="fnChangeMode"
>
<a-select-option value="default" key="default">
查看
</a-select-option>
<a-select-option value="edit" key="edit"> 编辑 </a-select-option>
</a-select>
<a-button type="primary">
<a-button type="primary" @click="fnAddEdge">
<template #icon>
<PlusOutlined />
</template>
{{ t('common.addText') }}
fnAddEdge
</a-button>
<a-button type="primary" danger ghost>
<a-button type="primary" ghost @click="fnGraphLoad">
<template #icon>
<DeleteOutlined />
</template>
{{ t('views.neUser.auth.batchDelText') }}
fnGraphLoad
</a-button>
<a-popconfirm
:title="t('views.neUser.sub.loadDataConfirm')"
:ok-text="t('common.ok')"
:cancel-text="t('common.cancel')"
>
<a-button type="dashed" danger>
<template #icon>
<SyncOutlined />
</template>
{{ t('views.neUser.sub.loadData') }}
</a-button>
</a-popconfirm>
<a-button type="dashed">
<a-button type="dashed" @click="fnGraphSave">
<template #icon>
<ImportOutlined />
</template>
{{ t('views.neUser.sub.import') }}
fnGraphSave
</a-button>
<a-popconfirm
:title="t('views.neUser.sub.exportConfirm')"
ok-text="TXT"
ok-type="default"
>
<a-button type="dashed">
<template #icon>
<ExportOutlined />
</template>
{{ t('views.neUser.sub.export') }}
</a-button>
</a-popconfirm>
</div>
</template>
<!-- 插槽-卡片右侧 -->
@@ -644,6 +905,162 @@ onMounted(() => {
<div ref="graphG6Dom" class="chart"></div>
</a-card>
<!-- 批量增加框 -->
<DraggableModal
width="800px"
:body-style="{ maxHeight: '650px', 'overflow-y': 'auto' }"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdge"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOkEdge"
@cancel="fnModalCancel"
>
<a-form
name="modalStateFromEdge"
layout="horizontal"
:label-col="{ span: 6 }"
:labelWrap="true"
>
<a-form-item
label="type"
name="type"
:label-col="{ span: 3 }"
v-bind="modalStateFormEdge.validateInfos.type"
>
<a-select v-model:value="modalState.formEdge.type">
<a-select-option
v-for="opt in edgeType"
:key="opt.type"
:value="opt.type"
>
{{ opt.type }} : {{ opt.description }}
</a-select-option>
</a-select>
</a-form-item>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="source"
name="source"
v-bind="modalStateFormEdge.validateInfos.source"
>
<a-input v-model:value="modalState.formEdge.source" allow-clear>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> 起始点 id </template>
<InfoCircleOutlined style="color: rgba(0, 0, 0, 0.45)" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="target"
name="target"
v-bind="modalStateFormEdge.validateInfos.target"
>
<a-input v-model:value="modalState.formEdge.target" allow-clear>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> 结束点 id </template>
<InfoCircleOutlined style="color: rgba(0, 0, 0, 0.45)" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="label" name="label">
<a-input v-model:value="modalState.formEdge.label" allow-clear>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> 文本文字如果没有则不会显示 </template>
<InfoCircleOutlined style="color: rgba(0, 0, 0, 0.45)" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="labelCfg" name="labelCfg">
{{ modalState.form.labelCfg }}
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="labelCfg.refX" name="labelCfg.refX">
<a-input
v-model:value="modalState.formEdge.labelCfg.refX"
allow-clear
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> 标签在 x 方向的偏移量 </template>
<InfoCircleOutlined style="color: rgba(0, 0, 0, 0.45)" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="labelCfg.refY" name="labelCfg.refY">
<a-input
v-model:value="modalState.formEdge.labelCfg.refY"
allow-clear
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> 标签在 x 方向的偏移量 </template>
<InfoCircleOutlined style="color: rgba(0, 0, 0, 0.45)" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="labelCfg.refX" name="labelCfg.refX">
<a-input
v-model:value="modalState.formEdge.labelCfg.refX"
allow-clear
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> 标签在 x 方向的偏移量 </template>
<InfoCircleOutlined style="color: rgba(0, 0, 0, 0.45)" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="labelCfg.refY" name="labelCfg.refY">
<a-input
v-model:value="modalState.formEdge.labelCfg.refY"
allow-clear
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> 标签在 x 方向的偏移量 </template>
<InfoCircleOutlined style="color: rgba(0, 0, 0, 0.45)" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
</a-form>
</DraggableModal>
</PageContainer>
</template>