feat: 拓扑编辑数据存储功能

This commit is contained in:
TsMask
2024-01-04 15:54:22 +08:00
parent 2ff313e575
commit 7e2f404ba7
8 changed files with 436 additions and 2523 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,187 @@
import { message, Form } from 'ant-design-vue/lib';
import { reactive, toRaw, watch } from 'vue';
import { graphG6, selectSourceTargetOptions } from './useGraph';
import useI18n from '@/hooks/useI18n';
/**图分组内置类型 */
export const comboTypeOptions = [
{
value: 'circle',
label: '圆形',
},
{
value: 'rect',
label: '矩形',
},
];
/**图分组标签文本位置 */
export const comboPositionOptions = [
{
value: 'top',
label: '上',
},
{
value: 'left',
label: '左',
},
{
value: 'right',
label: '右',
},
{
value: 'bottom',
label: '下',
},
{
value: 'center',
label: '居中',
},
];
export default function useCombo() {
const { t } = useI18n();
/**图分组信息状态类型 */
type ComboStateType = {
/**图分组原始数据 */
origin: Record<string, any>;
/**图分组表单数据 */
form: Record<string, any>;
};
/**图分组信息状态 */
let comboState: ComboStateType = reactive({
origin: {},
form: {
id: '',
type: 'rect',
parentId: '',
size: [40, 40],
padding: [30, 30, 30, 30],
style: {
radius: 2,
fill: '#ffffff',
stroke: '#ffffff',
lineWidth: 1,
cursor: 'grab',
fillOpacity: 0.5,
},
label: '',
labelCfg: {
refX: 10,
refY: 10,
position: 'top',
style: {
fill: '#000000',
fontSize: 12,
fontWeight: 500,
},
},
children: [], // 子元素
},
});
/**图分组对话分组内表单属性和校验规则 */
const comboStateForm = Form.useForm(
comboState.form,
reactive({
id: [{ required: true, message: '分组唯一标识 ID' }],
})
);
/**图分组编辑监听更新视图 */
watch(comboState.form, combo => {
const info = JSON.parse(JSON.stringify(combo));
// 新增不监听变化
const comboOriginId = comboState.origin.id;
const comboId = info.id;
if (comboId && comboId === comboOriginId) {
graphG6.value.clearItemStates(comboId, 'selected');
const data = graphG6.value.save();
const item = data.combos.find((item: any) => item.id === comboId);
Object.assign(item, combo);
// 无父组id时不要设置避免导致绘制失败
if (!combo.parentId) {
Reflect.deleteProperty(item, 'parentId');
}
graphG6.value.read(data);
}
});
/**图分组类型输入限制 */
function handleComboTypeChange(type: any) {
// 类型尺寸和边距
if (type === 'circle') {
comboState.form.size = 30;
comboState.form.padding = 30;
}
if (type === 'rect') {
comboState.form.size = [30, 20];
comboState.form.padding = [10, 20, 10, 20];
}
}
/**图分组新增或更新 */
function handleOkcombo() {
return comboStateForm
.validate()
.then(e => {
const combo = JSON.parse(JSON.stringify(comboState.form));
if (!combo.id) {
message.warn({
content: `分组元素ID错误`,
duration: 2,
});
return false;
}
const item = graphG6.value.findById(combo.id);
if (item) {
const data = graphG6.value.save();
const item = data.combos.find((item: any) => item.id === combo.id);
Object.assign(item, combo);
// 无父组id时不要设置避免导致绘制失败
if (!combo.parentId) {
Reflect.deleteProperty(item, 'parentId');
}
graphG6.value.read(data);
} else {
graphG6.value.createCombo(combo, combo.children);
}
comboStateForm.resetFields();
comboState.origin = {};
return true;
})
.catch(e => {
message.error(
t('common.errorFields', { num: e.errorFields.length }),
3
);
return false;
});
}
/**图分组取消还原 */
function handleCancelcombo() {
// 新增无原始数据
const origin = JSON.parse(JSON.stringify(comboState.origin));
if (origin.id) {
const data = graphG6.value.save();
const item = data.combos.find((combo: any) => combo.id === origin.id);
Object.assign(item, origin);
graphG6.value.read(data);
}
comboStateForm.resetFields();
comboState.origin = {};
}
return {
comboState,
comboStateForm,
handleComboTypeChange,
handleOkcombo,
handleCancelcombo,
};
}

View File

@@ -0,0 +1,169 @@
import { message, Form } from 'ant-design-vue/lib';
import { reactive, watch } from 'vue';
import useI18n from '@/hooks/useI18n';
import { graphG6 } from './useGraph';
/**图边内置边类型 */
export const edgeTypeOptions = [
{
value: 'line',
label: '直线,连接两个节点的直线',
},
{
value: 'polyline',
label: '折线,多段线段构成的折线,连接两个端点',
},
{
value: 'arc',
label: '圆弧线,连接两个节点的一段圆弧',
},
{
value: 'quadratic',
label: '二阶贝塞尔曲线,只有一个控制点的曲线',
},
{
value: 'cubic',
label: '三阶贝塞尔曲线,有两个控制点的曲线',
},
{
value: 'cubic-vertical',
label: '垂直方向的三阶贝塞尔曲线',
},
{
value: 'cubic-horizontal',
label: '水平方向的三阶贝塞尔曲线',
},
{
value: 'loop',
label: '自环',
},
];
/**图边标签文本位置 */
export const edgePositionOptions = [
{
value: 'start',
label: '开头',
},
{
value: 'middle',
label: '中间',
},
{
value: 'end',
label: '末尾',
},
];
export default function useEdge() {
const { t } = useI18n();
/**图边信息状态类型 */
type EdgeStateType = {
/**图边原始数据 */
origin: Record<string, any>;
/**图边表单数据 */
form: Record<string, any>;
};
/**图边信息状态 */
let edgeState: EdgeStateType = reactive({
origin: {},
form: {
id: '',
source: '',
target: '',
type: 'polyline',
style: {
offset: 20,
radius: 2,
stroke: '#ffffff',
lineWidth: 1,
cursor: 'pointer',
},
label: '',
labelCfg: {
refX: 0,
refY: 0,
position: 'middle',
autoRotate: false,
style: {
fill: '#ffffff',
fontSize: 12,
fontWeight: 500,
},
},
},
});
/**图边对话框内表单属性和校验规则 */
const edgeStateForm = Form.useForm(
edgeState.form,
reactive({
id: [{ required: true, message: '边唯一 ID' }],
source: [{ required: true, message: '起始点 id' }],
target: [{ required: true, message: '结束点 id' }],
type: [{ required: true, message: 'line' }],
})
);
/**图边编辑监听更新视图 */
watch(edgeState.form, edge => {
const info = JSON.parse(JSON.stringify(edge));
// 新增id是#不监听变化
const edgeId = info.id;
if (edgeId && edgeId !== '#') {
graphG6.value.clearItemStates(edgeId, 'selected');
graphG6.value.updateItem(edgeId, info);
}
});
/**图边新增或更新 */
function handleOkEdge() {
return edgeStateForm
.validate()
.then(e => {
const edge = JSON.parse(JSON.stringify(edgeState.form));
if (!edge.id) {
message.warn({
content: `边元素ID错误`,
duration: 2,
});
return false;
}
// 存在更新新增id是#不监听变化
const item = graphG6.value.findById(edge.id);
if (item) {
graphG6.value.updateItem(item, edge);
} else {
edge.id = `${edge.source}~${Date.now()}~${edge.target}`;
graphG6.value.addItem('edge', edge);
}
edgeStateForm.resetFields();
edgeState.origin = {};
return true;
})
.catch(e => {
message.error(
t('common.errorFields', { num: e.errorFields.length }),
3
);
return false;
});
}
/**图边取消还原 */
function handleCancelEdge() {
// 新增无原始数据
const origin = JSON.parse(JSON.stringify(edgeState.origin));
if (origin.id) {
graphG6.value.updateItem(origin.id, origin);
}
edgeStateForm.resetFields();
edgeState.origin = {};
}
return { edgeState, edgeStateForm, handleOkEdge, handleCancelEdge };
}

View File

