feat: 拓扑编辑多语言函数处理

This commit is contained in:
TsMask
2023-12-29 11:56:52 +08:00
parent e4c829a46d
commit d9c561e677
6 changed files with 2053 additions and 1946 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
import { message, Form } from 'ant-design-vue/lib'; import { message, Form } from 'ant-design-vue/lib';
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
import { graphG6 } from './useGraph'; import { graphG6 } from './useGraph';
import { number } from 'echarts'; import useI18n from '@/hooks/useI18n';
/**图分组内置类型 */ /**图分组内置类型 */
export const comboTypeOptions = [ export const comboTypeOptions = [
@@ -39,118 +39,130 @@ export const comboPositionOptions = [
}, },
]; ];
/**图分组信息状态类型 */ export default function useCombo() {
type ComboStateType = { const { t } = useI18n();
/**图分组原始数据 */
origin: Record<string, any>;
/**图分组表单数据 */
form: Record<string, any>;
};
/**图分组信息状态 */ /**图分组信息状态类型 */
export let comboState: ComboStateType = reactive({ type ComboStateType = {
origin: {}, /**图分组原始数据 */
form: { origin: Record<string, any>;
id: '', /**图分组表单数据 */
type: 'rect', form: Record<string, any>;
parentId: '', };
size: [40, 40],
padding: [30, 30, 30, 30], /**图分组信息状态 */
style: { let comboState: ComboStateType = reactive({
fill: '#ffffff', origin: {},
stroke: '#ffffff', form: {
lineWidth: 1, id: '',
}, type: 'rect',
label: '', parentId: '',
labelCfg: { size: [40, 40],
refX: 10, padding: [30, 30, 30, 30],
refY: 10,
position: 'top',
style: { style: {
fill: '#000000', fill: '#ffffff',
fontSize: 12, stroke: '#ffffff',
fontWeight: 500, lineWidth: 1,
},
label: '',
labelCfg: {
refX: 10,
refY: 10,
position: 'top',
style: {
fill: '#000000',
fontSize: 12,
fontWeight: 500,
},
}, },
}, },
}, });
});
/**图分组对话分组内表单属性和校验规则 */ /**图分组对话分组内表单属性和校验规则 */
export const comboStateForm = Form.useForm( const comboStateForm = Form.useForm(
comboState.form, comboState.form,
reactive({ reactive({
id: [{ required: true, message: '分组唯一标识 ID' }], id: [{ required: true, message: '分组唯一标识 ID' }],
}) })
); );
/**图分组编辑监听更新视图 */ /**图分组编辑监听更新视图 */
watch(comboState.form, combo => { watch(comboState.form, combo => {
const info = JSON.parse(JSON.stringify(combo)); const info = JSON.parse(JSON.stringify(combo));
const comboId = info.id; const comboId = info.id;
if (comboId) { if (comboId) {
graphG6.value.clearItemStates(comboId, 'selected'); graphG6.value.clearItemStates(comboId, 'selected');
console.log(info); console.log(info);
const data = graphG6.value.save(); const data = graphG6.value.save();
const item = data.combos.find((item: any) => item.id === combo.id); const item = data.combos.find((item: any) => item.id === combo.id);
Object.assign(item, combo); Object.assign(item, combo);
// 无父组id时不要设置避免导致绘制失败 // 无父组id时不要设置避免导致绘制失败
if (!combo.parentId) { if (!combo.parentId) {
Reflect.deleteProperty(item, 'parentId'); Reflect.deleteProperty(item, 'parentId');
}
graphG6.value.read(data);
} }
graphG6.value.read(data); });
}
});
/**图分组类型输入限制 */ /**图分组类型输入限制 */
export function handleComboTypeChange(type: any) { function handleComboTypeChange(type: any) {
// 类型尺寸和边距 // 类型尺寸和边距
if (type === 'circle') { if (type === 'circle') {
comboState.form.size = 30; comboState.form.size = 30;
comboState.form.padding = 30; comboState.form.padding = 30;
} }
if (type === 'rect') { if (type === 'rect') {
comboState.form.size = [30, 20]; comboState.form.size = [30, 20];
comboState.form.padding = [10, 20, 10, 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() { function handleOkcombo() {
const origin = JSON.parse(JSON.stringify(comboState.origin)); const combo = JSON.parse(JSON.stringify(comboState.form));
if (origin.id) { if (!combo.id) {
const data = graphG6.value.save(); message.warn({
const item = data.combos.find((combo: any) => combo.id === origin.id); content: `分组元素ID错误`,
Object.assign(item, origin); duration: 2,
graphG6.value.read(data); });
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(); comboStateForm.resetFields();
comboState.origin = {}; 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,
};
} }

View File

@@ -1,5 +1,6 @@
import { message, Form } from 'ant-design-vue/lib'; import { message, Form } from 'ant-design-vue/lib';
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
import useI18n from '@/hooks/useI18n';
import { graphG6 } from './useGraph'; import { graphG6 } from './useGraph';
/**图边内置边类型 */ /**图边内置边类型 */
@@ -54,98 +55,117 @@ export const edgePositionOptions = [
}, },
]; ];
/**图边信息状态类型 */ export default function useEdge() {
type EdgeStateType = { const { t } = useI18n();
/**图边原始数据 */
origin: Record<string, any>;
/**图边表单数据 */
form: Record<string, any>;
};
/**图边信息状态 */ /**图边信息状态类型 */
export let edgeState: EdgeStateType = reactive({ type EdgeStateType = {
origin: {}, /**图边原始数据 */
form: { origin: Record<string, any>;
id: '', /**图边表单数据 */
source: '', form: Record<string, any>;
target: '', };
type: 'polyline',
style: { /**图边信息状态 */
offset: 20, let edgeState: EdgeStateType = reactive({
radius: 2, origin: {},
stroke: '#ffffff', form: {
lineWidth: 1, id: '',
cursor: 'pointer', source: '',
}, target: '',
label: '', type: 'polyline',
labelCfg: {
refX: 0,
refY: 0,
position: 'middle',
autoRotate: false,
style: { style: {
fill: '#ffffff', offset: 20,
fontSize: 12, radius: 2,
fontWeight: 500, 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( const edgeStateForm = Form.useForm(
edgeState.form, edgeState.form,
reactive({ reactive({
id: [{ required: true, message: '边唯一 ID' }], id: [{ required: true, message: '边唯一 ID' }],
source: [{ required: true, message: '起始点 id' }], source: [{ required: true, message: '起始点 id' }],
target: [{ required: true, message: '结束点 id' }], target: [{ required: true, message: '结束点 id' }],
type: [{ required: true, message: 'line' }], type: [{ required: true, message: 'line' }],
}) })
); );
/**图边编辑监听更新视图 */ /**图边编辑监听更新视图 */
watch(edgeState.form, edge => { watch(edgeState.form, edge => {
const info = JSON.parse(JSON.stringify(edge)); const info = JSON.parse(JSON.stringify(edge));
const edgeId = info.id; const edgeId = info.id;
if (edgeId) { if (edgeId && edgeId !== '#') {
graphG6.value.clearItemStates(edgeId, 'selected'); graphG6.value.clearItemStates(edgeId, 'selected');
graphG6.value.updateItem(edgeId, info); graphG6.value.updateItem(edgeId, info);
} }
}); });
/**图边新增或更新 */ /**图边新增或更新 */
export function handleOkEdge() { function handleOkEdge() {
const edge = JSON.parse(JSON.stringify(edgeState.form)); return edgeStateForm
if (!edge.id) { .validate()
message.warn({ .then(e => {
content: `边元素ID错误`, const edge = JSON.parse(JSON.stringify(edgeState.form));
duration: 2, if (!edge.id) {
}); message.warn({
return false; 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); function handleCancelEdge() {
const item = graphG6.value.findById(edge.id); const origin = JSON.parse(JSON.stringify(edgeState.origin));
if (item) { if (origin.id) {
graphG6.value.updateItem(item, edge); graphG6.value.updateItem(origin.id, origin);
} else { // graphG6.value.removeItem(edgeOrigin.id);
edge.id = `${edge.source}~${Date.now()}~${edge.target}`; // graphG6.value.addItem('edge', edgeOrigin);
graphG6.value.addItem('edge', edge); edgeStateForm.resetFields();
} edgeState.origin = {};
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 = {};
} }
return { edgeState, edgeStateForm, handleOkEdge, handleCancelEdge };
} }

View File

@@ -1,3 +1,4 @@
import useI18n from '@/hooks/useI18n';
import { import {
Graph, Graph,
GraphData, GraphData,
@@ -9,462 +10,6 @@ import {
} from '@antv/g6'; } from '@antv/g6';
import { ref } from 'vue'; import { ref } from 'vue';
/**图实例对象 */
export const graphG6 = ref<any>(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 `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<div id="show" style="cursor: pointer; margin-bottom: 2px">
1. 显示所有隐藏项
</div>
</div>`;
},
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 `
<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':
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 `
<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':
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 = `
<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) {
console.log(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':
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 = `
<h4>graphEdgeTooltip</h4>
<ul>
<li>Label: ${e.item.getModel().label || e.item.getModel().id}</li>
</ul>`;
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<Record<string, any>[]>([]);
/**图元素选择嵌入框 */
export const selectComboOptions = ref<Record<string, any>[]>([]);
/**
* 图元素选择开始结束点数据获取
*/
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 = [ export const graphModeOptions = [
{ {
@@ -477,12 +22,494 @@ export const graphModeOptions = [
}, },
]; ];
/**图实例对象 */
export const graphG6 = ref<any>(null);
/**图事件变更 */
export const graphEvent = ref<{
type: string;
target: HTMLElement | (IShapeBase & ICanvas);
item: Item | null;
}>();
/**图模式选择项 */ /**图模式选择项 */
export const graphMode = ref<string>('default'); export const graphMode = ref<string>('default');
/**图模式改变 default | edit */ export default function useGraph() {
export function handleChangeMode(value: any) { //实例化i18n
console.log(value, JSON.parse(JSON.stringify(graphG6.value.save()))); const { t } = useI18n();
graphG6.value.setMode(value);
graphMode.value = graphG6.value.getCurrentMode(); /**图画布右击菜单 */
const graphCanvasMenu = new Menu({
offsetX: 6,
offseY: 10,
itemTypes: ['canvas'],
getContent(evt) {
return `
<div
style="
display: flex;
flex-direction: column;
width: 140px;
"
>
<div id="show" style="cursor: pointer; margin-bottom: 2px">
1. 显示所有隐藏项
</div>
<div id="create-node" style="cursor: pointer; margin-bottom: 2px">
2. 新增节点
</div>
<div id="create-edge" style="cursor: pointer; margin-bottom: 2px">
3. 新增边
</div>
<div id="create-combo" style="cursor: pointer; margin-bottom: 2px">
4. 新增分组
</div>
</div>`;
},
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 `
<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':
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 `
<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':
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 = `
<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) {
console.log(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':
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 = `
<h4>graphEdgeTooltip</h4>
<ul>
<li>Label: ${e.item.getModel().label || e.item.getModel().id}</li>
</ul>`;
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<Record<string, any>[]>([]);
/**图元素选择嵌入分组 */
const selectComboOptions = ref<Record<string, any>[]>([]);
/**
* 图元素选择开始结束点数据获取
*/
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,
};
} }

View File

@@ -1,6 +1,7 @@
import { message, Form } from 'ant-design-vue/lib'; import { message, Form } from 'ant-design-vue/lib';
import { reactive, watch } from 'vue'; import { reactive, watch } from 'vue';
import { graphG6 } from './useGraph'; import { graphG6 } from './useGraph';
import useI18n from '@/hooks/useI18n';
/**图节点内置边类型 */ /**图节点内置边类型 */
export const nodeTypeOptions = [ export const nodeTypeOptions = [
@@ -85,204 +86,216 @@ export const nodeImageOptions = [
{ value: '/svg/service_db.svg', label: '数据服务器' }, { value: '/svg/service_db.svg', label: '数据服务器' },
]; ];
/**图节点信息状态类型 */ export default function useNode() {
type NodeStateType = { const { t } = useI18n();
/**图节点原始数据 */
origin: Record<string, any>;
/**图节点表单数据 */
form: Record<string, any>;
};
/**图节点信息状态 */ /**图节点信息状态类型 */
export let nodeState: NodeStateType = reactive({ type NodeStateType = {
origin: {}, /**图节点原始数据 */
form: { origin: Record<string, any>;
id: '', /**图节点表单数据 */
comboId: '', form: Record<string, any>;
x: 0, };
y: 0,
type: 'circle', /**图节点信息状态 */
size: 30, let nodeState: NodeStateType = reactive({
anchorPoints: false, origin: {},
style: { form: {
fill: '#ffffff', id: '',
stroke: '#ffffff', comboId: '',
lineWidth: 1, x: 0,
}, y: 0,
label: '', type: 'circle',
labelCfg: { size: 30,
position: 'center', anchorPoints: false,
offset: 0,
style: { style: {
fill: '#000000', fill: '#ffffff',
fontSize: 12, stroke: '#ffffff',
fontWeight: 500, lineWidth: 1,
},
label: '',
labelCfg: {
position: 'center',
offset: 0,
style: {
fill: '#000000',
fontSize: 12,
fontWeight: 500,
},
}, },
}, },
}, });
});
/**图节点对话框内表单属性和校验规则 */ /**图节点对话框内表单属性和校验规则 */
export const nodeStateForm = Form.useForm( const nodeStateForm = Form.useForm(
nodeState.form, nodeState.form,
reactive({ reactive({
id: [{ required: true, message: '节点唯一 ID' }], id: [{ required: true, message: '节点唯一 ID' }],
type: [{ required: true, message: 'line' }], type: [{ required: true, message: 'line' }],
}) })
); );
/**图节点编辑监听更新视图 */ /**图节点编辑监听更新视图 */
watch(nodeState.form, node => { watch(nodeState.form, node => {
const info = JSON.parse(JSON.stringify(node)); const info = JSON.parse(JSON.stringify(node));
console.log(info); console.log(info);
const nodeId = info.id; const nodeId = info.id;
if (nodeId) { if (nodeId) {
// 图片类型需要移除style属性避免填充 // 图片类型需要移除style属性避免填充
if (info.type === 'image') { if (info.type === 'image') {
Reflect.deleteProperty(info, 'style'); Reflect.deleteProperty(info, 'style');
} }
graphG6.value.clearItemStates(nodeId, 'selected'); graphG6.value.clearItemStates(nodeId, 'selected');
graphG6.value.updateItem(nodeId, info); graphG6.value.updateItem(nodeId, info);
// 三角和图片的样式变更需要重绘才生效 // 三角和图片的样式变更需要重绘才生效
if ( if (
info.type === 'triangle' || info.type === 'triangle' ||
info.type === 'image' || info.type === 'image' ||
info.comboId !== nodeState.origin.comboId info.comboId !== nodeState.origin.comboId
) { ) {
graphG6.value.read(graphG6.value.save()); 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;
} }
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) { function handleNodeTypeChange(type: any) {
nodeState.form = Object.assign(nodeState.form, { // 设置图标属性
size: 40, if (['circle', 'ellipse', 'diamond', 'star', 'donut'].includes(type)) {
direction: origin.direction || 'up', // triangle 三角形的方向 const origin = nodeState.origin;
icon: Object.assign({ offset: 20 }, origin.icon), if (origin.icon) {
}); nodeState.form = Object.assign(nodeState.form, {
} else { size: origin.size,
nodeState.form = Object.assign(nodeState.form, { icon: origin.icon,
size: 30, });
direction: 'up', // triangle 三角形的方向 } else {
icon: { let size: number[] | number = [30];
show: false, 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', img: '/svg/service.svg',
width: 25, clipCfg: {
height: 25, show: false,
offset: 20, // triangle 特有 width: 0,
}, height: 0,
}); type: 'circle',
} },
} });
// 设置图片属性 }
if (type === 'image') { Reflect.deleteProperty(nodeState.form, 'style');
const origin = nodeState.origin;
if (origin.img) {
nodeState.form = Object.assign(nodeState.form, {
size: [30, 30],
img: origin.img,
clipCfg: origin.clipCfg,
});
} else { } else {
nodeState.form = Object.assign(nodeState.form, { // 当切换非图片时补充style属性
size: [30, 30], if (!Reflect.has(nodeState.form, 'style')) {
img: '/svg/service.svg', nodeState.form = Object.assign(nodeState.form, {
clipCfg: { style: {
show: false, fill: '#ffffff',
width: 0, stroke: '#ffffff',
height: 0, lineWidth: 1,
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,
},
});
} }
} }
}
/**图节点新增或更新 */ /**图节点新增或更新 */
export function handleOkNode() { function handleOkNode() {
const node = JSON.parse(JSON.stringify(nodeState.form)); const node = JSON.parse(JSON.stringify(nodeState.form));
if (!node.id) { if (!node.id) {
message.warn({ message.warn({
content: `节点元素ID错误`, content: `节点元素ID错误`,
duration: 2, duration: 2,
}); });
return false; return false;
} }
// graphG6.value.removeItem(node.id); // graphG6.value.removeItem(node.id);
// graphG6.value.addItem('node', node); // graphG6.value.addItem('node', node);
const item = graphG6.value.findById(node.id); const item = graphG6.value.findById(node.id);
if (item) { if (item) {
graphG6.value.updateItem(item, node); graphG6.value.updateItem(item, node);
} else { } else {
graphG6.value.addItem('node', node); 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);
// 三角和图片的样式变更需要重绘才生效 // 三角和图片的样式变更需要重绘才生效
if ( if (
origin.type === 'triangle' || node.type === 'triangle' ||
origin.type === 'image' || node.type === 'image' ||
origin.comboId !== nodeState.form.comboId node.comboId !== nodeState.origin.comboId
) { ) {
graphG6.value.read(graphG6.value.save()); graphG6.value.read(graphG6.value.save());
} }
nodeStateForm.resetFields(); nodeStateForm.resetFields();
nodeState.origin = {}; 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,
};
} }

File diff suppressed because it is too large Load Diff