diff --git a/src/views/monitor/topology-build/hooks/useCombo.ts b/src/views/monitor/topology-build/hooks/useCombo.ts new file mode 100644 index 00000000..e6912fb5 --- /dev/null +++ b/src/views/monitor/topology-build/hooks/useCombo.ts @@ -0,0 +1,156 @@ +import { message, Form } from 'ant-design-vue/lib'; +import { reactive, watch } from 'vue'; +import { graphG6 } from './useGraph'; +import { number } from 'echarts'; + +/**图分组内置类型 */ +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: '居中', + }, +]; + +/**图分组信息状态类型 */ +type ComboStateType = { + /**图分组原始数据 */ + origin: Record; + /**图分组表单数据 */ + form: Record; +}; + +/**图分组信息状态 */ +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', + style: { + fill: '#000000', + fontSize: 12, + fontWeight: 500, + }, + }, + }, +}); + +/**图分组对话分组内表单属性和校验规则 */ +export 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'); + } + 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'); + } + 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); + comboStateForm.resetFields(); + comboState.origin = {}; + } +} diff --git a/src/views/monitor/topology-build/hooks/useEdge.ts b/src/views/monitor/topology-build/hooks/useEdge.ts index 1929525a..8b3ce2cf 100644 --- a/src/views/monitor/topology-build/hooks/useEdge.ts +++ b/src/views/monitor/topology-build/hooks/useEdge.ts @@ -54,7 +54,7 @@ export const edgePositionOptions = [ }, ]; -/**边信息状态类型 */ +/**图边信息状态类型 */ type EdgeStateType = { /**图边原始数据 */ origin: Record; @@ -62,7 +62,7 @@ type EdgeStateType = { form: Record; }; -/**边信息状态 */ +/**图边信息状态 */ export let edgeState: EdgeStateType = reactive({ origin: {}, form: { diff --git a/src/views/monitor/topology-build/hooks/useGraph.ts b/src/views/monitor/topology-build/hooks/useGraph.ts index a3f86d89..e33c9689 100644 --- a/src/views/monitor/topology-build/hooks/useGraph.ts +++ b/src/views/monitor/topology-build/hooks/useGraph.ts @@ -57,6 +57,52 @@ const graphCanvasMenu = new Menu({ 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; } }, @@ -92,7 +138,7 @@ const graphNodeMenu = new Menu({ const targetId = target.id; switch (targetId) { case 'edit': - graphEvent.value = { type: `nodeMenu-${targetId}`, target, item }; + graphEvent.value = { type: `node-${targetId}`, target, item }; break; case 'hide': graphG6.value.hideItem(item); @@ -148,7 +194,7 @@ const graphEdgeMenu = new Menu({ const targetId = target.id; switch (targetId) { case 'edit': - graphEvent.value = { type: `edgeMenu-${targetId}`, target, item }; + graphEvent.value = { type: `edge-${targetId}`, target, item }; break; case 'hide': graphG6.value.hideItem(item); @@ -235,11 +281,15 @@ function fnGraphEvent(graph: Graph) { /**图元素选择开始结束点 */ 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({ @@ -249,13 +299,21 @@ function fnSelectSourceTargetOptionsData() { }); }); // 框 - graphG6.value.getCombos().forEach((combo1: any) => { - const info = JSON.parse(JSON.stringify(combo1.getModel())); - selectSourceTargetOptions.value.push({ + 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); }); } @@ -271,12 +329,6 @@ export function handleRanderGraph(container: HTMLElement, data: GraphData) { animate: true, fitCenter: true, modes: { - // default: [ - // // 允许拖拽画布、放缩画布、拖拽节点 - // 'drag-canvas', - // 'zoom-canvas', - // 'drag-node', - // ], default: [ { type: 'click-select', @@ -315,7 +367,7 @@ export function handleRanderGraph(container: HTMLElement, data: GraphData) { // ranksep: 10, // nodesep: 10, // }, - // 全局节点 矩形 + // 全局节点 defaultNode: { type: 'rect', size: [80, 40], @@ -344,7 +396,7 @@ export function handleRanderGraph(container: HTMLElement, data: GraphData) { }, direction: 'up', // triangle 三角形的方向 }, - // 全局边 三次贝塞尔曲线 + // 全局边 defaultEdge: { type: 'polyline', style: { @@ -366,19 +418,33 @@ export function handleRanderGraph(container: HTMLElement, data: GraphData) { }, }, }, - // defaultEdge: { - // type: 'line', - // }, - // 全局框节点 矩形 + // 全局框节点 defaultCombo: { type: 'rect', // Combo 类型 size: [40, 40], + padding: [30, 30, 30, 30], style: { - fillOpacity: 0.1, + 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, diff --git a/src/views/monitor/topology-build/hooks/useNode.ts b/src/views/monitor/topology-build/hooks/useNode.ts index ee4d1085..eec4b07a 100644 --- a/src/views/monitor/topology-build/hooks/useNode.ts +++ b/src/views/monitor/topology-build/hooks/useNode.ts @@ -98,10 +98,11 @@ export let nodeState: NodeStateType = reactive({ origin: {}, form: { id: '', + comboId: '', x: 0, y: 0, type: 'circle', - size: [0], + size: 30, anchorPoints: false, style: { fill: '#ffffff', @@ -133,6 +134,7 @@ export const nodeStateForm = Form.useForm( /**图节点编辑监听更新视图 */ watch(nodeState.form, node => { const info = JSON.parse(JSON.stringify(node)); + console.log(info); const nodeId = info.id; if (nodeId) { // 图片类型需要移除style属性,避免填充 @@ -142,23 +144,33 @@ watch(nodeState.form, node => { graphG6.value.clearItemStates(nodeId, 'selected'); graphG6.value.updateItem(nodeId, info); // 三角和图片的样式变更需要重绘才生效 - if (info.type === 'triangle' || info.type === 'image') { + if ( + info.type === 'triangle' || + info.type === 'image' || + info.comboId !== nodeState.origin.comboId + ) { graphG6.value.read(graphG6.value.save()); } } }); /**图节点类型输入限制 */ -export function handleTypeChange(type: any) { +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; + } nodeState.form = Object.assign(nodeState.form, { + size, icon: { show: false, img: '', @@ -172,11 +184,13 @@ export function handleTypeChange(type: any) { 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, @@ -193,11 +207,13 @@ export function handleTypeChange(type: any) { 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, @@ -222,35 +238,6 @@ export function handleTypeChange(type: any) { } } -/**图节点大小输入限制 */ -export function handleSizeChange(value: any) { - // 处理格式 - let intArr: number[] = []; - for (const v of value) { - const intV = parseInt(v); - if (!isNaN(intV)) { - intArr.push(intV); - } - } - // 节点类型限制size - const nodeType = nodeState.form.type; - switch (nodeType) { - case 'circle': - case 'star': - case 'donut': - intArr = intArr.slice(0, 1); - break; - case 'rect': - case 'ellipse': - case 'diamond': - case 'triangle': - case 'image': - intArr = intArr.slice(0, 2); - break; - } - nodeState.form.size = intArr; -} - /**图节点新增或更新 */ export function handleOkNode() { const node = JSON.parse(JSON.stringify(nodeState.form)); @@ -270,7 +257,11 @@ export function handleOkNode() { graphG6.value.addItem('node', node); } // 三角和图片的样式变更需要重绘才生效 - if (node.type === 'triangle' || node.type === 'image') { + if ( + node.type === 'triangle' || + node.type === 'image' || + node.comboId !== nodeState.origin.comboId + ) { graphG6.value.read(graphG6.value.save()); } nodeStateForm.resetFields(); @@ -284,10 +275,13 @@ export function handleCancelNode() { if (origin.id) { graphG6.value.updateItem(origin.id, origin); // 三角和图片的样式变更需要重绘才生效 - if (origin.type === 'triangle' || origin.type === 'image') { + if ( + origin.type === 'triangle' || + origin.type === 'image' || + origin.comboId !== nodeState.form.comboId + ) { graphG6.value.read(graphG6.value.save()); } - console.log(JSON.parse(JSON.stringify(nodeState.form))); nodeStateForm.resetFields(); nodeState.origin = {}; } diff --git a/src/views/monitor/topology-build/index.vue b/src/views/monitor/topology-build/index.vue index 78797adf..aedb9834 100644 --- a/src/views/monitor/topology-build/index.vue +++ b/src/views/monitor/topology-build/index.vue @@ -4,12 +4,13 @@ import { PageContainer } from 'antdv-pro-layout'; import useI18n from '@/hooks/useI18n'; import { handleRanderGraph, - handleChangeMode, graphEvent, graphG6, selectSourceTargetOptions, + selectComboOptions, graphMode, graphModeOptions, + handleChangeMode, } from './hooks/useGraph'; import { edgeTypeOptions, @@ -25,13 +26,21 @@ import { nodeDirectionOptions, nodeImageClipCfgOptions, nodeImageOptions, - handleTypeChange, - handleSizeChange, + handleNodeTypeChange, handleOkNode, handleCancelNode, nodeState, nodeStateForm, } from './hooks/useNode'; +import { + comboState, + comboStateForm, + comboTypeOptions, + comboPositionOptions, + handleComboTypeChange, + handleOkcombo, + handleCancelcombo, +} from './hooks/useCombo'; const { t } = useI18n(); @@ -44,7 +53,8 @@ watch(graphEvent, v => { const { type, target, item } = v; console.log(type, target, item); - if (type === 'edgeMenu-edit' && item) { + // 边 + if (type === 'edge-edit' && item) { const edge = item.getModel(); edgeState.origin = JSON.parse(JSON.stringify(edge)); edgeState.form = Object.assign(edgeState.form, edge); @@ -52,18 +62,30 @@ watch(graphEvent, v => { modalState.formType = 'edge'; modalState.visible = true; } - if (type === 'nodeMenu-edit' && item) { + // 节点 + 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 graphG6Data2 = reactive>({ +const graphG6Data = reactive>({ nodes: [ // 0 基站 { @@ -116,6 +138,7 @@ const graphG6Data2 = reactive>({ id: '2', x: 50, y: 450, + size: 23, label: 'O&M', type: 'triangle', icon: { @@ -360,7 +383,7 @@ const graphG6Data2 = reactive>({ ], }); -const graphG6Data = reactive>({ +const graphG6Data2 = reactive>({ nodes: [ { id: '0', @@ -2730,7 +2753,7 @@ type ModalStateType = { /**标题 */ title: string; /**图元素表单类型 */ - formType: 'edge' | 'node'; + formType: 'edge' | 'node' | 'combo'; /**确定按钮 loading */ confirmLoading: boolean; }; @@ -2750,20 +2773,22 @@ let modalState: ModalStateType = reactive({ function fnModalOk() { modalState.confirmLoading = true; const type = modalState.formType; + let result = false; // 边编辑确认 if (type === 'edge') { - const result = handleOkEdge(); - console.log(type, result); - modalState.visible = !result; - modalState.confirmLoading = !result; + result = handleOkEdge(); } // 节点编辑确认 if (type === 'node') { - const result = handleOkNode(); - console.log(type, result); - modalState.visible = !result; - modalState.confirmLoading = !result; + result = handleOkNode(); } + // 分租编辑确认 + if (type === 'combo') { + result = handleOkcombo(); + } + console.log(type, result); + modalState.visible = !result; + modalState.confirmLoading = !result; } /** @@ -2781,6 +2806,10 @@ function fnModalCancel() { if (type === 'node') { handleCancelNode(); } + // 分租编辑还原 + if (type === 'combo') { + handleCancelcombo(); + } } /**保存图数据 */ @@ -2863,14 +2892,273 @@ function fnGraphLoad() { @ok="fnModalOk" @cancel="fnModalCancel" > - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 标签文本及其配置 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2921,32 +3209,110 @@ function fnGraphLoad() { - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +