UNPKG

w-vue-middle

Version:

统一公共服务组件

725 lines (696 loc) 19.5 kB
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, }, }; }); } }