@@ -0,0 +1,574 @@
import useI18n from '@/hooks/useI18n';
import {
Graph,
GraphData,
ICanvas,
IShapeBase,
Item,
Menu,
Tooltip,
} from '@antv/g6';
import { ref } from 'vue';
/**图模式选择项 */
export const graphModeOptions = [
{
value: 'default',
label: '默认',
},
{
value: 'edit',
label: '编辑',
},
];
/**图实例对象 */
export const graphG6 = ref<any>(null);
/**图事件变更 */
export const graphEvent = ref<{
type: string;
target: HTMLElement | (IShapeBase & ICanvas);
item: Item | null;
}>();
/**图元素选择开始结束点 */
export const selectSourceTargetOptions = ref<Record<string, any>[]>([]);
/**图元素选择嵌入分组 */
export const selectComboOptions = ref<Record<string, any>[]>([]);
/**图模式选择项 */
export const graphMode = ref<'default' | 'edit'>('default');
export default function useGraph() {
//实例化i18n
const { t } = useI18n();
/**图画布右击菜单 */
const graphCanvasMenu = new Menu({
offsetX: 6,
offseY: 10,
itemTypes: ['canvas'],
getContent(evt) {
if (!evt) return '无';
const edit = graphMode.value === 'edit';
return `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<h3>画布</h3>
<div id="show" style="cursor: pointer; margin-bottom: 2px">
显示所有隐藏项
</div>
<div id="create-edge" style="cursor: pointer; margin-bottom: 2px; display: ${
edit ? 'black' : 'none'
}">
新增边
</div>
<div id="create-node" style="cursor: pointer; margin-bottom: 2px; display: ${
edit ? 'black' : 'none'
}">
新增节点
</div>
<div id="create-combo" style="cursor: pointer; margin-bottom: 2px; display: ${
edit ? 'black' : 'none'
}">
新增分组
</div>
</div>`;
},
handleMenuClick(target, item) {
console.log(target, item);
const targetId = target.id;
switch (targetId) {
case 'create-edge':
case 'create-node':
case 'create-combo':
graphEvent.value = { type: `canvas-${targetId}`, target, item };
break;
case 'show':
// 显示节点
graphG6.value.getNodes().forEach((node: any) => {
if (!node.isVisible()) {
graphG6.value.showItem(node);
graphG6.value.refreshItem(node);
}
});
// 显示边
graphG6.value.getEdges().forEach((edge: any) => {
if (!edge.isVisible()) {
graphG6.value.showItem(edge);
graphG6.value.refreshItem(edge);
}
});
// 显示分组
graphG6.value.getCombos().forEach((combo: any) => {
if (!combo.isVisible()) {
graphG6.value.showItem(combo);
graphG6.value.updateCombo(combo);
}
});
break;
}
},
});
/**图分组Combo 右击菜单 */
const graphComboMenu = new Menu({
offsetX: 6,
offseY: 10,
itemTypes: ['combo'],
getContent(evt) {
if (!evt) return '无';
const item = evt.item?.getModel();
const edit = graphMode.value === 'edit';
return `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<h3 style="margin-bottom: 8px">分组:${item?.label || item?.id}</h3>
<div id="hide" style="cursor: pointer; margin-bottom: 2px">
隐藏
</div>
<div id="edit" style="cursor: pointer; margin-bottom: 2px;display: ${
edit ? 'black' : 'none'
}">
编辑
</div>
<div id="delete" style="cursor: pointer; margin-bottom: 2px; display: ${
edit ? 'black' : 'none'
}">
删除
</div>
</div>
`;
},
handleMenuClick(target, item) {
console.log(target, item);
const targetId = target.id;
switch (targetId) {
case 'edit':
graphEvent.value = { type: `combo-${targetId}`, target, item };
break;
case 'delete':
graphG6.value.uncombo(item);
graphG6.value.updateCombos();
break;
case 'hide':
graphG6.value.hideItem(item);
break;
}
},
});
/**图节点右击菜单 */
const graphNodeMenu = new Menu({
offsetX: 6,
offseY: 10,
itemTypes: ['node'],
getContent(evt) {
if (!evt) return '无';
const item = evt.item?.getModel();
const edit = graphMode.value === 'edit';
return `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<h3 style="margin-bottom: 8px">节点:${item?.label || item?.id}</h3>
<div id="hide" style="cursor: pointer; margin-bottom: 4px">
隐藏
</div>
<div id="edit" style="cursor: pointer; margin-bottom: 4px; display: ${
edit ? 'black' : 'none'
}">
编辑
</div>
<div id="delete" style="cursor: pointer; margin-bottom: 4px; display: ${
edit ? 'black' : 'none'
}">
删除
</div>
</div>
`;
},
handleMenuClick(target, item) {
console.log(target, item);
const targetId = target.id;
switch (targetId) {
case 'edit':
graphEvent.value = { type: `node-${targetId}`, target, item };
break;
case 'delete':
graphG6.value.removeItem(item);
break;
case 'hide':
graphG6.value.hideItem(item);
break;
}
},
});
/**图节点展示 */
const graphNodeTooltip = new Tooltip({
offsetX: 10,
offsetY: 20,
getContent(evt) {
if (!evt) return '无';
const item = evt.item?.getModel();
return `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<h3>节点:${item?.label || item?.id}</h3>
</div>
`;
},
itemTypes: ['node'],
});
/**图边右击菜单 */
const graphEdgeMenu = new Menu({
offsetX: 6,
offseY: 10,
itemTypes: ['edge'],
getContent(evt) {
if (!evt) return '无';
const item = evt.item?.getModel();
const edit = graphMode.value === 'edit';
return `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<h3 style="margin-bottom: 8px">边:${item?.label || '无标签'}</h3>
<div id="hide" style="cursor: pointer; margin-bottom: 4px">
隐藏
</div>
<div id="edit" style="cursor: pointer; margin-bottom: 4px; display: ${
edit ? 'black' : 'none'
}">
编辑
</div>
<div id="delete" style="cursor: pointer; margin-bottom: 4px; display: ${
edit ? 'black' : 'none'
}">
删除
</div>
</div>
`;
},
handleMenuClick(target, item) {
console.log(target, item);
const targetId = target.id;
switch (targetId) {
case 'edit':
graphEvent.value = { type: `edge-${targetId}`, target, item };
break;
case 'delete':
graphG6.value.removeItem(item);
break;
case 'hide':
graphG6.value.hideItem(item);
break;
}
},
});
/**图边展示 */
const graphEdgeTooltip = new Tooltip({
offsetX: 10,
offsetY: 20,
getContent(evt) {
if (!evt) return '无';
const item = evt.item?.getModel();
return `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<h3>边: ${item?.label || '无标签'}</h3>
</div>
`;
},
itemTypes: ['edge'],
});
/**图绑定事件 */
function fnGraphEvent(graph: Graph) {
// 调用 graph.add / graph.addItem 方法之后触发
graph.on('afteradditem', evt => {
fnSelectSourceTargetOptionsData();
});
// 鼠标进入节点事件
graph.on('edge:mouseenter', (ev: any) => {
// 获得鼠标当前目标边
const edge = ev.item;
// 该边的起始点
const source = edge.getSource();
// 该边的结束点
const target = edge.getTarget();
// 先将边提前,再将端点提前。这样该边两个端点还是在该边上层,较符合常规。
// edge.toFront();
// source.toFront();
// target.toFront();
});
graph.on('edge:mouseleave', (ev: any) => {
// 获得图上所有边实例
const edges = graph.getEdges();
// 遍历边,将所有边的层级放置在后方,以恢复原样
// edges.forEach(edge => {
// edge.toBack();
// });
});
graph.on('node:mouseenter', evt => {
// 获得鼠标当前目标节点
const node = evt.item;
// 获取该节点的所有相关边
const edges = node && graph.getEdges();
// 遍历相关边,将所有相关边提前,再将相关边的两个端点提前,以保证相关边的端点在边的上方常规效果
// edges.forEach((edge: any) => {
// edge.toFront();
// edge.getSource().toFront();
// edge.getTarget().toFront();
// });
// graphEvent.value = {
// type: 'node:mouseenter',
// target: evt.target,
// item: evt.item,
// };
});
graph.on('node:mouseleave', (ev: any) => {
// 获得图上所有边实例
const edges = graph.getEdges();
// 遍历边,将所有边的层级放置在后方,以恢复原样
// edges.forEach(edge => {
// edge.toBack();
// });
});
}
/**
* 图元素选择开始结束点数据获取
*/
function fnSelectSourceTargetOptionsData() {
// 节点
selectSourceTargetOptions.value = [];
graphG6.value.getNodes().forEach((node: any) => {
const info = JSON.parse(JSON.stringify(node.getModel()));
selectSourceTargetOptions.value.push({
value: info.id,
label: info.label,
info,
});
});
// 分组
selectComboOptions.value = [
{
value: '',
label: '未分配',
},
];
graphG6.value.getCombos().forEach((combo: any) => {
const info = JSON.parse(JSON.stringify(combo.getModel()));
const comboInfo = {
value: info.id,
label: info.label,
info,
};
selectSourceTargetOptions.value.push(comboInfo);
selectComboOptions.value.push(comboInfo);
});
}
/**图数据渲染 */
function handleRanderGraph(
container: HTMLElement | undefined,
data: GraphData
) {
if (!container) return;
const { clientHeight, clientWidth } = container;
const graph = new Graph({
container: container,
width: clientWidth,
height: clientHeight,
animate: true,
fitCenter: true,
modes: {
default: [
{
type: 'click-select',
selectEdge: true,
},
'drag-combo',
{
type: 'drag-node',
onlyChangeComboSize: true,
},
'drag-canvas',
'zoom-canvas',
'collapse-expand-combo',
],
edit: [
{
type: 'click-select',
selectEdge: true,
},
{
type: 'drag-node',
shouldEnd: (e: any) => {
return true;
},
},
{ type: 'drag-combo' },
'drag-canvas',
'zoom-canvas',
{ type: 'create-edge', key: 'alt' },
],
},
groupByTypes: false,
// layout: {
// type: 'dagre',
// sortByCombo: false,
// ranksep: 10,
// nodesep: 10,
// },
// 全局节点
defaultNode: {
type: 'rect',
size: [80, 40],
style: {
radius: 4,
// fill: '#ffffff',
stroke: '#ffffff',
lineWidth: 1,
cursor: 'pointer',
},
labelCfg: {
position: 'center',
offset: 0,
style: {
fill: '#000000',
fontSize: 12,
fontWeight: 500,
},
},
icon: {
show: false,
img: '/svg/service.svg',
width: 25,
height: 25,
offset: 20, // triangle 特有
},
direction: 'up', // triangle 三角形的方向
},
// 全局边
defaultEdge: {
type: 'polyline',
style: {
offset: 20, // 拐弯处距离节点最小距离
radius: 2, // 拐弯处的圆角弧度,若不设置则为直角
stroke: '#ffffff',
lineWidth: 1,
cursor: 'pointer',
},
labelCfg: {
refX: 0,
refY: 0,
position: 'middle',
autoRotate: false,
style: {
fill: '#ffffff',
fontSize: 12,
fontWeight: 500,
},
},
},
// 全局分组节点
defaultCombo: {
type: 'rect', // Combo 类型
size: [40, 40],
padding: [30, 30, 30, 30],
style: {
radius: 2,
fill: '#ffffff',
stroke: '#ffffff',
lineWidth: 1,
cursor: 'grab',
fillOpacity: 0.5,
},
labelCfg: {
refX: 10,
refY: 10,
position: 'top',
style: {
fill: '#000000',
fontSize: 12,
fontWeight: 500,
},
},
},
plugins: [
graphCanvasMenu,
graphComboMenu,
graphNodeMenu,
graphNodeTooltip,
graphEdgeMenu,
graphEdgeTooltip,
],
});
graph.data(data);
graph.render();
// 图绑定事件
fnGraphEvent(graph);
graphG6.value = graph;
// 图元素选择开始结束点数据
fnSelectSourceTargetOptionsData();
return graph;
}
/**图模式改变 default | edit */
function handleChangeMode(value: any) {
// console.log(value, JSON.parse(JSON.stringify(graphG6.value.save())));
graphG6.value.setMode(value);
graphMode.value = graphG6.value.getCurrentMode();
}
return {
handleRanderGraph,
handleChangeMode,
};
}

