1. 需求描述
新增一个tab栏,实现“指标血缘”功能。
凸显这个核心指标,然后能够看到以这个核心指标为中心的一个辐射情况,前置依赖哪些指标,后续哪些指标依赖这个核心指标(后续依赖部分可以默认隐藏,然后用户可以展开查看)
可以追溯基于指标定义的指标引用&被引用关系
指标血缘展示指标名称及内容,内容根据指标类型区分。
现在分为四类:
- 基础指标,展示指标主题 + 取数说明
- 复合指标,展示指标计算逻辑 + 取数说明
- 填报指标,展示填报指标 + 取数说明
- 常数指标,展示常数指标 + 取数说明
2. 初识 jsPlumb
根据需求不难发现,需要我们来画一个流程图来表明指标血缘关系的展示。经过一番调研,我选用了 jsPlumb 这个库。
2.1 什么是 jsPlumb
jsPlumb是一个强大得JavaScript连线库,它提供了 html 元素的拖放、连线等功能,可绘制不同类型、样式的连线,适用于开发web页面的图表、建模工具等。同时也支持Vue、React和Angular 。
2.2 安装与引入
我在项目中用的是 2.15.6 版本
npm install jsplumb --save // npm安装
yarn add jsplumb // yarn安装
在组件中引入
import { jsPlumb } from "jsplumb";
2.3 基本元素组成
- Source: 源对象(即连线的起始位置),jsPlumb 通过元素的 id 属性获取对象
- Target: 目标对象(即连线的结束位置),jsPlumb 通过元素的 id 属性获取对象
- Connector:连接线(即DOM节点间的连线)
- Endpoint: 端点(即DOM节点上可以拖拽出连线的位置)
- Anchor: 连线末端的位置
- Overlay:连线相关的样式、信息等(添加到连接线上的附件,eg:箭头和标签)
2.4 常用方法
- ready() 确保jsPlumb 插件已经开始渲染
jsPlumb.ready(function() {
...
});
- batch() 绘制节点以及节点相关信息
jsPlumb.batch(function() {
for (var i = 0, j = connections.length; i < j; i++) {
jsPlumb.connect(connections[i]);
}
});
- bind() jsPlumb 节点渲染完成后,在这里可以添加事件,在初始化时,直接添加事件
// 连线事件
jsPlumb.bind("connection", (info, event) => {
this.bindLinkEvent(info.connection);
this.data.links.push([info.sourceId, info.targetId]);
});
- getInstance() jsPlumb默认注册在浏览器的window对象中,为整个页面提供了一个静态实例(jsPlumb)可以直接使用,当然你也可以使用getInstance方法来单独创建一个实例。即:
this.flowInst = jsPlumb.getInstance();
同时,getInstance 方法接受一个参数,可以更改实例的配置
this.flowInst = jsPlumb.getInstance({
Connector : [ "Bezier", { curviness: 150 } ],
Anchors : [ "TopCenter", "BottomCenter" ],
...
});
- jsPlumb.connect(…) 用于创建连线
this.flowInst.connect({
source: 'item_left', // 源
target: 'item_right', // 目标
endpoint: 'Dot' // 线的类型
})
- jsPlumb.addEndpoint(…) 用来增加端点
// id: 增加端点得id
// common:端点的配置信息
this.flowInst.addEndpoint(id,{common})
this.flowInst.addEndpoint('item_left', {
anchors: ['Right']
})
- jsPlumb.draggable() 节点是否可拖拽
let common = {
containment?: string
start?: (params:DragEventCallbackOptions) => void
drag?: (params:DragEventCallbackOptions) => void
stop?: (params:DragEventCallbackOptions) => void
cursor?: string
zIndex?: number
}
this.flowInst.draggable(node._id, {common});
此方法有两个参数:
第一个参数为可拖拽节点的id,
第二个参数为函数对象,有6个参数,start,drag,stop 三个函数中可以获取元素节点位置等。
2.5 常用配置
{
Anchor: "BottomCenter", //锚点位置,如left,top,bottom等;对任何没有声明描点的Endpoint设置锚点,用于source及诶单或target节点
Anchors: [ null, null ], //连线的source和target Anchor
ConnectionsDetachable: true, //连线是否可用鼠标分离
ConnectionOverlays: [ //连线的叠加组件,如箭头、标签
["Arrow", { //箭头参数设置
location: 1,
visible:true,
width:11,
length:11,
id:"ARROW",
events:{
click:function() { }
}
} ],
[ "Label", { //标签参数设置
location: 0.1,
id: "label",
cssClass: "aLabel", //hover时label的样式名
events:{
tap:function() { }
},
visible: true
}]
],
Connector: "Bezier", //连线的类型,流程图(Flowchart)、贝塞尔曲线(Bezier)、直线(Straight)等
Container: null, //父级元素id;假如页面元素所在上层不同,最外层父级一定要设置
DoNotThrowErrors: false, //如果请求不存在的Anchor、Endpoint或Connector,是否抛异常
DragOptions: {cursor: 'pointer', zIndex: 2000}, //通过jsPlumb.draggable拖拽元素时的默认参数设置
DropOptions: { }, //target Endpoint放置时的默认参数设置
Endpoint: "Dot", //端点(锚点)的样式声明,Dot(默认圆点)、Rectangle(矩形)、Blank(空)
Endpoints: [ null, null ], //用jsPlumb.connect创建连接时,source端点和target端点的样式设置
EndpointOverlays: [ ], //端点的叠加物
EndpointStyle: { fill : "#456" }, //端点的默认样式
EndpointStyles: [ null, null ], //连线的source和target端点的样式
EndpointHoverStyle: { fill: "#ec9f2e" }, //端点hover时的样式
EndpointHoverStyles: [ null, null ], //连线的source和target端点hover时的样式
HoverPaintStyle: {stroke: "#ec9f2e" }, //连线hover时的样式
LabelStyle: { color: "black" }, //标签的默认样式,用css写法。
LogEnabled: false, //是否开启jsPlumb内部日志
Overlays: [ ], //连线和端点的叠加物
MaxConnections: 1, //端点支持的最大连接数
PaintStyle: { lineWidth : 8, stroke : "#456" }, //连线样式
ReattachConnections: false, //是否重新连接使用鼠标分离的线?
RenderMode: "svg", //默认渲染模式
Scope: "jsPlumb_DefaultScope" //范围,具有相同scope的点才可连接?
}
3.开发过程
3.1 在组件中引入 jsPlumb 并定义基础设置
import 'jsplumb';
declare const jsPlumb: any;
// jsPlumb基础设置
const common = {
endpoint: 'Blank',
connector: ['Bezier', { curviness: 100 }],
anchor: ['Left', 'Right'],
maxConnections: -1
};
// jsPlumb连线箭头参数
const overlays = [
['Arrow', {
width: 10,
length: 12,
location: 1
}]
];
3.2 指标血缘设置连线
定义一些变量。因为该组件是个子组件,所以还要接收一些父组件传递过来的变量
public jsPlumbInstance: any; // jsPlumb 实例
public isShowQuote:boolean = false; // 是否展示被引用关系
...
@Input() public bloodKinshipIndicator: IBloodIndicatorDatasDto; // 血缘数据信息
...
IBloodIndicatorDatasDto 类型结构:
interface IBloodIndicatorDatasDto {
indicator?: IBloodIndicatorData;
beQuotedIndicators?: IBloodIndicatorData[];
quotedIndicators?: IBloodIndicatorData[];
}
interface IBloodIndicatorData {
indicatorId?: string;
indicatorName?: string;
indicatorType?: string;
indicatorTypeName?: string;
remark?: string;
topicName?: string;
formulaStr?: string;
id?: string;
}
因为当前组件是个子组件,在 ngOnChanges 时期,给指标血缘关系连线
public bloodConnect(): void {
this.jsPlumbInstance = jsPlumb.getInstance();
this.jsPlumbInstance.ready(() => {
let timer;
timer = setTimeout(() => {
clearTimeout(timer);
this.bloodKinshipIndicator.beQuotedIndicators.forEach(item => {
this.jsPlumbInstance.connect({
source: item.indicatorId,
target: 'indicator',
paintStyle: {
stroke: '#009FFF',
strokeWidth: 2
},
overlays
}, common);
});
if(this.isShowQuote && this.bloodKinshipIndicator.quotedIndicators.length) {
this.quotedConnect(this.bloodKinshipIndicator.quotedIndicators);
}
}, 800);
});
}
以某个指标为例,后端返回的关于血缘的JSON数据:
indicator即当前指标,quotedIndicators 即引用指标数组,beQuotedIndicators 即被引用指标数组
3.3 设置被引用关系连接
public quotedConnect(data:IBloodIndicatorData[]): void {
let timer;
timer = setTimeout(() => {
clearTimeout(timer);
data.forEach(item => {
this.jsPlumbInstance.connect({
source: 'indicator',
target: item.indicatorId,
paintStyle: {
stroke: '#355AF0',
strokeWidth: 2
},
overlays
}, common);
});
const element = document.getElementById('indicator');
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
}
3.4 是否展示被引用关系
public isShowQuoted(data:IBloodIndicatorData[]): void {
this.isShowQuote = !this.isShowQuote;
if(this.isShowQuote && data.length) {
this.quotedConnect(data);
} else {
this.bloodKinshipIndicator.quotedIndicators.forEach(item => {
this.jsPlumbInstance.deleteConnectionsForElement(item.indicatorId);
this.jsPlumbInstance.removeAllEndpoints(item.indicatorId); // 删除divID所有端点
});
}
}
该子组件的html代码,👇
...
<!-- 血缘 -->
<ng-template #KpibloodKinship let-type="type" let-data="data">
<div class="kpi-info kpi-blood">
<div class="bottom-item">
<div *ngFor="let item of data?.beQuotedIndicators;let i = index">
<div class="left-item" id = "{{ item.indicatorId }}">
<div class="son-item" nz-tooltip [nzTooltipTitle]="item?.indicatorName">{{ item?.indicatorName }}</div>
<div class="left-bottom">
<div *ngIf="item?.indicatorType === 'Indicator'">
<div class="left-title">计算逻辑:</div>
<div class="left-content" nz-tooltip [nzTooltipTitle]="item?.formulaStr">{{ item?.formulaStr}}</div>
</div>
<div *ngIf="item?.indicatorType !== 'Indicator'">
<div class="blood-title" *ngIf="item?.indicatorType !== 'Db'">{{ item?.indicatorTypeName }}</div>
<div class="blood-title" *ngIf="item?.indicatorType === 'Db'">{{ item?.topicName }}</div>
</div>
<div class="left-title">取数说明:</div>
<div class="blood-remark" nz-tooltip [nzTooltipTitle]="item?.remark">{{ item?.remark }}</div>
</div>
</div>
</div>
</div>
<div class="bottom-item">
<div class="center-box" id="indicator">
<div class="center-item" [ngClass]="getStringLength(data?.indicator.indicatorName)" nz-tooltip [nzTooltipTitle]="data?.indicator.indicatorName">{{data?.indicator.indicatorName}}</div>
<span class="center-point" [ngClass]="{'center-point-close': isShowQuote}" *ngIf="data?.quotedIndicators.length" (click)="isShowQuoted(data?.quotedIndicators)"></span>
<div class="center-bottom">
<div *ngIf="data?.indicator.indicatorType === 'Indicator'">
<div class="center-title">计算逻辑:</div>
<div class="center-content" nz-tooltip [nzTooltipTitle]="data?.indicator.formulaStr">{{ data?.indicator.formulaStr}}</div>
</div>
<div *ngIf="data?.indicator.indicatorType !== 'Indicator'">
<div class="blood-title" *ngIf="data?.indicator.indicatorType !== 'Db'">{{ data?.indicator.indicatorTypeName }}</div>
<div class="blood-title" *ngIf="data?.indicator.indicatorType === 'Db'">{{ data?.indicator.topicName }}</div>
</div>
<div class="center-title">取数说明:</div>
<div class="center-content" nz-tooltip [nzTooltipTitle]="data?.indicator.remark">{{ data?.indicator.remark }}</div>
</div>
</div>
</div>
<div class="bottom-item">
<div class="right-box" *ngFor="let item of data?.quotedIndicators;let i = index">
<div class="right-item" id="{{ item.indicatorId }}" [style.display]="isShowQuote ? 'block' : 'none'">
<div class="right-son-item" nz-tooltip [nzTooltipTitle]="item?.indicatorName">{{ item?.indicatorName }}</div>
<div class="right-bottom">
<div *ngIf="item?.indicatorType === 'Indicator'">
<div class="right-title">计算逻辑:</div>
<div class="right-content" nz-tooltip [nzTooltipTitle]="item?.formulaStr">{{ item?.formulaStr}}</div>
</div>
<div *ngIf="item?.indicatorType !== 'Indicator'">
<div class="blood-title" *ngIf="item?.indicatorType !== 'Db'">{{ item?.indicatorTypeName }}</div>
<div class="blood-title" *ngIf="item?.indicatorType === 'Db'">{{ item?.topicName }}</div>
</div>
<div class="right-title">取数说明:</div>
<div class="blood-remark" nz-tooltip [nzTooltipTitle]="item?.remark">{{ item?.remark }}</div>
</div>
</div>
</div>
</div>
</div>
</ng-template>
该指标血缘效果图:
3.5 清除 jsplumb
public clearJsPlumb(flag?: boolean): void {
this.isShowQuote = false;
if(flag) {
this.tabCode = '';
}
if(this.jsPlumbInstance) {
this.jsPlumbInstance.deleteEveryConnection();
this.jsPlumbInstance.deleteEveryEndpoint();
this.jsPlumbInstance.reset();
}
}
在切换 tab 和关闭抽屉时,调用 clearJsPlumb 方法即可清除 jsplumb