CN-1087 fix: 关系图逻辑重构交互优化
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
@import 'views/entityExplorer/search/explorer-search';
|
@import 'views/entityExplorer/search/explorer-search';
|
||||||
@import 'views/entityExplorer/entity-filter';
|
@import 'views/entityExplorer/entity-filter';
|
||||||
@import 'views/entityExplorer/entity-detail';
|
@import 'views/entityExplorer/entity-detail';
|
||||||
|
@import 'views/entityExplorer/entity-graph';
|
||||||
@import 'views/entityExplorer/entityList/entity-list';
|
@import 'views/entityExplorer/entityList/entity-list';
|
||||||
@import './views/entityExplorer/entityList/card';
|
@import './views/entityExplorer/entityList/card';
|
||||||
@import './views/entityExplorer/entityList/row';
|
@import './views/entityExplorer/entityList/row';
|
||||||
@@ -86,5 +87,5 @@
|
|||||||
@import 'views/charts2/digitalCertificate';
|
@import 'views/charts2/digitalCertificate';
|
||||||
@import 'views/charts2/entityDetailBasicInfo';
|
@import 'views/charts2/entityDetailBasicInfo';
|
||||||
|
|
||||||
@import "views/charts2/graphRightListBlock";
|
@import "views/entityExplorer/graphRightListBlock";
|
||||||
@import "views/charts2/graphRightDetailBlock";
|
@import "views/entityExplorer/graphRightDetailBlock";
|
||||||
|
|||||||
248
src/assets/css/components/views/entityExplorer/entity-graph.scss
Normal file
248
src/assets/css/components/views/entityExplorer/entity-graph.scss
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
.graph-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 100px;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
.toolbar__tools {
|
||||||
|
display: flex;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
margin-right: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: #575757;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.entity-graph {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.entity-graph__chart {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.graph-node {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background-color: transparent !important;
|
||||||
|
transition: linear all .2s;
|
||||||
|
|
||||||
|
.graph-node__text {
|
||||||
|
width: 120px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #353636;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.graph-node--root {
|
||||||
|
padding-top: 18px;
|
||||||
|
width: 82px;
|
||||||
|
height: 82px;
|
||||||
|
border: 5px solid !important;
|
||||||
|
// 覆盖自带的node点击效果
|
||||||
|
&.rel-node-checked {
|
||||||
|
box-shadow: none;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
&.graph-node--ip {
|
||||||
|
border-color: #CBD9BB !important;
|
||||||
|
box-shadow: 0 0 0 8px rgba(126,159,84,0.14);
|
||||||
|
i {
|
||||||
|
color: #7E9F54;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.graph-node--domain {
|
||||||
|
border-color: #AFDEED !important;
|
||||||
|
box-shadow: 0 0 0 8px rgba(56,172,210,0.14);
|
||||||
|
i {
|
||||||
|
color: #38ACD2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.graph-node--app {
|
||||||
|
border-color: #F5DAA3 !important;
|
||||||
|
box-shadow: 0 0 0 8px rgba(229,162,25,0.14);
|
||||||
|
i {
|
||||||
|
color: #E5A219;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
.graph-node__text {
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.graph-node--primary {
|
||||||
|
padding-top: 20px;
|
||||||
|
width: 66px;
|
||||||
|
height: 66px;
|
||||||
|
border: 1px solid #A7B0B9 !important;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
// 覆盖自带的node点击效果
|
||||||
|
&.rel-node-checked {
|
||||||
|
box-shadow: 0 0 0 5px rgba(151,151,151,0.21);
|
||||||
|
animation: none;
|
||||||
|
border-color: #778391 !important;
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #778391;
|
||||||
|
}
|
||||||
|
.graph-node__text {
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.graph-node--entity {
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
justify-content: center;
|
||||||
|
border: none !important;
|
||||||
|
|
||||||
|
&.graph-node--ip {
|
||||||
|
i {
|
||||||
|
color: #7E9F54;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.graph-node--domain {
|
||||||
|
i {
|
||||||
|
color: #38ACD2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.graph-node--app {
|
||||||
|
i {
|
||||||
|
color: #E5A219;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-graph__right-box {
|
||||||
|
& > div {
|
||||||
|
left: unset !important;
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
.entity-graph__detail {
|
||||||
|
height: calc(100% - 100px) !important;
|
||||||
|
top: 100px !important;
|
||||||
|
// border-left: 1px solid #E2E5EC;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.g6-component-tooltip {
|
||||||
|
box-shadow: -1px 1px 10px -1px rgba(205,205,205,0.85);
|
||||||
|
|
||||||
|
.primary-node-tooltip {
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
.tooltip__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: #717171;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.tooltip__title {
|
||||||
|
padding-left: 6px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 15px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tooltip__content {
|
||||||
|
padding-top: 10px;
|
||||||
|
color: #222;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
span:first-of-type {
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.entity-node-tooltip {
|
||||||
|
width: 300px;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
.tooltip__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.tooltip__title {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 15px;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
width: 3px !important;
|
||||||
|
height: 12px !important;
|
||||||
|
background: #38ACD2;
|
||||||
|
border-radius: 1px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.content-tag-list {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.entity-tag {
|
||||||
|
margin: 10px 9px 10px 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
$normal-color: #778391;
|
||||||
|
$normal-light-color: #F7F8F9;
|
||||||
|
$negative-color: #E26154;
|
||||||
|
$negative-light-color: #FEF6F5;
|
||||||
|
$positive-color: #749F4D;
|
||||||
|
$positive-light-color: #F7FAF5;
|
||||||
|
&.entity-tag--level-two-normal {
|
||||||
|
border-color: $normal-color;
|
||||||
|
color: $normal-color;
|
||||||
|
background-color: $normal-light-color;
|
||||||
|
}
|
||||||
|
&.entity-tag--level-two-negative {
|
||||||
|
border-color: $negative-color;
|
||||||
|
color: $negative-color;
|
||||||
|
background-color: $negative-light-color;
|
||||||
|
}
|
||||||
|
&.entity-tag--level-two-positive {
|
||||||
|
border-color: $positive-color;
|
||||||
|
color: $positive-color;
|
||||||
|
background-color: $positive-light-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/entityGraph',
|
path: '/entityGraph',
|
||||||
component: () => import('@/views/entityExplorer/EntityGraph')
|
component: () => import('@/views/entityExplorer/EntityGraph2')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/detection',
|
path: '/detection',
|
||||||
|
|||||||
924
src/views/entityExplorer/EntityGraph2.vue
Normal file
924
src/views/entityExplorer/EntityGraph2.vue
Normal file
@@ -0,0 +1,924 @@
|
|||||||
|
<template>
|
||||||
|
<div class="entity-graph">
|
||||||
|
<div class="entity-graph__chart" id="entityGraph">
|
||||||
|
</div>
|
||||||
|
<div class="entity-graph__right-box">
|
||||||
|
<el-drawer
|
||||||
|
v-model="rightBox.show"
|
||||||
|
direction="rtl"
|
||||||
|
custom-class="entity-graph__detail"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:modal="false"
|
||||||
|
:size="400"
|
||||||
|
:with-header="false"
|
||||||
|
destroy-on-close>
|
||||||
|
<graph-entity-list
|
||||||
|
v-if="rightBox.mode === 'list'"
|
||||||
|
:node="rightBox.node"
|
||||||
|
:loading="rightBox.loading"
|
||||||
|
@expandList="expandList"
|
||||||
|
@closeBlock="onCloseBlock"
|
||||||
|
>
|
||||||
|
</graph-entity-list>
|
||||||
|
<graph-entity-detail
|
||||||
|
v-else-if="rightBox.mode === 'detail'"
|
||||||
|
:node="rightBox.node"
|
||||||
|
:loading="rightBox.loading"
|
||||||
|
@expandDetailList="expandDetailList"
|
||||||
|
>
|
||||||
|
</graph-entity-detail>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import GraphEntityList from '@/views/entityExplorer/entityGraph/GraphEntityList'
|
||||||
|
import GraphEntityDetail from '@/views/entityExplorer/entityGraph/GraphEntityDetail'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { ref, shallowRef } from 'vue'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import G6, { Algorithm } from '@antv/g6'
|
||||||
|
import Node, { nodeType, queryRelatedEntity } from './entityGraph/node'
|
||||||
|
import Edge from './entityGraph/edge'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EntityRelationship',
|
||||||
|
components: {
|
||||||
|
GraphEntityList,
|
||||||
|
GraphEntityDetail
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
chartOption: {
|
||||||
|
container: 'entityGraph',
|
||||||
|
layout: {
|
||||||
|
type: 'force',
|
||||||
|
preventOverlap: true,
|
||||||
|
linkDistance: (d) => {
|
||||||
|
if (d.target.type === nodeType.rootNode || d.target.type === nodeType.listNode) {
|
||||||
|
return 200
|
||||||
|
}
|
||||||
|
return 80
|
||||||
|
},
|
||||||
|
nodeStrength: (d) => {
|
||||||
|
if (d.type === nodeType.rootNode || d.type === nodeType.listNode) {
|
||||||
|
return -100
|
||||||
|
}
|
||||||
|
return -10
|
||||||
|
},
|
||||||
|
edgeStrength: (d) => {
|
||||||
|
if (d.target.type === nodeType.rootNode || d.target.type === nodeType.listNode) {
|
||||||
|
return 0.1
|
||||||
|
}
|
||||||
|
return 0.7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modes: {
|
||||||
|
default: ['drag-canvas', 'drag-nodes', 'click-select', 'zoom-canvas']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async init () {
|
||||||
|
this.registerElements() // 注册自定义node
|
||||||
|
const tooltip = this.buildTooltip() // tooltip组件
|
||||||
|
const toolbar = this.buildToolbar() // 工具栏组装件
|
||||||
|
this.chartOption.plugins = [tooltip] // 注册组件
|
||||||
|
this.graph = new G6.Graph(this.chartOption)
|
||||||
|
const initialData = await this.generateInitialData()// 初始化数据
|
||||||
|
this.graph.data(initialData)
|
||||||
|
this.graph.render()
|
||||||
|
const rootNode = this.graph.findById(this.entity.entityName)
|
||||||
|
this.bindEvents() // 绑定事件
|
||||||
|
this.graph.emit('node:click', { item: rootNode, target: rootNode.getKeyShape() }) // 手动触发rootNode的点击事件
|
||||||
|
},
|
||||||
|
registerElements () {
|
||||||
|
const _this = this
|
||||||
|
G6.registerNode(
|
||||||
|
'rootNode',
|
||||||
|
{
|
||||||
|
draw (cfg, group) {
|
||||||
|
group.addShape('circle', {
|
||||||
|
name: 'rootSelectedShadow',
|
||||||
|
attrs: {
|
||||||
|
r: 25, // selected时38.5
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
fill: getRootNodeStyle(cfg.myData.entityType).selectedShadowColor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
group.addShape('circle', {
|
||||||
|
name: 'rootShadow',
|
||||||
|
attrs: {
|
||||||
|
r: 31.5,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
fill: getRootNodeStyle(cfg.myData.entityType).shadowColor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const node = group.addShape('circle', {
|
||||||
|
name: 'rootInner',
|
||||||
|
attrs: {
|
||||||
|
r: 26,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
fill: '#FFFFFF',
|
||||||
|
stroke: getRootNodeStyle(cfg.myData.entityType).borderColor,
|
||||||
|
lineWidth: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
group.addShape('image', {
|
||||||
|
attrs: {
|
||||||
|
x: -getRootNodeStyle(cfg.myData.entityType).iconWidth / 2,
|
||||||
|
y: -getRootNodeStyle(cfg.myData.entityType).iconHeight / 2,
|
||||||
|
height: getRootNodeStyle(cfg.myData.entityType).iconHeight,
|
||||||
|
width: getRootNodeStyle(cfg.myData.entityType).iconWidth,
|
||||||
|
img: getIconUrl(cfg.myData.entityType, true, true)
|
||||||
|
},
|
||||||
|
name: 'rootIcon',
|
||||||
|
draggable: true
|
||||||
|
})
|
||||||
|
// 为label建一个box,避免鼠标事件中断
|
||||||
|
group.addShape('rect', {
|
||||||
|
name: 'rootLabelBox',
|
||||||
|
attrs: {
|
||||||
|
width: 64,
|
||||||
|
height: 40,
|
||||||
|
x: -32,
|
||||||
|
y: 10,
|
||||||
|
fill: 'transparent'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
group.addShape('text', {
|
||||||
|
attrs: {
|
||||||
|
text: cfg.label,
|
||||||
|
x: 0,
|
||||||
|
y: 42,
|
||||||
|
fontSize: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
textBaseline: 'middle',
|
||||||
|
fill: '#353636'
|
||||||
|
},
|
||||||
|
name: 'rootLabel'
|
||||||
|
})
|
||||||
|
return node
|
||||||
|
},
|
||||||
|
setState (name, value, node) {
|
||||||
|
const group = node.getContainer()
|
||||||
|
const model = node.getModel()
|
||||||
|
const selectedShadowShape = group.get('children')[0]
|
||||||
|
const shadowShape = group.get('children')[1]
|
||||||
|
if (name === 'hover') {
|
||||||
|
// 鼠标悬浮时,若不处于selected状态,才继续处理
|
||||||
|
if (!node.hasState('mySelected')) {
|
||||||
|
// hover时,加深shadow的颜色
|
||||||
|
if (value) {
|
||||||
|
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).hoveredShadowColor)
|
||||||
|
} else {
|
||||||
|
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).shadowColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (name === 'mySelected') {
|
||||||
|
// select事件,取消自身hover状态,取消其他node和edge的高亮状态,高亮自身和自身相关的edge
|
||||||
|
node.setState('hover', false)
|
||||||
|
// 变更样式
|
||||||
|
if (value) {
|
||||||
|
// 取消其他高亮
|
||||||
|
const nodes = _this.graph.getNodes()
|
||||||
|
const edges = _this.graph.getEdges()
|
||||||
|
nodes.forEach(n => {
|
||||||
|
if (n.getModel().id !== node.getModel().id) {
|
||||||
|
n.setState('mySelected', false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
edges.forEach(n => {
|
||||||
|
n.setState('mySelected', false)
|
||||||
|
})
|
||||||
|
// 高亮自身
|
||||||
|
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).hoveredShadowColor)
|
||||||
|
selectedShadowShape.attr('r', 38.5)
|
||||||
|
// 高亮自身相关的edge
|
||||||
|
const aboutEdges = node.getEdges()
|
||||||
|
aboutEdges.forEach(n => {
|
||||||
|
n.setState('mySelected', true)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
shadowShape.attr('fill', getRootNodeStyle(model.myData.entityType).shadowColor)
|
||||||
|
selectedShadowShape.attr('r', 25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'single-node'
|
||||||
|
)
|
||||||
|
G6.registerNode(
|
||||||
|
'listNode',
|
||||||
|
{
|
||||||
|
draw (cfg, group) {
|
||||||
|
group.addShape('circle', {
|
||||||
|
name: 'listShadow',
|
||||||
|
attrs: {
|
||||||
|
r: 20, // selected时26.5
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
fill: getListNodeStyle(cfg.myData.entityType).hoveredShadowColor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const node = group.addShape('circle', {
|
||||||
|
name: 'listInner',
|
||||||
|
attrs: {
|
||||||
|
r: 21,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
fill: '#FFFFFF',
|
||||||
|
stroke: getListNodeStyle(cfg.myData.entityType).borderColor,
|
||||||
|
lineWidth: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
group.addShape('image', {
|
||||||
|
attrs: {
|
||||||
|
x: -getListNodeStyle(cfg.myData.entityType).iconWidth / 2,
|
||||||
|
y: -getListNodeStyle(cfg.myData.entityType).iconHeight / 2,
|
||||||
|
height: getListNodeStyle(cfg.myData.entityType).iconHeight,
|
||||||
|
width: getListNodeStyle(cfg.myData.entityType).iconWidth,
|
||||||
|
img: getIconUrl(cfg.myData.entityType, false, false)
|
||||||
|
},
|
||||||
|
name: 'listIcon',
|
||||||
|
draggable: true
|
||||||
|
})
|
||||||
|
// 为label建一个box,避免鼠标事件中断
|
||||||
|
group.addShape('rect', {
|
||||||
|
name: 'rootLabelBox',
|
||||||
|
attrs: {
|
||||||
|
width: 46,
|
||||||
|
height: 30,
|
||||||
|
x: -23,
|
||||||
|
y: 12,
|
||||||
|
fill: 'transparent'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
group.addShape('text', {
|
||||||
|
attrs: {
|
||||||
|
text: cfg.label,
|
||||||
|
x: 0,
|
||||||
|
y: 34,
|
||||||
|
fontSize: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
textBaseline: 'middle',
|
||||||
|
fill: '#353636'
|
||||||
|
},
|
||||||
|
name: 'listLabel'
|
||||||
|
})
|
||||||
|
return node
|
||||||
|
},
|
||||||
|
setState (name, value, node) {
|
||||||
|
const group = node.getContainer()
|
||||||
|
const model = node.getModel()
|
||||||
|
const shadowShape = group.get('children')[0]
|
||||||
|
const innerShape = group.get('children')[1]
|
||||||
|
if (name === 'hover') {
|
||||||
|
// 鼠标悬浮时,若不处于selected状态,才继续处理
|
||||||
|
if (!node.hasState('mySelected')) {
|
||||||
|
// hover时,出现shadow
|
||||||
|
shadowShape.attr('fill', getListNodeStyle(model.myData.entityType).hoveredShadowColor)
|
||||||
|
if (value) {
|
||||||
|
shadowShape.attr('r', 26.5)
|
||||||
|
} else {
|
||||||
|
shadowShape.attr('r', 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (name === 'mySelected') {
|
||||||
|
// select事件,取消自身hover状态,取消其他node和edge的高亮状态,高亮自身和自身相关的edge
|
||||||
|
node.setState('hover', false)
|
||||||
|
if (value) {
|
||||||
|
// 取消其他高亮
|
||||||
|
const nodes = _this.graph.getNodes()
|
||||||
|
const edges = _this.graph.getEdges()
|
||||||
|
nodes.forEach(n => {
|
||||||
|
if (n.getModel().id !== node.getModel().id) {
|
||||||
|
n.setState('mySelected', false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
edges.forEach(n => {
|
||||||
|
n.setState('mySelected', false)
|
||||||
|
})
|
||||||
|
// 高亮自身
|
||||||
|
shadowShape.attr('fill', getListNodeStyle(model.myData.entityType).selectedShadowColor)
|
||||||
|
shadowShape.attr('r', 26.5)
|
||||||
|
innerShape.attr('stroke', getListNodeStyle(model.myData.entityType).selectedBorderColor)
|
||||||
|
// 高亮自身相关的edge
|
||||||
|
const aboutEdges = node.getEdges()
|
||||||
|
aboutEdges.forEach(n => {
|
||||||
|
n.setState('mySelected', true)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
shadowShape.attr('fill', getListNodeStyle(model.myData.entityType).hoveredShadowColor)
|
||||||
|
shadowShape.attr('r', 20)
|
||||||
|
innerShape.attr('stroke', getListNodeStyle(model.myData.entityType).borderColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'single-node'
|
||||||
|
)
|
||||||
|
G6.registerNode(
|
||||||
|
'entityNode',
|
||||||
|
{
|
||||||
|
draw (cfg, group) {
|
||||||
|
group.addShape('circle', {
|
||||||
|
name: 'entityShadow',
|
||||||
|
attrs: {
|
||||||
|
r: 18,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
fill: 'transparent'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return group.addShape('image', {
|
||||||
|
attrs: {
|
||||||
|
x: -getEntityNodeStyle(cfg.myData.entityType).iconWidth / 2,
|
||||||
|
y: -getEntityNodeStyle(cfg.myData.entityType).iconHeight / 2,
|
||||||
|
height: getEntityNodeStyle(cfg.myData.entityType).iconHeight,
|
||||||
|
width: getEntityNodeStyle(cfg.myData.entityType).iconWidth,
|
||||||
|
img: getIconUrl(cfg.myData.entityType, true, false)
|
||||||
|
},
|
||||||
|
name: 'entityIcon'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setState (name, value, node) {
|
||||||
|
const group = node.getContainer()
|
||||||
|
const model = node.getModel()
|
||||||
|
const shadowShape = group.get('children')[0]
|
||||||
|
if (name === 'mySelected') {
|
||||||
|
// select事件,取消自身hover状态,取消其他node和edge的高亮状态,高亮自身和自身相关的edge
|
||||||
|
node.setState('hover', false)
|
||||||
|
if (value) {
|
||||||
|
// 取消其他高亮
|
||||||
|
const nodes = _this.graph.getNodes()
|
||||||
|
const edges = _this.graph.getEdges()
|
||||||
|
nodes.forEach(n => {
|
||||||
|
if (n.getModel().id !== node.getModel().id) {
|
||||||
|
n.setState('mySelected', false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
edges.forEach(n => {
|
||||||
|
n.setState('mySelected', false)
|
||||||
|
})
|
||||||
|
// 高亮自身
|
||||||
|
shadowShape.attr('fill', getEntityNodeStyle(model.myData.entityType).selectedShadowColor)
|
||||||
|
// 高亮自身相关的edge
|
||||||
|
const aboutEdges = node.getEdges()
|
||||||
|
aboutEdges.forEach(n => {
|
||||||
|
n.setState('mySelected', true)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
shadowShape.attr('fill', 'transparent')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'single-node'
|
||||||
|
)
|
||||||
|
G6.registerNode(
|
||||||
|
'tempNode',
|
||||||
|
{
|
||||||
|
draw (cfg, group) {
|
||||||
|
group.addShape('text', {
|
||||||
|
attrs: {
|
||||||
|
text: cfg.label,
|
||||||
|
x: 0,
|
||||||
|
y: 25,
|
||||||
|
fontSize: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
textBaseline: 'middle',
|
||||||
|
fill: '#353636',
|
||||||
|
opacity: 0.7
|
||||||
|
},
|
||||||
|
name: 'tempLabel'
|
||||||
|
})
|
||||||
|
return group.addShape('image', {
|
||||||
|
attrs: {
|
||||||
|
x: -getListNodeStyle(cfg.myData.entityType).iconWidth / 2,
|
||||||
|
y: -getListNodeStyle(cfg.myData.entityType).iconHeight / 2,
|
||||||
|
height: getListNodeStyle(cfg.myData.entityType).iconHeight,
|
||||||
|
width: getListNodeStyle(cfg.myData.entityType).iconWidth,
|
||||||
|
img: getIconUrl(cfg.myData.entityType, false, false),
|
||||||
|
opacity: 0.7
|
||||||
|
},
|
||||||
|
name: 'tempIcon'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setState (name, value, node) {
|
||||||
|
// 重写setState,避免鼠标事件导致重新加载图片
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'single-node'
|
||||||
|
)
|
||||||
|
function getRootNodeStyle (entityType) {
|
||||||
|
switch (entityType) {
|
||||||
|
case 'ip': {
|
||||||
|
return {
|
||||||
|
iconWidth: 26,
|
||||||
|
iconHeight: 23,
|
||||||
|
borderColor: 'rgba(126,159,84,1)',
|
||||||
|
shadowColor: 'rgba(126,159,84,0.21)',
|
||||||
|
hoveredShadowColor: 'rgba(126,159,84,0.36)',
|
||||||
|
selectedShadowColor: 'rgba(126,159,84,0.1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'domain': {
|
||||||
|
return {
|
||||||
|
iconWidth: 28,
|
||||||
|
iconHeight: 24,
|
||||||
|
borderColor: 'rgba(56,172,210,1)',
|
||||||
|
shadowColor: 'rgba(56,172,210,0.21)',
|
||||||
|
hoveredShadowColor: 'rgba(56,172,210,0.36)',
|
||||||
|
selectedShadowColor: 'rgba(56,172,210,0.1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'app': {
|
||||||
|
return {
|
||||||
|
iconWidth: 24,
|
||||||
|
iconHeight: 26,
|
||||||
|
borderColor: 'rgba(229,162,25,1)',
|
||||||
|
shadowColor: 'rgba(229,162,25,0.21)',
|
||||||
|
hoveredShadowColor: 'rgba(229,162,25,0.36)',
|
||||||
|
selectedShadowColor: 'rgba(229,162,25,0.1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
iconWidth: 26,
|
||||||
|
iconHeight: 23,
|
||||||
|
borderColor: 'rgba(126,159,84,1)',
|
||||||
|
shadowColor: 'rgba(126,159,84,0.21)',
|
||||||
|
hoveredShadowColor: 'rgba(126,159,84,0.36)',
|
||||||
|
selectedShadowColor: 'rgba(126,159,84,0.1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getListNodeStyle (entityType) {
|
||||||
|
switch (entityType) {
|
||||||
|
case 'ip': {
|
||||||
|
return {
|
||||||
|
iconWidth: 24,
|
||||||
|
iconHeight: 21,
|
||||||
|
borderColor: 'rgba(119,131,145,0.6)',
|
||||||
|
selectedBorderColor: 'rgba(126,159,84,1)',
|
||||||
|
hoveredShadowColor: 'rgba(151,151,151,0.21)',
|
||||||
|
selectedShadowColor: 'rgba(126,159,84,0.21)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'domain': {
|
||||||
|
return {
|
||||||
|
iconWidth: 24,
|
||||||
|
iconHeight: 24,
|
||||||
|
borderColor: 'rgba(119,131,145,0.6)',
|
||||||
|
selectedBorderColor: 'rgba(56,172,210,1)',
|
||||||
|
hoveredShadowColor: 'rgba(151,151,151,0.21)',
|
||||||
|
selectedShadowColor: 'rgba(56,172,210,0.21)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'app': {
|
||||||
|
return {
|
||||||
|
iconWidth: 22,
|
||||||
|
iconHeight: 24,
|
||||||
|
borderColor: 'rgba(119,131,145,0.6)',
|
||||||
|
selectedBorderColor: 'rgba(229,162,25,1)',
|
||||||
|
hoveredShadowColor: 'rgba(151,151,151,0.21)',
|
||||||
|
selectedShadowColor: 'rgba(229,162,25,0.21)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
iconWidth: 24,
|
||||||
|
iconHeight: 21,
|
||||||
|
borderColor: 'rgba(119,131,145,0.6)',
|
||||||
|
selectedBorderColor: 'rgba(126,159,84,1)',
|
||||||
|
hoveredShadowColor: 'rgba(151,151,151,0.21)',
|
||||||
|
selectedShadowColor: 'rgba(126,159,84,0.21)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getEntityNodeStyle (entityType) {
|
||||||
|
switch (entityType) {
|
||||||
|
case 'ip': {
|
||||||
|
return {
|
||||||
|
iconWidth: 22,
|
||||||
|
iconHeight: 20,
|
||||||
|
selectedShadowColor: 'rgba(126,159,84,0.1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'domain': {
|
||||||
|
return {
|
||||||
|
iconWidth: 22,
|
||||||
|
iconHeight: 22,
|
||||||
|
selectedShadowColor: 'rgba(56,172,210,0.1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'app': {
|
||||||
|
return {
|
||||||
|
iconWidth: 21,
|
||||||
|
iconHeight: 24,
|
||||||
|
selectedShadowColor: 'rgba(229,162,25,0.1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
iconWidth: 22,
|
||||||
|
iconHeight: 20,
|
||||||
|
borderColor: 'rgba(119,131,145,1)',
|
||||||
|
selectedBorderColor: 'rgba(126,159,84,1)',
|
||||||
|
hoveredShadowColor: 'rgba(151,151,151,0.21)',
|
||||||
|
selectedShadowColor: 'rgba(126,159,84,0.1)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getIconUrl (entityType, colored, isRoot) {
|
||||||
|
let suffix = ''
|
||||||
|
if (entityType === 'domain' && isRoot) {
|
||||||
|
suffix = '-colored2'
|
||||||
|
} else {
|
||||||
|
suffix = colored ? '-colored' : ''
|
||||||
|
}
|
||||||
|
return require(`@/assets/img/entity-symbol2/${entityType}${suffix}.svg`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bindEvents () {
|
||||||
|
const _this = this
|
||||||
|
this.graph.on('node:dragstart', (e) => {
|
||||||
|
_this.graph.layout()
|
||||||
|
refreshDraggedNodePosition(e)
|
||||||
|
})
|
||||||
|
this.graph.on('node:drag', function (e) {
|
||||||
|
refreshDraggedNodePosition(e)
|
||||||
|
})
|
||||||
|
this.graph.on('node:dragend', function (e) {
|
||||||
|
e.item.get('model').fx = null
|
||||||
|
e.item.get('model').fy = null
|
||||||
|
})
|
||||||
|
this.graph.on('node:click', async function (e) {
|
||||||
|
const node = e.item
|
||||||
|
const nodeModel = node.get('model')
|
||||||
|
if (nodeModel.type !== 'tempNode') {
|
||||||
|
cleanTempNodesAndTempEdges()
|
||||||
|
// 点击entityNode,查询数据,并根据数据生成tempNode
|
||||||
|
if (nodeModel.type === nodeType.entityNode) {
|
||||||
|
_this.rightBox.mode = 'detail'
|
||||||
|
// 若已查过数据,不重复查询
|
||||||
|
if (!nodeModel.myData.relatedEntity) {
|
||||||
|
await nodeModel.queryDetailData()
|
||||||
|
}
|
||||||
|
let change = false
|
||||||
|
Object.keys(nodeModel.myData.relatedEntity).forEach(k => {
|
||||||
|
if (nodeModel.myData.relatedEntity[k].total) {
|
||||||
|
// 若已有同级同类型的listNode,不生成此tempNode
|
||||||
|
const neighbors = _this.graph.getNeighbors(nodeModel.id, 'target')
|
||||||
|
const hasListNode = neighbors.some(b => b.get('model').myData.entityType === k)
|
||||||
|
if (!hasListNode) {
|
||||||
|
change = true
|
||||||
|
const tempNode = new Node(
|
||||||
|
nodeType.tempNode,
|
||||||
|
`${nodeModel.id}__${k}__temp`,
|
||||||
|
{
|
||||||
|
entityType: k,
|
||||||
|
// TODO k2-k1=1+k1k2
|
||||||
|
...generateTempNodeCoordinate(nodeModel.sourceNode, e)
|
||||||
|
},
|
||||||
|
nodeModel
|
||||||
|
)
|
||||||
|
const tempEdge = new Edge(nodeModel, tempNode, 'temp')
|
||||||
|
_this.graph.addItem('node', tempNode)
|
||||||
|
_this.graph.addItem('edge', tempEdge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
change && _this.graph.layout()
|
||||||
|
} else if (nodeModel.type === nodeType.listNode) {
|
||||||
|
_this.rightBox.mode = 'list'
|
||||||
|
} else if (nodeModel.type === nodeType.rootNode) {
|
||||||
|
_this.rightBox.mode = 'detail'
|
||||||
|
}
|
||||||
|
_this.rightBox.node = _.cloneDeep(nodeModel)
|
||||||
|
} else {
|
||||||
|
// 点击tempNode,根据source生成listNode和entityNode以及对应的edge。查完entityNode的接口再删除临时node和edge。
|
||||||
|
// 若已达第六层,则只生成listNode,不再展开entityNode
|
||||||
|
const nodes = []
|
||||||
|
const edges = []
|
||||||
|
const listNode = new Node(
|
||||||
|
nodeType.listNode,
|
||||||
|
`${nodeModel.sourceNode.id}__${nodeModel.myData.entityType}-list`,
|
||||||
|
{
|
||||||
|
entityType: nodeModel.myData.entityType,
|
||||||
|
x: nodeModel.x,
|
||||||
|
y: nodeModel.y
|
||||||
|
},
|
||||||
|
nodeModel.sourceNode
|
||||||
|
)
|
||||||
|
nodes.push(listNode)
|
||||||
|
edges.push(new Edge(nodeModel.sourceNode, listNode))
|
||||||
|
|
||||||
|
// 判断listNode的sourceNode层级,若大于等于10(即第6层listNode),则不继续拓展entity node,并给用户提示。否则拓展entity node
|
||||||
|
const level = _this.getNodeLevel(listNode.sourceNode.id)
|
||||||
|
if (level < 10) {
|
||||||
|
const entities = await queryRelatedEntity(nodeModel.sourceNode, listNode.myData.entityType)
|
||||||
|
nodeModel.sourceNode.myData.relatedEntity[listNode.myData.entityType].list.push(...entities.list)
|
||||||
|
entities.list.forEach(entity => {
|
||||||
|
const entityNode = new Node(nodeType.entityNode, entity.vertex, {
|
||||||
|
entityType: listNode.myData.entityType,
|
||||||
|
entityName: entity.vertex,
|
||||||
|
x: e.x + Math.random() * 100 - 50,
|
||||||
|
y: e.y + Math.random() * 100 - 50
|
||||||
|
}, listNode)
|
||||||
|
nodes.push(entityNode)
|
||||||
|
edges.push(new Edge(listNode, entityNode))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// TODO 提示大于5层
|
||||||
|
}
|
||||||
|
nodes.forEach(n => {
|
||||||
|
_this.graph.addItem('node', n)
|
||||||
|
})
|
||||||
|
edges.forEach(edge => {
|
||||||
|
_this.graph.addItem('edge', edge)
|
||||||
|
})
|
||||||
|
cleanTempNodesAndTempEdges()
|
||||||
|
_this.graph.layout()
|
||||||
|
}
|
||||||
|
|
||||||
|
node.setState('mySelected', true)
|
||||||
|
})
|
||||||
|
this.graph.on('node:mouseenter', function (e) {
|
||||||
|
e.item.setState('hover', true)
|
||||||
|
})
|
||||||
|
this.graph.on('node:mouseleave', function (e) {
|
||||||
|
e.item.setState('hover', false)
|
||||||
|
})
|
||||||
|
function refreshDraggedNodePosition (e) {
|
||||||
|
const model = e.item.get('model')
|
||||||
|
model.fx = e.x
|
||||||
|
model.fy = e.y
|
||||||
|
}
|
||||||
|
function cleanTempNodesAndTempEdges () {
|
||||||
|
// 清除现有tempNode和tempEdge
|
||||||
|
const tempNodes = _this.graph.findAll('node', node => node.get('model').type === nodeType.tempNode)
|
||||||
|
tempNodes.forEach(n => {
|
||||||
|
_this.graph.removeItem(n, false)
|
||||||
|
})
|
||||||
|
const tempEdges = _this.graph.findAll('edge', edge => edge.get('model').isTemp)
|
||||||
|
tempEdges.forEach(n => {
|
||||||
|
_this.graph.removeItem(n, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function generateTempNodeCoordinate (sourceNode, event) {
|
||||||
|
const sx = sourceNode.x
|
||||||
|
const sy = sourceNode.y
|
||||||
|
const cx = event.x
|
||||||
|
const cy = event.y
|
||||||
|
return {
|
||||||
|
x: cx + cx - sx + Math.random() * 30 - 15,
|
||||||
|
y: cy + cy - sy + Math.random() * 30 - 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async generateInitialData () {
|
||||||
|
const nodes = []
|
||||||
|
const edges = []
|
||||||
|
// 生成rootNode,并查询相关数据
|
||||||
|
const rootNode = new Node(nodeType.rootNode, this.entity.entityName, {
|
||||||
|
entityType: this.entity.entityType,
|
||||||
|
entityName: this.entity.entityName
|
||||||
|
})
|
||||||
|
await rootNode.queryDetailData()
|
||||||
|
nodes.push(rootNode)
|
||||||
|
|
||||||
|
// 生成listNode和entityNode及edge
|
||||||
|
if (rootNode.myData.relatedEntity) {
|
||||||
|
// listNode
|
||||||
|
const listNodes = []
|
||||||
|
const keys = Object.keys(rootNode.myData.relatedEntity)
|
||||||
|
keys.forEach(k => {
|
||||||
|
if (rootNode.myData.relatedEntity[k].total) {
|
||||||
|
const listNode = new Node(nodeType.listNode, `${rootNode.id}__${k}-list`, { entityType: k }, rootNode)
|
||||||
|
listNodes.push(listNode)
|
||||||
|
edges.push(new Edge(rootNode, listNode))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// entityNode
|
||||||
|
const entityNodes = []
|
||||||
|
for (const node of listNodes) {
|
||||||
|
const entities = await queryRelatedEntity(rootNode, node.myData.entityType)
|
||||||
|
rootNode.myData.relatedEntity[node.myData.entityType].list = entities.list
|
||||||
|
entities.list.forEach(entity => {
|
||||||
|
const entityNode = new Node(
|
||||||
|
nodeType.entityNode,
|
||||||
|
entity.vertex,
|
||||||
|
{
|
||||||
|
entityType: node.myData.entityType,
|
||||||
|
entityName: entity.vertex
|
||||||
|
},
|
||||||
|
node
|
||||||
|
)
|
||||||
|
entityNodes.push(entityNode)
|
||||||
|
edges.push(new Edge(node, entityNode))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
nodes.push(...listNodes, ...entityNodes)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
nodes: nodes,
|
||||||
|
edges: edges
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getNodeLevel (id) {
|
||||||
|
const { findShortestPath } = Algorithm
|
||||||
|
const info = findShortestPath(this.graph.save(), this.entity.entityName, id)
|
||||||
|
return info.length
|
||||||
|
},
|
||||||
|
onCloseBlock () {
|
||||||
|
// todo 关闭右侧graph面板
|
||||||
|
this.rightBox.mode = ''
|
||||||
|
this.rightBox.show = false
|
||||||
|
},
|
||||||
|
buildTooltip () {
|
||||||
|
const _this = this
|
||||||
|
return new G6.Tooltip({
|
||||||
|
offsetX: 10,
|
||||||
|
offsetY: 20,
|
||||||
|
itemTypes: ['node'],
|
||||||
|
getContent (e) {
|
||||||
|
const node = e.item.getModel()
|
||||||
|
if (node.type === nodeType.listNode) {
|
||||||
|
let iconClass = ''
|
||||||
|
let title = ''
|
||||||
|
let total = 0
|
||||||
|
let loadedCount = 0
|
||||||
|
switch (node.myData.entityType) {
|
||||||
|
case 'ip': {
|
||||||
|
iconClass = 'cn-icon cn-icon-resolve-ip'
|
||||||
|
title = _this.$t('entities.graph.resolveIp')
|
||||||
|
total = _.get(node.sourceNode.myData, 'relatedEntity.ip.total', 0)
|
||||||
|
loadedCount = _.get(node.sourceNode.myData, 'relatedEntity.ip.list', []).length
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'domain': {
|
||||||
|
iconClass = 'cn-icon cn-icon-subdomain'
|
||||||
|
title = node.isSubdomain() ? _this.$t('entities.subdomain') : _this.$t('entity.graph.resolveDomain')
|
||||||
|
total = _.get(node.sourceNode.myData, 'relatedEntity.domain.total', 0)
|
||||||
|
loadedCount = _.get(node.sourceNode.myData, 'relatedEntity.domain.list', []).length
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'app': {
|
||||||
|
iconClass = 'cn-icon cn-icon-app-name'
|
||||||
|
title = _this.$t('entities.tab.relatedApp')
|
||||||
|
total = _.get(node.sourceNode.myData, 'relatedEntity.app.total', 0)
|
||||||
|
loadedCount = _.get(node.sourceNode.myData, 'relatedEntity.app.list', []).length
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `<div class="primary-node-tooltip">
|
||||||
|
<div class="tooltip__header"><i class="${iconClass}"></i><span class="tooltip__title">${title}</span></div>
|
||||||
|
<div class="tooltip__content">
|
||||||
|
<span>${_this.$t('entity.graph.associatedCount')}: ${total}</span>
|
||||||
|
<span>${_this.$t('entity.graph.expandedEntityCount')}: ${loadedCount}</span>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
} else if (node.type === nodeType.entityNode || node.type === nodeType.rootNode) {
|
||||||
|
if (node.myData && node.myData.tags && node.myData.tags.length > 0) {
|
||||||
|
return `<div class="entity-node-tooltip">
|
||||||
|
<div class="tooltip__header">
|
||||||
|
<span class="tooltip__title">${node.id}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip__content">
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="header-icon"></div>
|
||||||
|
<span>${_this.$t('entity.graph.tags')}</span>
|
||||||
|
</div>
|
||||||
|
<div class="content-tag-list">${generateTagHTML(node.myData.tags)}</div>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
} else {
|
||||||
|
return `<div style="padding: 0 6px; font-size: 15px; line-height: 15px; color: #111;">${node.id}</div>`
|
||||||
|
}
|
||||||
|
} else if (node.type === nodeType.tempNode) {
|
||||||
|
return node.label
|
||||||
|
}
|
||||||
|
function generateTagHTML (tags) {
|
||||||
|
let html = ''
|
||||||
|
if (tags) {
|
||||||
|
tags.forEach(t => {
|
||||||
|
html += `<div class="entity-tag entity-tag--level-two-${t.type}">
|
||||||
|
<span>${t.value}</span>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
buildToolbar () {
|
||||||
|
const tc = document.createElement('div')
|
||||||
|
tc.id = 'toolbarContainer'
|
||||||
|
tc.className = 'graph-toolbar'
|
||||||
|
document.body.appendChild(tc)
|
||||||
|
const toolbar = new G6.ToolBar({
|
||||||
|
container: tc,
|
||||||
|
className: 'toolbar__tools',
|
||||||
|
getContent: () => {
|
||||||
|
return `<ul>
|
||||||
|
<li code='zoomOut'><i class="cn-icon cn-icon-zoom-in"></i></li>
|
||||||
|
<li code='zoomIn'><i class="cn-icon cn-icon-zoom-out"></i></li>
|
||||||
|
<li code='undo'><i class="cn-icon cn-icon-next-step"></i></li>
|
||||||
|
<li code='redo'><i class="cn-icon cn-icon-pre-step"></i></li>
|
||||||
|
<li code='autoZoom'><i class="cn-icon cn-icon-auto-zoom"></i></li>
|
||||||
|
</ul>`
|
||||||
|
},
|
||||||
|
handleClick: (code, graph) => {
|
||||||
|
toolbar.handleDefaultOperator(code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return toolbar
|
||||||
|
},
|
||||||
|
async expandList (nodeId) {
|
||||||
|
const node = this.graph.findById(nodeId)
|
||||||
|
const model = node.getModel()
|
||||||
|
const sourceNode = this.graph.findById(model.sourceNode.id)
|
||||||
|
const sourceModel = sourceNode.getModel()
|
||||||
|
const expandType = sourceModel.myData.entityType
|
||||||
|
if (sourceModel.myData.relatedEntity[expandType].list.length < 50) {
|
||||||
|
const entities = await queryRelatedEntity(sourceModel, expandType)
|
||||||
|
sourceModel.myData.relatedEntity[expandType].list.push(...entities.list)
|
||||||
|
entities.list.forEach(entity => {
|
||||||
|
const entityNodeModel = new Node(nodeType.entityNode, entity.vertex, {
|
||||||
|
entityType: expandType,
|
||||||
|
entityName: entity.vertex,
|
||||||
|
x: model.x + Math.random() * 500 - 250,
|
||||||
|
y: model.y + Math.random() * 500 - 250
|
||||||
|
}, model)
|
||||||
|
this.graph.addItem('node', entityNodeModel)
|
||||||
|
const edge = new Edge(model, entityNodeModel)
|
||||||
|
this.graph.addItem('edge', edge)
|
||||||
|
this.graph.layout()
|
||||||
|
this.graph.setItemState(this.graph.findById(edge.id), 'mySelected', true)
|
||||||
|
})
|
||||||
|
this.rightBox.node = _.cloneDeep(model)
|
||||||
|
} else {
|
||||||
|
// TODO 提示超过50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async expandDetailList (nodeId, expandType) {
|
||||||
|
const node = this.graph.findById(nodeId)
|
||||||
|
if (node) {
|
||||||
|
const nodeModel = node.getModel()
|
||||||
|
if (nodeModel.myData.relatedEntity[expandType].list.length < 50) {
|
||||||
|
const entities = await queryRelatedEntity(nodeModel, expandType)
|
||||||
|
nodeModel.myData.relatedEntity[expandType].list.push(...entities.list)
|
||||||
|
const neighbors = node.getNeighbors('target')
|
||||||
|
const listNode = neighbors.find(n => n.getModel().myData.entityType === expandType)
|
||||||
|
entities.list.forEach(entity => {
|
||||||
|
const entityNodeModel = new Node(nodeType.entityNode, entity.vertex, {
|
||||||
|
entityType: expandType,
|
||||||
|
entityName: entity.vertex,
|
||||||
|
x: listNode.getModel().x + Math.random() * 500 - 250,
|
||||||
|
y: listNode.getModel().y + Math.random() * 500 - 250
|
||||||
|
}, listNode.getModel())
|
||||||
|
this.graph.addItem('node', entityNodeModel)
|
||||||
|
this.graph.addItem('edge', new Edge(listNode.getModel(), entityNodeModel))
|
||||||
|
this.graph.layout()
|
||||||
|
})
|
||||||
|
this.rightBox.node = _.cloneDeep(nodeModel)
|
||||||
|
} else {
|
||||||
|
// TODO 提示超过50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted () {
|
||||||
|
if (this.entity.entityType && this.entity.entityName) {
|
||||||
|
await this.init()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup () {
|
||||||
|
const route = useRoute()
|
||||||
|
const { entityType, entityName } = route.query
|
||||||
|
const entity = {
|
||||||
|
entityType,
|
||||||
|
entityName
|
||||||
|
}
|
||||||
|
// 关系图
|
||||||
|
const graph = shallowRef(null)
|
||||||
|
|
||||||
|
const rightBox = ref({
|
||||||
|
mode: 'detail', // list | detail
|
||||||
|
show: true,
|
||||||
|
node: null,
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
entity,
|
||||||
|
rightBox,
|
||||||
|
graph
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
418
src/views/entityExplorer/entityGraph/GraphEntityDetail.vue
Normal file
418
src/views/entityExplorer/entityGraph/GraphEntityDetail.vue
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
<template>
|
||||||
|
<!--title-->
|
||||||
|
<div class="graph-detail-basic-info">
|
||||||
|
<div style="display: flex">
|
||||||
|
<div class="graph-detail__icon"><i :class="iconClass"></i></div>
|
||||||
|
|
||||||
|
<div class="graph-detail-header">
|
||||||
|
<div class="entity-graph-type">{{ $_.get(node, 'myData.entityType') ? entityType[$_.get(node, 'myData.entityType')] : '-' }}</div>
|
||||||
|
<div class="graph-basic-info">
|
||||||
|
<div class="graph-basic-info-name__block">
|
||||||
|
<div class="graph-basic-info-name" :title="$_.get(node, 'id', '')" id="entityName">{{ $_.get(node, 'id', '') }}</div>
|
||||||
|
<div class="graph-basic-info-icon" @click="copyEntityName">
|
||||||
|
<i class="cn-icon cn-icon-copy"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<i class="cn-icon cn-icon-close graph-close" @click="closeBlock"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--basic info-->
|
||||||
|
<div class="digital-certificate graph-basic-info__block">
|
||||||
|
<div class="digital-certificate-header padding-b-10">
|
||||||
|
<div class="digital-certificate-header__icon graph-header__icon"></div>
|
||||||
|
<div class="graph-basic-info__block-title">
|
||||||
|
{{ $t('overall.basicInfo') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="graph-basic-info__block-content">
|
||||||
|
<div class="graph-content-item" v-for="detail in detailCards" :key="detail.name">
|
||||||
|
<div class="graph-content-item-label">{{ detail.label }}:</div>
|
||||||
|
<div class="graph-content-item-value">{{ detail.value || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--关系拓展-->
|
||||||
|
<div class="digital-certificate graph-basic-info__block">
|
||||||
|
<loading :loading="loading" size="small"></loading>
|
||||||
|
<div class="digital-certificate-header padding-b-10">
|
||||||
|
<div class="digital-certificate-header__icon graph-header__icon"></div>
|
||||||
|
<div class="graph-basic-info__block-title">
|
||||||
|
{{ $t('entity.graph.relationshipExpansion') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="graph-basic-info__block-content" style="margin-top: -4px;">
|
||||||
|
<div v-for="item in relationList" :key="item.name">
|
||||||
|
<div class="graph-content-item graph-content-relationship-item" v-if="item.value === item.total">
|
||||||
|
<div class="graph-relationship-item-label">
|
||||||
|
<i class="margin-r-6" :class="item.icon"></i>
|
||||||
|
<span>{{ item.label }}:</span>
|
||||||
|
</div>
|
||||||
|
<div class="graph-relationship-item-value">
|
||||||
|
<span class="margin-r-6">{{ item.value }}/{{ item.total }}</span>
|
||||||
|
<i class="cn-icon cn-icon-expand-relationship" :style="{color: iconColor(item.value, item.total)}"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="graph-content-item graph-content-relationship-item">
|
||||||
|
<div class="graph-relationship-item-label">
|
||||||
|
<i class="graph-relationship-item-label-icon margin-r-6" :class="item.icon"></i>
|
||||||
|
<span>{{ item.label }}:</span>
|
||||||
|
</div>
|
||||||
|
<div class="graph-relationship-item-value">
|
||||||
|
<span class="margin-r-6">{{ item.value }}/{{ item.total }}</span>
|
||||||
|
<i class="cn-icon cn-icon-expand-relationship graph-expand-relationship__icon" :style="{color: iconColor(item.value, item.total)}" @click="expandRelation(item.name)"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--标签-->
|
||||||
|
<div v-if="$_.get(node, 'myData.tags', []).length > 0" class="digital-certificate graph-basic-info__block">
|
||||||
|
<div class="digital-certificate-header padding-b-10">
|
||||||
|
<div class="digital-certificate-header__icon graph-header__icon"></div>
|
||||||
|
<div class="graph-basic-info__block-title">
|
||||||
|
{{ $t('entity.graph.tags') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="entity-detail graph-basic-info__block-content">
|
||||||
|
<div class="graph-tag-list">
|
||||||
|
<div v-for="ic in $_.get(node, 'myData.tags', [])" :key="ic.value">
|
||||||
|
<div class="entity-tag graph-tag-item" :class="`entity-tag--level-two-${ic.type}`">
|
||||||
|
<span>{{ic.value}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { copySelectionText, selectElementText } from '@/utils/tools'
|
||||||
|
import { entityType, riskLevelMapping } from '@/utils/constants'
|
||||||
|
import chartMixin from '@/views/charts2/chart-mixin'
|
||||||
|
import { dateFormatByAppearance } from '@/utils/date-util'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import Loading from '@/components/common/Loading'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GraphEntityDetail',
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mixins: [chartMixin],
|
||||||
|
components: {
|
||||||
|
Loading
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
entityType
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
appRisk () {
|
||||||
|
return function (level) {
|
||||||
|
const m = riskLevelMapping.find(mapping => {
|
||||||
|
return mapping.value == level
|
||||||
|
})
|
||||||
|
return (m && m.name) || level
|
||||||
|
}
|
||||||
|
},
|
||||||
|
iconClass () {
|
||||||
|
let className
|
||||||
|
switch (_.get(this.node, 'myData.entityType', '')) {
|
||||||
|
case ('ip'): {
|
||||||
|
className = 'cn-icon cn-icon-ip2'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ('domain'): {
|
||||||
|
className = 'cn-icon cn-icon-domain2'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case ('app'): {
|
||||||
|
className = 'cn-icon cn-icon-app2'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return className
|
||||||
|
},
|
||||||
|
handleDate () {
|
||||||
|
return function (key) {
|
||||||
|
const date = _.get(this.node, key, '')
|
||||||
|
return date ? dateFormatByAppearance(date) : '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
node: {
|
||||||
|
deep: true,
|
||||||
|
handler (n) {
|
||||||
|
this.handleDetailData(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.handleDetailData(this.node)
|
||||||
|
},
|
||||||
|
setup (props) {
|
||||||
|
const detailCards = ref([])
|
||||||
|
const relationList = ref([])
|
||||||
|
const tagList = ref([])
|
||||||
|
|
||||||
|
return {
|
||||||
|
detailCards,
|
||||||
|
relationList,
|
||||||
|
tagList
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** 复制实体名称 */
|
||||||
|
copyEntityName () {
|
||||||
|
selectElementText(document.getElementById('entityName'))
|
||||||
|
if (copySelectionText()) {
|
||||||
|
this.$message.success(this.$t('tip.copySuccess'))
|
||||||
|
} else {
|
||||||
|
this.$message.error('Unknown error')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 修改关系拓展图标颜色,全部拓展浅灰色,否则深灰色 */
|
||||||
|
iconColor (length, total) {
|
||||||
|
if (length < total) {
|
||||||
|
if (length === 50) {
|
||||||
|
return 'rgba(57, 57, 57, 0.5)'
|
||||||
|
} else {
|
||||||
|
return 'rgba(57, 57, 57, 1)'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 'rgba(57, 57, 57, 0.5)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 关闭右侧详情栏
|
||||||
|
closeBlock () {
|
||||||
|
this.$emit('closeBlock')
|
||||||
|
},
|
||||||
|
/** 构造地址,国-省市-市 */
|
||||||
|
handleLocation (data) {
|
||||||
|
const location = []
|
||||||
|
if (data.country) {
|
||||||
|
location.push(data.country)
|
||||||
|
}
|
||||||
|
if (data.province) {
|
||||||
|
location.push(data.province)
|
||||||
|
}
|
||||||
|
if (data.city) {
|
||||||
|
location.push(data.city)
|
||||||
|
}
|
||||||
|
return location.join(' - ')
|
||||||
|
},
|
||||||
|
/** 关系拓展 */
|
||||||
|
expandRelation (name) {
|
||||||
|
this.$emit('expandDetailList', this.node.id, name)
|
||||||
|
},
|
||||||
|
httpError (e) {
|
||||||
|
this.isNoData = false
|
||||||
|
this.showError = true
|
||||||
|
this.errorMsg = this.errorMsgHandler(e)
|
||||||
|
},
|
||||||
|
location (detailData) {
|
||||||
|
let location = ''
|
||||||
|
if (detailData) {
|
||||||
|
const data = detailData.basicInfo.location
|
||||||
|
if (data) {
|
||||||
|
if (data.city) {
|
||||||
|
location = data.city
|
||||||
|
}
|
||||||
|
if (data.province) {
|
||||||
|
location = location ? `${data.province}, ${location}` : data.province
|
||||||
|
}
|
||||||
|
if (data.country) {
|
||||||
|
location = location ? `${data.country}, ${location}` : data.country
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return location || '-'
|
||||||
|
},
|
||||||
|
handleDetailData (node) {
|
||||||
|
const n = node
|
||||||
|
const type = _.get(n, 'myData.entityType', '')
|
||||||
|
switch (type) {
|
||||||
|
case 'ip': {
|
||||||
|
this.detailCards = [
|
||||||
|
{ name: 'asn', label: this.$t('entities.asNumber'), value: _.get(n, 'myData.basicInfo.asn.asn', '-') },
|
||||||
|
{
|
||||||
|
name: 'asOrg',
|
||||||
|
label: this.$t('entities.asOrg'),
|
||||||
|
value: _.get(n.myData, 'basicInfo.asn.organization', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isp',
|
||||||
|
label: this.$t('entities.graph.isp'),
|
||||||
|
value: _.get(n.myData, 'basicInfo.location.isp', '-')
|
||||||
|
},
|
||||||
|
{ name: 'location', label: this.$t('overall.location'), value: this.location(n.myData) }
|
||||||
|
]
|
||||||
|
this.relationList = [
|
||||||
|
{
|
||||||
|
icon: 'cn-icon cn-icon-subdomain',
|
||||||
|
name: 'domain',
|
||||||
|
label: this.$t('entity.graph.resolveDomain'),
|
||||||
|
value: _.get(n.myData, 'relatedEntity.domain.list', []).length,
|
||||||
|
total: _.get(n.myData, 'relatedEntity.domain.total', 0) || 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'cn-icon cn-icon-app-name',
|
||||||
|
name: 'app',
|
||||||
|
label: this.$t('entities.tab.relatedApp'),
|
||||||
|
value: _.get(n.myData, 'relatedEntity.app.list', []).length,
|
||||||
|
total: _.get(n.myData, 'relatedEntity.app.total', '0') || 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'domain': {
|
||||||
|
const expireDate = _.get(n.myData, 'basicInfo.whois.expireDate', '')
|
||||||
|
const createDate = _.get(n.myData, 'basicInfo.whois.createDate', '')
|
||||||
|
this.detailCards = [
|
||||||
|
{
|
||||||
|
name: 'categoryName',
|
||||||
|
label: this.$t('entities.category'),
|
||||||
|
value: _.get(n.myData, 'basicInfo.category.name', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categoryGroup',
|
||||||
|
label: this.$t('entities.group'),
|
||||||
|
value: _.get(n.myData, 'basicInfo.category.group', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reputationLevel',
|
||||||
|
label: this.$t('entities.creditLevel2'),
|
||||||
|
value: _.get(n.myData, 'basicInfo.category.reputationLevel', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'expireDate',
|
||||||
|
label: this.$t('entities.graph.expirationDate'),
|
||||||
|
value: expireDate ? dateFormatByAppearance(expireDate) : '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'registrarName',
|
||||||
|
label: this.$t('entities.registrar'),
|
||||||
|
value: _.get(n.myData, 'basicInfo.whois.registrarName', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'registrantOrg',
|
||||||
|
label: this.$t('entities.registry'),
|
||||||
|
value: _.get(n.myData, 'basicInfo.whois.registrantOrg', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'registrantCountry',
|
||||||
|
label: this.$t('entities.registrationCountry'),
|
||||||
|
value: _.get(n.myData, 'basicInfo.whois.registrantCountry', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'createDate',
|
||||||
|
label: this.$t('entities.registrationDate'),
|
||||||
|
value: createDate ? dateFormatByAppearance(createDate) : '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'email',
|
||||||
|
label: this.$t('entities.registryEmail'),
|
||||||
|
value: _.get(n.myData, 'basicInfo.whois.email', '-')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
this.relationList = [
|
||||||
|
{
|
||||||
|
icon: 'cn-icon cn-icon-resolve-ip',
|
||||||
|
name: 'ip',
|
||||||
|
label: this.$t('entities.graph.resolveIp'),
|
||||||
|
value: _.get(n.myData, 'relatedEntity.ip.list', []).length,
|
||||||
|
total: _.get(n.myData, 'relatedEntity.ip.total', '0') || 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'cn-icon cn-icon-subdomain',
|
||||||
|
name: 'domain',
|
||||||
|
label: this.$t('entities.subdomain'),
|
||||||
|
value: _.get(n.myData, 'relatedEntity.domain.list', []).length,
|
||||||
|
total: _.get(n.myData, 'relatedEntity.domain.total', '0') || 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'cn-icon cn-icon-app-name',
|
||||||
|
name: 'app',
|
||||||
|
label: this.$t('entities.tab.relatedApp'),
|
||||||
|
value: _.get(n.myData, 'relatedEntity.app.list', []).length,
|
||||||
|
total: _.get(n.myData, 'relatedEntity.app.total', 0) || 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'app': {
|
||||||
|
this.detailCards = [
|
||||||
|
{
|
||||||
|
name: 'appCategory',
|
||||||
|
label: this.$t('entities.category'),
|
||||||
|
value: _.get(n.myData, 'detail.category.appCategory', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'appSubcategory',
|
||||||
|
label: this.$t('entities.subcategory'),
|
||||||
|
value: _.get(n.myData, 'detail.category.appSubcategory', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'appRisk',
|
||||||
|
label: this.$t('entities.riskLevel'),
|
||||||
|
value: _.get(n.myData, 'detail.category.appRisk', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'appTechnology',
|
||||||
|
label: this.$t('overall.technology'),
|
||||||
|
value: _.get(n.myData, 'detail.category.appTechnology', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'appLongname',
|
||||||
|
label: this.$t('overall.appFullName'),
|
||||||
|
value: _.get(n.myData, 'detail.category.appLongname', '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'appDescription',
|
||||||
|
label: this.$t('config.dataSource.description'),
|
||||||
|
value: _.get(n.myData, 'detail.category.appDescription', '-')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
this.relationList = [
|
||||||
|
{
|
||||||
|
icon: 'cn-icon cn-icon-resolve-ip',
|
||||||
|
name: 'ip',
|
||||||
|
label: this.$t('entities.tab.relatedIp'),
|
||||||
|
value: _.get(n.myData, 'relatedEntity.ip.list', []).length,
|
||||||
|
total: _.get(n.myData, 'relatedEntity.ip.total', '0') || 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'cn-icon cn-icon-subdomain',
|
||||||
|
name: 'domain',
|
||||||
|
label: this.$t('entities.graph.relatedDomain'),
|
||||||
|
value: _.get(n.myData, 'relatedEntity.domain.list', []).length,
|
||||||
|
total: _.get(n.myData, 'relatedEntity.domain.total', '0') || 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
189
src/views/entityExplorer/entityGraph/GraphEntityList.vue
Normal file
189
src/views/entityExplorer/entityGraph/GraphEntityList.vue
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<loading :loading="loading" size="small"></loading>
|
||||||
|
<div class="graph-list-header">
|
||||||
|
<div>
|
||||||
|
<div class="graph-list-header-title">
|
||||||
|
<i class="cn-icon cn-icon-resolve-ip graph-list-header-icon"></i>
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="graph-list-header-number">
|
||||||
|
{{ $t('entity.graph.associatedCount') }}: <span>{{$_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.total', '-')}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<i class="cn-icon cn-icon-close graph-close" @click="closeBlock"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="graph-list-expand-btn-block">
|
||||||
|
<div class="graph-list-expand-btn" :class="{ 'graph-list-expand-btn--disabled': expandBtnDisable }" style="display: inline-flex;" @click="expandList">
|
||||||
|
<i class="cn-icon cn-icon-expand-continue graph-expand-continue"></i>
|
||||||
|
{{ $t('entity.graph.continueToExpand') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="digital-certificate">
|
||||||
|
<div class="digital-certificate-header padding-b-16">
|
||||||
|
<div class="digital-certificate-header__icon graph-header__icon"></div>
|
||||||
|
<div class="graph-list-content-header ">
|
||||||
|
{{ $t('entity.graph.expandedEntityCount') }}:
|
||||||
|
<span>{{$_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.list', []).length}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="graph-list-content">
|
||||||
|
<div v-for="(item, index) in $_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.list', [])" :key="index">
|
||||||
|
<div class="graph-list-item-ip"><span @click="expandDetail">{{ item.vertex }}</span></div>
|
||||||
|
<template v-if="$_.get(node, 'myData.entityType', '') === 'ip'">
|
||||||
|
<div class="graph-list-item-block">
|
||||||
|
<div class="graph-list-item padding-b-4">
|
||||||
|
<div class="graph-list-item-label">{{ $t('overall.location') }}:</div>
|
||||||
|
<div class="graph-list-item-value graph-list-item-value1" style="display: flex;align-items: center;">
|
||||||
|
<!-- <img :src="require(`../../../../public/images/flag/${item.flag}.svg`)" class="graph-list-country-flag"/>-->
|
||||||
|
<span>{{ $_.get(item.detail, 'location.city', '-') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="graph-list-item">
|
||||||
|
<div class="graph-list-item-label">ASN:</div>
|
||||||
|
<div class="graph-list-item-value">{{ $_.get(item.detail, 'asn.asn', '-') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="$_.get(node, 'myData.entityType', '') === 'domain'">
|
||||||
|
<div class="graph-list-item-block">
|
||||||
|
<div class="graph-list-item__app">
|
||||||
|
<div class="graph-list-item-label__app">{{$t('entities.category')}}:</div>
|
||||||
|
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.name', '-') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="graph-list-item__app">
|
||||||
|
<div class="graph-list-item-label__app">{{$t('entities.group')}}:</div>
|
||||||
|
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.group', '-') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="graph-list-item__app">
|
||||||
|
<div class="graph-list-item-label__app">{{$t('entities.registration')}}:</div>
|
||||||
|
<div class="graph-list-item-value">{{ $_.get(item.detail, 'whois.registrantCountry', '-') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="graph-list-item__app">
|
||||||
|
<div class="graph-list-item-label__app">{{$t('entities.registry')}}:</div>
|
||||||
|
<div class="graph-list-item-value">{{ $_.get(item.detail, 'whois.registrantOrg', '-') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="$_.get(node, 'myData.entityType', '') === 'app'">
|
||||||
|
<div class="graph-list-item-block">
|
||||||
|
<div class="graph-list-item__app">
|
||||||
|
<div class="graph-list-item-label__app">APP ID:</div>
|
||||||
|
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appId', '-') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="graph-list-item__app">
|
||||||
|
<div class="graph-list-item-label__app">{{$t('entities.category')}}:</div>
|
||||||
|
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appCategory', '-') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="graph-list-item__app">
|
||||||
|
<div class="graph-list-item-label__app">{{$t('entities.subcategory')}}:</div>
|
||||||
|
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appSubcategory', '-') }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="graph-list-item__app">
|
||||||
|
<div class="graph-list-item-label__app">{{$t('entities.riskLevel')}}:</div>
|
||||||
|
<div class="graph-list-item-value">{{ appRisk($_.get(item.detail, 'category.appRisk', '-')) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="graph-list-item__app">
|
||||||
|
<div class="graph-list-item-label__app">{{$t('config.dataSource.description')}}:</div>
|
||||||
|
<div class="graph-list-item-value">{{ $_.get(item.detail, 'category.appDescription', '-') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="graph-list-dividing-line" v-if="index !== $_.get(node, 'sourceNode.myData.relatedEntity.' + $_.get(node, 'myData.entityType') + '.list', []).length - 1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Loading from '@/components/common/Loading'
|
||||||
|
import { riskLevelMapping } from '@/utils/constants'
|
||||||
|
import { scrollToTop } from '@/utils/tools'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GraphEntityList',
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Loading
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
expandBtnDisable () {
|
||||||
|
const loaded = _.get(this.node, 'sourceNode.myData.relatedEntity.' + _.get(this.node, 'myData.entityType') + '.list', []).length
|
||||||
|
const total = _.get(this.node, 'sourceNode.myData.relatedEntity.' + _.get(this.node, 'myData.entityType') + '.total', 0)
|
||||||
|
return !(loaded < total && total > 10 && (loaded && loaded < 50))
|
||||||
|
},
|
||||||
|
appRisk () {
|
||||||
|
return function (level) {
|
||||||
|
const m = riskLevelMapping.find(mapping => {
|
||||||
|
return mapping.value == level
|
||||||
|
})
|
||||||
|
return (m && m.name) || level
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title () {
|
||||||
|
let title = ''
|
||||||
|
if (this.node) {
|
||||||
|
const entityType = _.get(this.node, 'myData.entityType', '')
|
||||||
|
if (entityType) {
|
||||||
|
const sourceType = _.get(this.node, 'sourceNode.myData.entityType', '')
|
||||||
|
switch (entityType) {
|
||||||
|
case 'ip': {
|
||||||
|
if (sourceType === 'domain') {
|
||||||
|
title = this.$t('entities.graph.resolveIp')
|
||||||
|
} else {
|
||||||
|
title = this.$t('entities.tab.relatedIp')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'domain': {
|
||||||
|
if (sourceType === 'ip') {
|
||||||
|
title = this.$t('entity.graph.resolveDomain')
|
||||||
|
} else if (sourceType === 'domain') {
|
||||||
|
title = this.$t('entities.subdomain')
|
||||||
|
} else {
|
||||||
|
title = this.$t('entities.relatedDomain')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'app': {
|
||||||
|
title = this.$t('entities.tab.relatedApp')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeBlock () {
|
||||||
|
this.$emit('closeBlock')
|
||||||
|
},
|
||||||
|
expandDetail () {
|
||||||
|
this.$emit('expandDetail', this.node.id)
|
||||||
|
},
|
||||||
|
expandList () {
|
||||||
|
// 继续拓展ip列表,传递信息,调用接口
|
||||||
|
this.$emit('expandList', _.get(this.node, 'id'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
scrollToTop()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
52
src/views/entityExplorer/entityGraph/edge.js
Normal file
52
src/views/entityExplorer/entityGraph/edge.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import G6 from '@antv/g6'
|
||||||
|
|
||||||
|
export default class Edge {
|
||||||
|
constructor (sourceNode, targetNode, type) {
|
||||||
|
this.id = sourceNode.id + '__' + targetNode.id
|
||||||
|
this.source = sourceNode.id
|
||||||
|
this.target = targetNode.id
|
||||||
|
this.isTemp = type === 'temp'
|
||||||
|
this.style = type === 'temp' ? tempStyles.style : normalStyles.style
|
||||||
|
this.stateStyles = type === 'temp' ? tempStyles.stateStyles : normalStyles.stateStyles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalStyles = {
|
||||||
|
style: {
|
||||||
|
stroke: '#BEBEBE',
|
||||||
|
endArrow: {
|
||||||
|
path: G6.Arrow.triangle(5, 5),
|
||||||
|
fill: '#BEBEBE'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stateStyles: {
|
||||||
|
mySelected: {
|
||||||
|
stroke: '#778391',
|
||||||
|
endArrow: {
|
||||||
|
path: G6.Arrow.triangle(5, 5),
|
||||||
|
fill: '#778391'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tempStyles = {
|
||||||
|
style: {
|
||||||
|
endArrow: {
|
||||||
|
path: G6.Arrow.triangle(5, 5),
|
||||||
|
fill: '#DDD'
|
||||||
|
},
|
||||||
|
stroke: '#DDD',
|
||||||
|
lineDash: [3, 2]
|
||||||
|
},
|
||||||
|
stateStyles: {
|
||||||
|
mySelected: {
|
||||||
|
stroke: '#DDD',
|
||||||
|
endArrow: {
|
||||||
|
path: G6.Arrow.triangle(5, 5),
|
||||||
|
fill: '#DDD'
|
||||||
|
},
|
||||||
|
lineDash: [3, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
182
src/views/entityExplorer/entityGraph/node.js
Normal file
182
src/views/entityExplorer/entityGraph/node.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import _ from 'lodash'
|
||||||
|
import i18n from '@/i18n'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { api } from '@/utils/api'
|
||||||
|
import { entityDetailTags, psiphon3IpType } from '@/utils/constants'
|
||||||
|
|
||||||
|
export default class Node {
|
||||||
|
/*
|
||||||
|
* type: 对应nodeType
|
||||||
|
* cfg: { entityType, entityName, x, y, fx, fy }
|
||||||
|
* */
|
||||||
|
constructor (type, id, cfg, sourceNode) {
|
||||||
|
this.type = type
|
||||||
|
this.id = id
|
||||||
|
this.x = _.get(cfg, 'x', null)
|
||||||
|
this.y = _.get(cfg, 'y', null)
|
||||||
|
this.fx = _.get(cfg, 'fx', null)
|
||||||
|
this.fy = _.get(cfg, 'fy', null)
|
||||||
|
this.myData = {
|
||||||
|
entityType: cfg.entityType,
|
||||||
|
entityName: cfg.entityName
|
||||||
|
}
|
||||||
|
if (sourceNode) {
|
||||||
|
this.sourceNode = sourceNode
|
||||||
|
}
|
||||||
|
this.label = this.generateLabel()
|
||||||
|
}
|
||||||
|
|
||||||
|
generateLabel () {
|
||||||
|
switch (this.type) {
|
||||||
|
case nodeType.rootNode:
|
||||||
|
case nodeType.entityNode: {
|
||||||
|
return this.id
|
||||||
|
}
|
||||||
|
case nodeType.listNode: {
|
||||||
|
return `${this.getLabelText()}(${_.get(this.sourceNode.myData, 'relatedEntity.' + this.myData.entityType + '.total', 0)})`
|
||||||
|
}
|
||||||
|
case nodeType.tempNode: {
|
||||||
|
return this.getLabelText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabelText () {
|
||||||
|
if (this.myData.entityType === 'ip') {
|
||||||
|
if (this.sourceNode.myData.entityType === 'app') {
|
||||||
|
return i18n.global.t('entities.tab.relatedIp')
|
||||||
|
} else if (this.sourceNode.myData.entityType === 'domain') {
|
||||||
|
return i18n.global.t('entities.graph.resolveIp')
|
||||||
|
}
|
||||||
|
} else if (this.myData.entityType === 'domain') {
|
||||||
|
if (this.sourceNode.myData.entityType === 'ip') {
|
||||||
|
return i18n.global.t('entities.graph.resolvedDomain')
|
||||||
|
} else if (this.sourceNode.myData.entityType === 'app') {
|
||||||
|
return i18n.global.t('entities.graph.relatedDomain')
|
||||||
|
} else if (this.sourceNode.myData.entityType === 'domain') {
|
||||||
|
return i18n.global.t('entities.subdomain')
|
||||||
|
}
|
||||||
|
} else if (this.myData.entityType === 'app') {
|
||||||
|
return i18n.global.t('entities.tab.relatedApp')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubdomain () {
|
||||||
|
if (this.sourceNode) {
|
||||||
|
return this.sourceNode.myData.entityType === 'domain' && this.myData.entityType === 'domain'
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询basicInfo、tags、关联实体数量
|
||||||
|
async queryDetailData () {
|
||||||
|
const entityType = this.myData.entityType
|
||||||
|
const entityName = this.myData.entityName
|
||||||
|
|
||||||
|
this.myData.basicInfo = await this.queryEntityBasicInfo(entityType, entityName)
|
||||||
|
|
||||||
|
const tags = await this.queryTags(entityType, entityName)
|
||||||
|
let _tags = []
|
||||||
|
Object.keys(tags).forEach(k => {
|
||||||
|
if (k !== 'userDefinedTags' && tags[k]) {
|
||||||
|
Object.keys(tags[k]).forEach(k2 => {
|
||||||
|
const find = entityDetailTags[entityType].find(t => t.name === k2)
|
||||||
|
if (find) {
|
||||||
|
_tags.push({ key: k2, value: this.tagValueHandler(k, k2, tags[k][k2]), type: find.type })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (_.isArray(tags.userDefinedTags)) {
|
||||||
|
_tags = _.concat(_tags, tags.userDefinedTags.map(tag => ({ value: tag.tagValue, type: 'normal' })))
|
||||||
|
}
|
||||||
|
this.myData.tags = _tags
|
||||||
|
|
||||||
|
const relatedEntityTotalCount = await this.queryRelatedEntityCount(entityType, entityName)
|
||||||
|
this.myData.relatedEntity = {
|
||||||
|
ip: { total: relatedEntityTotalCount.ipCount, list: [] },
|
||||||
|
domain: { total: this.myData.entityType === 'domain' ? relatedEntityTotalCount.subDomainCount : relatedEntityTotalCount.domainCount, list: [] },
|
||||||
|
app: { total: relatedEntityTotalCount.appCount, list: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryEntityBasicInfo (entityType, entityName) {
|
||||||
|
const response = await axios.get(`${api.entity.entityGraph.basicInfo}/${entityType}?resource=${entityName}`).catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
return response.data.data
|
||||||
|
} else {
|
||||||
|
console.error(response)
|
||||||
|
throw response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryTags (entityType, entityName) {
|
||||||
|
const response = await axios.get(`${api.entity.entityGraph.tags}/${entityType}?resource=${entityName}`).catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
return response.data.data
|
||||||
|
} else {
|
||||||
|
console.error(response)
|
||||||
|
throw response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async queryRelatedEntityCount (entityType, entityName) {
|
||||||
|
const response = await axios.get(`${api.entity.entityGraph.relatedEntityCount}/${entityType}?resource=${entityName}`).catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
return response.data.data
|
||||||
|
} else {
|
||||||
|
console.error(response)
|
||||||
|
throw response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tagValueHandler (k, k2, value) {
|
||||||
|
if (k === 'psiphon3Ip') {
|
||||||
|
if (k2 === 'type') {
|
||||||
|
const find = psiphon3IpType.find(t => t.value === value)
|
||||||
|
if (find) {
|
||||||
|
return find.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const nodeType = {
|
||||||
|
rootNode: 'rootNode',
|
||||||
|
listNode: 'listNode',
|
||||||
|
entityNode: 'entityNode',
|
||||||
|
tempNode: 'tempNode'
|
||||||
|
}
|
||||||
|
export async function queryRelatedEntity (node, targetEntityType) {
|
||||||
|
let _targetEntityType = targetEntityType
|
||||||
|
if (node.myData.entityType === 'domain' && targetEntityType === 'domain') {
|
||||||
|
_targetEntityType = 'subdomain'
|
||||||
|
}
|
||||||
|
let url = `${api.entity.entityGraph[`${node.myData.entityType}Related${_.upperFirst(_targetEntityType)}`]}?resource=${node.myData.entityName}&pageSize=10`
|
||||||
|
const current = node.myData.relatedEntity[targetEntityType].list.length
|
||||||
|
const pageNo = parseInt(current / 10) + 1
|
||||||
|
url += `&pageNo=${pageNo}`
|
||||||
|
const response = await axios.get(url).catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
if (response.data && response.data.code === 200) {
|
||||||
|
return response.data.data
|
||||||
|
} else {
|
||||||
|
console.error(response)
|
||||||
|
throw response
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user