View File

@@ -0,0 +1,323 @@
import { message, Form } from 'ant-design-vue/lib';
import { reactive, watch } from 'vue';
import { graphG6 } from './useGraph';
import useI18n from '@/hooks/useI18n';
/**图节点内置边类型 */
export const nodeTypeOptions = [
{
value: 'circle',
label: '圆形',
},
{
value: 'rect',
label: '矩形',
},
{
value: 'ellipse',
label: '椭圆',
},
{
value: 'diamond',
label: '菱形',
},
{
value: 'triangle',
label: '三角形',
},
{
value: 'star',
label: '星形',
},
{
value: 'image',
label: '图片',
},
// {
// value: 'donut',
// label: '面包圈',
// },
];
/**图节点标签文本位置 */
export const nodePositionOptions = [
{
value: 'top',
label: '上',
},
{
value: 'left',
label: '左',
},
{
value: 'right',
label: '右',
},
{
value: 'bottom',
label: '下',
},
{
value: 'center',
label: '居中',
},
];
/**图节点三角形方向 */
export const nodeDirectionOptions = [
{ value: 'up', label: '向上' },
{ value: 'down', label: '向下' },
{ value: 'left', label: '向左' },
{ value: 'right', label: '向右' },
];
/**图节点图片裁剪的形状 */
export const nodeImageClipCfgOptions = [
{ value: 'circle', label: '圆形' },
{ value: 'rect', label: '矩形' },
{ value: 'ellipse', label: '椭圆' },
];
/**图节点图片来源 */
export const nodeImageOptions = [
{ value: '/svg/base.svg', label: '基站' },
{ value: '/svg/cloud.svg', label: '云' },
{ value: '/svg/service.svg', label: '服务器' },
{ value: '/svg/service_db.svg', label: '数据服务器' },
];
export default function useNode() {
const { t } = useI18n();
/**图节点信息状态类型 */
type NodeStateType = {
/**图节点原始数据 */
origin: Record<string, any>;
/**图节点表单数据 */
form: Record<string, any>;
};
/**图节点信息状态 */
let nodeState: NodeStateType = reactive({
origin: {},
form: {
id: '',
comboId: '',
x: 0,
y: 0,
type: 'rect',
size: [80, 40],
anchorPoints: false,
style: {
fill: '#ffffff',
stroke: '#ffffff',
lineWidth: 1,
},
label: '',
labelCfg: {
position: 'center',
offset: 0,
style: {
fill: '#000000',
fontSize: 12,
fontWeight: 500,
},
},
},
});
/**图节点对话框内表单属性和校验规则 */
const nodeStateForm = Form.useForm(
nodeState.form,
reactive({
id: [{ required: true, message: '节点唯一 ID' }],
type: [{ required: true, message: 'line' }],
})
);
/**图节点编辑监听更新视图 */
watch(nodeState.form, node => {
const info = JSON.parse(JSON.stringify(node));
// 新增不监听变化
const nodeOriginId = nodeState.origin.id;
const nodeId = info.id;
if (nodeId && nodeId === nodeOriginId) {
// 图片类型需要移除style属性避免填充
if (info.type === 'image') {
Reflect.deleteProperty(info, 'style');
}
graphG6.value.clearItemStates(nodeId, 'selected');
graphG6.value.updateItem(nodeId, info);
// 三角和图片的样式变更需要重绘才生效
if (
info.type === 'triangle' ||
info.type === 'image' ||
info.comboId !== nodeState.origin.comboId
) {
graphG6.value.read(graphG6.value.save());
}
}
});
/**图节点类型输入限制 */
function handleNodeTypeChange(type: any) {
// 设置图标属性
if (['circle', 'ellipse', 'diamond', 'star', 'donut'].includes(type)) {
let size: number[] | number = [40, 30];
if (['circle', 'star', 'donut'].includes(type)) {
size = 60;
}
const origin = nodeState.origin;
if (origin.icon) {
nodeState.form = Object.assign(nodeState.form, {
size,
icon: origin.icon,
});
} else {
nodeState.form = Object.assign(nodeState.form, {
size,
icon: {
show: false,
img: '',
width: 25,
height: 25,
},
});
}
} else if (type === 'triangle') {
// 三角
const origin = nodeState.origin;
if (origin.icon) {
nodeState.form = Object.assign(nodeState.form, {
size: 40,
direction: origin.direction || 'up', // triangle 三角形的方向
icon: Object.assign({ offset: 20 }, origin.icon),
});
} else {
nodeState.form = Object.assign(nodeState.form, {
size: 30,
direction: 'up', // triangle 三角形的方向
icon: {
show: false,
img: '/svg/service.svg',
width: 25,
height: 25,
offset: 20, // triangle 特有
},
});
}
}
// 设置图片属性
if (type === 'image') {
const origin = nodeState.origin;
if (origin.img) {
nodeState.form = Object.assign(nodeState.form, {
size: [30, 30],
img: origin.img,
clipCfg: origin.clipCfg,
});
} else {
nodeState.form = Object.assign(nodeState.form, {
size: [30, 30],
img: '/svg/service.svg',
clipCfg: {
show: false,
width: 0,
height: 0,
type: 'circle',
},
});
}
Reflect.deleteProperty(nodeState.form, 'style');
} else {
// 当切换非图片时补充style属性
if (!Reflect.has(nodeState.form, 'style')) {
nodeState.form = Object.assign(nodeState.form, {
style: {
fill: '#ffffff',
stroke: '#ffffff',
lineWidth: 1,
},
});
}
}
// 设置矩形大小
if (type === 'rect') {
nodeState.form = Object.assign(nodeState.form, {
size: [80, 40],
});
}
}
/**图节点新增或更新 */
function handleOkNode() {
return nodeStateForm
.validate()
.then(e => {
const node = JSON.parse(JSON.stringify(nodeState.form));
if (!node.id) {
message.warn({
content: `节点元素ID错误`,
duration: 2,
});
return false;
}
// 存在更新,新增不监听变化
const item = graphG6.value.findById(node.id);
if (item) {
graphG6.value.updateItem(item, node);
} else {
graphG6.value.addItem('node', node);
}
// 三角和图片的样式变更需要重绘才生效
if (
node.type === 'triangle' ||
node.type === 'image' ||
node.comboId !== nodeState.origin.comboId
) {
graphG6.value.read(graphG6.value.save());
}
nodeStateForm.resetFields();
nodeState.origin = {};
return true;
})
.catch(e => {
message.error(
t('common.errorFields', { num: e.errorFields.length }),
3
);
return false;
});
}
/**图节点取消还原 */
function handleCancelNode() {
// 新增无原始数据
const origin = JSON.parse(JSON.stringify(nodeState.origin));
if (origin.id) {
graphG6.value.updateItem(origin.id, origin);
// 三角和图片的样式变更需要重绘才生效
if (
origin.type === 'triangle' ||
origin.type === 'image' ||
origin.comboId !== nodeState.form.comboId
) {
graphG6.value.read(graphG6.value.save());
}
}
nodeStateForm.resetFields();
nodeState.origin = {};
}
return {
nodeState,
nodeStateForm,
handleNodeTypeChange,
handleOkNode,
handleCancelNode,
};
}

