w-vue-middle
Version:
统一公共服务组件
725 lines (696 loc) • 19.5 kB
JavaScript
import '@antv/x6-vue-shape';
const { Graph } = require('@antv/x6');
const Hierarchy = require('@antv/hierarchy');
class BaseLane {
constructor(options = {}) {
this.graph = null;
this.laneRef = options.laneRef;
this.taskNode = options.renderTask;
}
// 初始化画布
initGraph(graphOptions) {
if (!this.laneRef) {
throw new Error($t('未指定绘制容器'));
return;
}
this.graph = new Graph({
container: this.laneRef,
interacting: {
nodeMovable: false,
},
autoResize: true,
...graphOptions,
});
}
richFilterTree(json) {
// 扁平数据转树结构(时间复杂度O(2n),优于递归算法)
let resMap = {},
resArr = [];
for (const item of json) {
resMap[item.id] = { ...item, children: [] };
}
for (const item of json) {
if (item.pid === null) {
resArr.push(resMap[item.id]);
} else {
if (!resMap[item.pid]) {
resMap[item.pid] = {
children: [],
};
}
resMap[item.pid].children.push(resMap[item.id]);
}
}
return resArr;
}
findItem(obj, id) {
// 查找当前节点及子节点
if (obj.id === id) {
return {
node: obj,
};
}
const { children } = obj;
if (children) {
for (let i = 0, len = children.length; i < len; i++) {
const res = this.findItem(children[i], id);
if (res) {
return {
node: res.node,
};
}
}
}
return null;
}
}
export class FullLinkLane extends BaseLane {
constructor(options) {
super(options);
this.laneCells = [];
this.laneNodes = []; // 节点
this.laneEdges = []; // 线
this.nodeX = 0;
this.nodeP = [];
this.maxNodeH = 0;
this.nextNodesIds = [];
this.nodeData = {};
this.copyNodeData = [];
this.options = {
chainLineDirection: 'toLeft', // 默认线箭头方向指向左
laneType: 'ZB',
laneHeaderComp: null, // 泳道 - header
LaneViewTop: 120,
laneX: 20, // 左右padding
laneY: 6, // 上下padding
laneV: 140, // 垂直方向节点间的间距
laneW: 550, // 泳道宽
laneH: 90, // 节点高
laneSpace: 40, // 节点间的间距
laneZbW: 300, // 指标节点宽
laneNotZbW: 350, // 非指标节点宽
laneTop: 90, // 节点top
laneNext: 350, // 最后一个泳道宽
nodeData: [], // 节点数据
...options,
};
}
initCanvas() {
this.registerCustomNodeAndEdge();
this.initGraph({
grid: true,
panning: {
enabled: true,
eventTypes: ['leftMouseDown', 'rightMouseDown', 'mouseWheel'],
},
mousewheel: {
enabled: true,
zoomAtMousePosition: true,
modifiers: 'ctrl',
minScale: 0.1,
maxScale: 3,
},
connecting: {
router: {
name: 'er',
args: {
offset: 'center',
direction: 'H',
},
},
},
selecting: true,
});
this.renderMindMap();
this.finishCanvas();
}
formatData() {
// 格式化数据
const nodeItem = (node) => {
const pkColumnH = node.data.pkColumnList?.length ? node.data.pkColumnList.length * 20 : 0;
const columnH = node.data.columnList?.length ? 20 : 0;
const valueDataH = node.data.valueData ? 20 : 0;
const nodeWH = {
[this.options.laneType === 'ZB' ? 'quota' : 'api']: {
width: 300,
height: 110,
},
quota_dev: {
width: 300,
height: 110,
},
caliber_hos: {
width: 300,
height: 110,
},
card: {
width: 140,
height: 48,
},
table: {
width: 350,
// height: 110 + (node.data.pkColumnList?.length ? node.data.pkColumnList.length * 20 : 20),
height: 80 + pkColumnH + columnH + valueDataH,
},
};
let result = {
...node,
type: {
[this.options.laneType === 'ZB' ? 'quota' : 'api']: 'custom-lane-zb-table',
quota_dev: 'custom-lane-zb-table',
caliber_hos: 'custom-lane-zb-table',
card: 'custom-lane-task',
table: 'custom-lane-table',
}[node.type],
width: nodeWH[node.type].width,
height: nodeWH[node.type].height,
};
if (node.data && node.data.jobType == 4) {
result.width = 0;
result.height = 0;
}
return result;
};
// 备份节点数据(to:动态添加节点)
this.copyNodeData = this.options.nodeData.map((node) => nodeItem(node));
const tempData = this.richFilterTree(this.copyNodeData)[0];
this.options.nodeData.forEach((node) => {
if (this.checkLastNodeCollapsed(node)) {
if (!node.data.ifHighlight) {
// 删除jobType为1且ifHighlight为false的子节点
const nodeItem = this.findItem(tempData, node.id);
if (nodeItem && nodeItem.node && nodeItem.node.children) {
nodeItem.node.children = [];
}
} else {
this.nextNodesIds.push(node.id);
}
}
});
this.nodeData = tempData;
// 绘制
this.initCanvas();
}
addTool(node) {
// 添加工具
const baseUrl = process.env.BASE_URL;
node.addTools(
{
name: 'button',
args: {
markup: [
{
tagName: 'image',
selector: 'img',
attrs: {
width: 24,
height: 24,
background: 'red',
x: node.size().width + 12,
y: node.size().height / 2 - 12,
'xlink:href': node.prop('collapsed')
? `${baseUrl || '.'}/static/images/subtract.svg`
: `${baseUrl || '.'}/static/images/plus.svg`,
// 'xlink:href': node.prop('collapsed') ? `./static/images/subtract.svg` : `./static/images/plus.svg`,
cursor: 'pointer',
},
},
],
x: 0,
y: 0,
onClick: ({ cell }) => {
if (this.checkLastNodeCollapsed(cell)) {
cell.prop('collapsed') ? this.removeNodes(cell) : this.addNodes(cell);
// 重新渲染
this.laneCells = [];
this.renderMindMap();
this.finishCanvas('refresh');
} else {
cell.removeTools();
cell.prop('collapsed', !cell.prop('collapsed'));
this.toggleNextNodes(cell);
this.addTool(cell);
}
},
},
},
'button-collapsed',
{
silent: false,
},
);
}
checkLastNodeCollapsed(node) {
// 校验最后一层节点是否可收缩
// console.log('node', node);
if (
(this.options.chainLineDirection === 'toLeft' && ['1', '4'].includes(node.data.jobType)) ||
(this.options.chainLineDirection === 'toRight' && ['3', '4'].includes(node.data.jobType))
) {
return true;
}
return false;
}
removeNodes(node) {
// 删除节点
node.prop('collapsed', false);
const _index = this.nextNodesIds.findIndex((id) => id === node.id);
this.nextNodesIds.splice(_index, 1);
const res = this.findItem(this.nodeData, node.id);
const nodeItem = res?.node;
if (nodeItem && nodeItem.children) {
nodeItem.children = [];
}
}
addNodes(node) {
// 添加节点
node.prop('collapsed', true);
this.nextNodesIds.push(node.id);
const res = this.findItem(this.nodeData, node.id);
const nodeItem = res?.node;
// 过滤被删除节点的子级
const filterNodeChild = () => {
const tempNodes = this.copyNodeData
.filter((node) => node.type === 'custom-lane-task' && !this.nextNodesIds.includes(node.id))
.map((o) => o.id);
return this.richFilterTree(
this.copyNodeData.filter((node) => !tempNodes.includes(node.pid)),
)[0];
};
if (nodeItem && nodeItem.children?.length === 0) {
const copyChildItem = JSON.parse(
JSON.stringify(this.findItem(filterNodeChild(), nodeItem.id)),
);
nodeItem.children.push(...copyChildItem?.node.children);
}
}
toggleNextNodes(node) {
// 切换节点显隐
const collapsed = node.prop('collapsed');
const run = (next) => {
const nextNodes =
this.options.chainLineDirection === 'toLeft'
? this.graph.getPredecessors(next, { distance: 1 }) // 前序节点
: this.graph.getSuccessors(next, { distance: 1 }); // 后续节点
if (nextNodes) {
nextNodes.forEach((nextNode) => {
nextNode.toggleVisible(collapsed);
if (nextNode.prop('collapsed')) {
run(nextNode);
}
});
}
};
run(node);
}
finishCanvas(status) {
// 任务节点添加按钮
this.graph.resetCells(this.laneCells);
this.graph.getNodes().forEach((node) => {
node.prop('collapsed', true); // 展开
if (node.shape === 'custom-lane-task') {
if (this.checkLastNodeCollapsed(node)) {
// 展开高亮任务的子节点
node.prop(
'collapsed',
status === 'refresh'
? this.nextNodesIds.includes(node.id)
: node.data.ifHighlight || false,
);
}
node.removeTools();
this.addTool(node);
}
});
}
traverseLane(result) {
const mindComponent = {
'custom-lane-zb-table': this.options.renderVueLaneZbTable,
'custom-lane-table': this.options.renderVueLaneTable,
'custom-lane-task': this.options.renderVueLaneTask,
};
const getSourceAndTarget = (hierarchyItem, id) => {
// 处理箭头方向
return {
toLeft: {
source: {
cell: id,
anchor: { name: 'left' },
},
target: {
cell: hierarchyItem.id,
anchor: { name: 'right' },
},
},
toRight: {
source: {
cell: hierarchyItem.id,
anchor: { name: 'right' },
},
target: {
cell: id,
anchor: { name: 'left' },
},
},
};
};
const traverse = (hierarchyItem) => {
if (hierarchyItem) {
const { data, children } = hierarchyItem;
const options = {
...data,
shape: data.type,
x: hierarchyItem.x,
y: hierarchyItem.y,
};
// console.log('data', data);
const node = this.graph.createNode(
data.type === 'custom-text'
? { ...options, label: data.data.name }
: { ...options, component: mindComponent[data.type](data, this.graph) },
);
this.laneCells.push(node);
if (children) {
children.forEach((item) => {
const { id, data } = item;
this.laneCells.push(
this.graph.createEdge({
shape: 'custom-edge-label',
...getSourceAndTarget(hierarchyItem, id)[this.options.chainLineDirection],
attrs: {
line: {
stroke: data.data.ifHighlight ? '#2D5AFA' : '#C9C9C9',
// stroke: '#C9C9C9',
strokeWidth: 2,
strokeDasharray: 5,
},
},
}),
);
traverse(item);
});
}
}
};
traverse(result);
}
renderMindMap() {
// 渲染思维导图
const result = Hierarchy.mindmap(this.nodeData, {
direction: 'H',
getHeight(d) {
return d.height;
},
getWidth(d) {
return d.width;
},
getHGap() {
return 60;
},
getVGap() {
return 40;
},
getSide: () => {
return 'right';
},
});
this.traverseLane(result);
}
registerCustomNodeAndEdge() {
// 节点、线注册
const customNodeAndEdge = [
{
name: 'custom-lane',
options: {
inherit: 'vue-shape',
},
},
{
name: 'custom-lane-zb-table',
options: {
inherit: 'vue-shape',
},
},
{
name: 'custom-lane-table',
options: {
inherit: 'vue-shape',
},
},
{
name: 'custom-edge-label',
options: {
inherit: 'edge',
},
},
{
name: 'custom-lane-task',
options: {
inherit: 'vue-shape',
},
},
{
name: 'custom-text',
options: {
inherit: 'rect',
width: 0,
height: 0,
attrs: {
text: {
fill: '#999',
fontSize: 14,
lineHeight: 20,
},
},
},
},
];
customNodeAndEdge.forEach((item) => {
if (item.name === 'custom-edge-label') {
Graph.registerEdge(item.name, item.options, true);
} else {
Graph.registerNode(item.name, item.options, true);
}
});
}
}
export class ColumnRelation extends BaseLane {
constructor(options) {
super(options);
this.laneCells = [];
this.laneNodes = []; // 节点
this.laneEdges = []; // 线
this.options = {
nodeData: [], // 节点数据
lineData: [], // 线数据
...options,
};
this.initCanvas();
}
initCanvas() {
// 注册
this.registerPortLayout();
this.registerCustomNodeAndEdge();
// 节点、线
this.formatNodeAndLine();
this.initGraph({
connecting: {
router: {
name: 'er',
args: {
offset: 25,
direction: 'H',
},
},
},
});
this.laneCells.push(
...[...this.laneNodes, ...this.laneEdges].map((item) =>
this.graph[item.shape === 'custom-edge-label' ? 'createEdge' : 'createNode'](item),
),
);
this.graph.resetCells(this.laneCells);
this.setLaneLayout();
}
setLaneLayout() {
const nodeLayout = {
source: [],
target: [],
};
const nodes = this.graph.getNodes();
let nodeH = 5;
nodes.forEach((node) => {
const ports = node.ports.items;
if (node.data.sourceTable) {
// 源
nodeLayout.source.push(ports.length);
node.translate(10, nodeH);
nodeH += 36 + ports.length * 40;
} else {
nodeLayout.target.push(ports.length);
node.translate(580, 5);
}
});
let viewH = 0;
nodeLayout.source.forEach((item, i) => {
viewH += item * 40 + (i + 1) * 36;
});
this.laneRef.style.height = viewH + 'px';
}
registerPortLayout() {
Graph.registerPortLayout(
'erPortPosition',
(portsPositionArgs) => {
return portsPositionArgs.map((_, index) => {
return {
position: {
x: 0,
y: (index + 1) * 36,
},
angle: 0,
};
});
},
true,
);
}
registerCustomNodeAndEdge() {
const customNodeAndEdge = [
{
name: 'er-rect',
options: {
inherit: 'rect',
markup: [
{
tagName: 'rect',
selector: 'body',
},
{
tagName: 'text',
selector: 'label',
},
],
attrs: {
rect: {
strokeWidth: 1,
stroke: '#DFE5F9',
fill: '#E1E8FF',
},
label: {
fontWeight: 'bold',
fill: '#000',
fontSize: 12,
strokeWidth: 0,
},
},
ports: {
groups: {
list: {
markup: [
{
tagName: 'rect',
selector: 'portBody',
},
{
tagName: 'text',
selector: 'portNameLabel',
},
],
attrs: {
portBody: {
width: 280,
height: 32,
strokeWidth: 0,
fill: '#EFF4FF',
magnet: true,
fontSize: 12,
},
portNameLabel: {
ref: 'portBody',
refX: 0.5,
refY: 0.5,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
fill: '#333',
fontSize: 12,
textWrap: {
width: 240,
ellipsis: true,
},
},
},
position: 'erPortPosition',
},
},
},
},
},
{
name: 'custom-edge-label',
options: {
inherit: 'edge',
attrs: {
line: {
stroke: '#2D5AFA',
strokeWidth: 1,
strokeDasharray: 3,
},
},
},
},
];
customNodeAndEdge.forEach((item) => {
if (item.name === 'custom-edge-label') {
Graph.registerEdge(item.name, item.options, true);
} else {
Graph.registerNode(item.name, item.options, true);
}
});
}
formatNodeAndLine() {
this.laneNodes = this.options.nodeData.map((item, i) => {
return {
id: item.tableId,
shape: 'er-rect',
label: `${item.tableName}(${item.tableCode})`,
width: 280,
height: 36,
attrs: {
label: {
textWrap: {
width: 240,
ellipsis: true,
},
},
},
ports: item.columnList.map((item) => {
return {
id: item.columnId,
group: 'list',
attrs: {
portNameLabel: {
text: `${item.columnName}(${item.columnCode})`,
},
},
};
}),
data: {
sourceTable: item.sourceTable,
},
};
});
this.laneEdges = this.options.lineData.map((item, i) => {
return {
shape: 'custom-edge-label',
source: {
cell: item.sourceTableId,
port: item.sourceId,
},
target: {
cell: item.targetTableId,
port: item.targetId,
},
};
});
}
}