Merge branch 'dev-force-graph' of https://git.mesalab.cn/cyber-narrator/cn-ui into dev-force-graph
Conflicts: package-lock.json package.json src/views/entityExplorer/EntityGraph.vue src/views/entityExplorer/entityGraph/link.js src/views/entityExplorer/entityGraph/node.js
This commit is contained in:
@@ -3,9 +3,9 @@
|
||||
class="entity-explorer"
|
||||
:class="{'entity-explorer--show-list': showList}">
|
||||
<!-- 顶部工具栏,在列表页显示 -->
|
||||
<div class="explorer-top-tools explorer-top-tools-new" style="margin: 2px 0;" v-show="showList">
|
||||
<div class="explorer-top-tools explorer-top-tools-new" v-show="showList">
|
||||
<div class="explorer-entity-top-tools">
|
||||
<div class="explorer-top-tools-title" style="padding: 0;margin-left: -10px;">{{$t('network.entity')}}</div>
|
||||
<div class="explorer-top-tools-title tools-title__margin">{{$t('network.entity')}}</div>
|
||||
<date-time-range
|
||||
class="date-time-range"
|
||||
:start-time="timeFilter.startTime"
|
||||
@@ -25,7 +25,7 @@
|
||||
@search="search"
|
||||
></explorer-search>
|
||||
<!-- 内容区 -->
|
||||
<div v-if="showList" style="display: flex;flex-direction: row;padding-bottom: 20px;">
|
||||
<div v-if="showList" class="explorer-content">
|
||||
<entity-filter
|
||||
:filter-data="newFilterData"
|
||||
:q="q"
|
||||
@@ -40,27 +40,20 @@
|
||||
@search="search"
|
||||
></explorer-search>
|
||||
|
||||
<div style="display: flex;flex-direction: column;height: calc(100% - 42px);">
|
||||
<div class="explorer-result" v-if="showList" style="position: relative;">
|
||||
<loading :loading="loadingCount" style="width: 240px"></loading>
|
||||
<span>{{ summaryCount.totalCount }} </span>{{$t('overall.results')}},IP
|
||||
<span>{{ summaryCount.ipCount }}</span>,{{$t('overall.domain')}}
|
||||
<span>{{ summaryCount.domainCount }}</span>,APP
|
||||
<span>{{ summaryCount.appCount }}</span>
|
||||
<div class="explorer-list">
|
||||
<div class="explorer-result" v-if="showList" style="position: relative;display: flex">
|
||||
<loading :loading="loadingCount" class="explorer-result-loading"></loading>
|
||||
<span>{{ summaryCount.totalCount }} </span>{{$t('overall.results')}},IP<span class="margin-r-3"></span>
|
||||
<span>{{ summaryCount.ipCount }}</span>,{{$t('overall.domain')}}<span class="margin-r-3"></span>
|
||||
<span>{{ summaryCount.domainCount }}</span>,APP<span class="margin-r-3"></span>
|
||||
<span>{{ summaryCount.appCount }}</span>,{{$t('overall.subscriber')}}<span class="margin-r-3"></span>
|
||||
<span>{{ summaryCount.subscriberCount }}</span>
|
||||
|
||||
<span class="entity-hide-entity" v-if="q">
|
||||
<span v-if="listData.length !== 0">
|
||||
<!-- <el-checkbox-->
|
||||
<!-- v-model="isHideRelatedEntities"-->
|
||||
<!-- :label="$t('entity.hideRelatedEntities')"-->
|
||||
<!-- :disabled="listData.length===0"-->
|
||||
<!-- @change="hideRelatedEntities"-->
|
||||
<!-- size="large" />-->
|
||||
<el-radio-group v-model="isHideRelatedEntities" @change="hideRelatedEntities" class="ml-4">
|
||||
<el-radio :label="true" size="large">{{ $t('entity.entityMode') }}</el-radio>
|
||||
<el-radio :label="false" style="margin-left: -10px;" size="large">{{ $t('entity.relatedMode') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</span>
|
||||
<el-radio-group v-model="isHideRelatedEntities" @change="hideRelatedEntities" class="ml-4">
|
||||
<el-radio :label="true" size="large">{{ $t('entity.entityMode') }}</el-radio>
|
||||
<el-radio :label="false" class="margin-l__10" size="large">{{ $t('entity.relatedMode') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -172,6 +165,36 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<div class="entity-overview">
|
||||
<div class="overview-left">
|
||||
<span class="overview-left-loading">
|
||||
<span class="overview-left-loading-span">{{ $t('overall.subscriber') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="overview-right">
|
||||
<div class="right-row margin-b-6">
|
||||
<i class="cn-icon cn-icon-proportion entity-explorer-total__icon"></i>
|
||||
<div class="right-label">{{ $t('network.total') }}</div>
|
||||
<div class="right-label-loading">
|
||||
<loading :loading="loadingIp" size="small"></loading>
|
||||
<div class="right-value">{{ numberWithCommas(entitySubscriberTotal) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-row">
|
||||
<i class="cn-icon cn-icon-active"></i>
|
||||
<div class="right-label">{{ $t('entity.active') }}</div>
|
||||
<div class="right-label-loading">
|
||||
<loading :loading="loadingSubscriberActive" size="small"></loading>
|
||||
<div class="right-value-block">
|
||||
<span class="margin-r-6">{{ numberWithCommas(entitySubscriberActive) }}</span>
|
||||
<span class="last-hour">{{ $t('entity.inLastHour') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,7 +217,7 @@ import {
|
||||
numberWithCommas
|
||||
} from '@/utils/tools'
|
||||
import Parser from '@/components/advancedSearch/meta/parser'
|
||||
import { handleErrorTip } from '@/components/advancedSearch/meta/error'
|
||||
import { handleErrorTip, invalidErrorTip } from '@/components/advancedSearch/meta/error'
|
||||
import { columnList } from '@/utils/static-data'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { columnType } from '@/components/advancedSearch/meta/meta'
|
||||
@@ -226,6 +249,10 @@ export default {
|
||||
entityIpNew: '-',
|
||||
entityIpActive: '-',
|
||||
|
||||
entitySubscriberTotal: '-',
|
||||
entitySubscriberNew: '-',
|
||||
entitySubscriberActive: '-',
|
||||
|
||||
newFilterData: [
|
||||
{
|
||||
icon: 'cn-icon cn-icon-registration-country',
|
||||
@@ -310,10 +337,12 @@ export default {
|
||||
loadingApp: false,
|
||||
loadingDomain: false,
|
||||
loadingIp: false,
|
||||
loadingSubscriber: false,
|
||||
// Active
|
||||
loadingAppActive: false,
|
||||
loadingDomainActive: false,
|
||||
loadingIpActive: false,
|
||||
loadingSubscriberActive: false,
|
||||
|
||||
initFlag: true, // 初始化标志,避免初始化时pageSize和pageNo会调用搜索
|
||||
timer: null, // 初始化标志的延时器,需要销毁
|
||||
@@ -321,7 +350,8 @@ export default {
|
||||
totalCount: 0,
|
||||
domainCount: 0,
|
||||
ipCount: 0,
|
||||
appCount: 0
|
||||
appCount: 0,
|
||||
subscriberCount: 0
|
||||
},
|
||||
loadingCount: false, // 实体基数统计的loading
|
||||
keywordList: []
|
||||
@@ -539,6 +569,21 @@ export default {
|
||||
},
|
||||
/** 新版查询filter数据 */
|
||||
queryFilterNew (params) {
|
||||
const subscriberList = ['subscriber.id', 'subscriber.phone_number', 'subscriber.imei', 'subscriber.imsi', 'subscriber.apn']
|
||||
let subscriberFlag = false
|
||||
subscriberList.forEach(item => {
|
||||
if (params.q.indexOf(item) > -1) {
|
||||
subscriberFlag = true
|
||||
}
|
||||
})
|
||||
if (subscriberFlag) {
|
||||
this.newFilterData.forEach(item => {
|
||||
item.loading = false
|
||||
item.firstLoad = false
|
||||
item.data = []
|
||||
})
|
||||
return true
|
||||
}
|
||||
const queryParams = {
|
||||
startTime: getSecond(params.startTime),
|
||||
endTime: getSecond(params.endTime),
|
||||
@@ -614,13 +659,15 @@ export default {
|
||||
this.listData = []
|
||||
this.$nextTick(() => {
|
||||
this.listData = response.data.data.list
|
||||
if (this.listData.length === 0) {
|
||||
/* if (this.listData.length === 0) {
|
||||
this.isHideRelatedEntities = false
|
||||
}
|
||||
} */
|
||||
})
|
||||
} else {
|
||||
this.$message.error(response.data.message)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.listData = []
|
||||
}).finally(() => {
|
||||
this.listLoading = false
|
||||
})
|
||||
@@ -639,11 +686,11 @@ export default {
|
||||
this.summaryCount = response.data.data
|
||||
this.pageObj.total = response.data.data.totalCount
|
||||
} else {
|
||||
this.summaryCount = { totalCount: 0, domainCount: 0, ipCount: 0, appCount: 0 }
|
||||
this.summaryCount = { totalCount: 0, domainCount: 0, ipCount: 0, appCount: 0, subscriberCount: 0 }
|
||||
}
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
this.summaryCount = { totalCount: 0, domainCount: 0, ipCount: 0, appCount: 0 }
|
||||
this.summaryCount = { totalCount: 0, domainCount: 0, ipCount: 0, appCount: 0, subscriberCount: 0 }
|
||||
}).finally(() => {
|
||||
this.loadingCount = false
|
||||
})
|
||||
@@ -672,20 +719,27 @@ export default {
|
||||
this.loadingApp = true
|
||||
this.loadingDomain = true
|
||||
this.loadingIp = true
|
||||
this.loadingSubscriber = true
|
||||
// Active
|
||||
this.loadingAppActive = true
|
||||
this.loadingDomainActive = true
|
||||
this.loadingIpActive = true
|
||||
this.loadingSubscriberActive = true
|
||||
|
||||
axios.get(api.entity.entityList.entityTotal).then(response => {
|
||||
if (response.status === 200) {
|
||||
this.entityDomainTotal = response.data.data.domainCount
|
||||
this.entityIpTotal = response.data.data.ipCount
|
||||
this.entityAppTotal = response.data.data.appCount
|
||||
this.entitySubscriberTotal = response.data.data.subscriberCount
|
||||
}
|
||||
}).catch((e) => {
|
||||
this.$message.error(e.response.data.message)
|
||||
}).finally(() => {
|
||||
this.loadingDomain = false
|
||||
this.loadingIp = false
|
||||
this.loadingApp = false
|
||||
this.loadingSubscriber = false
|
||||
})
|
||||
// Active
|
||||
axios.get(api.entity.entityList.entityActive).then(response => {
|
||||
@@ -693,10 +747,15 @@ export default {
|
||||
this.entityDomainActive = response.data.data.domainCount
|
||||
this.entityIpActive = response.data.data.ipCount
|
||||
this.entityAppActive = response.data.data.appCount
|
||||
this.entitySubscriberActive = response.data.data.subscriberCount
|
||||
}
|
||||
}).catch((e) => {
|
||||
this.$message.error(e.response.data.message)
|
||||
}).finally(() => {
|
||||
this.loadingDomainActive = false
|
||||
this.loadingIpActive = false
|
||||
this.loadingAppActive = false
|
||||
this.loadingSubscriberActive = false
|
||||
})
|
||||
},
|
||||
setListMode (mode) {
|
||||
@@ -747,7 +806,7 @@ export default {
|
||||
this.$message.error(handleErrorTip(errorList[0]))
|
||||
}
|
||||
} else {
|
||||
this.$message.error(this.$t('tip.invalidQueryField') + ' ' + keyInfo.key)
|
||||
this.$message.error(invalidErrorTip(keyInfo))
|
||||
}
|
||||
} else {
|
||||
this.search({ q: '', str: '', metaList: [] })
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
<div class="filter__body" style="position: relative">
|
||||
<loading :loading="item.loading" style="top: -5px;"></loading>
|
||||
<loading :loading="item.loading" class="filter__body-loading"></loading>
|
||||
|
||||
<div class="filter__body-item"
|
||||
v-for="(data, i) in item.data.slice(0, item.showNum)"
|
||||
@@ -37,17 +37,16 @@
|
||||
<div class="filter__body-item-right">{{ data.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div @click="showMoreFilter(item, index)"
|
||||
<span @click="showMoreFilter(item, index)"
|
||||
:class="item.showNum === item.data.length ? 'filter-no-show-more' : 'filter-show-more'">
|
||||
{{ $t('overall.showMore') }}
|
||||
</div>
|
||||
</span>
|
||||
<div class="filter-hr"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<loading v-else-if="isFirstLoad" :loading="isFirstLoad"></loading>
|
||||
<chart-no-data v-else style="padding-top: 40px"></chart-no-data>
|
||||
|
||||
<chart-no-data v-else class="padding-t-40"></chart-no-data>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -38,10 +38,112 @@ import GraphEntityDetail from '@/views/entityExplorer/entityGraph/GraphEntityDet
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import ForceGraph from 'force-graph'
|
||||
import Node, { nodeType } from './entityGraph/node'
|
||||
import Link, { linkType } from './entityGraph/link'
|
||||
import * as d3 from 'd3'
|
||||
import Node, {nodeType, queryRelatedEntity} from './entityGraph/node'
|
||||
import testData from './testData'
|
||||
import _ from 'lodash'
|
||||
import Link from './entityGraph/link'
|
||||
import Edge from '@/views/entityExplorer/entityGraph/edge'
|
||||
import {Algorithm} from '@antv/g6'
|
||||
|
||||
function getRootNodeStyle (entityType) {
|
||||
switch (entityType) {
|
||||
case 'ip': {
|
||||
return {
|
||||
iconWidth: 16,
|
||||
iconHeight: 13,
|
||||
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: 16,
|
||||
iconHeight: 16,
|
||||
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: 14,
|
||||
iconHeight: 16,
|
||||
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: 16,
|
||||
iconHeight: 13,
|
||||
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) {
|
||||
let iconWidth = 14
|
||||
let iconHeight = 12
|
||||
switch (entityType) {
|
||||
case 'ip': {
|
||||
iconWidth = 14
|
||||
iconHeight = 12
|
||||
break
|
||||
}
|
||||
case 'domain': {
|
||||
iconWidth = 14
|
||||
iconHeight = 14
|
||||
break
|
||||
}
|
||||
case 'app': {
|
||||
iconWidth = 12
|
||||
iconHeight = 14
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
iconWidth,
|
||||
iconHeight,
|
||||
borderColor: 'rgba(119,131,145,0.5)',
|
||||
selectedBorderColor: 'rgba(119,131,145,0.8)',
|
||||
hoveredShadowColor: 'rgba(151,151,151,0.21)',
|
||||
selectedShadowColor: 'rgba(151,151,151,0.4)'
|
||||
}
|
||||
}
|
||||
function getEntityNodeStyle (entityType) {
|
||||
let iconWidth = 14
|
||||
let iconHeight = 12
|
||||
switch (entityType) {
|
||||
case 'ip': {
|
||||
iconWidth = 14
|
||||
iconHeight = 12
|
||||
break
|
||||
}
|
||||
case 'domain': {
|
||||
iconWidth = 14
|
||||
iconHeight = 14
|
||||
break
|
||||
}
|
||||
case 'app': {
|
||||
iconWidth = 12
|
||||
iconHeight = 14
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
iconWidth,
|
||||
iconHeight,
|
||||
hoveredShadowColor: 'rgba(151,151,151,0.12)',
|
||||
selectedShadowColor: 'rgba(151,151,151,0.24)'
|
||||
}
|
||||
}
|
||||
export default {
|
||||
name: 'EntityRelationship',
|
||||
components: {
|
||||
@@ -60,16 +162,338 @@ export default {
|
||||
try {
|
||||
const initialData = await this.generateInitialData()
|
||||
this.initialData = _.cloneDeep(initialData) // 初始化数据
|
||||
ForceGraph()(document.getElementById('entityGraph'))
|
||||
.graphData(initialData)
|
||||
.nodeRelSize(18)
|
||||
.onNodeHover(node => {
|
||||
})
|
||||
.nodeCanvasObject((node, ctx) => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 18, 0, 2 * Math.PI, false)
|
||||
ctx.stroke()
|
||||
})
|
||||
this.graph = ForceGraph()(document.getElementById('entityGraph'))
|
||||
.graphData(this.initialData)
|
||||
.nodeRelSize(16)
|
||||
.onNodeClick(async node => {
|
||||
if (this.currentSelectedNode !== node) {
|
||||
// 控制节点的选中状态
|
||||
if (node === this.currentHoveredNode) {
|
||||
this.currentHoveredNode = null
|
||||
}
|
||||
this.currentSelectedNode = node
|
||||
|
||||
switch (node.type) {
|
||||
/* node 是 entityNode,则查接口展开 tempNode */
|
||||
case nodeType.entityNode: {
|
||||
// 先清除 tempNode 和 tempLink
|
||||
this.cleanTempItems()
|
||||
// 若已查过数据,不重复查询
|
||||
if (!node.myData.relatedEntities) {
|
||||
await node.queryDetailData()
|
||||
}
|
||||
// 生成 tempNode 和 tempLink
|
||||
const tempNodes = []
|
||||
const tempLinks = []
|
||||
const { sourceNodes, targetNodes } = this.getNeighborItems(node)
|
||||
Object.keys(node.myData.relatedEntities).forEach(k => {
|
||||
if (node.myData.relatedEntities[k].total) {
|
||||
// 若已有同级同类型的 listNode,不生成此 tempNode
|
||||
const hasListNode = targetNodes.some(b => b.myData.entityType === k)
|
||||
if (!hasListNode) {
|
||||
const tempNode = new Node(
|
||||
nodeType.tempNode,
|
||||
`${node.id}__${k}__temp`,
|
||||
{
|
||||
entityType: k
|
||||
},
|
||||
node
|
||||
)
|
||||
const tempLink = new Link(node, tempNode, 'temp')
|
||||
tempNodes.push(tempNode)
|
||||
tempLinks.push(tempLink)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (tempNodes.length) {
|
||||
node.fx = node.x
|
||||
node.fy = node.y
|
||||
this.addItems(tempNodes, tempLinks)
|
||||
}
|
||||
this.rightBox.node = node
|
||||
this.rightBox.mode = 'detail'
|
||||
break
|
||||
}
|
||||
case nodeType.listNode: {
|
||||
this.rightBox.node = node
|
||||
this.rightBox.mode = 'list'
|
||||
break
|
||||
}
|
||||
case nodeType.rootNode: {
|
||||
this.rightBox.node = node
|
||||
this.rightBox.mode = 'detail'
|
||||
break
|
||||
}
|
||||
/* node 是 entityNode,则查接口展开 tempNode */
|
||||
case nodeType.tempNode: {
|
||||
// 点击 tempNode,根据 source 生成 listNode 以及对应的 link
|
||||
const nodes = []
|
||||
const links = []
|
||||
const listNode = new Node(
|
||||
nodeType.listNode,
|
||||
`${node.sourceNode.id}__${node.myData.entityType}-list`,
|
||||
{
|
||||
entityType: node.myData.entityType
|
||||
},
|
||||
node.sourceNode
|
||||
)
|
||||
nodes.push(listNode)
|
||||
links.push(new Link(node.sourceNode, listNode))
|
||||
|
||||
// 若未达第六层,为新的 listNode 生成 entityNode 和 link
|
||||
const level = this.getNodeLevel(listNode.sourceNode.id)
|
||||
if (level < 10) {
|
||||
this.rightBox.loading = true
|
||||
try {
|
||||
const entities = await queryRelatedEntity(node.sourceNode, listNode.myData.entityType)
|
||||
this.pushRelatedEntities(node, entities.list)
|
||||
|
||||
entities.list.forEach(entity => {
|
||||
const entityNode = new Node(nodeType.entityNode, entity.vertex, {
|
||||
entityType: listNode.myData.entityType,
|
||||
entityName: entity.vertex
|
||||
}, listNode)
|
||||
nodes.push(entityNode)
|
||||
links.push(new Link(listNode, entityNode))
|
||||
})
|
||||
} catch (e) {
|
||||
this.$message.error(this.errorMsgHandler(e))
|
||||
} finally {
|
||||
this.rightBox.loading = false
|
||||
}
|
||||
} else {
|
||||
this.$message.warning(this.$t('tip.maxExpandDepth'))
|
||||
}
|
||||
|
||||
// 查完 entityNode 的接口再删除 tempNode 和 tempEdge
|
||||
this.addItems(nodes, links)
|
||||
this.cleanTempItems()
|
||||
// 手动高亮listNode
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// 若 node 未拖拽过,取消固定位置
|
||||
if (this.draggedNodes.indexOf(node.id) === -1) {
|
||||
node.fx = null
|
||||
node.fy = null
|
||||
}
|
||||
})
|
||||
.onNodeHover(node => {
|
||||
if (!node) {
|
||||
this.currentHoveredNode = null
|
||||
} else if (node !== this.currentSelectedNode) {
|
||||
this.currentHoveredNode = node || null
|
||||
}
|
||||
})
|
||||
.onNodeDragEnd(node => {
|
||||
this.draggedNodes.push(node.id)
|
||||
})
|
||||
.autoPauseRedraw(false)
|
||||
.nodeCanvasObject((node, ctx) => {
|
||||
/*
|
||||
* 共有4种 nodeType,3种 entityType
|
||||
* */
|
||||
switch (node.type) {
|
||||
case nodeType.rootNode: {
|
||||
const nodeStyle = getRootNodeStyle(node.myData.entityType)
|
||||
// 如果是鼠标点击高亮的,最外层加上第三层圆环
|
||||
if (node === this.currentSelectedNode) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 24, 0, 2 * Math.PI, false)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = nodeStyle.selectedShadowColor
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
// 第二层圆环
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 16, 0, 2 * Math.PI, false)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = node === this.currentSelectedNode || node === this.currentHoveredNode ?
|
||||
nodeStyle.hoveredShadowColor :
|
||||
nodeStyle.shadowColor
|
||||
ctx.fill()
|
||||
// 内部挖空
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 12, 0, 2 * Math.PI, false)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fill()
|
||||
|
||||
// 第一层圆环
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 12, 0, 2 * Math.PI, false)
|
||||
ctx.closePath()
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeStyle = nodeStyle.borderColor
|
||||
ctx.stroke()
|
||||
// 图片
|
||||
ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight);
|
||||
// 文字
|
||||
ctx.font = '8px NotoSansSChineseRegular'
|
||||
const textWidth = ctx.measureText(node.label).width
|
||||
const bckgDimensions = [textWidth, 8].map(n => n + 8 * 0.2)
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2 + 21, ...bckgDimensions) // 文字的白底
|
||||
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillStyle = '#353636'
|
||||
ctx.fillText(node.label, node.x, node.y + 21)
|
||||
break
|
||||
}
|
||||
case nodeType.listNode: {
|
||||
const nodeStyle = getListNodeStyle(node.myData.entityType)
|
||||
// 如果是鼠标点击或者悬停的,有一层圆环
|
||||
if (node === this.currentSelectedNode || node === this.currentHoveredNode) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 14, 0, 2 * Math.PI, false)
|
||||
ctx.closePath()
|
||||
if (node === this.currentSelectedNode) {
|
||||
ctx.fillStyle = nodeStyle.selectedShadowColor
|
||||
} else {
|
||||
ctx.fillStyle = nodeStyle.hoveredShadowColor
|
||||
}
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
// 内部填白
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 11, 0, 2 * Math.PI, false)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fill()
|
||||
|
||||
// 第一层圆环
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 11, 0, 2 * Math.PI, false)
|
||||
ctx.closePath()
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeStyle = nodeStyle.borderColor
|
||||
ctx.stroke()
|
||||
// 图片
|
||||
ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
|
||||
// 文字
|
||||
ctx.font = '8px NotoSansSChineseRegular'
|
||||
const textWidth = ctx.measureText(node.label).width
|
||||
const bckgDimensions = [textWidth, 8].map(n => n + 8 * 0.2)
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
|
||||
ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2 + 18, ...bckgDimensions) // 文字的白底
|
||||
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillStyle = '#353636'
|
||||
ctx.fillText(node.label, node.x, node.y + 18)
|
||||
break
|
||||
}
|
||||
case nodeType.entityNode: {
|
||||
const nodeStyle = getEntityNodeStyle(node.myData.entityType)
|
||||
// 先画个白底圆环,避免 link 穿过不好看
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 8, 0, 2 * Math.PI, false)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.fill()
|
||||
// 如果是鼠标点击或者悬停的,有一层圆环
|
||||
if (node === this.currentSelectedNode || node === this.currentHoveredNode) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 10, 0, 2 * Math.PI, false)
|
||||
ctx.closePath()
|
||||
if (node === this.currentSelectedNode) {
|
||||
ctx.fillStyle = nodeStyle.selectedShadowColor
|
||||
} else {
|
||||
ctx.fillStyle = nodeStyle.hoveredShadowColor
|
||||
}
|
||||
ctx.fill()
|
||||
}
|
||||
// 图片
|
||||
ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
|
||||
break
|
||||
}
|
||||
case nodeType.tempNode: {
|
||||
const nodeStyle = getEntityNodeStyle(node.myData.entityType)
|
||||
// 先画个白底圆环,避免 link 穿过不好看
|
||||
ctx.beginPath()
|
||||
ctx.arc(node.x, node.y, 8, 0, 2 * Math.PI, false)
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.fill()
|
||||
// 画透明度0.7的图标
|
||||
ctx.globalAlpha = 0.7
|
||||
ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight)
|
||||
ctx.globalAlpha = 1
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.linkCanvasObject((link, ctx) => {
|
||||
const start = link.source
|
||||
const end = link.target
|
||||
const width = 1 // 线宽
|
||||
const arrowSize = 3 // 箭头大小
|
||||
const shortenedLength = 14 // link 末端缩短长度
|
||||
|
||||
// 计算箭头角度
|
||||
const dx = end.x - start.x
|
||||
const dy = end.y - start.y
|
||||
const angle = Math.atan2(dy, dx) // 计算与x轴的角度(弧度)
|
||||
const lineEndX = end.x - shortenedLength * Math.cos(angle)
|
||||
const lineEndY = end.y - shortenedLength * Math.sin(angle)
|
||||
const arrowEndX = lineEndX + arrowSize * Math.cos(angle)
|
||||
const arrowEndY = lineEndY + arrowSize * Math.sin(angle)
|
||||
|
||||
// 绘制线
|
||||
let color
|
||||
ctx.beginPath()
|
||||
if (link.isTemp) {
|
||||
ctx.setLineDash([2, 2])
|
||||
color = 'rgba(119,131,145,0.2)' // 虚线颜色
|
||||
} else {
|
||||
ctx.setLineDash([])
|
||||
if (this.currentSelectedNode === link.source || this.currentSelectedNode === link.target) {
|
||||
color = 'rgba(119,131,145,0.8)' // 高亮线颜色
|
||||
} else {
|
||||
color = 'rgba(119,131,145,0.3)' // 普通线颜色
|
||||
}
|
||||
}
|
||||
ctx.moveTo(start.x, start.y)
|
||||
ctx.lineTo(lineEndX, lineEndY)
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = width
|
||||
ctx.stroke()
|
||||
|
||||
// 绘制箭头
|
||||
ctx.save() // 保存当前状态以便之后恢复
|
||||
ctx.translate(arrowEndX, arrowEndY) // 将坐标原点移动到箭头末端
|
||||
ctx.rotate(angle) // 根据链接方向旋转坐标系
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, 0)
|
||||
ctx.lineTo(-arrowSize, arrowSize) // 绘制箭头的一边
|
||||
ctx.lineTo(-arrowSize, -arrowSize) // 绘制箭头的另一边
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = color
|
||||
ctx.fill()
|
||||
ctx.restore() // 恢复之前保存的状态
|
||||
})
|
||||
.d3Force('center', null)
|
||||
.d3Force('collide', d3.forceCollide(d => d.r))
|
||||
.d3Force('link', d3.forceLink().distance(link => {
|
||||
if (link.source.type === nodeType.rootNode) {
|
||||
return 120
|
||||
}
|
||||
return 60
|
||||
}))
|
||||
.cooldownTime(1500)
|
||||
setTimeout(() => {
|
||||
const { nodes, links } = this.graph.graphData()
|
||||
const rootNode = nodes.find(n => n.type === nodeType.rootNode)
|
||||
rootNode.fx = rootNode.x
|
||||
rootNode.fy = rootNode.y
|
||||
this.graph.graphData({ nodes, links })
|
||||
this.graph.zoomToFit(200, 250)
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.$message.error(this.errorMsgHandler(e))
|
||||
@@ -80,25 +504,27 @@ export default {
|
||||
async generateInitialData () {
|
||||
const nodes = []
|
||||
const links = []
|
||||
|
||||
const rootNode = new Node(nodeType.rootNode, this.entity.entityName, this.entity)
|
||||
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.data.relatedEntities) {
|
||||
// 生成listNode和entityNode及link
|
||||
if (rootNode.myData.relatedEntities) {
|
||||
// listNode
|
||||
const listNodes = []
|
||||
const keys = Object.keys(rootNode.data.relatedEntities)
|
||||
const keys = Object.keys(rootNode.myData.relatedEntities)
|
||||
keys.forEach(k => {
|
||||
if (rootNode.data.relatedEntities[k].total) {
|
||||
if (rootNode.myData.relatedEntities[k].total) {
|
||||
const listNode = new Node(
|
||||
nodeType.listNode,
|
||||
`${rootNode.id}__${k}-list`,
|
||||
{
|
||||
entityType: k
|
||||
},
|
||||
rootNode
|
||||
nodeType.listNode,
|
||||
`${rootNode.id}__${k}-list`,
|
||||
{
|
||||
entityType: k
|
||||
},
|
||||
rootNode
|
||||
)
|
||||
listNodes.push(listNode)
|
||||
links.push(new Link(rootNode, listNode))
|
||||
@@ -106,27 +532,29 @@ export default {
|
||||
})
|
||||
// entityNode
|
||||
const entityNodes = []
|
||||
for (const listNode of listNodes) {
|
||||
const entities = await rootNode.queryRelatedEntities(listNode.data.entityType)
|
||||
for (const node of listNodes) {
|
||||
const entities = await queryRelatedEntity(rootNode, node.myData.entityType)
|
||||
this.pushRelatedEntities(node, entities.list)
|
||||
|
||||
entities.list.forEach(entity => {
|
||||
const entityNode = new Node(
|
||||
nodeType.entityNode,
|
||||
entity.vertex,
|
||||
{
|
||||
entityType: listNode.data.entityType,
|
||||
entityName: entity.vertex
|
||||
},
|
||||
listNode
|
||||
nodeType.entityNode,
|
||||
entity.vertex,
|
||||
{
|
||||
entityType: node.myData.entityType,
|
||||
entityName: entity.vertex
|
||||
},
|
||||
node
|
||||
)
|
||||
entityNodes.push(entityNode)
|
||||
links.push(new Link(listNode, entityNode))
|
||||
links.push(new Link(node, entityNode))
|
||||
})
|
||||
}
|
||||
nodes.push(...listNodes, ...entityNodes)
|
||||
}
|
||||
return {
|
||||
nodes,
|
||||
links
|
||||
nodes: nodes,
|
||||
links: links
|
||||
}
|
||||
},
|
||||
expandList () {
|
||||
@@ -140,6 +568,75 @@ export default {
|
||||
},
|
||||
resize () {
|
||||
|
||||
},
|
||||
addItems (toAddNodes, toAddLinks) {
|
||||
if (toAddNodes.length || toAddLinks.length) {
|
||||
const { nodes, links } = this.graph.graphData()
|
||||
const nodes2 = toAddNodes.filter(n => !nodes.some(n2 => n.id === n2.id))
|
||||
nodes.push(...nodes2)
|
||||
links.push(...toAddLinks)
|
||||
this.graph.graphData({ nodes, links })
|
||||
}
|
||||
},
|
||||
removeItems (toRemoveNodes, toRemoveLinks) {
|
||||
let { nodes, links } = this.graph.graphData()
|
||||
nodes = nodes.filter(n => toRemoveNodes.some(n2 => n.id === n2.id))
|
||||
links = links.filter(l => toRemoveLinks.some(l2 => l2.id === l.id))
|
||||
this.graph.graphData({ nodes, links })
|
||||
},
|
||||
cleanTempItems () {
|
||||
let { nodes, links } = this.graph.graphData()
|
||||
nodes = nodes.filter(n => n.type !== nodeType.tempNode)
|
||||
links = links.filter(l => !l.isTemp)
|
||||
this.graph.graphData({ nodes, links })
|
||||
},
|
||||
getNeighborItems (node) {
|
||||
const { links } = this.graph.graphData()
|
||||
const neighboringNodes = []
|
||||
const neighboringTargetNodes = []
|
||||
const neighboringSourceNodes = []
|
||||
const neighboringTargetLinks = []
|
||||
const neighboringSourceLinks = []
|
||||
const neighboringLinks = links.filter(l => {
|
||||
if (l.target === node || l.source === node) {
|
||||
neighboringNodes.push(l.target === node ? l.source : l.target)
|
||||
if (l.target === node) {
|
||||
neighboringSourceNodes.push(l.source)
|
||||
neighboringSourceLinks.push(l)
|
||||
} else {
|
||||
neighboringTargetNodes.push(l.target)
|
||||
neighboringTargetLinks.push(l)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return {
|
||||
nodes: neighboringNodes,
|
||||
targetNodes: neighboringTargetNodes,
|
||||
sourceNodes: neighboringSourceNodes,
|
||||
links: neighboringLinks,
|
||||
targetLinks: neighboringTargetLinks,
|
||||
sourceLinks: neighboringSourceLinks
|
||||
}
|
||||
},
|
||||
getNodeLevel (id) {
|
||||
const { findShortestPath } = Algorithm
|
||||
const { nodes, links } = this.graph.graphData()
|
||||
const g6FormatData = { nodes: _.cloneDeep(nodes), edges: _.cloneDeep(links) }
|
||||
g6FormatData.edges.forEach(l => {
|
||||
l.source = l.source.id
|
||||
l.target = l.target.id
|
||||
})
|
||||
console.info(g6FormatData)
|
||||
const info = findShortestPath(g6FormatData, this.entity.entityName, id)
|
||||
return info.length
|
||||
},
|
||||
pushRelatedEntities (node, entities) {
|
||||
if (!this.relatedEntitiesList[node.id]) {
|
||||
this.relatedEntitiesList[node.id] = { ip: [], domain: [], app: [] }
|
||||
}
|
||||
this.relatedEntitiesList[node.id][node.myData.entityType] = entities
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -170,10 +667,22 @@ export default {
|
||||
node: null,
|
||||
loading: true
|
||||
})
|
||||
// 右侧关联实体列表
|
||||
const relatedEntitiesList = ref({})
|
||||
// 记录鼠标交互的node
|
||||
const currentHoveredNode = shallowRef(null)
|
||||
const currentSelectedNode = shallowRef(null)
|
||||
const draggedNodes = shallowRef([]) // 记录拖拽过的node
|
||||
|
||||
return {
|
||||
entity,
|
||||
rightBox,
|
||||
graph
|
||||
graph,
|
||||
relatedEntitiesList,
|
||||
currentHoveredNode,
|
||||
currentSelectedNode,
|
||||
draggedNodes,
|
||||
centerCoordinate: [100, 100]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<!--title-->
|
||||
<div class="graph-detail-basic-info">
|
||||
<div style="display: flex">
|
||||
<div class="graph-detail-basic-info__block">
|
||||
<div class="graph-detail__icon"><i :class="iconClass"></i></div>
|
||||
|
||||
<div class="graph-detail-header">
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="graph-basic-info__block-content" style="margin-top: -4px;">
|
||||
<div class="graph-basic-info__block-content margin-t--4">
|
||||
<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">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</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">
|
||||
<div class="graph-list-expand-btn graph-list-expand-btn__display" :class="{ 'graph-list-expand-btn--disabled': expandBtnDisable }" @click="expandList">
|
||||
<i class="cn-icon cn-icon-expand-continue graph-expand-continue"></i>
|
||||
{{ $t('entity.graph.continueToExpand') }}
|
||||
</div>
|
||||
@@ -39,8 +39,8 @@
|
||||
<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;line-height: 14px;">
|
||||
<div style="line-height: 10px">
|
||||
<div class="graph-list-item-value graph-list-item-value1">
|
||||
<div>
|
||||
<img v-if="getCountry(item.detail)===countryNameIdMapping.Unknown || !countryNameIdMapping[getCountry(item.detail)]" src="../../../../public/images/flag/Unknown.svg" class="graph-list-country-flag">
|
||||
<img v-else :src="require(`../../../../public/images/flag/${countryNameIdMapping[getCountry(item.detail)]}.png`)" class="graph-list-country-flag" >
|
||||
</div>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
export default class Edge {
|
||||
constructor (sourceNode, targetNode, type = linkType.normal) {
|
||||
export default class Link {
|
||||
constructor (sourceNode, targetNode, type) {
|
||||
this.id = sourceNode.id + '__' + targetNode.id
|
||||
this.source = sourceNode.id
|
||||
this.target = targetNode.id
|
||||
this.type = type
|
||||
this.isTemp = type === 'temp'
|
||||
}
|
||||
}
|
||||
|
||||
export const linkType = {
|
||||
normal: 'normal',
|
||||
temp: 'temp'
|
||||
}
|
||||
|
||||
@@ -1,75 +1,141 @@
|
||||
import _ from 'lodash'
|
||||
import { entityDefaultColor, entityType } from '@/utils/constants'
|
||||
import { formatTags } from '@/utils/tools'
|
||||
import { generateLabel, queryEntityBasicInfo, queryTags, queryRelatedEntityCount } from '@/views/entityExplorer/entityGraph/utils'
|
||||
import { api } from '@/utils/api'
|
||||
import i18n from '@/i18n'
|
||||
import axios from 'axios'
|
||||
import { api } from '@/utils/api'
|
||||
import { entityDefaultColor, intentColor } from '@/utils/constants'
|
||||
import { formatTags } from '@/utils/tools'
|
||||
|
||||
export default class Node {
|
||||
/*
|
||||
* type: 对应nodeType
|
||||
* cfg: { entityType, entityName }
|
||||
* cfg: { entityType, entityName, x, y, fx, fy }
|
||||
* */
|
||||
constructor (type, id, data, sourceNode) {
|
||||
constructor (type, id, cfg, sourceNode) {
|
||||
this.type = type
|
||||
this.id = id
|
||||
this.data = {
|
||||
entityType: data.entityType,
|
||||
entityName: data.entityName,
|
||||
isSubdomain: sourceNode ? sourceNode.data.entityType === entityType.domain && data.entityType === entityType.domain : false
|
||||
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
|
||||
}
|
||||
this.label = generateLabel(type, id, data, sourceNode)
|
||||
if (sourceNode) {
|
||||
this.sourceNode = sourceNode
|
||||
}
|
||||
this.label = this.generateLabel()
|
||||
|
||||
const img = new Image()
|
||||
img.src = this.getIconUrl(cfg.entityType, type === nodeType.rootNode)
|
||||
this.img = img
|
||||
}
|
||||
|
||||
// listNode和entityNode专用,查询实体信息
|
||||
generateLabel () {
|
||||
switch (this.type) {
|
||||
case nodeType.rootNode:
|
||||
case nodeType.entityNode: {
|
||||
return this.id
|
||||
}
|
||||
case nodeType.listNode: {
|
||||
return `${this.getLabelText()}(${_.get(this.sourceNode.myData, 'relatedEntities.' + 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 ''
|
||||
}
|
||||
|
||||
getIconUrl (entityType, colored) {
|
||||
const suffix = colored ? '-colored' : ''
|
||||
return require(`@/assets/img/entity-symbol2/${entityType}${suffix}.svg`)
|
||||
}
|
||||
|
||||
isSubdomain () {
|
||||
if (this.sourceNode) {
|
||||
return this.sourceNode.myData.entityType === 'domain' && this.myData.entityType === 'domain'
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询basicInfo、tags、关联实体数量
|
||||
async queryDetailData () {
|
||||
const entityType = this.data.entityType
|
||||
const entityName = this.data.entityName
|
||||
const entityType = this.myData.entityType
|
||||
const entityName = this.myData.entityName
|
||||
|
||||
this.data.basicInfo = await queryEntityBasicInfo(entityType, entityName)
|
||||
this.myData.basicInfo = await this.queryEntityBasicInfo(entityType, entityName)
|
||||
|
||||
const tags = await queryTags(entityType, entityName)
|
||||
const tags = await this.queryTags(entityType, entityName)
|
||||
let _tags = []
|
||||
formatTags(tags, entityType, _tags)
|
||||
if (_.isArray(tags.userDefinedTags)) {
|
||||
_tags = _.concat(_tags, tags.userDefinedTags.map(tag => ({ value: tag.tagValue, color: tag.knowledgeBase ? tag.knowledgeBase.color : entityDefaultColor })))
|
||||
if (_.isArray(tags.tags)) {
|
||||
_tags = _.concat(_tags, tags.tags.map(tag => ({ value: tag.name, color: intentColor[tag.intent] || entityDefaultColor })))
|
||||
}
|
||||
this.data.tags = _tags
|
||||
this.myData.tags = _tags
|
||||
|
||||
const relatedEntityTotalCount = await queryRelatedEntityCount(entityType, entityName)
|
||||
this.data.relatedEntities = {
|
||||
ip: { total: relatedEntityTotalCount.ipCount, pageNo: 0 },
|
||||
domain: { total: relatedEntityTotalCount.domainCount, pageNo: 0 },
|
||||
app: { total: relatedEntityTotalCount.appCount, pageNo: 0 }
|
||||
const relatedEntityTotalCount = await this.queryRelatedEntitiesCount(entityType, entityName)
|
||||
this.myData.relatedEntities = {
|
||||
ip: { total: relatedEntityTotalCount.ipCount, loadedCount: 0 },
|
||||
domain: { total: this.myData.entityType === 'domain' ? relatedEntityTotalCount.subDomainCount : relatedEntityTotalCount.domainCount, loadedCount: 0 },
|
||||
app: { total: relatedEntityTotalCount.appCount, loadedCount: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
getNeighbors (gData) {
|
||||
const links = gData.links.filter(l => l.source === this.id || l.target === this.id)
|
||||
const nodes = gData.nodes.filter(n => links.some(l => l.source === n.id || l.target === n.id))
|
||||
return { links, nodes }
|
||||
}
|
||||
|
||||
// 获取唯一source,listNode和tempNode专用,因为entityNode的source可能有多个,rootNode无source
|
||||
getSourceNode (gData) {
|
||||
const links = gData.links.filter(l => l.target === this.id)
|
||||
const nodes = gData.nodes.find(n => links.some(l => l.source === n.id))
|
||||
return nodes.length > 0 ? nodes[0] : null
|
||||
}
|
||||
|
||||
// listNode和entityNode专用,查询关联实体列表
|
||||
async queryRelatedEntities (targetEntityType) {
|
||||
let _targetEntityType = targetEntityType
|
||||
if (this.data.entityType === entityType.domain && targetEntityType === entityType.domain) {
|
||||
_targetEntityType = 'subdomain'
|
||||
}
|
||||
const url = `${api.entity.entityGraph[`${this.data.entityType}Related${_.upperFirst(_targetEntityType)}`]}?resource=${this.data.entityName}&pageSize=10&pageNo=${this.data.relatedEntities[targetEntityType].pageNo + 1}`
|
||||
const response = await axios.get(url).catch(e => {
|
||||
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.status === 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.status === 200) {
|
||||
return response.data.data
|
||||
} else {
|
||||
console.error(response)
|
||||
throw response
|
||||
}
|
||||
}
|
||||
|
||||
async queryRelatedEntitiesCount (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.status === 200) {
|
||||
this.data.relatedEntities[targetEntityType].pageNo += 1
|
||||
return response.data.data
|
||||
} else {
|
||||
console.error(response)
|
||||
@@ -77,10 +143,29 @@ export default class Node {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const nodeType = {
|
||||
rootNode: 'rootNode',
|
||||
listNode: 'listNode',
|
||||
entityNode: 'entityNode',
|
||||
entityNode: 'en-tityNode',
|
||||
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.relatedEntities[targetEntityType].loadedCount
|
||||
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.status === 200) {
|
||||
return response.data.data
|
||||
} else {
|
||||
console.error(response)
|
||||
throw response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
<div class="cn-entity__icon"><i :class="iconClass"></i></div>
|
||||
<div class="cn-entity__row">
|
||||
<!--标签-->
|
||||
<div class="cn-entity__header" style="display: flex;">
|
||||
<span class="cn-entity__header-title" v-high-light="keywordList">{{ entityData.entityValue || 'Unknown' }}</span>
|
||||
<span v-show="entityData.isRelated">
|
||||
<div class="cn-entity__header my__display1">
|
||||
<template v-if="entityData.entityType">
|
||||
<span v-if="entityData.entityType==='subscriber_id'" v-high-light="keywordList">{{ entityData.phone_number }}</span>
|
||||
<span v-else v-high-light="keywordList">{{ entityData.entityValue || 'Unknown' }}</span>
|
||||
</template>
|
||||
<span v-show="entityData.isRelated" class="cn-entity__header-icon">
|
||||
<el-popover
|
||||
popper-class="my-popper-class"
|
||||
placement="right"
|
||||
@@ -89,6 +92,18 @@
|
||||
<span class="row-item-value">{{ entityData.category ? appRisk(entityData.category.appRisk) : '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="entityData.entityType === 'subscriber_id'">
|
||||
<div class="basic-info__item">
|
||||
<i class="cn-icon cn-icon-account-info"></i>
|
||||
<span class="row-item-label">{{ $t('entity.subscriberId') }} : </span>
|
||||
<span class="row-item-value" v-high-light="keywordList">{{ $_.get(entityData, 'entityValue', '-') || '-' }}</span>
|
||||
</div>
|
||||
<div class="basic-info__item">
|
||||
<i class="cn-icon cn-icon-location"></i>
|
||||
<span class="row-item-label">{{ $t('overall.location') }} : </span>
|
||||
<span class="row-item-value">{{$_.get(entityData, 'subscriber_longitude', '-') || '-'}}, {{$_.get(entityData, 'subscriber_latitude', '-') || '-'}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 通用字段 -->
|
||||
<div class="basic-info__item">
|
||||
<div class="item__box">
|
||||
@@ -102,21 +117,7 @@
|
||||
<!-- 曲线-->
|
||||
<div class="item-box-loading">
|
||||
<loading :loading="loading" size="small"></loading>
|
||||
<div
|
||||
class="row__charts"
|
||||
:id="`entityDetailSend${entityType}${listMode}`"
|
||||
v-if="entityData.entityType === 'domain'">
|
||||
</div>
|
||||
<div
|
||||
class="row__charts"
|
||||
:id="`entityDetailSend${entityType}${listMode}`"
|
||||
v-if="entityData.entityType === 'app'">
|
||||
</div>
|
||||
<div
|
||||
class="row__charts"
|
||||
:id="`entityDetailSend${entityType}${listMode}`"
|
||||
v-if="entityData.entityType === 'ip'">
|
||||
</div>
|
||||
<div class="row__charts" :id="`entityDetailSend${entityType}${listMode}`"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,27 +131,13 @@
|
||||
</span>
|
||||
<div class="item-box-loading">
|
||||
<loading :loading="loading" size="small"></loading>
|
||||
<div
|
||||
class="row__charts"
|
||||
:id="`entityDetailReceived${entityType}${listMode}`"
|
||||
v-if="entityData.entityType === 'domain'">
|
||||
</div>
|
||||
<div
|
||||
class="row__charts"
|
||||
:id="`entityDetailReceived${entityType}${listMode}`"
|
||||
v-if="entityData.entityType === 'app'">
|
||||
</div>
|
||||
<div
|
||||
class="row__charts"
|
||||
:id="`entityDetailReceived${entityType}${listMode}`"
|
||||
v-if="entityData.entityType === 'ip'">
|
||||
</div>
|
||||
<div class="row__charts" :id="`entityDetailReceived${entityType}${listMode}`"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--score分数-->
|
||||
<div class="basic-info__item" style="display: flex;align-items: center;">
|
||||
<div class="basic-info__item my__display">
|
||||
<i class="cn-icon cn-icon-Score"></i>
|
||||
<div class="row-item-label">
|
||||
<span class="row-item-label">{{ $t('network.score') }} : </span>
|
||||
@@ -158,7 +145,7 @@
|
||||
<template v-if="!loadingNetworkQuality && score !=='-'">
|
||||
<span v-for="(dot, i) in scoreDot" :key="i" :class="dot.class"></span>
|
||||
</template>
|
||||
<span style="padding-left: 4px;">{{score}}</span>
|
||||
<span class="padding-l-4">{{score}}</span>
|
||||
<loading :loading="loadingNetworkQuality" size="small"></loading>
|
||||
</span>
|
||||
</div>
|
||||
@@ -166,24 +153,23 @@
|
||||
|
||||
<!--事件数量,即Performance和security事件总和-->
|
||||
<div class="basic-info__item" style="position: relative">
|
||||
<div v-if="eventNum>0 && !loadingEvent" style="display: flex;align-items: center;">
|
||||
<div v-if="eventNum>0 && !loadingEvent" class="my__display">
|
||||
<i class="cn-icon cn-icon-Event1"></i>
|
||||
<div class="row-item-label">
|
||||
<span class="row-item-label">{{ $t('dnsInsight.event') }} : </span>
|
||||
<span class="row-item-value">{{ eventNum }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <loading :loading="loadingEvent" size="small" style="width: 90px"></loading>-->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="new-show-detail">
|
||||
<div @click="showDetail"><i class="cn-icon cn-icon-detail"></i>{{ $t('overall.detail') }} ></div>
|
||||
<div @click="showGraph"><i class="cn-icon cn-icon-graph"></i>{{ $t('entities.graph') }} ></div>
|
||||
<div class="show-detail__block">
|
||||
<div class="new-show-detail">
|
||||
<div @click="showDetail"><i class="cn-icon cn-icon-detail"></i>{{ $t('overall.detail') }} ></div>
|
||||
<div @click="showGraph" v-if="entity.entityType !== 'subscriber_id'"><i class="cn-icon cn-icon-graph"></i>{{ $t('entities.graph') }} ></div>
|
||||
</div>
|
||||
</div>
|
||||
<el-collapse-transition>
|
||||
<div class="cn-entity__detail-overview" v-if="!isCollapse">
|
||||
@@ -202,7 +188,7 @@ import relatedServer from '@/mixins/relatedServer'
|
||||
import Loading from '@/components/common/Loading'
|
||||
import axios from 'axios'
|
||||
import { api } from '@/utils/api'
|
||||
import { entityDefaultColor } from '@/utils/constants'
|
||||
import { entityDefaultColor, intentColor } from '@/utils/constants'
|
||||
import _ from 'lodash'
|
||||
import { getTagColor, formatTags } from '@/utils/tools'
|
||||
|
||||
@@ -277,6 +263,10 @@ export default {
|
||||
url = api.entity.entityList.appBasicInfo
|
||||
break
|
||||
}
|
||||
case ('subscriber_id'): {
|
||||
url = api.entity.entityList.subscriberBasicInfo
|
||||
break
|
||||
}
|
||||
}
|
||||
axios.get(`${url}?resource=${this.entity.entityValue}`).then(response => {
|
||||
this.$nextTick(() => {
|
||||
@@ -295,21 +285,19 @@ export default {
|
||||
url = api.entity.entityList.ipTags
|
||||
break
|
||||
}
|
||||
case ('app'): {
|
||||
url = api.entity.entityList.appTags
|
||||
break
|
||||
}
|
||||
}
|
||||
axios.get(`${url}?resource=${this.entity.entityValue}`).then(responese => {
|
||||
const res = responese.data
|
||||
if (responese.status === 200) {
|
||||
formatTags(res.data, this.entity.entityType, this.levelTwoTags)
|
||||
if (_.isArray(res.data.userDefinedTags)) {
|
||||
this.levelTwoTags = _.concat(this.levelTwoTags, res.data.userDefinedTags.map(tag => ({ value: tag.tagValue, color: tag.knowledgeBase ? tag.knowledgeBase.color : entityDefaultColor })))
|
||||
if (this.entity.entityType === 'domain' || this.entity.entityType === 'ip') {
|
||||
axios.get(`${url}?resource=${this.entity.entityValue}`).then(responese => {
|
||||
const res = responese.data
|
||||
if (responese.status === 200) {
|
||||
formatTags(res.data, this.entity.entityType, this.levelTwoTags)
|
||||
if (_.isArray(res.data.tags)) {
|
||||
this.levelTwoTags = _.concat(this.levelTwoTags, res.data.tags.map(tag => ({ value: tag.name, color: intentColor[tag.intent] || entityDefaultColor })))
|
||||
}
|
||||
this.hideTagArea = _.isEmpty(this.levelTwoTags)
|
||||
}
|
||||
this.hideTagArea = _.isEmpty(this.levelTwoTags)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
/* 切换折叠状态 */
|
||||
switchCollapse () {
|
||||
@@ -318,7 +306,7 @@ export default {
|
||||
if (this.isCollapse) {
|
||||
this.$refs.rowBlock.style.cssText = 'background-color: none'
|
||||
} else {
|
||||
this.$refs.rowBlock.style.cssText = 'background-color: #fff'
|
||||
this.$refs.rowBlock.style.cssText = 'background-color: var(--el-fill-color-blank)'
|
||||
}
|
||||
this.$emit('switchCollapse', this.isCollapse, this.index)
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="overview-item">
|
||||
<div class="overview__title">{{$t('overall.traffic')}}</div>
|
||||
<div class="overview__content overview__content-loading">
|
||||
<loading :loading="loadingTraffic" size="small" inner-style="left: 8.75rem;" style="width: 50%;"></loading>
|
||||
<loading :loading="loadingTraffic" size="small" inner-style="left: 8.75rem;" class="content-loading"></loading>
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.peak')}}</div>
|
||||
<div class="row__content">
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.throughput')}}</div>
|
||||
<div class="row__contents">
|
||||
<div class="row__content">
|
||||
<div class="row__content row__content-sent">
|
||||
<div class="row__charts-msg">{{$t('overall.sent')}}:
|
||||
{{valueToRangeValue(entityData.bytesSentRate, unitTypes.bps).join(' ') !== '- ' ? valueToRangeValue(entityData.bytesSentRate, unitTypes.bps).join(' ') : '-'}}
|
||||
</div>
|
||||
@@ -68,13 +68,13 @@
|
||||
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('entities.networkQualityRating')}}</div>
|
||||
<div style="position: relative;">
|
||||
<div class="entity-score__block">
|
||||
<div class="entity-score" v-if="!loadingNetworkQuality && score !=='-'">
|
||||
<span v-for="(dot, i) in scoreDot" :key="i" :class="dot.class"></span>
|
||||
<span style="padding-left: 4px;">{{score}}</span>
|
||||
<span class="entity-score__padding">{{score}}</span>
|
||||
</div>
|
||||
<div class="entity-score" v-else>{{score}}</div>
|
||||
<loading :loading="loadingNetworkQuality" size="small" style="left: 1rem;width: 50%;"></loading>
|
||||
<loading :loading="loadingNetworkQuality" size="small" class="entity-score__loading"></loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,8 +87,8 @@
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('entities.tab.relatedDomain')}}</div>
|
||||
<div class="row__content overview__row-related">
|
||||
<div v-if="loadingRelationshipOne" style="position: relative;width: 450px;">
|
||||
<loading :loading="loadingRelationshipOne" size="small" style="left: 1rem;"></loading>
|
||||
<div v-if="loadingRelationshipOne" class="relationship-one__loading">
|
||||
<loading :loading="loadingRelationshipOne" size="small" class="one__loading"></loading>
|
||||
</div>
|
||||
|
||||
<div class="data-item high-light-block" v-high-light="keywordList" v-show="item.show" v-for="(item, index) in relationshipDataOne" :key="index">
|
||||
@@ -97,7 +97,7 @@
|
||||
<div v-if="relationshipDataOne.length===0 && !loadingRelationshipOne">-</div>
|
||||
|
||||
<div v-if="relationshipShowOne">
|
||||
<div class="data-item show-more-related" id="related-app-more" @click.stop="showMoreApp" style="position: relative">...</div>
|
||||
<div class="data-item show-more-related" id="related-app-more" @click.stop="showMoreApp">...</div>
|
||||
<div v-if="isShowMoreApp" class="app-popover_block" id="showRelatedApp">
|
||||
<div class="popover-content" v-for="(item, index) in relationshipDataOne" :key="index">
|
||||
<span v-if="!item.show" class="high-light-block" v-high-light="keywordList">{{item.value}}</span>
|
||||
@@ -111,8 +111,8 @@
|
||||
<div class="row__label row__label--width130">{{$t('entities.tab.relatedIp')}}</div>
|
||||
|
||||
<div class="row__content">
|
||||
<div v-if="loadingRelationshipTwo" style="position: relative;width: 450px;">
|
||||
<loading :loading="loadingRelationshipTwo" size="small" style="left: 1rem;"></loading>
|
||||
<div v-if="loadingRelationshipTwo" class="relationship-two__loading">
|
||||
<loading :loading="loadingRelationshipTwo" size="small" class="one__loading"></loading>
|
||||
</div>
|
||||
|
||||
<div class="data-item high-light-block" v-high-light="keywordList" v-show="item.show" v-for="(item, index) in relationshipDataTwo" :key="index">
|
||||
@@ -121,7 +121,7 @@
|
||||
<div v-if="relationshipDataTwo.length===0 && !loadingRelationshipTwo">-</div>
|
||||
|
||||
<div v-if="relationshipShowTwo">
|
||||
<div class="data-item show-more-related" id="related-domain-more" @click.stop="showMoreDomain" style="position: relative">...</div>
|
||||
<div class="data-item show-more-related" id="related-domain-more" @click.stop="showMoreDomain">...</div>
|
||||
<div v-if="isShowMoreDomain" class="domain-popover_block" id="showRelatedDomain">
|
||||
<div v-for="(item, index) in relationshipDataTwo" :key="index">
|
||||
<span v-if="!item.show" class="high-light-block" v-high-light="keywordList">{{item.value}}</span>
|
||||
@@ -138,7 +138,7 @@
|
||||
<div class="overview__content overview__content-loading">
|
||||
<loading :loading="loadingAlert" size="small" inner-style="left: 10rem"></loading>
|
||||
<div class="overview__row" v-if="performanceData.length === 0">
|
||||
<span class="no-recent-alerts"><i class="el-icon-success"></i>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
<span class="no-recent-alerts"><el-icon><SuccessFilled /></el-icon>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
</div>
|
||||
<div class="overview__row" v-if="performanceData.length > 0">
|
||||
<div class="row__label row__label--width130">{{$t('entities.recentAlert')}}</div>
|
||||
@@ -147,9 +147,9 @@
|
||||
<div class="overview__row overview__row--small-font" v-for="(performance, index) in entityData.performanceList" :key="index">
|
||||
<div class="row__label row__label--width130">{{dateFormatByAppearance(performance.startTime) || '-'}}</div>
|
||||
<div class="row__content row__content--width90">
|
||||
<div class="alert-level-tag alert-level-tag--high" :class="iconClass(performance)">{{performance.eventSeverity}}</div>
|
||||
<div class="alert-level-tag" :class="`alert-level-tag--${iconClass(performance)}`">{{performance.eventSeverity}}</div>
|
||||
</div>
|
||||
<div class="row__content-loading" style="position: relative;">
|
||||
<div class="row__content-loading">
|
||||
<div class="performance-event-remark">{{performance.eventType}}</div>
|
||||
</div>
|
||||
<div class="row__desc"></div>
|
||||
@@ -164,7 +164,7 @@
|
||||
<div class="overview__content overview__content-loading">
|
||||
<loading :loading="loadingSecurityEvents" size="small" inner-style="left: 10rem"></loading>
|
||||
<div class="overview__row" v-if="securityData.length === 0">
|
||||
<span class="no-recent-alerts"><i class="el-icon-success"></i>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
<span class="no-recent-alerts"><el-icon><SuccessFilled /></el-icon>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
</div>
|
||||
<div class="overview__row" v-if="securityData.length > 0">
|
||||
<div class="row__label row__label--width130">{{$t('entities.recentSecurity')}}</div>
|
||||
@@ -174,7 +174,7 @@
|
||||
<div class="overview__row overview__row--small-font" v-for="(security, index) in entityData.securityList" :key="index">
|
||||
<div class="row__label row__label--width130">{{security.startTime ? dateFormatByAppearance(Number(security.startTime)) : '-'}}</div>
|
||||
<div class="row__content row__content--width90">
|
||||
<div class="alert-level-tag alert-level-tag--high" :class="iconClass(security)">{{security.eventSeverity}}</div>
|
||||
<div class="alert-level-tag" :class="`alert-level-tag--${iconClass(security)}`">{{security.eventSeverity}}</div>
|
||||
</div>
|
||||
<div class="cn-detection__header">
|
||||
<i class="cn-icon cn-icon-attacker"></i>
|
||||
@@ -321,7 +321,7 @@ export default {
|
||||
getMillisecond,
|
||||
dateFormatByAppearance,
|
||||
getQueryParams (dateRangeValue) {
|
||||
if (dateRangeValue && (!this.timeFilter.startTime || !this.timeFilter.endTime)) {
|
||||
if (dateRangeValue) {
|
||||
// range取 config.js 中配置的值
|
||||
const { startTime, endTime } = getNowTime(dateRangeValue)
|
||||
return {
|
||||
@@ -374,7 +374,7 @@ export default {
|
||||
return {
|
||||
chart: {
|
||||
params: {
|
||||
url: `${api.entity.entityList.appTrafficMap}?resource={{resource}}&country={{country}}`,
|
||||
url: `${api.entity.entityList.appTrafficMap}?resource={{resource}}&countryRegion={{countryRegion}}&startTime={{startTime}}&endTime={{endTime}}`,
|
||||
unitType: 'number',
|
||||
valueColumn: 'sessions'
|
||||
},
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
<template v-else-if="entity.entityType === 'app'">
|
||||
<app-overview :entity="entity" :time-filter="timeFilter" :keywordList="keywordList" @reloadEntity="getEntity" @eventNum="getEventNum"></app-overview>
|
||||
</template>
|
||||
<template v-else-if="entity.entityType === 'subscriber_id'">
|
||||
<subscriber-overview :entity="entity" :time-filter="timeFilter" :keywordList="keywordList" @reloadEntity="getEntity" @eventNum="getEventNum"></subscriber-overview>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +19,7 @@
|
||||
import App from './App'
|
||||
import Domain from './Domain'
|
||||
import Ip from './Ip'
|
||||
import Subscriber from './Subscriber'
|
||||
|
||||
export default {
|
||||
/* 详情概览 */
|
||||
@@ -28,7 +32,8 @@ export default {
|
||||
components: {
|
||||
'domain-overview': Domain,
|
||||
'app-overview': App,
|
||||
'ip-overview': Ip
|
||||
'ip-overview': Ip,
|
||||
'subscriber-overview': Subscriber
|
||||
},
|
||||
methods: {
|
||||
getEntity (data) {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<div class="overview-item">
|
||||
<div class="overview__title">{{$t('overall.traffic')}}</div>
|
||||
<div class="overview__content overview__content-loading">
|
||||
<loading :loading="loadingTraffic" size="small" inner-style="left: 8.75rem;" style="width: 50%;"></loading>
|
||||
<loading :loading="loadingTraffic" size="small" inner-style="left: 8.75rem;" class="content-loading"></loading>
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.peak')}}</div>
|
||||
<div class="row__content">
|
||||
@@ -51,7 +51,7 @@
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.throughput')}}</div>
|
||||
<div class="row__contents">
|
||||
<div class="row__content">
|
||||
<div class="row__content row__content-sent">
|
||||
<div class="row__charts-msg">{{$t('overall.sent')}}:
|
||||
{{valueToRangeValue(entityData.bytesSentRate, unitTypes.bps).join(' ') !== '- ' ? valueToRangeValue(entityData.bytesSentRate, unitTypes.bps).join(' ') : '-'}}
|
||||
</div>
|
||||
@@ -74,13 +74,13 @@
|
||||
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('entities.networkQualityRating')}}</div>
|
||||
<div style="position: relative;">
|
||||
<div class="entity-score__block">
|
||||
<div class="entity-score" v-if="!loadingNetworkQuality && score !=='-'">
|
||||
<span v-for="(dot, i) in scoreDot" :key="i" :class="dot.class"></span>
|
||||
<span style="padding-left: 4px;">{{score}}</span>
|
||||
<span class="entity-score__padding">{{score}}</span>
|
||||
</div>
|
||||
<div class="entity-score" v-else>{{score}}</div>
|
||||
<loading :loading="loadingNetworkQuality" size="small" style="left: 1rem;width: 50%;"></loading>
|
||||
<loading :loading="loadingNetworkQuality" size="small" class="entity-score__loading"></loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,8 +93,8 @@
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('entities.tab.relatedApp')}}</div>
|
||||
<div class="row__content overview__row-related">
|
||||
<div v-if="loadingRelationshipOne" style="position: relative;width: 450px;">
|
||||
<loading :loading="loadingRelationshipOne" size="small" style="left: 1rem;"></loading>
|
||||
<div v-if="loadingRelationshipOne" class="relationship-one__loading">
|
||||
<loading :loading="loadingRelationshipOne" size="small" class="one__loading"></loading>
|
||||
</div>
|
||||
|
||||
<div class="data-item high-light-block" v-high-light="keywordList" v-show="item.show" v-for="(item, index) in relationshipDataOne" :key="index">
|
||||
@@ -103,7 +103,7 @@
|
||||
<div v-if="relationshipDataOne.length===0 && !loadingRelationshipOne">-</div>
|
||||
|
||||
<div v-if="relationshipShowOne">
|
||||
<div class="data-item show-more-related" id="related-app-more" @click.stop="showMoreApp" style="position: relative">...</div>
|
||||
<div class="data-item show-more-related" id="related-app-more" @click.stop="showMoreApp">...</div>
|
||||
<div v-if="isShowMoreApp" class="app-popover_block" id="showRelatedApp">
|
||||
<div class="popover-content" v-for="(item, index) in relationshipDataOne" :key="index">
|
||||
<span v-if="!item.show" class="high-light-block" v-high-light="keywordList">{{item.value}}</span>
|
||||
@@ -116,8 +116,8 @@
|
||||
<div class="overview__row overview__row-related">
|
||||
<div class="row__label row__label--width130">{{$t('entities.tab.relatedIp')}}</div>
|
||||
<div class="row__content">
|
||||
<div v-if="loadingRelationshipTwo" style="position: relative;width: 450px;">
|
||||
<loading :loading="loadingRelationshipTwo" size="small" style="left: 1rem;"></loading>
|
||||
<div v-if="loadingRelationshipTwo" class="relationship-two__loading">
|
||||
<loading :loading="loadingRelationshipTwo" size="small" class="one__loading"></loading>
|
||||
</div>
|
||||
<div class="data-item high-light-block" v-high-light="keywordList" v-show="item.show" v-for="(item, index) in relationshipDataTwo" :key="index">
|
||||
{{item.value}}
|
||||
@@ -125,7 +125,7 @@
|
||||
<div v-if="relationshipDataTwo.length===0 && !loadingRelationshipTwo">-</div>
|
||||
|
||||
<div v-if="relationshipShowTwo">
|
||||
<div class="data-item show-more-related" id="related-domain-more" @click.stop="showMoreDomain" style="position: relative">...</div>
|
||||
<div class="data-item show-more-related" id="related-domain-more" @click.stop="showMoreDomain">...</div>
|
||||
<div v-if="isShowMoreDomain" class="domain-popover_block" id="showRelatedDomain">
|
||||
<div v-for="(item, index) in relationshipDataTwo" :key="index">
|
||||
<span v-if="!item.show" class="high-light-block" v-high-light="keywordList">{{item.value}}</span>
|
||||
@@ -142,7 +142,7 @@
|
||||
<div class="overview__content overview__content-loading">
|
||||
<loading :loading="loadingAlert" size="small" inner-style="left: 10rem"></loading>
|
||||
<div class="overview__row" v-if="performanceData.length === 0">
|
||||
<span class="no-recent-alerts"><i class="el-icon-success"></i>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
<span class="no-recent-alerts"><el-icon><SuccessFilled /></el-icon>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
</div>
|
||||
<div class="overview__row" v-if="performanceData.length > 0">
|
||||
<div class="row__label row__label--width130">{{$t('entities.recentAlert')}}</div>
|
||||
@@ -151,9 +151,9 @@
|
||||
<div class="overview__row overview__row--small-font" v-for="(performance, index) in entityData.performanceList" :key="index">
|
||||
<div class="row__label row__label--width130">{{performance.startTime ? dateFormatByAppearance(Number(performance.startTime)) : '-'}}</div>
|
||||
<div class="row__content row__content--width90">
|
||||
<div class="alert-level-tag alert-level-tag--high" :class="iconClass(performance)">{{performance.eventSeverity}}</div>
|
||||
<div class="alert-level-tag" :class="`alert-level-tag--${iconClass(performance)}`">{{performance.eventSeverity}}</div>
|
||||
</div>
|
||||
<div class="row__content-loading" style="position: relative;">
|
||||
<div class="row__content-loading">
|
||||
<div class="performance-event-remark">{{performance.eventType}}</div>
|
||||
</div>
|
||||
<div class="row__desc"></div>
|
||||
@@ -168,7 +168,7 @@
|
||||
<div class="overview__content overview__content-loading">
|
||||
<loading :loading="loadingSecurityEvents" size="small" inner-style="left: 10rem"></loading>
|
||||
<div class="overview__row" v-if="securityData.length === 0">
|
||||
<span class="no-recent-alerts"><i class="el-icon-success"></i>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
<span class="no-recent-alerts"><el-icon><SuccessFilled /></el-icon>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
</div>
|
||||
<div class="overview__row" v-if="securityData.length > 0">
|
||||
<div class="row__label row__label--width130">{{$t('entities.recentSecurity')}}</div>
|
||||
@@ -178,7 +178,7 @@
|
||||
<div class="overview__row overview__row--small-font" v-for="(security, i) in entityData.securityList" :key="i">
|
||||
<div class="row__label row__label--width130">{{security.startTime ? dateFormatByAppearance(Number(security.startTime)) : '-'}}</div>
|
||||
<div class="row__content row__content--width90">
|
||||
<div class="alert-level-tag alert-level-tag--high" :class="iconClass(security)">{{security.eventSeverity}}</div>
|
||||
<div class="alert-level-tag" :class="`alert-level-tag--${iconClass(security)}`">{{security.eventSeverity}}</div>
|
||||
</div>
|
||||
<div class="cn-detection__header">
|
||||
<i class="cn-icon cn-icon-attacker"></i>
|
||||
@@ -325,7 +325,7 @@ export default {
|
||||
getMillisecond,
|
||||
dateFormatByAppearance,
|
||||
getQueryParams (dateRangeValue) {
|
||||
if (dateRangeValue && (!this.timeFilter.startTime || !this.timeFilter.endTime)) {
|
||||
if (dateRangeValue) {
|
||||
// range取 config.js 中配置的值
|
||||
const { startTime, endTime } = getNowTime(dateRangeValue)
|
||||
return {
|
||||
@@ -400,7 +400,7 @@ export default {
|
||||
return {
|
||||
chart: {
|
||||
params: {
|
||||
url: `${api.entity.entityList.domainTrafficMap}?resource={{resource}}&country={{country}}`,
|
||||
url: `${api.entity.entityList.domainTrafficMap}?resource={{resource}}&countryRegion={{countryRegion}}&startTime={{startTime}}&endTime={{endTime}}`,
|
||||
unitType: 'number',
|
||||
valueColumn: 'sessions'
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.location')}}</div>
|
||||
<div class="row__content">
|
||||
<div v-if="entity.location" style="display: flex">
|
||||
<div v-if="entity.location" class="row__content-display">
|
||||
<div v-if="entity.location.country">
|
||||
<img v-if="entity.location.country===countryNameIdMapping.Unknown || !countryNameIdMapping[entity.location.country]" src="../../../../../public/images/flag/Unknown.svg" class="filter-country-flag">
|
||||
<img v-else :src="require(`../../../../../public/images/flag/${countryNameIdMapping[entity.location.country]}.png`)" class="filter-country-flag" >
|
||||
@@ -25,14 +25,14 @@
|
||||
</div>
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('entities.openPort')}}</div>
|
||||
<div class="row__content high-location" style="word-break: break-word;" v-high-light="keywordList">{{ openPort }}</div>
|
||||
<div class="row__content high-location row__content__word" v-high-light="keywordList">{{ openPort }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-item" v-if="entityData.dnsServerRole">
|
||||
<div class="overview__title">{{$t('overall.dnsServerInfo')}}</div>
|
||||
<div class="overview__content overview__content-loading">
|
||||
<loading :loading="loadingDns" size="small" inner-style="left: 8.75rem;" style="width: 50%;"></loading>
|
||||
<loading :loading="loadingDns" size="small" inner-style="left: 8.75rem;" class="content-loading"></loading>
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.dnsServerInfo.role')}}</div>
|
||||
<div class="row__content">{{$_.get(entityData, 'dnsServerRole') || '-'}}</div>
|
||||
@@ -71,7 +71,7 @@
|
||||
<div class="overview-item">
|
||||
<div class="overview__title">{{$t('overall.traffic')}}</div>
|
||||
<div class="overview__content overview__content-loading">
|
||||
<loading :loading="loadingTraffic" size="small" inner-style="left: 8.75rem;" style="width: 50%;"></loading>
|
||||
<loading :loading="loadingTraffic" size="small" inner-style="left: 8.75rem;" class="content-loading"></loading>
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.peak')}}</div>
|
||||
<div class="row__content">
|
||||
@@ -87,7 +87,7 @@
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.throughput')}}</div>
|
||||
<div class="row__contents">
|
||||
<div class="row__content">
|
||||
<div class="row__content row__content-sent">
|
||||
<div class="row__charts-msg">{{$t('overall.sent')}}:
|
||||
{{valueToRangeValue(entityData.bytesSentRate, unitTypes.bps).join(' ') !== '- ' ? valueToRangeValue(entityData.bytesSentRate, unitTypes.bps).join(' ') : '-'}}
|
||||
</div>
|
||||
@@ -109,13 +109,13 @@
|
||||
</div>
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('entities.networkQualityRating')}}</div>
|
||||
<div style="position: relative;">
|
||||
<div class="entity-score__block">
|
||||
<div class="entity-score" v-if="!loadingNetworkQuality && score !=='-'">
|
||||
<span v-for="(dot, i) in scoreDot" :key="i" :class="dot.class"></span>
|
||||
<span style="padding-left: 4px;">{{score}}</span>
|
||||
<span class="entity-score__padding">{{score}}</span>
|
||||
</div>
|
||||
<div class="entity-score" v-else>{{score}}</div>
|
||||
<loading :loading="loadingNetworkQuality" size="small" style="left: 1rem;width: 50%;"></loading>
|
||||
<loading :loading="loadingNetworkQuality" size="small" class="entity-score__loading"></loading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,8 +128,8 @@
|
||||
<div class="overview__row overview__row-related">
|
||||
<div class="row__label row__label--width130">{{$t('entities.tab.relatedApp')}}</div>
|
||||
<div class="row__content">
|
||||
<div v-if="loadingRelationshipOne" style="position: relative;width: 450px;">
|
||||
<loading :loading="loadingRelationshipOne" size="small" style="left: 1rem;"></loading>
|
||||
<div v-if="loadingRelationshipOne" class="relationship-one__loading">
|
||||
<loading :loading="loadingRelationshipOne" size="small" class="one__loading"></loading>
|
||||
</div>
|
||||
|
||||
<div class="data-item high-light-block" v-high-light="keywordList" v-show="item.show" v-for="(item, index) in relationshipDataOne" :key="index">
|
||||
@@ -138,7 +138,7 @@
|
||||
<div v-if="relationshipDataOne.length===0 && !loadingRelationshipOne">-</div>
|
||||
|
||||
<div v-if="relationshipShowOne">
|
||||
<div class="data-item show-more-related" id="related-app-more" @click.stop="showMoreApp" style="position: relative">...</div>
|
||||
<div class="data-item show-more-related" id="related-app-more" @click.stop="showMoreApp">...</div>
|
||||
<div v-if="isShowMoreApp" class="app-popover_block" id="showRelatedApp">
|
||||
<div class="popover-content" v-for="(item, index) in relationshipDataOne" :key="index">
|
||||
<span v-if="!item.show" class="high-light-block" v-high-light="keywordList">{{item.value}}</span>
|
||||
@@ -151,8 +151,8 @@
|
||||
<div class="overview__row overview__row-related">
|
||||
<div class="row__label row__label--width130">{{$t('entities.relatedDomain')}}</div>
|
||||
<div class="row__content">
|
||||
<div v-if="loadingRelationshipTwo" style="position: relative;width: 450px;">
|
||||
<loading :loading="loadingRelationshipTwo" size="small" style="left: 1rem;"></loading>
|
||||
<div v-if="loadingRelationshipTwo" class="relationship-two__loading">
|
||||
<loading :loading="loadingRelationshipTwo" size="small" class="one__loading"></loading>
|
||||
</div>
|
||||
|
||||
<div class="data-item high-light-block" v-high-light="keywordList" v-show="item.show" v-for="(item, index) in relationshipDataTwo" :key="index">
|
||||
@@ -161,7 +161,7 @@
|
||||
<div v-if="relationshipDataTwo.length===0 && !loadingRelationshipTwo">-</div>
|
||||
|
||||
<div v-if="relationshipShowTwo">
|
||||
<div class="data-item show-more-related" id="related-domain-more" @click.stop="showMoreDomain" style="position: relative">...</div>
|
||||
<div class="data-item show-more-related" id="related-domain-more" @click.stop="showMoreDomain">...</div>
|
||||
<div v-if="isShowMoreDomain" class="domain-popover_block" id="showRelatedDomain">
|
||||
<div v-for="(item, index) in relationshipDataTwo" :key="index">
|
||||
<span v-if="!item.show" class="high-light-block" v-high-light="keywordList">{{item.value}}</span>
|
||||
@@ -178,7 +178,7 @@
|
||||
<div class="overview__content overview__content-loading">
|
||||
<loading :loading="loadingAlert" size="small" inner-style="left: 10rem"></loading>
|
||||
<div class="overview__row" v-if="performanceData.length === 0">
|
||||
<span class="no-recent-alerts"><i class="el-icon-success"></i>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
<span class="no-recent-alerts"><el-icon><SuccessFilled /></el-icon>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
</div>
|
||||
<div class="overview__row" v-if="performanceData.length > 0">
|
||||
<div class="row__label row__label--width130">{{$t('entities.recentAlert')}}</div>
|
||||
@@ -187,9 +187,9 @@
|
||||
<div class="overview__row overview__row--small-font" v-for="(performance, index) in entityData.performanceList" :key="index">
|
||||
<div class="row__label row__label--width130">{{performance.startTime ? dateFormatByAppearance(Number(performance.startTime)) : '-'}}</div>
|
||||
<div class="row__content row__content--width90">
|
||||
<div class="alert-level-tag alert-level-tag--high" :class="iconClass(performance)">{{performance.eventSeverity}}</div>
|
||||
<div class="alert-level-tag" :class="`alert-level-tag--${iconClass(performance)}`">{{performance.eventSeverity}}</div>
|
||||
</div>
|
||||
<div class="row__content-loading" style="position: relative;">
|
||||
<div class="row__content-loading">
|
||||
<div class="performance-event-remark">{{performance.eventType}}</div>
|
||||
</div>
|
||||
<div class="row__desc"></div>
|
||||
@@ -201,10 +201,10 @@
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<div class="overview__title">{{$t('entities.securityEvents')}}</div>
|
||||
<div class="overview__content overview__content-loading">
|
||||
<div class="overview__content overview__content-loading" style="position: relative">
|
||||
<loading :loading="loadingSecurityEvents" size="small" inner-style="left: 10rem"></loading>
|
||||
<div class="overview__row" v-if="securityData.length === 0">
|
||||
<span class="no-recent-alerts"><i class="el-icon-success"></i>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
<span class="no-recent-alerts"><el-icon><SuccessFilled /></el-icon>{{$t('relationShip.noRecentAlerts')}}</span>
|
||||
</div>
|
||||
<div class="overview__row" v-if="securityData.length > 0">
|
||||
<div class="row__label row__label--width130">{{$t('entities.recentSecurity')}}</div>
|
||||
@@ -214,9 +214,9 @@
|
||||
<div class="overview__row overview__row--small-font" v-for="(security, index) in entityData.securityList" :key="index">
|
||||
<div class="row__label row__label--width130">{{security.startTime ? dateFormatByAppearance(Number(security.startTime)) : '-'}}</div>
|
||||
<div class="row__content row__content--width90">
|
||||
<div class="alert-level-tag alert-level-tag--high" :class="iconClass(security)">{{security.eventSeverity}}</div>
|
||||
<div class="alert-level-tag" :class="`alert-level-tag--${iconClass(security)}`">{{security.eventSeverity}}</div>
|
||||
</div>
|
||||
<div class="cn-detection__header" >
|
||||
<div class="cn-detection__header">
|
||||
<i class="cn-icon cn-icon-attacker"></i>
|
||||
<span>{{ security.offenderIp }}</span>
|
||||
<div class="domain" v-if="security.offenderIp===security.serverIp">{{ security.domain }}</div>
|
||||
@@ -409,7 +409,7 @@ export default {
|
||||
getMillisecond,
|
||||
dateFormatByAppearance,
|
||||
getQueryParams (dateRangeValue) {
|
||||
if (dateRangeValue && (!this.timeFilter.startTime || !this.timeFilter.endTime)) {
|
||||
if (dateRangeValue) {
|
||||
// range取 config.js 中配置的值
|
||||
const { startTime, endTime } = getNowTime(dateRangeValue)
|
||||
return {
|
||||
@@ -483,7 +483,7 @@ export default {
|
||||
return {
|
||||
chart: {
|
||||
params: {
|
||||
url: `${api.entity.entityList.ipTrafficMap}?resource={{resource}}&country={{country}}`,
|
||||
url: `${api.entity.entityList.ipTrafficMap}?resource={{resource}}&countryRegion={{countryRegion}}&startTime={{startTime}}&endTime={{endTime}}`,
|
||||
unitType: 'number',
|
||||
valueColumn: 'sessions'
|
||||
},
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<div class="overview-item">
|
||||
<div class="overview__title">{{$t('overall.basicInfo')}}</div>
|
||||
<div class="overview__content">
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{ $t('entity.subscriberId') }}</div>
|
||||
<div class="row__content">{{$_.get(entity, 'entityValue', '-') || '-'}}</div>
|
||||
</div>
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.location')}}</div>
|
||||
<div class="row__content" v-high-light="keywordList">{{$_.get(entity, 'subscriber_longitude', '-') || '-'}}, {{$_.get(entity, 'subscriber_latitude', '-') || '-'}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-item">
|
||||
<div class="overview__title">{{$t('overall.traffic')}}</div>
|
||||
<div class="overview__content overview__content-loading">
|
||||
<loading :loading="loadingTraffic" size="small" inner-style="left: 8.75rem;" style="width: 50%;"></loading>
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.peak')}}</div>
|
||||
<div class="row__content">
|
||||
{{valueToRangeValue(entityData.max, unitTypes.bps).join(' ') !== '- ' ? valueToRangeValue(entityData.max, unitTypes.bps).join(' ') : '-'}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.average')}}</div>
|
||||
<div class="row__content">
|
||||
{{valueToRangeValue(entityData.avg, unitTypes.bps).join(' ') !== '- ' ? valueToRangeValue(entityData.avg, unitTypes.bps).join(' ') : '-'}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview__row">
|
||||
<div class="row__label row__label--width130">{{$t('overall.throughput')}}</div>
|
||||
<div class="row__contents row__contents-subscriber">
|
||||
<div class="row__content row__content-sent">
|
||||
<div class="row__charts-msg">{{$t('overall.sent')}}:
|
||||
{{valueToRangeValue(entityData.bytesSentRate, unitTypes.bps).join(' ') !== '- ' ? valueToRangeValue(entityData.bytesSentRate, unitTypes.bps).join(' ') : '-'}}
|
||||
</div>
|
||||
<!-- 曲线-->
|
||||
<div class="row__content-loading">
|
||||
<div class="row__charts" :id="`entityDetailSend${entity.entityValue}`" ></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row__content row__content-accept">
|
||||
<div class="row__charts-msg">{{$t('overall.received')}}:
|
||||
{{valueToRangeValue(entityData.bytesReceivedRate, unitTypes.bps).join(' ') !== '- ' ? valueToRangeValue(entityData.bytesReceivedRate, unitTypes.bps).join(' ') : '-'}}
|
||||
</div>
|
||||
<!-- 曲线-->
|
||||
<div class="row__content-loading">
|
||||
<div class="row__charts" :id="`entityDetailReceived${entity.entityValue}`" ></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-item">
|
||||
<div class="overview__title">{{$t('overall.relationship')}}</div>
|
||||
<div class="overview__content domain__content">
|
||||
<div class="overview__tags domain__tags" ref="relationship"></div>
|
||||
<div class="overview__row overview__row-related">
|
||||
<div class="row__label row__label--width130">{{$t('entities.tab.relatedApp')}}</div>
|
||||
<div class="row__content">
|
||||
<div v-if="loadingRelationshipOne" style="position: relative;width: 450px;">
|
||||
<loading :loading="loadingRelationshipOne" size="small" style="left: 1rem;"></loading>
|
||||
</div>
|
||||
|
||||
<div class="data-item high-light-block" v-high-light="keywordList" v-show="item.show" v-for="(item, index) in relationshipDataOne" :key="index">
|
||||
{{item.value}}
|
||||
</div>
|
||||
<div v-if="relationshipDataOne.length===0 && !loadingRelationshipOne">-</div>
|
||||
|
||||
<div v-if="relationshipShowOne">
|
||||
<div class="data-item show-more-related" id="related-app-more" @click.stop="showMoreApp" style="position: relative">...</div>
|
||||
<div v-if="isShowMoreApp" class="app-popover_block" id="showRelatedApp">
|
||||
<div class="popover-content" v-for="(item, index) in relationshipDataOne" :key="index">
|
||||
<span v-if="!item.show" class="high-light-block" v-high-light="keywordList">{{item.value}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-map overview-map--app">
|
||||
<!-- <chart-->
|
||||
<!-- :chart-info="chart"-->
|
||||
<!-- :chart-data="chartData"-->
|
||||
<!-- :entity="entityCopy"-->
|
||||
<!-- :query-params="queryParams"-->
|
||||
<!-- :hide-header="true"-->
|
||||
<!-- :loading="loadingMap"-->
|
||||
<!-- @getCurrentTimeRange="getCurrentTimeRange"-->
|
||||
<!-- ></chart>-->
|
||||
<subscriber-map :entity="entity"></subscriber-map>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from '@/utils/api'
|
||||
import entityDetailMixin from './entityDetailMixin'
|
||||
import { unitTypes } from '@/utils/constants'
|
||||
import { valueToRangeValue } from '@/utils/unit-convert'
|
||||
import _ from 'lodash'
|
||||
import relatedServer from '@/mixins/relatedServer'
|
||||
import { dateFormatByAppearance, getMillisecond, getSecond, getNowTime } from '@/utils/date-util'
|
||||
import Loading from '@/components/common/Loading'
|
||||
import { ref } from 'vue'
|
||||
import SubscriberMap from '@/views/entityExplorer/entityList/detailOverview/SubscriberMap'
|
||||
|
||||
export default {
|
||||
name: 'Subscriber',
|
||||
mixins: [entityDetailMixin, relatedServer],
|
||||
components: {
|
||||
Loading,
|
||||
SubscriberMap
|
||||
},
|
||||
props: {
|
||||
keywordList: Array
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// entityData: {}
|
||||
entityType: 'subscriber',
|
||||
// trafficUrl: api.entityAppDetailTraffic,
|
||||
trafficUrl: api.entity.entityList.subscriberThroughput,
|
||||
// relationUrl: api.entityAppDetailRelation,
|
||||
// networkQuantityUrl: api.entityAppDetailNetworkQuantity,
|
||||
networkQuantityUrl: api.entity.entityList.subscriberPerformance,
|
||||
// linkInUrl: api.entityAppDetailLinkIn,
|
||||
// linkOutUrl: api.entityAppDetailLinkOut,
|
||||
// performanceUrl: api.entityAppDetailPerformance,
|
||||
// securityUrl: api.entityAppDetailSecurity,
|
||||
// trafficUrlMap: api.entityAppDetailTrafficMap,
|
||||
// trafficUrlMap: api.entity.entityList.appTrafficMap,
|
||||
// relatedServerDomainUrl: api.entity.entityList.appRelatedDomain,
|
||||
relatedServerAppUrl: api.entity.entityList.subscriberRelatedApp,
|
||||
chartData: null,
|
||||
listMode: 'list',
|
||||
singleValues: {
|
||||
chartInfos: [
|
||||
{
|
||||
params: {
|
||||
icon: 'cn-icon cn-icon-time',
|
||||
unitType: unitTypes.time,
|
||||
iconColor: '#2ca3fe',
|
||||
iconBackgroundColor: '#eff6fe'
|
||||
},
|
||||
type: 507,
|
||||
i18n: 'entities.avgRoundTripTime'
|
||||
},
|
||||
{
|
||||
params: {
|
||||
icon: 'cn-icon cn-icon-http',
|
||||
unitType: unitTypes.time,
|
||||
iconColor: '#2ca3fe',
|
||||
iconBackgroundColor: '#eff6fe'
|
||||
},
|
||||
type: 507,
|
||||
i18n: 'entities.httpResponseLatency'
|
||||
},
|
||||
{
|
||||
params: {
|
||||
icon: 'cn-icon cn-icon-ssl',
|
||||
unitType: unitTypes.time,
|
||||
iconColor: '#2ca3fe',
|
||||
iconBackgroundColor: '#eff6fe'
|
||||
},
|
||||
type: 507,
|
||||
i18n: 'entities.sslConLatency'
|
||||
},
|
||||
{
|
||||
params: {
|
||||
icon: 'cn-icon cn-icon-package-loss',
|
||||
unitType: unitTypes.percent,
|
||||
iconColor: '#2ca3fe',
|
||||
iconBackgroundColor: '#eff6fe'
|
||||
},
|
||||
type: 507,
|
||||
i18n: 'entities.sequenceGapLossPercent'
|
||||
},
|
||||
{
|
||||
params: {
|
||||
icon: 'cn-icon cn-icon-upload',
|
||||
unitType: unitTypes.percent,
|
||||
iconColor: '#2ca3fe',
|
||||
iconBackgroundColor: '#eff6fe'
|
||||
},
|
||||
type: 507,
|
||||
i18n: 'entities.pktRetransPercent'
|
||||
}
|
||||
],
|
||||
chartDatas: [null, null, null, null, null]
|
||||
},
|
||||
loadingTraffic: false,
|
||||
loadingRelationshipOne: false,
|
||||
loadingRelationshipTwo: false,
|
||||
loadingNetworkQuality: false,
|
||||
loadingOut: false,
|
||||
loadingIn: false,
|
||||
loadingAlert: false,
|
||||
loadingSecurityEvents: false,
|
||||
loadingMap: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getMillisecond,
|
||||
dateFormatByAppearance,
|
||||
getQueryParams (dateRangeValue) {
|
||||
if (dateRangeValue) {
|
||||
// range取 config.js 中配置的值
|
||||
const { startTime, endTime } = getNowTime(dateRangeValue)
|
||||
return {
|
||||
startTime: getSecond(startTime),
|
||||
endTime: getSecond(endTime),
|
||||
resource: this.entity.entityValue
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
startTime: getSecond(this.timeFilter.startTime),
|
||||
endTime: getSecond(this.timeFilter.endTime),
|
||||
resource: this.entity.entityValue
|
||||
}
|
||||
}
|
||||
},
|
||||
getPerformanceQueryParams () {
|
||||
return {
|
||||
appName: this.entity.entityValue
|
||||
}
|
||||
},
|
||||
handleRelationData (result) {
|
||||
this.entityData.domainCount = result.domainCount
|
||||
this.entityData.ipCount = result.ipCount
|
||||
},
|
||||
queryRelated () {
|
||||
this.getRelatedServerDataOne(this.relatedServerAppUrl)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.queryParams = this.getQueryParams()
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
this.queryRelated()
|
||||
}, 250)
|
||||
})
|
||||
},
|
||||
setup (props) {
|
||||
const entityData = ref({ ...props.entity })
|
||||
return {
|
||||
unitTypes,
|
||||
entityCopy: {
|
||||
..._.cloneDeep(props.entity)
|
||||
},
|
||||
valueToRangeValue,
|
||||
entityData
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,713 @@
|
||||
<template>
|
||||
<div class="subscriber-map">
|
||||
<div class="subscriber-map-body" style="height: 100%">
|
||||
<loading :loading="trackingMapLoading" @click="developmentClick"></loading>
|
||||
<div id="subscriberMap" class="entity-subscriber-map"></div>
|
||||
</div>
|
||||
|
||||
<div class="geo-analysis__hexagon-tooltip" id="tooltip" :class="`geo-analysis__hexagon-tooltip--${tooltip.type}`" v-if="tooltip.showMarkerTooltip || tooltip.showPolygonTooltip" :style="{'left': `${tooltip.x}px`, 'top': `${tooltip.y}px`}" @mouseenter="tooltipMouseEnter" @mouseleave="tooltipMouseLeave">
|
||||
<div class="hexagon-tooltip__header" :style="`background-color: ${tooltipHeaderColor}`">
|
||||
<div class="header__icon">
|
||||
<svg v-if="tooltip.type === tooltipType.hexagon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="36" height="36"><path d="M747.52 921.088H277.504L42.496 514.048l235.008-407.04H747.52l235.008 407.04-235.008 407.04z m-425.472-76.8h381.44l190.464-330.24-190.464-330.24h-381.44l-190.464 330.24 190.464 330.24z"></path></svg>
|
||||
<template v-else-if="tooltip.type === tooltipType.human">
|
||||
<div class="icon__box">
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"><path d="M366.689524 690.468571l87.283809 83.821715-75.434666 195.486476c-10.971429 27.794286-43.105524 42.081524-72.265143 32.036571-29.159619-10.24-44.080762-41.252571-33.450667-69.241904L366.689524 690.468571zM203.824762 476.306286l51.785143-95.183238a162.279619 162.279619 0 0 1 59.245714-59.977143c119.710476-68.266667 134.777905-67.291429 149.942857-66.218667l80.798476 5.168762c24.868571 0.975238 42.081524 7.314286 125.025524 124.14781a21.26019 21.26019 0 0 0 14.092191 8.289523l99.132952 14.482286c12.873143 1.852952 24.478476 8.582095 32.182857 18.67581a45.494857 45.494857 0 0 1 8.825905 35.108571 46.665143 46.665143 0 0 1-19.504762 30.866286 50.468571 50.468571 0 0 1-36.571429 8.435809l-99.181714-14.433524a119.954286 119.954286 0 0 1-79.774476-47.640381c-4.388571-6.241524-7.558095-11.361524-11.849143-16.579047l-63.634286 193.487238 88.405334 84.845714c12.970667 12.385524 23.698286 29.013333 30.232381 45.494857l67.876571 190.366477c5.022476 13.409524 4.193524 28.233143-2.291809 41.057523a54.613333 54.613333 0 0 1-32.182858 27.160381c-5.851429 2.048-12.092952 3.120762-18.383238 3.169524a58.075429 58.075429 0 0 1-53.930666-36.181333l-67.876572-190.366476c-1.024-2.096762-2.096762-3.120762-3.218285-5.168762L365.616762 623.177143a84.894476 84.894476 0 0 1-24.868572-80.700953l34.523429-146.919619c-3.413333 2.340571-7.070476 4.388571-10.776381 6.290286a58.806857 58.806857 0 0 0-22.674286 22.723048L290.133333 519.801905a49.834667 49.834667 0 0 1-43.105523 24.819809 59.977143 59.977143 0 0 1-22.674286-5.12 46.518857 46.518857 0 0 1-20.48-63.146666z m209.67619-360.448C420.08381 58.465524 473.86819 17.066667 533.650286 23.30819 593.383619 29.549714 636.537905 81.13981 630.00381 138.48381c-6.534095 57.392762-60.269714 98.840381-120.05181 92.550095-59.782095-6.241524-102.985143-57.782857-96.451048-115.175619z"></path></svg>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="tooltip.type === tooltipType.baseStation">
|
||||
<div class="icon__box">
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"><path d="M164.901926 519.585185a391.35763 391.35763 0 0 1-30.151111-152.38637c0-52.527407 10.05037-105.054815 30.151111-152.348445 15.094519-47.29363 45.283556-89.353481 80.516741-126.103703L174.990222 15.17037C129.706667 62.464 94.511407 114.991407 69.328593 178.062222 44.183704 241.095111 34.133333 304.165926 34.133333 367.198815c0 63.070815 10.05037 131.375407 35.19526 189.174518 25.182815 57.799111 60.378074 115.598222 105.661629 162.891852l70.428445-73.576296c-35.233185-36.788148-65.422222-78.810074-80.516741-126.103704z" p-id="8786"></path><path d="M255.469037 477.563259c15.094519 36.788148 35.195259 68.266667 60.340148 94.549334l70.428445-73.576297a233.168593 233.168593 0 0 1-40.201482-57.761185c-5.044148-26.282667-10.088296-47.29363-10.088296-73.576296 0-26.244741 5.044148-47.29363 15.094518-68.266667 5.006222-26.282667 20.100741-42.097778 35.19526-63.070815l-70.428445-73.576296c-25.144889 26.282667-45.24563 57.799111-60.340148 94.587259-15.094519 36.788148-20.100741 73.576296-20.100741 110.364445 0 36.788148 5.006222 73.53837 20.100741 110.326518zM436.527407 367.198815c0 43.538963 33.792 78.810074 75.472593 78.810074s75.472593-35.271111 75.472593-78.810074c0-43.501037-33.792-78.810074-75.472593-78.810074s-75.472593 35.271111-75.472593 78.810074zM637.76237 498.574222l70.428445 73.576297c25.144889-26.282667 45.24563-57.837037 60.340148-94.58726 15.094519-36.788148 20.100741-73.576296 20.100741-110.364444 0-36.788148-5.006222-73.576296-20.100741-110.364445-15.094519-36.750222-35.195259-68.266667-60.340148-94.549333l-70.428445 73.576296c15.094519 15.739259 30.189037 36.788148 40.201482 57.799111 10.088296 21.010963 15.132444 47.29363 15.132444 68.266667 0 26.282667-5.044148 47.331556-15.094518 68.342519-10.05037 31.516444-25.144889 47.29363-40.239408 68.266666z" p-id="8787"></path><path d="M954.671407 178.062222C929.488593 114.991407 894.293333 62.464 849.009778 15.17037L778.619259 88.746667c35.233185 36.788148 60.378074 78.810074 80.516741 126.103703 20.100741 47.255704 30.151111 99.821037 30.151111 152.348445 0 52.565333-10.05037 105.092741-30.151111 152.38637-20.100741 47.29363-45.283556 89.315556-80.516741 126.103704l70.428445 73.576296c45.283556-47.29363 80.478815-99.858963 105.661629-162.891852 25.144889-63.070815 35.195259-126.103704 35.19526-189.174518 0-63.032889-10.05037-131.337481-35.19526-189.136593zM210.185481 1024h603.629038L512 551.10163 210.185481 1024z m186.102519-105.054815L512 740.238222l115.674074 178.631111h-231.348148z"></path></svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="header__title">
|
||||
<template v-if="tooltip.type === tooltipType.hexagon">HEX</template>
|
||||
<template v-else-if="tooltip.type === tooltipType.human">MSISDN</template>
|
||||
<template v-else-if="tooltip.type === tooltipType.baseStation">CID</template>
|
||||
</div>
|
||||
<div class="header__content">
|
||||
<template v-if="tooltip.type === tooltipType.hexagon">{{currentPolygon.hexId}}</template>
|
||||
<template v-else-if="tooltip.type === tooltipType.human">{{currentSubscriber.subscriberDto.phoneNumber}}</template>
|
||||
<template v-else-if="tooltip.type === tooltipType.baseStation">0xxa8805</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hexagon-tooltip__body">
|
||||
<template v-if="tooltip.type === tooltipType.hexagon">
|
||||
<template v-for="(item, i) in JSON.parse(currentPolygon.locations)" :key="item.hexId">
|
||||
<div class="body__timeline" v-if="i < 5">
|
||||
<div class="timeline-symbol"></div>
|
||||
<div>
|
||||
<div class="body__item">
|
||||
<div class="item__label">{{ $t('location.location') }}</div>
|
||||
<div class="item__value">{{item.longitude}}, {{item.latitude}}</div>
|
||||
</div>
|
||||
<div class="body__item">
|
||||
<div class="item__label">Time</div>
|
||||
<div class="item__value">{{dateFormatByAppearance(Number(item.time))}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="body__timeline" v-if="JSON.parse(currentPolygon.locations).length > 5">...</div>
|
||||
</template>
|
||||
<template v-else-if="tooltip.type === tooltipType.human">
|
||||
<div class="body__item">
|
||||
<div class="item__label">ID</div>
|
||||
<div class="item__value">{{currentSubscriber.subscriberId}}</div>
|
||||
</div>
|
||||
<div class="body__item">
|
||||
<div class="item__label">{{$t('entities.group')}}</div>
|
||||
<div class="item__value">Terrorist</div>
|
||||
</div>
|
||||
<div class="body__item">
|
||||
<div class="item__label">{{$t('overall.info')}}</div>
|
||||
<div class="item__value">Leader</div>
|
||||
</div>
|
||||
<div class="body__item">
|
||||
<div class="item__label">{{$t('overall.location')}}</div>
|
||||
<div class="item__value">China, Shanghai</div>
|
||||
</div>
|
||||
<div class="body__tracking" @click="trackSubscriber(currentSubscriber)">{{$t('location.traceTracking')}}</div>
|
||||
</template>
|
||||
<template v-else-if="tooltip.type === tooltipType.baseStation">
|
||||
<div class="body__item">
|
||||
<div class="item__label">{{ $t('location.locationAreaCode') }}</div>
|
||||
<div class="item__value">12</div>
|
||||
</div>
|
||||
<div class="body__item">
|
||||
<div class="item__label">{{ $t('location.mobileNetworkCode') }}</div>
|
||||
<div class="item__value">1</div>
|
||||
</div>
|
||||
<div class="body__item">
|
||||
<div class="item__label">{{ $t('location.communicationType') }}</div>
|
||||
<div class="item__value">4G</div>
|
||||
</div>
|
||||
<div class="body__item">
|
||||
<div class="item__label">{{$t('overall.location')}}</div>
|
||||
<div class="item__value">China, Shanghai</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import chartMixin from '@/views/charts2/chart-mixin'
|
||||
import maplibregl from 'maplibre-gl'
|
||||
import mapStyle from '@/views/charts2/charts/entityDetail/mapStyle'
|
||||
import axios from 'axios'
|
||||
import { api } from '@/utils/api'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
import { getNowTime, getSecond, dateFormatByAppearance } from '@/utils/date-util'
|
||||
import unitConvert from '@/utils/unit-convert'
|
||||
import { defaultMapConfig, storageKey, unitTypes } from '@/utils/constants'
|
||||
import { h3ToGeo, h3ToGeoBoundary } from 'h3-js'
|
||||
import { overwriteUrl, urlParamsHandler } from '@/utils/tools'
|
||||
import Loading from '@/components/common/Loading'
|
||||
const humanSvg = '<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M366.689524 690.468571l87.283809 83.821715-75.434666 195.486476c-10.971429 27.794286-43.105524 42.081524-72.265143 32.036571-29.159619-10.24-44.080762-41.252571-33.450667-69.241904L366.689524 690.468571zM203.824762 476.306286l51.785143-95.183238a162.279619 162.279619 0 0 1 59.245714-59.977143c119.710476-68.266667 134.777905-67.291429 149.942857-66.218667l80.798476 5.168762c24.868571 0.975238 42.081524 7.314286 125.025524 124.14781a21.26019 21.26019 0 0 0 14.092191 8.289523l99.132952 14.482286c12.873143 1.852952 24.478476 8.582095 32.182857 18.67581a45.494857 45.494857 0 0 1 8.825905 35.108571 46.665143 46.665143 0 0 1-19.504762 30.866286 50.468571 50.468571 0 0 1-36.571429 8.435809l-99.181714-14.433524a119.954286 119.954286 0 0 1-79.774476-47.640381c-4.388571-6.241524-7.558095-11.361524-11.849143-16.579047l-63.634286 193.487238 88.405334 84.845714c12.970667 12.385524 23.698286 29.013333 30.232381 45.494857l67.876571 190.366477c5.022476 13.409524 4.193524 28.233143-2.291809 41.057523a54.613333 54.613333 0 0 1-32.182858 27.160381c-5.851429 2.048-12.092952 3.120762-18.383238 3.169524a58.075429 58.075429 0 0 1-53.930666-36.181333l-67.876572-190.366476c-1.024-2.096762-2.096762-3.120762-3.218285-5.168762L365.616762 623.177143a84.894476 84.894476 0 0 1-24.868572-80.700953l34.523429-146.919619c-3.413333 2.340571-7.070476 4.388571-10.776381 6.290286a58.806857 58.806857 0 0 0-22.674286 22.723048L290.133333 519.801905a49.834667 49.834667 0 0 1-43.105523 24.819809 59.977143 59.977143 0 0 1-22.674286-5.12 46.518857 46.518857 0 0 1-20.48-63.146666z m209.67619-360.448C420.08381 58.465524 473.86819 17.066667 533.650286 23.30819 593.383619 29.549714 636.537905 81.13981 630.00381 138.48381c-6.534095 57.392762-60.269714 98.840381-120.05181 92.550095-59.782095-6.241524-102.985143-57.782857-96.451048-115.175619z"></path></svg>'
|
||||
const baseStationSvg = '<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M164.901926 519.585185a391.35763 391.35763 0 0 1-30.151111-152.38637c0-52.527407 10.05037-105.054815 30.151111-152.348445 15.094519-47.29363 45.283556-89.353481 80.516741-126.103703L174.990222 15.17037C129.706667 62.464 94.511407 114.991407 69.328593 178.062222 44.183704 241.095111 34.133333 304.165926 34.133333 367.198815c0 63.070815 10.05037 131.375407 35.19526 189.174518 25.182815 57.799111 60.378074 115.598222 105.661629 162.891852l70.428445-73.576296c-35.233185-36.788148-65.422222-78.810074-80.516741-126.103704z" p-id="8786"></path><path d="M255.469037 477.563259c15.094519 36.788148 35.195259 68.266667 60.340148 94.549334l70.428445-73.576297a233.168593 233.168593 0 0 1-40.201482-57.761185c-5.044148-26.282667-10.088296-47.29363-10.088296-73.576296 0-26.244741 5.044148-47.29363 15.094518-68.266667 5.006222-26.282667 20.100741-42.097778 35.19526-63.070815l-70.428445-73.576296c-25.144889 26.282667-45.24563 57.799111-60.340148 94.587259-15.094519 36.788148-20.100741 73.576296-20.100741 110.364445 0 36.788148 5.006222 73.53837 20.100741 110.326518zM436.527407 367.198815c0 43.538963 33.792 78.810074 75.472593 78.810074s75.472593-35.271111 75.472593-78.810074c0-43.501037-33.792-78.810074-75.472593-78.810074s-75.472593 35.271111-75.472593 78.810074zM637.76237 498.574222l70.428445 73.576297c25.144889-26.282667 45.24563-57.837037 60.340148-94.58726 15.094519-36.788148 20.100741-73.576296 20.100741-110.364444 0-36.788148-5.006222-73.576296-20.100741-110.364445-15.094519-36.750222-35.195259-68.266667-60.340148-94.549333l-70.428445 73.576296c15.094519 15.739259 30.189037 36.788148 40.201482 57.799111 10.088296 21.010963 15.132444 47.29363 15.132444 68.266667 0 26.282667-5.044148 47.331556-15.094518 68.342519-10.05037 31.516444-25.144889 47.29363-40.239408 68.266666z" p-id="8787"></path><path d="M954.671407 178.062222C929.488593 114.991407 894.293333 62.464 849.009778 15.17037L778.619259 88.746667c35.233185 36.788148 60.378074 78.810074 80.516741 126.103703 20.100741 47.255704 30.151111 99.821037 30.151111 152.348445 0 52.565333-10.05037 105.092741-30.151111 152.38637-20.100741 47.29363-45.283556 89.315556-80.516741 126.103704l70.428445 73.576296c45.283556-47.29363 80.478815-99.858963 105.661629-162.891852 25.144889-63.070815 35.195259-126.103704 35.19526-189.174518 0-63.032889-10.05037-131.337481-35.19526-189.136593zM210.185481 1024h603.629038L512 551.10163 210.185481 1024z m186.102519-105.054815L512 740.238222l115.674074 178.631111h-231.348148z"></path></svg>'
|
||||
|
||||
export default {
|
||||
name: 'EntityDetailMap',
|
||||
mixins: [chartMixin],
|
||||
components: {
|
||||
Loading
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tooltipType: {
|
||||
hexagon: 'hexagon',
|
||||
baseStation: 'base-station',
|
||||
human: 'human'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.initMap()
|
||||
},
|
||||
unmounted () {
|
||||
this.mapChart && this.mapChart?.remove?.()
|
||||
this.mapChart = null
|
||||
},
|
||||
computed: {
|
||||
tooltipHeaderColor () {
|
||||
if (this.tooltip.type === this.tooltipType.hexagon) {
|
||||
const color = this.currentPolygon.color.split(',')
|
||||
color[0] = color[0].split('[')[1]
|
||||
color[2] = color[2].split(']')[0]
|
||||
return `rgba(${color.join(',')},.8)`
|
||||
} else if (this.tooltip.type === this.tooltipType.human) {
|
||||
return '#38ACD2'
|
||||
} else if (this.tooltip.type === this.tooltipType.baseStation) {
|
||||
return '#233447'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 切换追踪的用户
|
||||
currentShowSubscriber (n) {
|
||||
this.mapChart.getLayer('trackingHexagon') && this.mapChart.removeLayer('trackingHexagon')
|
||||
this.mapChart.getLayer('trackingLine') && this.mapChart.removeLayer('trackingLine')
|
||||
this.mapChart.getSource('trackingHexGrid') && this.mapChart.removeSource('trackingHexGrid')
|
||||
this.mapChart.getSource('trackingLineSource') && this.mapChart.removeSource('trackingLineSource')
|
||||
this.trackingHumanMarker.remove && this.trackingHumanMarker.remove()
|
||||
this.trackingHumanMarker = {}
|
||||
if (n) {
|
||||
this.renderTrackingHexagon()
|
||||
}
|
||||
},
|
||||
timeFilter (n) {
|
||||
this.unbindTrackingHexagonEvents()
|
||||
this.mapChart.getLayer('trackingHexagon') && this.mapChart.removeLayer('trackingHexagon')
|
||||
this.mapChart.getLayer('trackingLine') && this.mapChart.removeLayer('trackingLine')
|
||||
this.mapChart.getSource('trackingHexGrid') && this.mapChart.removeSource('trackingHexGrid')
|
||||
this.mapChart.getSource('trackingLineSource') && this.mapChart.removeSource('trackingLineSource')
|
||||
this.trackingHumanMarker.remove && this.trackingHumanMarker.remove()
|
||||
this.trackingHumanMarker = {}
|
||||
this.changeTimeFilterToInitList()
|
||||
this.initTraceTrackingTab()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dateFormatByAppearance,
|
||||
async initMap () {
|
||||
const _this = this
|
||||
this.toggleLoading(false)
|
||||
this.mapChart = new maplibregl.Map({
|
||||
container: 'subscriberMap',
|
||||
style: mapStyle,
|
||||
center: this.center,
|
||||
maxZoom: this.maxZoom,
|
||||
minZoom: this.minZoom,
|
||||
zoom: this.defaultZoom
|
||||
})
|
||||
maplibregl.addProtocol('cn', (params, callback) => { // 切片显示接口 防止跨域的问题
|
||||
fetch(`${params.url.split('://')[1]}`)
|
||||
.then(t => {
|
||||
if (t.status == 200) {
|
||||
t.arrayBuffer().then(arr => {
|
||||
callback(null, arr, null, null)
|
||||
})
|
||||
} else {
|
||||
callback(new Error(`Tile fetch error: ${t.statusText}`))
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
callback(new Error(e))
|
||||
})
|
||||
return { cancel: () => { } }
|
||||
})
|
||||
this.mapChart.on('load', async () => {
|
||||
const baseStationData = await _this.queryBaseStation()
|
||||
_this.renderMarker(baseStationData, _this.tooltipType.baseStation)
|
||||
|
||||
_this.initTraceTrackingTab()
|
||||
})
|
||||
},
|
||||
async initTraceTrackingTab () {
|
||||
await this.queryTraceTracking()
|
||||
if (!this.currentShowSubscriber && this.trackingSubscriber) {
|
||||
this.currentShowSubscriber = this.trackingSubscriber
|
||||
}
|
||||
this.renderTrackingHexagon()
|
||||
},
|
||||
async queryBaseStation () {
|
||||
// this.loading.baseStationLoading = true
|
||||
try {
|
||||
// const response = await axios.get(api.location.baseStation)
|
||||
const response = [
|
||||
{
|
||||
longitude: 116.38,
|
||||
latitude: 39.9
|
||||
},
|
||||
{
|
||||
longitude: 116.39,
|
||||
latitude: 39.9
|
||||
},
|
||||
{
|
||||
longitude: 116.383,
|
||||
latitude: 39.886
|
||||
},
|
||||
{
|
||||
longitude: 116.378,
|
||||
latitude: 39.902
|
||||
},
|
||||
{
|
||||
longitude: 116.369,
|
||||
latitude: 39.91
|
||||
},
|
||||
{
|
||||
longitude: 116.38,
|
||||
latitude: 39.91
|
||||
}
|
||||
]
|
||||
return response // response.data.data.list
|
||||
} catch (e) {
|
||||
this.errorMsgHandler(e)
|
||||
console.error(e)
|
||||
} finally {
|
||||
// this.loading.baseStationLoading = false
|
||||
}
|
||||
return []
|
||||
},
|
||||
reload (startTime, endTime, dateRangeValue) {
|
||||
this.timeFilter = { startTime: getSecond(startTime), endTime: getSecond(endTime), dateRangeValue: dateRangeValue }
|
||||
const { query } = this.$route
|
||||
this.$store.commit('setTimeRangeArray', [this.timeFilter.startTime, this.timeFilter.endTime])
|
||||
this.$store.commit('setTimeRangeFlag', dateRangeValue.value)
|
||||
|
||||
const newUrl = urlParamsHandler(window.location.href, query, {
|
||||
startTime: this.timeFilter.startTime,
|
||||
endTime: this.timeFilter.endTime,
|
||||
range: dateRangeValue.value
|
||||
})
|
||||
overwriteUrl(newUrl)
|
||||
},
|
||||
async queryTraceTracking () {
|
||||
const params = {
|
||||
startTime: getSecond(this.timeFilter.startTime),
|
||||
endTime: getSecond(this.timeFilter.endTime),
|
||||
dateRangeValue: this.timeFilter.dateRangeValue,
|
||||
subscriberIds: `'${this.entity.entityValue}'`,
|
||||
level: this.mapLevel
|
||||
}
|
||||
this.trackingSubscriber.subscriberId = this.entity.entityValue
|
||||
this.trackingMapLoading = true
|
||||
try {
|
||||
const response = await axios.get(api.location.tracking, { params })
|
||||
this.trackingMapLoading = false
|
||||
if (response.data.data.result) {
|
||||
this.trackingNoData = false
|
||||
this.showTrackingError = false
|
||||
const find = response.data.data.result.find(item => item.subscriberId === this.trackingSubscriber.subscriberId)
|
||||
if (find) {
|
||||
this.trackingSubscriber.trackRecords = find.trackRecords
|
||||
} else {
|
||||
this.trackingSubscriber.trackRecords = []
|
||||
}
|
||||
this.trackingSubscriber.show = false
|
||||
} else {
|
||||
this.trackingSubscriber.trackRecords = []
|
||||
}
|
||||
|
||||
// 计算停留时间
|
||||
if (this.trackingSubscriber.trackRecords.length > 0) {
|
||||
const trackRecords = this.trackingSubscriber.trackRecords
|
||||
// 初始化时间线可视范围角标
|
||||
if (trackRecords.length < 11) {
|
||||
this.trackingSubscriber.scrollStartIndex = 0
|
||||
this.trackingSubscriber.scrollEndIndex = trackRecords.length
|
||||
} else {
|
||||
this.trackingSubscriber.scrollStartIndex = 0
|
||||
this.trackingSubscriber.scrollEndIndex = 11
|
||||
}
|
||||
|
||||
if (trackRecords && trackRecords.length > 0) {
|
||||
for (let i = 0; i < trackRecords.length; i++) {
|
||||
if (i > 0) {
|
||||
if ((trackRecords[i - 1].subscriberLongitude === trackRecords[i].subscriberLongitude) && (trackRecords[i - 1].subscriberLatitude === trackRecords[i].subscriberLatitude)) {
|
||||
// 如果连续两条地址重复,则将时间累加,并将上一条删除,键值-1继续循环
|
||||
if (i > 1 && trackRecords[i - 2]) {
|
||||
const stayTime = unitConvert(trackRecords[i - 2].time - trackRecords[i].time, unitTypes.time, 's')
|
||||
if (Number(stayTime[0]) === Number(Number(stayTime[0]).toFixed(0))) {
|
||||
stayTime[0] = Number(stayTime[0]).toFixed(0)
|
||||
}
|
||||
trackRecords[i].stayTime = stayTime.join(' ')
|
||||
} else {
|
||||
// 数据只有2条,或者第1条和第2条地点重复,删除1,合并时间
|
||||
const stayTime = unitConvert(trackRecords[i - 1].time - trackRecords[i].time, unitTypes.time, 's')
|
||||
if (Number(stayTime[0]) === Number(Number(stayTime[0]).toFixed(0))) {
|
||||
stayTime[0] = Number(stayTime[0]).toFixed(0)
|
||||
}
|
||||
trackRecords[i].stayTime = stayTime.join(' ')
|
||||
}
|
||||
trackRecords.splice(i - 1, 1)
|
||||
i = i - 1
|
||||
} else {
|
||||
const stayTime = unitConvert(trackRecords[i - 1].time - trackRecords[i].time, unitTypes.time, 's')
|
||||
if (Number(stayTime[0]) === Number(Number(stayTime[0]).toFixed(0))) {
|
||||
stayTime[0] = Number(stayTime[0]).toFixed(0)
|
||||
}
|
||||
trackRecords[i].stayTime = stayTime.join(' ')
|
||||
}
|
||||
if (i === trackRecords.length - 1) {
|
||||
// 初始化数据时,重置偏移量和列表高度
|
||||
this.trackingSubscriber.startOffset = 0
|
||||
this.trackingSubscriber.listHeight = i * this.scrollInfo.itemSize
|
||||
}
|
||||
} else {
|
||||
trackRecords[i].stayTime = '-'
|
||||
}
|
||||
}
|
||||
}
|
||||
this.trackingSubscriber.trackRecords = trackRecords
|
||||
} else {
|
||||
this.trackingNoData = true
|
||||
}
|
||||
} catch (e) {
|
||||
this.showTrackingError = true
|
||||
this.trackingNoData = false
|
||||
this.trackingErrorMsg = this.errorMsgHandler(e)
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.trackingMapLoading = false
|
||||
}
|
||||
},
|
||||
onScroll (e) {
|
||||
// 当前滚动位置
|
||||
const scrollTop = e.target.scrollTop
|
||||
// 列表开始索引
|
||||
const startIndex = Math.floor(scrollTop / this.scrollInfo.itemSize) || 0
|
||||
// 列表结束索引
|
||||
const endIndex = Math.ceil((scrollTop + this.scrollInfo.containerHeight) / this.scrollInfo.itemSize)
|
||||
this.trackingSubscriber.scrollStartIndex = startIndex
|
||||
this.trackingSubscriber.scrollEndIndex = endIndex
|
||||
// 列表距离顶部距离
|
||||
this.trackingSubscriber.startOffset = scrollTop - (scrollTop % this.scrollInfo.itemSize)
|
||||
},
|
||||
tooltipMouseEnter () {
|
||||
this.tooltip.mouseInMarkerOrTooltip = true
|
||||
},
|
||||
tooltipMouseLeave (event) {
|
||||
if (this.currentMarkerDom && !this.currentMarkerDom.contains(event.relatedTarget)) {
|
||||
this.tooltip.mouseInMarkerOrTooltip = false
|
||||
this.tooltip.showMarkerTooltip = false
|
||||
this.currentMarkerDom.classList.remove('map-marker--hover')
|
||||
}
|
||||
},
|
||||
trackingHexagonMouseEnter () {
|
||||
this.tooltip.mouseIsInPolygon = true
|
||||
},
|
||||
trackingHexagonMouseLeave () {
|
||||
this.tooltip.showPolygonTooltip = false
|
||||
this.tooltip.mouseIsInPolygon = false
|
||||
// 去掉上一块的高亮
|
||||
this.hoverTrigger('trackingHexGrid', this.currentPolygon.id, false)
|
||||
},
|
||||
trackingHexagonMouseMove (e) {
|
||||
const { originalEvent, features } = e
|
||||
if (!this.tooltip.mouseInMarkerOrTooltip) {
|
||||
this.tooltip.showPolygonTooltip = true
|
||||
this.tooltip.type = this.tooltipType.hexagon
|
||||
if (this.tooltip.type === this.tooltipType.hexagon && this.currentPolygon.id && this.currentPolygon.id !== features[0].id) {
|
||||
// 去掉上一块的高亮
|
||||
this.hoverTrigger('trackingHexGrid', this.currentPolygon.id, false)
|
||||
}
|
||||
this.currentPolygon = features[0].properties
|
||||
this.currentPolygon.id = features[0].id
|
||||
this.currentPolygon.location = `${h3ToGeo(this.currentPolygon.hexId)[1]}, ${h3ToGeo(this.currentPolygon.hexId)[0]}`
|
||||
this.tooltip.x = originalEvent.clientX + 15
|
||||
this.tooltip.y = originalEvent.clientY + 5
|
||||
|
||||
// 鼠标滑过高亮
|
||||
this.hoverTrigger('trackingHexGrid', this.currentPolygon.id, true)
|
||||
}
|
||||
},
|
||||
bindMarkerEvent (el, markerData, type) {
|
||||
el.addEventListener('mouseenter', e => {
|
||||
this.currentMarkerDom = el
|
||||
if (type === this.tooltipType.human) {
|
||||
this.currentSubscriber = markerData
|
||||
if (!this.tooltip.mouseInMarkerOrTooltip) {
|
||||
this.tooltip.x = e.clientX + 15 - e.offsetX
|
||||
this.tooltip.y = (e.clientY + 15 - e.offsetY + this.tooltipDomHeight.human) > this.mapDomHeight ? (this.mapDomHeight - this.tooltipDomHeight.human) : (e.clientY + 15 - e.offsetY)
|
||||
}
|
||||
} else if (type === this.tooltipType.baseStation) {
|
||||
this.currentBaseStation = markerData
|
||||
if (!this.tooltip.mouseInMarkerOrTooltip) {
|
||||
this.tooltip.x = e.clientX + 15 - e.offsetX
|
||||
this.tooltip.y = (e.clientY + 15 - e.offsetY + this.tooltipDomHeight.baseStation) > this.mapDomHeight ? (this.mapDomHeight - this.tooltipDomHeight.baseStation) : (e.clientY + 15 - e.offsetY)
|
||||
}
|
||||
}
|
||||
|
||||
this.tooltip.mouseInMarkerOrTooltip = true
|
||||
this.tooltip.type = type
|
||||
this.tooltip.showMarkerTooltip = true
|
||||
el.classList.add('map-marker--hover')
|
||||
})
|
||||
el.addEventListener('mouseleave', event => {
|
||||
const tooltipDom = document.getElementById('tooltip')
|
||||
if (!tooltipDom.contains(event.relatedTarget)) {
|
||||
el.classList.remove('map-marker--hover')
|
||||
this.tooltip.mouseInMarkerOrTooltip = false
|
||||
this.tooltip.showMarkerTooltip = false
|
||||
}
|
||||
})
|
||||
if (type === this.tooltipType.human) {
|
||||
el.addEventListener('click', e => {
|
||||
this.humanMarkers.forEach(m => {
|
||||
m.getElement().classList.remove('map-marker--highlight')
|
||||
})
|
||||
if (this.highlightSubscriber.subscriberId !== markerData.subscriberId) {
|
||||
el.classList.add('map-marker--highlight')
|
||||
this.highlightSubscriber = markerData
|
||||
// 将滚动条跳转到对应位置
|
||||
document.querySelector(`#locationMap-subscriberId-${markerData.subscriberId}`).scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
} else {
|
||||
this.highlightSubscriber = {}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
// 地图上人图标鼠标悬浮框中点击追踪事件
|
||||
trackSubscriber (subscriber) {
|
||||
const find = this.trackingSubscribers.find(s => s.subscriberId === subscriber.subscriberId)
|
||||
if (!find) {
|
||||
this.trackingSubscribers.push({ ...subscriber, show: false, showLine: false, scrollStartIndex: 0, scrollEndIndex: 11, startOffset: 0, listHeight: 0 })
|
||||
}
|
||||
this.currentShowSubscriber = subscriber
|
||||
this.tooltip.showMarkerTooltip = false
|
||||
},
|
||||
unbindTrackingHexagonEvents () {
|
||||
this.mapChart.off('mouseenter', this.trackingHexagonMouseEnter)
|
||||
this.mapChart.off('mouseleave', this.trackingHexagonMouseLeave)
|
||||
this.mapChart.off('mousemove', this.trackingHexagonMouseMove)
|
||||
},
|
||||
renderTrackingMarker (coordinates) {
|
||||
const el = document.createElement('div')
|
||||
el.className = 'map-tracking-marker'
|
||||
el.innerHTML = `<div class="tracking-marker__inner-circle">${humanSvg}</div>`
|
||||
this.trackingHumanMarker = new maplibregl.Marker({ element: el }).setLngLat(coordinates).addTo(this.mapChart)
|
||||
},
|
||||
renderTrackingHexagon () {
|
||||
if (!this.currentShowSubscriber) {
|
||||
return true
|
||||
}
|
||||
const currentShowSubscriberRecords = this.currentShowSubscriber.trackRecords
|
||||
if (currentShowSubscriberRecords && currentShowSubscriberRecords.length > 0) {
|
||||
// 六边形
|
||||
this.trackingPolygonSourceData = this.hexagonDataConverter(this.currentShowSubscriber.trackRecords)
|
||||
this.mapChart.addSource('trackingHexGrid', {
|
||||
type: 'geojson',
|
||||
data: this.trackingPolygonSourceData
|
||||
})
|
||||
this.mapChart.addLayer({
|
||||
id: 'trackingHexagon',
|
||||
type: 'fill',
|
||||
source: 'trackingHexGrid',
|
||||
layout: {},
|
||||
paint: {
|
||||
'fill-color': ['get', 'color'],
|
||||
'fill-opacity': ['case', ['boolean', ['feature-state', 'hover'], false], 0.75, 0.5]
|
||||
}
|
||||
})
|
||||
// 轨迹线
|
||||
const mapLineSourceData = this.mapLineDataConverter()
|
||||
this.mapChart.addSource('trackingLineSource', {
|
||||
type: 'geojson',
|
||||
data: mapLineSourceData
|
||||
})
|
||||
this.mapChart.addLayer({
|
||||
id: 'trackingLine',
|
||||
type: 'line',
|
||||
source: 'trackingLineSource',
|
||||
paint: {
|
||||
'line-color': 'rgba(222, 52, 52, .8)',
|
||||
'line-width': 3
|
||||
}
|
||||
})
|
||||
// 最后所在地的图标
|
||||
const coordinate = h3ToGeo(currentShowSubscriberRecords[0].hexId)
|
||||
this.renderTrackingMarker([coordinate[1], coordinate[0]])
|
||||
this.bindTrackingHexagonEvents()
|
||||
}
|
||||
},
|
||||
mapLineDataConverter () {
|
||||
const records = this.currentShowSubscriber.trackRecords
|
||||
const feature = {
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: records.map(d => {
|
||||
const cs = h3ToGeo(d.hexId)
|
||||
return [cs[1], cs[0]]
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: [feature]
|
||||
}
|
||||
},
|
||||
renderMarker (data, type) {
|
||||
let svg
|
||||
if (type === this.tooltipType.baseStation) {
|
||||
svg = baseStationSvg
|
||||
} else if (type === this.tooltipType.human) {
|
||||
svg = humanSvg
|
||||
}
|
||||
try {
|
||||
data.forEach(marker => {
|
||||
if (type === this.tooltipType.human && marker.subscriberDto) {
|
||||
const el = document.createElement('div')
|
||||
el.className = `map-marker map-marker--${type}`
|
||||
if (marker.subscriberId === this.highlightSubscriber.subscriberId) {
|
||||
el.classList.add('map-marker--highlight')
|
||||
}
|
||||
el.innerHTML = svg
|
||||
// 鼠标事件,控制tooltip显示和marker尺寸
|
||||
this.bindMarkerEvent(el, marker, type)
|
||||
const mapMarker = new maplibregl.Marker({ element: el }).setLngLat([marker.subscriberDto.subscriberLongitude, marker.subscriberDto.subscriberLatitude]).addTo(this.mapChart)
|
||||
mapMarker.subscriberId = marker.subscriberId
|
||||
this.humanMarkers.push(mapMarker)
|
||||
} else if (type === this.tooltipType.baseStation) {
|
||||
const el = document.createElement('div')
|
||||
el.className = `map-marker map-marker--${type}`
|
||||
el.innerHTML = svg
|
||||
// 鼠标事件,控制tooltip显示和marker尺寸
|
||||
this.bindMarkerEvent(el, marker, type)
|
||||
const mapMarker = new maplibregl.Marker({ element: el }).setLngLat([marker.longitude, marker.latitude]).addTo(this.mapChart)
|
||||
this.baseStationMarkers.push(mapMarker)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
hexagonDataConverter (data) {
|
||||
const featureCollection = { type: 'FeatureCollection' }
|
||||
// 对hexId去重,将重复的hexId的时间合并
|
||||
const hexagons = []
|
||||
data.forEach(d => {
|
||||
const find = hexagons.find(h => h.hexId === d.hexId)
|
||||
if (!find) {
|
||||
hexagons.push({ hexId: d.hexId, locations: [{ time: d.time, longitude: d.subscriberLongitude, latitude: d.subscriberLatitude }] })
|
||||
} else {
|
||||
if (find.locations.length < 6) {
|
||||
find.locations.push({ time: d.time, longitude: d.subscriberLongitude, latitude: d.subscriberLatitude })
|
||||
}
|
||||
}
|
||||
})
|
||||
featureCollection.features = hexagons.map((d, i) => ({
|
||||
id: i + 100000,
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Polygon',
|
||||
coordinates: [
|
||||
h3ToGeoBoundary(d.hexId, true)
|
||||
]
|
||||
},
|
||||
properties: {
|
||||
hexId: d.hexId,
|
||||
number: d.number,
|
||||
locations: d.locations,
|
||||
color: [37, 55, 128]
|
||||
}
|
||||
}))
|
||||
return featureCollection
|
||||
},
|
||||
bindTrackingHexagonEvents () {
|
||||
this.mapChart.on('mouseenter', 'trackingHexagon', this.trackingHexagonMouseEnter)
|
||||
this.mapChart.on('mouseleave', 'trackingHexagon', this.trackingHexagonMouseLeave)
|
||||
this.mapChart.on('mousemove', 'trackingHexagon', this.trackingHexagonMouseMove)
|
||||
},
|
||||
hoverTrigger (source, id, hover) {
|
||||
this.mapChart.setFeatureState({ source, id }, { hover })
|
||||
},
|
||||
timelineMouseEnter (subscriber, record) {
|
||||
this.trackingPolygonSourceData.features.forEach(f => {
|
||||
this.hoverTrigger('trackingHexGrid', f.id, false)
|
||||
})
|
||||
if (this.currentShowSubscriber.subscriberId === subscriber.subscriberId) {
|
||||
const find = this.trackingPolygonSourceData.features.find(d => d.properties.hexId === record.hexId)
|
||||
if (find) {
|
||||
this.hoverTrigger('trackingHexGrid', find.id, true)
|
||||
}
|
||||
}
|
||||
},
|
||||
timelineMouseLeave (subscriber, record) {
|
||||
if (this.currentShowSubscriber.subscriberId === subscriber.subscriberId) {
|
||||
const find = this.trackingPolygonSourceData.features.find(d => d.properties.hexId === record.hexId)
|
||||
if (find) {
|
||||
this.hoverTrigger('trackingHexGrid', find.id, false)
|
||||
}
|
||||
}
|
||||
},
|
||||
changeTimeFilterToInitList () {
|
||||
this.trackingSubscriber.scrollStartIndex = 0
|
||||
this.trackingSubscriber.scrollEndIndex = 11
|
||||
this.trackingSubscriber.startOffset = 0
|
||||
this.trackingSubscriber.listHeight = 0
|
||||
},
|
||||
developmentClick () {
|
||||
const dev = process.env.NODE_ENV
|
||||
if (dev) {
|
||||
this.trackingMapLoading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
setup () {
|
||||
const dateRangeValue = DEFAULT_TIME_FILTER_RANGE.entity.subscriberMap || 60
|
||||
const timeFilter = ref({ dateRangeValue })
|
||||
const { startTime, endTime } = getNowTime(dateRangeValue)
|
||||
timeFilter.value.startTime = startTime
|
||||
timeFilter.value.endTime = endTime
|
||||
|
||||
const currentPoint = ref({})
|
||||
const tooltip = ref({
|
||||
showTooltip: false,
|
||||
type: ''
|
||||
})
|
||||
const myChart = shallowRef({})
|
||||
const trackingSubscriber = ref({
|
||||
subscriberId: '',
|
||||
show: false,
|
||||
showLine: false,
|
||||
scrollStartIndex: 0,
|
||||
scrollEndIndex: 11,
|
||||
startOffset: 0,
|
||||
listHeight: 0,
|
||||
trackRecords: []
|
||||
})
|
||||
const trackingSubscriberList = ref([])
|
||||
const scrollInfo = ref({
|
||||
itemSize: 50,
|
||||
containerHeight: 660
|
||||
})
|
||||
const trackingNoData = ref(true)
|
||||
const showTrackingError = ref(false)
|
||||
const trackingErrorMsg = ref('')
|
||||
const tooltipDomHeight = {
|
||||
hexagon: 153,
|
||||
baseStation: 153,
|
||||
human: 167
|
||||
}
|
||||
const currentPolygon = ref({})
|
||||
const currentSubscriber = ref({})
|
||||
const trackingHumanMarker = shallowRef({})
|
||||
const mapChart = shallowRef(null)
|
||||
const baseStationMarkers = shallowRef([])
|
||||
|
||||
const mapConfig = localStorage.getItem(storageKey.mapConfig) ? JSON.parse(localStorage.getItem(storageKey.mapConfig)) : defaultMapConfig
|
||||
const trackingMapLoading = ref(true)
|
||||
return {
|
||||
timeFilter,
|
||||
currentPoint,
|
||||
tooltip,
|
||||
myChart,
|
||||
maxZoom: mapConfig.maxZoom, // 地图最小缩放比例
|
||||
minZoom: mapConfig.minZoom, // 地图最大缩放比例
|
||||
mapLevel: mapConfig.mapLevel, // 地图精度 1、2、3
|
||||
defaultZoom: 11, // 地图默认缩放比例
|
||||
center: mapConfig.center, // 地图默认中心点。北京:[116.38, 39.9] 纽约:[-73.94539, 40.841843]
|
||||
trackingSubscriber,
|
||||
trackingSubscriberList,
|
||||
scrollInfo,
|
||||
trackingNoData,
|
||||
showTrackingError,
|
||||
trackingErrorMsg,
|
||||
tooltipDomHeight,
|
||||
currentPolygon,
|
||||
currentSubscriber,
|
||||
trackingHumanMarker,
|
||||
trackingPolygonSourceData: shallowRef({}),
|
||||
mapChart,
|
||||
baseStationMarkers,
|
||||
trackingMapLoading
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -51,6 +51,10 @@ export default {
|
||||
className = 'cn-icon cn-icon-app2'
|
||||
break
|
||||
}
|
||||
case ('subscriber_id'): {
|
||||
className = 'cn-icon cn-icon-pedestrian'
|
||||
break
|
||||
}
|
||||
default: break
|
||||
}
|
||||
return className
|
||||
@@ -120,13 +124,22 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
showDetail () {
|
||||
let entityType = _.cloneDeep(this.entityData.entityType)
|
||||
if (entityType === 'subscriber_id') {
|
||||
entityType = entityType.slice(0, -3)
|
||||
}
|
||||
const queryParam = {
|
||||
entityType: entityType,
|
||||
entityName: this.entityData.entityValue,
|
||||
range: this.timeFilter.dateRangeValue
|
||||
}
|
||||
if (queryParam.range === -1) {
|
||||
queryParam.startTime = this.timeFilter.startTime
|
||||
queryParam.endTime = this.timeFilter.endTime
|
||||
}
|
||||
const { href } = this.$router.resolve({
|
||||
path: '/entity/detail',
|
||||
query: {
|
||||
entityType: this.entityData.entityType,
|
||||
entityName: this.entityData.entityValue,
|
||||
range: this.timeFilter.dateRangeValue
|
||||
}
|
||||
query: queryParam
|
||||
})
|
||||
window.open(href, '_blank')
|
||||
},
|
||||
@@ -264,6 +277,10 @@ export default {
|
||||
this.performanceEventUrl = api.entity.entityList.appEventPerformance
|
||||
break
|
||||
}
|
||||
case 'subscriber_id': {
|
||||
this.trafficUrl = api.entity.entityList.subscriberThroughput
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -348,8 +365,10 @@ export default {
|
||||
this.initUrl()
|
||||
setTimeout(() => {
|
||||
this.queryEntityDetailTraffic()
|
||||
this.queryNetworkQuantity()
|
||||
this.queryEventNum()
|
||||
if (this.entity.entityType !== 'subscriber_id') {
|
||||
this.queryNetworkQuantity()
|
||||
this.queryEventNum()
|
||||
}
|
||||
})
|
||||
},
|
||||
beforeUnmount () {
|
||||
|
||||
Reference in New Issue
Block a user