View File

@@ -0,0 +1,359 @@
<script setup lang="ts">
import { reactive, onMounted, ref, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import useI18n from '@/hooks/useI18n';
import GraphEditModal from './components/GraphEditModal.vue';
import useGraph, {
graphG6,
graphMode,
graphModeOptions,
} from './/hooks/useGraph';
import {
delGraphData,
getGraphData,
getGraphGroups,
saveGraphData,
} from '@/api/monitor/topology';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { Form, Modal, message } from 'ant-design-vue/lib';
const { t } = useI18n();
const { handleRanderGraph, handleChangeMode } = useGraph();
/**图DOM节点实例对象 */
const graphG6Dom = ref<HTMLElement | undefined>(undefined);
/**图状态 */
const graphState = reactive<Record<string, any>>({
/**当前图组名 */
group: '',
groupOptions: [],
/**图数据 */
data: {
combos: [],
edges: [],
nodes: [],
},
});
/**
* 图组变更
* @param value 变更值
*/
function fnGraphGroupChange(value: any) {
if (value) {
fnGraphDataLoad(true);
}
}
/**
* 获取图组名称数据
* @param reload 是否重载数据
*/
function fnGraphDataGroups(reload: boolean = false) {
getGraphGroups()
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
graphState.groupOptions = res.data.map(str => {
return { label: str, value: str };
});
// 非重载
if (!reload) {
graphState.group = res.data[0];
fnGraphDataLoad(false);
}
}
})
.finally(() => {
if (!graphState.group) {
handleRanderGraph(graphG6Dom.value, graphState.data);
message.warning({
content: '暂无图组数据,可使用【图模式-编辑】进行新增图组',
duration: 5,
});
}
});
}
/**
* 获取图组数据渲染到画布
* @param reload 是否重载数据
*/
function fnGraphDataLoad(reload: boolean = false) {
getGraphData(graphState.group)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
graphState.data = res.data;
}
})
.finally(() => {
// 重载数据
if (reload) {
graphG6.value.read(graphState.data);
} else {
handleRanderGraph(graphG6Dom.value, graphState.data);
}
});
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**框是否显示 */
visible: boolean;
/**标题 */
title: string;
/**表单数据 */
form: Record<string, any>;
/**操作类型 */
type: 'save' | 'delete';
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visible: false,
title: '图组',
form: {
group: '',
},
type: 'save',
confirmLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.form,
reactive({
group: [
{
required: true,
message: '图组不能为空',
},
],
})
);
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
const from = toRaw(modalState.form);
modalStateFrom
.validate()
.then(e => {
modalState.confirmLoading = true;
const hide = message.loading({ content: t('common.loading') });
// 根据类型选择函数
saveGraphData(from.group, graphG6.value.save())
.then((res: any) => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: `保存图组【${from.group}】成功`,
duration: 3,
});
fnGraphDataGroups(true);
graphState.group = from.group;
} else {
message.error({
content: `保存图组【${from.group}】失败`,
duration: 3,
});
}
})
.finally(() => {
hide();
fnModalCancel();
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(t('common.errorFields', { num: e.errorFields.length }), 3);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.type = 'save';
modalState.visible = false;
modalStateFrom.resetFields();
}
/**图组数据保存 */
function fnGraphDataSave() {
modalState.form.group = graphState.group;
modalState.type = 'save';
modalState.title = '图组信息保存';
modalState.visible = true;
}
/**图组数据删除 */
function fnGraphDataDelete() {
if (!graphState.group) {
handleRanderGraph(graphG6Dom.value, graphState.data);
message.warning({
content: '暂无图组数据,可使用【图模式-编辑】进行新增图组',
duration: 5,
});
}
Modal.confirm({
title: t('common.tipTitle'),
content: `确认要删除图组名为【${graphState.group}】的数据吗?`,
onOk() {
const hide = message.loading({ content: t('common.loading') });
delGraphData(graphState.group)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', {
msg: t('common.deleteText'),
}),
duration: 3,
});
// 重加载图组数据
fnGraphDataGroups(true);
if (graphState.groupOptions.length > 0) {
graphState.group = graphState.groupOptions[0].value;
fnGraphDataLoad(true);
}
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
});
},
});
}
onMounted(() => {
fnGraphDataGroups();
});
</script>
<template>
<PageContainer>
<a-card
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="graphState" name="graphState" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="4" :md="12" :xs="24">
<a-form-item label="图模式" name="graphMode">
<a-select
:value="graphMode"
:options="graphModeOptions"
@change="handleChangeMode"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="图组" name="group ">
<a-select
v-model:value="graphState.group"
:options="graphState.groupOptions"
:placeholder="t('common.selectPlease')"
@change="fnGraphGroupChange"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<div class="button-container" style="margin-bottom: -12px">
<span>图组名{{ graphState.group }}</span>
</div>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<div class="button-container" style="margin-bottom: -12px">
<template v-if="graphMode === 'edit'">
<a-button type="primary" size="small" @click="fnGraphDataSave">
<template #icon>
<SaveOutlined />
</template>
保存
</a-button>
<a-button
type="ghost"
danger
size="small"
@click="fnGraphDataDelete"
>
<template #icon>
<DeleteOutlined />
</template>
删除
</a-button>
</template>
</div>
</template>
<div ref="graphG6Dom" class="chart"></div>
</a-card>
<!-- 图元素修改框 -->
<GraphEditModal></GraphEditModal>
<!-- 图保存图组名修改框 -->
<a-modal
width="500px"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visible"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form
name="modalStateFrom"
layout="horizontal"
:label-col="{ span: 6 }"
:labelWrap="true"
>
<a-row :gutter="16">
<a-col :lg="24" :md="24" :xs="24">
<a-form-item
label="图组名"
name="group"
v-bind="modalStateFrom.validateInfos.group"
help="已存在图组名会更新图数据,不存在则新增图组"
>
<a-auto-complete
v-model:value="modalState.form.group"
:options="graphState.groupOptions"
allow-clear
placeholder="图组名"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</PageContainer>
</template>
<style lang="less" scoped>
.chart {
width: 100%;
height: calc(100vh - 300px);
background-color: rgb(43, 47, 51);
}
</style>