diff --git a/src/views/monitor/topology-build/components/GraphEditModal.vue b/src/views/monitor/topology-build/components/GraphEditModal.vue new file mode 100644 index 00000000..f9f6f970 --- /dev/null +++ b/src/views/monitor/topology-build/components/GraphEditModal.vue @@ -0,0 +1,1150 @@ + + + + + diff --git a/src/views/monitor/topology-build/hooks/useCombo.ts b/src/views/monitor/topology-build/hooks/useCombo.ts index e6912fb5..7f893a90 100644 --- a/src/views/monitor/topology-build/hooks/useCombo.ts +++ b/src/views/monitor/topology-build/hooks/useCombo.ts @@ -1,7 +1,7 @@ import { message, Form } from 'ant-design-vue/lib'; import { reactive, watch } from 'vue'; import { graphG6 } from './useGraph'; -import { number } from 'echarts'; +import useI18n from '@/hooks/useI18n'; /**图分组内置类型 */ export const comboTypeOptions = [ @@ -39,118 +39,130 @@ export const comboPositionOptions = [ }, ]; -/**图分组信息状态类型 */ -type ComboStateType = { - /**图分组原始数据 */ - origin: Record; - /**图分组表单数据 */ - form: Record; -}; +export default function useCombo() { + const { t } = useI18n(); -/**图分组信息状态 */ -export let comboState: ComboStateType = reactive({ - origin: {}, - form: { - id: '', - type: 'rect', - parentId: '', - size: [40, 40], - padding: [30, 30, 30, 30], - style: { - fill: '#ffffff', - stroke: '#ffffff', - lineWidth: 1, - }, - label: '', - labelCfg: { - refX: 10, - refY: 10, - position: 'top', + /**图分组信息状态类型 */ + type ComboStateType = { + /**图分组原始数据 */ + origin: Record; + /**图分组表单数据 */ + form: Record; + }; + + /**图分组信息状态 */ + let comboState: ComboStateType = reactive({ + origin: {}, + form: { + id: '', + type: 'rect', + parentId: '', + size: [40, 40], + padding: [30, 30, 30, 30], style: { - fill: '#000000', - fontSize: 12, - fontWeight: 500, + fill: '#ffffff', + stroke: '#ffffff', + lineWidth: 1, + }, + label: '', + labelCfg: { + refX: 10, + refY: 10, + position: 'top', + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, }, }, - }, -}); + }); -/**图分组对话分组内表单属性和校验规则 */ -export const comboStateForm = Form.useForm( - comboState.form, - reactive({ - id: [{ required: true, message: '分组唯一标识 ID' }], - }) -); + /**图分组对话分组内表单属性和校验规则 */ + const comboStateForm = Form.useForm( + comboState.form, + reactive({ + id: [{ required: true, message: '分组唯一标识 ID' }], + }) + ); -/**图分组编辑监听更新视图 */ -watch(comboState.form, combo => { - const info = JSON.parse(JSON.stringify(combo)); - const comboId = info.id; - if (comboId) { - graphG6.value.clearItemStates(comboId, 'selected'); - console.log(info); - 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'); + /**图分组编辑监听更新视图 */ + watch(comboState.form, combo => { + const info = JSON.parse(JSON.stringify(combo)); + const comboId = info.id; + if (comboId) { + graphG6.value.clearItemStates(comboId, 'selected'); + console.log(info); + 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); } - graphG6.value.read(data); - } -}); + }); -/**图分组类型输入限制 */ -export 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]; - } -} - -/**图分组新增或更新 */ -export function handleOkcombo() { - 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); - if (!combo.parentId) { - Reflect.deleteProperty(item, 'parentId'); + /**图分组类型输入限制 */ + 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]; } - graphG6.value.read(data); - } else { - graphG6.value.createCombo(combo, []); } - comboStateForm.resetFields(); - comboState.origin = {}; - return true; -} -/**图分组取消还原 */ -export 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); + /**图分组新增或更新 */ + function handleOkcombo() { + 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); + if (!combo.parentId) { + Reflect.deleteProperty(item, 'parentId'); + } + graphG6.value.read(data); + } else { + graphG6.value.createCombo(combo, []); + } comboStateForm.resetFields(); comboState.origin = {}; + return true; } + + /**图分组取消还原 */ + 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, + }; } diff --git a/src/views/monitor/topology-build/hooks/useEdge.ts b/src/views/monitor/topology-build/hooks/useEdge.ts index 8b3ce2cf..7828b00a 100644 --- a/src/views/monitor/topology-build/hooks/useEdge.ts +++ b/src/views/monitor/topology-build/hooks/useEdge.ts @@ -1,5 +1,6 @@ import { message, Form } from 'ant-design-vue/lib'; import { reactive, watch } from 'vue'; +import useI18n from '@/hooks/useI18n'; import { graphG6 } from './useGraph'; /**图边内置边类型 */ @@ -54,98 +55,117 @@ export const edgePositionOptions = [ }, ]; -/**图边信息状态类型 */ -type EdgeStateType = { - /**图边原始数据 */ - origin: Record; - /**图边表单数据 */ - form: Record; -}; +export default function useEdge() { + const { t } = useI18n(); -/**图边信息状态 */ -export 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, + /**图边信息状态类型 */ + type EdgeStateType = { + /**图边原始数据 */ + origin: Record; + /**图边表单数据 */ + form: Record; + }; + + /**图边信息状态 */ + let edgeState: EdgeStateType = reactive({ + origin: {}, + form: { + id: '', + source: '', + target: '', + type: 'polyline', style: { - fill: '#ffffff', - fontSize: 12, - fontWeight: 500, + 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, + }, }, }, - }, -}); + }); -/**图边对话框内表单属性和校验规则 */ -export 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' }], - }) -); + /**图边对话框内表单属性和校验规则 */ + 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)); - const edgeId = info.id; - if (edgeId) { - graphG6.value.clearItemStates(edgeId, 'selected'); - graphG6.value.updateItem(edgeId, info); - } -}); + /**图边编辑监听更新视图 */ + watch(edgeState.form, edge => { + const info = JSON.parse(JSON.stringify(edge)); + const edgeId = info.id; + if (edgeId && edgeId !== '#') { + graphG6.value.clearItemStates(edgeId, 'selected'); + graphG6.value.updateItem(edgeId, info); + } + }); -/**图边新增或更新 */ -export function handleOkEdge() { - const edge = JSON.parse(JSON.stringify(edgeState.form)); - if (!edge.id) { - message.warn({ - content: `边元素ID错误`, - duration: 2, - }); - return false; + /**图边新增或更新 */ + 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; + } + + debugger; + // graphG6.value.removeItem(edge.id); + // edge.id = `${edge.source}~${Date.now()}~${edge.target}`; + // graphG6.value.addItem('edge', edge); + 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; + }); } - // graphG6.value.removeItem(edge.id); - // edge.id = `${edge.source}~${Date.now()}~${edge.target}`; - // graphG6.value.addItem('edge', edge); - 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; -} - -/**图边取消还原 */ -export function handleCancelEdge() { - const origin = JSON.parse(JSON.stringify(edgeState.origin)); - if (origin.id) { - graphG6.value.updateItem(origin.id, origin); - // graphG6.value.removeItem(edgeOrigin.id); - // graphG6.value.addItem('edge', edgeOrigin); - edgeStateForm.resetFields(); - edgeState.origin = {}; + + /**图边取消还原 */ + function handleCancelEdge() { + const origin = JSON.parse(JSON.stringify(edgeState.origin)); + if (origin.id) { + graphG6.value.updateItem(origin.id, origin); + // graphG6.value.removeItem(edgeOrigin.id); + // graphG6.value.addItem('edge', edgeOrigin); + edgeStateForm.resetFields(); + edgeState.origin = {}; + } } + + return { edgeState, edgeStateForm, handleOkEdge, handleCancelEdge }; } diff --git a/src/views/monitor/topology-build/hooks/useGraph.ts b/src/views/monitor/topology-build/hooks/useGraph.ts index e33c9689..435eed8b 100644 --- a/src/views/monitor/topology-build/hooks/useGraph.ts +++ b/src/views/monitor/topology-build/hooks/useGraph.ts @@ -1,3 +1,4 @@ +import useI18n from '@/hooks/useI18n'; import { Graph, GraphData, @@ -9,462 +10,6 @@ import { } from '@antv/g6'; import { ref } from 'vue'; -/**图实例对象 */ -export const graphG6 = ref(null); - -/**图事件变更 */ -export const graphEvent = ref<{ - type: string; - target: HTMLElement | (IShapeBase & ICanvas); - item: Item | null; -}>(); - -/**图画布右击菜单 */ -const graphCanvasMenu = new Menu({ - offsetX: 6, - offseY: 10, - itemTypes: ['canvas'], - getContent(evt) { - return ` -
-
- 1. 显示所有隐藏项 -
-
`; - }, - handleMenuClick(target, item) { - console.log(target, item); - const targetId = target.id; - switch (targetId) { - 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) { - console.log(evt); - return ` -
-
- 1. 编辑 -
-
- 2. 隐藏 -
-
- `; - }, - handleMenuClick(target, item) { - console.log(target, item); - const targetId = target.id; - switch (targetId) { - case 'edit': - graphEvent.value = { type: `combo-${targetId}`, target, item }; - break; - case 'hide': - graphG6.value.hideItem(item); - break; - } - }, -}); - -/**图节点右击菜单 */ -const graphNodeMenu = new Menu({ - offsetX: 6, - offseY: 10, - itemTypes: ['node'], - getContent(evt) { - console.log(evt); - return ` -
-
- 1. 编辑 -
-
- 2. 隐藏 -
-
- `; - }, - handleMenuClick(target, item) { - console.log(target, item); - const targetId = target.id; - switch (targetId) { - case 'edit': - graphEvent.value = { type: `node-${targetId}`, target, item }; - break; - case 'hide': - graphG6.value.hideItem(item); - break; - } - }, -}); - -/**图节点展示 */ -const graphNodeTooltip = new Tooltip({ - offsetX: 10, - offsetY: 20, - getContent(e: any) { - const outDiv = document.createElement('div'); - outDiv.style.width = '180px'; - outDiv.innerHTML = ` -

自定义tooltip

-
    -
  • Label: ${e.item.getModel().label || e.item.getModel().id}
  • -
`; - return outDiv; - }, - itemTypes: ['node'], -}); - -/**图边右击菜单 */ -const graphEdgeMenu = new Menu({ - offsetX: 6, - offseY: 10, - itemTypes: ['edge'], - getContent(evt) { - console.log(evt); - return ` -
-
- 1. 编辑 -
-
- 2. 隐藏 -
-
- `; - }, - handleMenuClick(target, item) { - console.log(target, item); - const targetId = target.id; - switch (targetId) { - case 'edit': - graphEvent.value = { type: `edge-${targetId}`, target, item }; - 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 = ` -

graphEdgeTooltip

-
    -
  • Label: ${e.item.getModel().label || e.item.getModel().id}
  • -
`; - return outDiv; - }, - 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(); - // }); - }); -} - -/**图元素选择开始结束点 */ -export const selectSourceTargetOptions = ref[]>([]); - -/**图元素选择嵌入框 */ -export const selectComboOptions = ref[]>([]); - -/** - * 图元素选择开始结束点数据获取 - */ -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); - }); -} - -/**图数据渲染 */ -export function handleRanderGraph(container: HTMLElement, 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: 8, - // 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; -} - /**图模式选择项 */ export const graphModeOptions = [ { @@ -477,12 +22,494 @@ export const graphModeOptions = [ }, ]; +/**图实例对象 */ +export const graphG6 = ref(null); + +/**图事件变更 */ +export const graphEvent = ref<{ + type: string; + target: HTMLElement | (IShapeBase & ICanvas); + item: Item | null; +}>(); + /**图模式选择项 */ export const graphMode = ref('default'); -/**图模式改变 default | edit */ -export function handleChangeMode(value: any) { - console.log(value, JSON.parse(JSON.stringify(graphG6.value.save()))); - graphG6.value.setMode(value); - graphMode.value = graphG6.value.getCurrentMode(); +export default function useGraph() { + //实例化i18n + const { t } = useI18n(); + + /**图画布右击菜单 */ + const graphCanvasMenu = new Menu({ + offsetX: 6, + offseY: 10, + itemTypes: ['canvas'], + getContent(evt) { + return ` +
+
+ 1. 显示所有隐藏项 +
+
+ 2. 新增节点 +
+
+ 3. 新增边 +
+
+ 4. 新增分组 +
+
`; + }, + handleMenuClick(target, item) { + console.log(target, item); + const targetId = target.id; + switch (targetId) { + case 'create-node': + case 'create-edge': + 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) { + console.log(evt); + return ` +
+
+ 1. 编辑 +
+
+ 2. 隐藏 +
+
+ `; + }, + handleMenuClick(target, item) { + console.log(target, item); + const targetId = target.id; + switch (targetId) { + case 'edit': + graphEvent.value = { type: `combo-${targetId}`, target, item }; + break; + case 'hide': + graphG6.value.hideItem(item); + break; + } + }, + }); + + /**图节点右击菜单 */ + const graphNodeMenu = new Menu({ + offsetX: 6, + offseY: 10, + itemTypes: ['node'], + getContent(evt) { + console.log(evt); + return ` +
+
+ 1. 编辑 +
+
+ 2. 隐藏 +
+
+ `; + }, + handleMenuClick(target, item) { + console.log(target, item); + const targetId = target.id; + switch (targetId) { + case 'edit': + graphEvent.value = { type: `node-${targetId}`, target, item }; + break; + case 'hide': + graphG6.value.hideItem(item); + break; + } + }, + }); + + /**图节点展示 */ + const graphNodeTooltip = new Tooltip({ + offsetX: 10, + offsetY: 20, + getContent(e: any) { + const outDiv = document.createElement('div'); + outDiv.style.width = '180px'; + outDiv.innerHTML = ` +

自定义tooltip

+
    +
  • Label: ${e.item.getModel().label || e.item.getModel().id}
  • +
`; + return outDiv; + }, + itemTypes: ['node'], + }); + + /**图边右击菜单 */ + const graphEdgeMenu = new Menu({ + offsetX: 6, + offseY: 10, + itemTypes: ['edge'], + getContent(evt) { + console.log(evt); + return ` +
+
+ 1. 编辑 +
+
+ 2. 隐藏 +
+
+ `; + }, + handleMenuClick(target, item) { + console.log(target, item); + const targetId = target.id; + switch (targetId) { + case 'edit': + graphEvent.value = { type: `edge-${targetId}`, target, item }; + 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 = ` +

graphEdgeTooltip

+
    +
  • Label: ${e.item.getModel().label || e.item.getModel().id}
  • +
`; + return outDiv; + }, + 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(); + // }); + }); + } + + /**图元素选择开始结束点 */ + const selectSourceTargetOptions = ref[]>([]); + + /**图元素选择嵌入分组 */ + const selectComboOptions = ref[]>([]); + + /** + * 图元素选择开始结束点数据获取 + */ + 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, 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: 8, + // 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 { + selectSourceTargetOptions, + selectComboOptions, + handleRanderGraph, + handleChangeMode, + }; } diff --git a/src/views/monitor/topology-build/hooks/useNode.ts b/src/views/monitor/topology-build/hooks/useNode.ts index eec4b07a..ff01e4b0 100644 --- a/src/views/monitor/topology-build/hooks/useNode.ts +++ b/src/views/monitor/topology-build/hooks/useNode.ts @@ -1,6 +1,7 @@ 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 = [ @@ -85,204 +86,216 @@ export const nodeImageOptions = [ { value: '/svg/service_db.svg', label: '数据服务器' }, ]; -/**图节点信息状态类型 */ -type NodeStateType = { - /**图节点原始数据 */ - origin: Record; - /**图节点表单数据 */ - form: Record; -}; +export default function useNode() { + const { t } = useI18n(); -/**图节点信息状态 */ -export let nodeState: NodeStateType = reactive({ - origin: {}, - form: { - id: '', - comboId: '', - x: 0, - y: 0, - type: 'circle', - size: 30, - anchorPoints: false, - style: { - fill: '#ffffff', - stroke: '#ffffff', - lineWidth: 1, - }, - label: '', - labelCfg: { - position: 'center', - offset: 0, + /**图节点信息状态类型 */ + type NodeStateType = { + /**图节点原始数据 */ + origin: Record; + /**图节点表单数据 */ + form: Record; + }; + + /**图节点信息状态 */ + let nodeState: NodeStateType = reactive({ + origin: {}, + form: { + id: '', + comboId: '', + x: 0, + y: 0, + type: 'circle', + size: 30, + anchorPoints: false, style: { - fill: '#000000', - fontSize: 12, - fontWeight: 500, + fill: '#ffffff', + stroke: '#ffffff', + lineWidth: 1, + }, + label: '', + labelCfg: { + position: 'center', + offset: 0, + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, }, }, - }, -}); + }); -/**图节点对话框内表单属性和校验规则 */ -export const nodeStateForm = Form.useForm( - nodeState.form, - reactive({ - id: [{ required: true, message: '节点唯一 ID' }], - type: [{ required: true, message: 'line' }], - }) -); + /**图节点对话框内表单属性和校验规则 */ + 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)); - console.log(info); - const nodeId = info.id; - if (nodeId) { - // 图片类型需要移除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()); - } - } -}); - -/**图节点类型输入限制 */ -export function handleNodeTypeChange(type: any) { - // 设置图标属性 - if (['circle', 'ellipse', 'diamond', 'star', 'donut'].includes(type)) { - const origin = nodeState.origin; - if (origin.icon) { - nodeState.form = Object.assign(nodeState.form, { - size: origin.size, - icon: origin.icon, - }); - } else { - let size: number[] | number = [30]; - if (['circle', 'star', 'donut'].includes(type)) { - size = 30; + /**图节点编辑监听更新视图 */ + watch(nodeState.form, node => { + const info = JSON.parse(JSON.stringify(node)); + console.log(info); + const nodeId = info.id; + if (nodeId) { + // 图片类型需要移除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()); } - 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, + }); + + /**图节点类型输入限制 */ + function handleNodeTypeChange(type: any) { + // 设置图标属性 + if (['circle', 'ellipse', 'diamond', 'star', 'donut'].includes(type)) { + const origin = nodeState.origin; + if (origin.icon) { + nodeState.form = Object.assign(nodeState.form, { + size: origin.size, + icon: origin.icon, + }); + } else { + let size: number[] | number = [30]; + if (['circle', 'star', 'donut'].includes(type)) { + size = 30; + } + 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', - 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, - }); + clipCfg: { + show: false, + width: 0, + height: 0, + type: 'circle', + }, + }); + } + Reflect.deleteProperty(nodeState.form, 'style'); } 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, - }, - }); + // 当切换非图片时补充style属性 + if (!Reflect.has(nodeState.form, 'style')) { + nodeState.form = Object.assign(nodeState.form, { + style: { + fill: '#ffffff', + stroke: '#ffffff', + lineWidth: 1, + }, + }); + } } } -} -/**图节点新增或更新 */ -export function handleOkNode() { - const node = JSON.parse(JSON.stringify(nodeState.form)); - if (!node.id) { - message.warn({ - content: `节点元素ID错误`, - duration: 2, - }); - return false; - } - // graphG6.value.removeItem(node.id); - // graphG6.value.addItem('node', node); - 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; -} - -/**图节点取消还原 */ -export function handleCancelNode() { - const origin = JSON.parse(JSON.stringify(nodeState.origin)); - if (origin.id) { - graphG6.value.updateItem(origin.id, origin); + /**图节点新增或更新 */ + function handleOkNode() { + const node = JSON.parse(JSON.stringify(nodeState.form)); + if (!node.id) { + message.warn({ + content: `节点元素ID错误`, + duration: 2, + }); + return false; + } + // graphG6.value.removeItem(node.id); + // graphG6.value.addItem('node', node); + const item = graphG6.value.findById(node.id); + if (item) { + graphG6.value.updateItem(item, node); + } else { + graphG6.value.addItem('node', node); + } // 三角和图片的样式变更需要重绘才生效 if ( - origin.type === 'triangle' || - origin.type === 'image' || - origin.comboId !== nodeState.form.comboId + node.type === 'triangle' || + node.type === 'image' || + node.comboId !== nodeState.origin.comboId ) { graphG6.value.read(graphG6.value.save()); } nodeStateForm.resetFields(); nodeState.origin = {}; + return true; } + + /**图节点取消还原 */ + 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, + }; } diff --git a/src/views/monitor/topology-build/index.vue b/src/views/monitor/topology-build/index.vue index aedb9834..38417af0 100644 --- a/src/views/monitor/topology-build/index.vue +++ b/src/views/monitor/topology-build/index.vue @@ -2,88 +2,18 @@ import { reactive, onMounted, ref, watch } from 'vue'; import { PageContainer } from 'antdv-pro-layout'; import useI18n from '@/hooks/useI18n'; -import { - handleRanderGraph, - graphEvent, +import GraphEditModal from './components/GraphEditModal.vue'; +import useGraph, { graphG6, - selectSourceTargetOptions, - selectComboOptions, graphMode, graphModeOptions, - handleChangeMode, -} from './hooks/useGraph'; -import { - edgeTypeOptions, - edgePositionOptions, - handleOkEdge, - handleCancelEdge, - edgeState, - edgeStateForm, -} from './hooks/useEdge'; -import { - nodeTypeOptions, - nodePositionOptions, - nodeDirectionOptions, - nodeImageClipCfgOptions, - nodeImageOptions, - handleNodeTypeChange, - handleOkNode, - handleCancelNode, - nodeState, - nodeStateForm, -} from './hooks/useNode'; -import { - comboState, - comboStateForm, - comboTypeOptions, - comboPositionOptions, - handleComboTypeChange, - handleOkcombo, - handleCancelcombo, -} from './hooks/useCombo'; - +} from './/hooks/useGraph'; const { t } = useI18n(); +const { handleRanderGraph, handleChangeMode } = useGraph(); /**图DOM节点实例对象 */ const graphG6Dom = ref(undefined); -/**图监听事件变更 */ -watch(graphEvent, v => { - if (!v) return; - const { type, target, item } = v; - console.log(type, target, item); - - // 边 - if (type === 'edge-edit' && item) { - const edge = item.getModel(); - edgeState.origin = JSON.parse(JSON.stringify(edge)); - edgeState.form = Object.assign(edgeState.form, edge); - modalState.title = '边信息编辑'; - modalState.formType = 'edge'; - modalState.visible = true; - } - // 节点 - if (type === 'node-edit' && item) { - const node = item.getModel(); - console.log(node.comboId); - nodeState.origin = JSON.parse(JSON.stringify(node)); - nodeState.form = Object.assign(nodeState.form, node); - modalState.title = '节点信息编辑'; - modalState.formType = 'node'; - modalState.visible = true; - } - // 分组 - if (type === 'combo-edit' && item) { - const combo = item.getModel(); - console.log(JSON.parse(JSON.stringify(combo))); - comboState.origin = JSON.parse(JSON.stringify(combo)); - comboState.form = Object.assign(comboState.form, combo); - modalState.title = '框信息编辑'; - modalState.formType = 'combo'; - modalState.visible = true; - } -}); - /**图数据 */ const graphG6Data = reactive>({ nodes: [ @@ -2746,72 +2676,6 @@ onMounted(() => { fnRanderGraph(graphG6Data); }); -/**对话框对象信息状态类型 */ -type ModalStateType = { - /**对话框是否显示 */ - visible: boolean; - /**标题 */ - title: string; - /**图元素表单类型 */ - formType: 'edge' | 'node' | 'combo'; - /**确定按钮 loading */ - confirmLoading: boolean; -}; - -/**对话框对象信息状态 */ -let modalState: ModalStateType = reactive({ - visible: false, - title: '图信息', - formType: 'edge', - confirmLoading: false, -}); - -/** - * 对话框弹出确认执行函数 - * 进行表达规则校验 - */ -function fnModalOk() { - modalState.confirmLoading = true; - const type = modalState.formType; - let result = false; - // 边编辑确认 - if (type === 'edge') { - result = handleOkEdge(); - } - // 节点编辑确认 - if (type === 'node') { - result = handleOkNode(); - } - // 分租编辑确认 - if (type === 'combo') { - result = handleOkcombo(); - } - console.log(type, result); - modalState.visible = !result; - modalState.confirmLoading = !result; -} - -/** - * 对话框弹出关闭执行函数 - * 进行表达规则校验 - */ -function fnModalCancel() { - modalState.visible = false; - const type = modalState.formType; - // 边编辑还原 - if (type === 'edge') { - handleCancelEdge(); - } - // 节点编辑还原 - if (type === 'node') { - handleCancelNode(); - } - // 分租编辑还原 - if (type === 'combo') { - handleCancelcombo(); - } -} - /**保存图数据 */ function fnGraphSave() { sessionStorage.setItem('graph', JSON.stringify(graphG6.value.save())); @@ -2880,986 +2744,7 @@ function fnGraphLoad() {
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 标签文本及其配置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 标签文本及其配置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 标签文本及其配置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +