CN-988: 实体详情--安全事件和性能事件tab开发
This commit is contained in:
@@ -107,6 +107,9 @@
|
|||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 0 2px;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.detection-event-severity-color-block {
|
.detection-event-severity-color-block {
|
||||||
width: 5px;
|
width: 5px;
|
||||||
@@ -143,12 +146,12 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.basic-info__item {
|
.basic-info__item {
|
||||||
padding-right: 40px;
|
padding-right: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
padding-right: 6px;
|
padding-right: 5px;
|
||||||
color: #8FA1BE;
|
color: #8FA1BE;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.basic-info__item {
|
.basic-info__item {
|
||||||
padding-right: 40px;
|
padding-right: 30px;
|
||||||
|
|
||||||
.item__box {
|
.item__box {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
padding-right: 6px;
|
padding-right: 5px;
|
||||||
color: #8FA1BE;
|
color: #8FA1BE;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height: 13px;
|
height: 13px;
|
||||||
|
|||||||
@@ -66,6 +66,84 @@ if (openMock) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Mock.mock(new RegExp(BASE_CONFIG.baseUrl + 'interface/entityDetail/securityEvent.*'), 'get', function (requestObj) {
|
||||||
|
const result = [
|
||||||
|
{
|
||||||
|
eventId: '1298414830886991872',
|
||||||
|
securityType: 'command and control',
|
||||||
|
domain: null,
|
||||||
|
offenderIp: '213.186.33.5',
|
||||||
|
victimIp: '116.178.217.92',
|
||||||
|
offenderDomain: 'baidu.com',
|
||||||
|
victimDomain: 'mi.com',
|
||||||
|
eventSeverity: 'Critical',
|
||||||
|
malwareName: 'NetWire RC',
|
||||||
|
cryptominingPool: null,
|
||||||
|
startTime: 1683186600,
|
||||||
|
durationMs: 300000,
|
||||||
|
endTime: 1683186900
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventId: '1298414830886991873',
|
||||||
|
securityType: 'command and control',
|
||||||
|
domain: null,
|
||||||
|
offenderIp: '213.186.33.5',
|
||||||
|
victimIp: '116.178.217.93',
|
||||||
|
offenderDomain: 'baidu.com',
|
||||||
|
victimDomain: 'mi.com',
|
||||||
|
eventSeverity: 'Low',
|
||||||
|
malwareName: 'NetWire RC',
|
||||||
|
cryptominingPool: null,
|
||||||
|
startTime: 1683186600,
|
||||||
|
durationMs: 300000,
|
||||||
|
endTime: 1683186900
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
msg: 'success',
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
result: result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Mock.mock(new RegExp(BASE_CONFIG.baseUrl + 'interface/entityDetail/performanceEvent.*'), 'get', function (requestObj) {
|
||||||
|
const result = [
|
||||||
|
{
|
||||||
|
eventId: '1308078720390412288',
|
||||||
|
entityType: 'ip',
|
||||||
|
serverIp: '116.178.78.180',
|
||||||
|
domain: null,
|
||||||
|
appName: null,
|
||||||
|
eventSeverity: 'Critical',
|
||||||
|
eventType: 'Http error',
|
||||||
|
startTime: 1683250500,
|
||||||
|
durationMs: 900000,
|
||||||
|
endTime: 1683251400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
eventId: '1308078720390412289',
|
||||||
|
entityType: 'ip',
|
||||||
|
serverIp: '116.178.78.180',
|
||||||
|
domain: null,
|
||||||
|
appName: null,
|
||||||
|
eventSeverity: 'Info',
|
||||||
|
eventType: 'Http error',
|
||||||
|
startTime: 1683250500,
|
||||||
|
durationMs: 900000,
|
||||||
|
endTime: 1683251400
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
msg: 'success',
|
||||||
|
code: 200,
|
||||||
|
data: {
|
||||||
|
result: result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getQuery = (url) => {
|
const getQuery = (url) => {
|
||||||
|
|||||||
@@ -234,7 +234,9 @@ export const api = {
|
|||||||
drilldownTrafficAnalysis: '/interface/dns/overview/drilldown/trafficAnalysis'
|
drilldownTrafficAnalysis: '/interface/dns/overview/drilldown/trafficAnalysis'
|
||||||
},
|
},
|
||||||
entity: {
|
entity: {
|
||||||
totalTrafficAnalysis: 'interface/entityDetail/totalTrafficAnalysis'
|
totalTrafficAnalysis: 'interface/entityDetail/totalTrafficAnalysis',
|
||||||
|
securityEvent: 'interface/entityDetail/securityEvent',
|
||||||
|
performanceEvent: 'interface/entityDetail/performanceEvent'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,13 @@ export const eventSeverityColor = {
|
|||||||
low: '#FFD82D',
|
low: '#FFD82D',
|
||||||
info: '#D1BD50'
|
info: '#D1BD50'
|
||||||
}
|
}
|
||||||
|
export const eventSeverityColor1 = {
|
||||||
|
Critical: '#D84C4C',
|
||||||
|
High: '#FE845D',
|
||||||
|
Medium: '#FFB65A',
|
||||||
|
Low: '#FFD82D',
|
||||||
|
Info: '#D1BD50'
|
||||||
|
}
|
||||||
export const securityType = {
|
export const securityType = {
|
||||||
commandAndControl: 'common and control',
|
commandAndControl: 'common and control',
|
||||||
payloadDelivery: 'payload delivery',
|
payloadDelivery: 'payload delivery',
|
||||||
|
|||||||
160
src/views/charts2/charts/entityDetail/tabs/PerformanceEvent.vue
Normal file
160
src/views/charts2/charts/entityDetail/tabs/PerformanceEvent.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<chart-no-data v-if="isNoData && !showError"></chart-no-data>
|
||||||
|
<div
|
||||||
|
class="detection-border"
|
||||||
|
v-for="item in eventList"
|
||||||
|
:key="item.eventId"
|
||||||
|
style="margin-bottom: 10px;">
|
||||||
|
<div class="cn-detection--list">
|
||||||
|
<div class="cn-detection__case" style="height: 46px">
|
||||||
|
<div class="cn-detection__icon" :style="`background-color: ${eventSeverityColor1[item.eventSecurity]}`"></div>
|
||||||
|
<div class="cn-detection__row">
|
||||||
|
<div class="cn-detection__header">
|
||||||
|
<span
|
||||||
|
class="detection-event-severity-color-block"
|
||||||
|
:style="`background-color: ${eventSeverityColor1[item.eventSeverity]}`">
|
||||||
|
</span>
|
||||||
|
<span class="detection-event-severity-block" style="margin-right: 30px">
|
||||||
|
{{ item.eventType || '-' }}
|
||||||
|
</span>
|
||||||
|
<div class="cn-detection__body">
|
||||||
|
<div class="body__basic-info">
|
||||||
|
<div class="basic-info">
|
||||||
|
<div class="basic-info__item" v-if="item.eventSeverity">
|
||||||
|
<i class="cn-icon cn-icon-severity-level"></i>
|
||||||
|
<span>{{ $t('network.severity') }} : </span>
|
||||||
|
<span>{{ item.eventSeverity || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="basic-info__item">
|
||||||
|
<i class="cn-icon cn-icon-time2"></i>
|
||||||
|
<span>{{ $t('detection.list.startTime') }} : </span>
|
||||||
|
<span>{{ dateFormatByAppearance(item.startTime) || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="basic-info__item">
|
||||||
|
<i class="cn-icon cn-icon-Duration"></i>
|
||||||
|
<span>{{ $t('overall.duration') }} : </span>
|
||||||
|
<span>{{ unitConvert(item.durationMs, 'time', null, null, 0).join(' ') || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getMillisecond, getNowTime, getSecond } from '@/utils/date-util'
|
||||||
|
import { eventSeverityColor1 } from '@/utils/constants'
|
||||||
|
import unitConvert from '@/utils/unit-convert'
|
||||||
|
import axios from '_axios@0.21.4@axios'
|
||||||
|
import { api } from '@/utils/api'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import chartMixin from '@/views/charts2/chart-mixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PerformanceEvent',
|
||||||
|
mixins: [chartMixin],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
eventList: [],
|
||||||
|
showError: false,
|
||||||
|
eventSeverityColor1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup () {
|
||||||
|
const { query } = useRoute()
|
||||||
|
// 获取url携带的range、startTime、endTime
|
||||||
|
const rangeParam = query.range
|
||||||
|
const startTimeParam = query.startTime
|
||||||
|
const endTimeParam = query.endTime
|
||||||
|
// 若url携带了,使用携带的值,否则使用默认值。
|
||||||
|
|
||||||
|
const dateRangeValue = rangeParam ? parseInt(query.range) : 60
|
||||||
|
const timeFilter = ref({ dateRangeValue })
|
||||||
|
if (!startTimeParam || !endTimeParam) {
|
||||||
|
const {
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
} = getNowTime(60)
|
||||||
|
timeFilter.value.startTime = startTime
|
||||||
|
timeFilter.value.endTime = endTime
|
||||||
|
} else {
|
||||||
|
timeFilter.value.startTime = parseInt(startTimeParam)
|
||||||
|
timeFilter.value.endTime = parseInt(endTimeParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timeFilter
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.initData()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pointColor () {
|
||||||
|
return function (item) {
|
||||||
|
let color = '#8FA1BE'
|
||||||
|
if (item.startTime && item.endTime) {
|
||||||
|
if (getMillisecond(item.endTime) - getMillisecond(item.startTime) < 5 * 60 * 1000) {
|
||||||
|
color = '#D84C4C'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { backgroundColor: color }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
unitConvert,
|
||||||
|
initData () {
|
||||||
|
const params = {
|
||||||
|
startTime: getSecond(this.timeFilter.startTime),
|
||||||
|
endTime: getSecond(this.timeFilter.endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggleLoading(true)
|
||||||
|
axios.get(api.entity.performanceEvent, { params: params }).then(response => {
|
||||||
|
const res = response.data
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.isNoData = res.data.result.length === 0
|
||||||
|
this.showError = false
|
||||||
|
if (!this.isNoData) {
|
||||||
|
this.eventList = res.data.result
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.httpError(res)
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
this.httpError(e)
|
||||||
|
}).finally(() => {
|
||||||
|
this.toggleLoading(false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
httpError (e) {
|
||||||
|
this.isNoData = false
|
||||||
|
this.showError = true
|
||||||
|
this.errorMsg = this.errorMsgHandler(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detection-border {
|
||||||
|
border: 1px solid #E2E5EC;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-info__item-duration {
|
||||||
|
display: inline-block;
|
||||||
|
height: 6px;
|
||||||
|
width: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
184
src/views/charts2/charts/entityDetail/tabs/SecurityEvent.vue
Normal file
184
src/views/charts2/charts/entityDetail/tabs/SecurityEvent.vue
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<chart-no-data v-if="isNoData && !showError"></chart-no-data>
|
||||||
|
<div
|
||||||
|
class="detection-border"
|
||||||
|
v-for="item in eventList"
|
||||||
|
:key="item.eventId"
|
||||||
|
style="margin-bottom: 10px;">
|
||||||
|
<div class="cn-detection--list">
|
||||||
|
<div class="cn-detection__case" style="height: 70px">
|
||||||
|
<div class="cn-detection__icon" :style="`background-color: ${eventSeverityColor1[item.eventSecurity]}`"></div>
|
||||||
|
<div class="cn-detection__row">
|
||||||
|
<div class="cn-detection__header">
|
||||||
|
<span
|
||||||
|
class="detection-event-severity-color-block"
|
||||||
|
:style="`background-color: ${eventSeverityColor1[item.eventSeverity]}`">
|
||||||
|
</span>
|
||||||
|
<span class="detection-event-severity-block">{{ item.securityType || '-' }}</span>
|
||||||
|
<i class="cn-icon cn-icon-attacker"></i>{{ item.offenderIp || '-' }}
|
||||||
|
<div class="domain">{{ item.offenderDomain }}</div>
|
||||||
|
<span class="line">-------</span>
|
||||||
|
<span class="circle"></span>
|
||||||
|
<i class="cn-icon cn-icon-attacked"></i>{{ item.victimIp || '-' }}
|
||||||
|
<div class="domain">{{ item.victimDomain }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="cn-detection__body">
|
||||||
|
<div class="body__basic-info">
|
||||||
|
<div class="basic-info">
|
||||||
|
<div class="basic-info__item" v-if="item.eventSecurity">
|
||||||
|
<i class="cn-icon cn-icon-severity-level"></i>
|
||||||
|
<span>{{ $t('detection.list.eventSecurity') }} : </span>
|
||||||
|
<span>{{ item.eventSecurity || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="basic-info__item" v-if="item.eventSeverity">
|
||||||
|
<i class="cn-icon cn-icon-severity-level"></i>
|
||||||
|
<span>{{ $t('network.severity') }} : </span>
|
||||||
|
<span>{{ item.eventSeverity || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="basic-info__item" v-if="item.eventType">
|
||||||
|
<i class="cn-icon cn-icon-event-type"></i>
|
||||||
|
<span>{{ $t('detections.eventType') }} : </span>
|
||||||
|
<span>{{ item.eventType || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="basic-info__item" v-if="item.malwareName">
|
||||||
|
<i class="cn-icon cn-icon-trojan"></i>
|
||||||
|
<span>{{ $t('detection.list.malwareName') }} : </span>
|
||||||
|
<span>{{ item.malwareName || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="basic-info__item" v-if="item.cryptominingPool">
|
||||||
|
<i class="cn-icon cn-icon-mining-pool"></i>
|
||||||
|
<span>{{ $t('detection.list.cryptominingPool') }} : </span>
|
||||||
|
<span>{{ item.cryptominingPool || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="basic-info__item">
|
||||||
|
<i class="cn-icon cn-icon-time2"></i>
|
||||||
|
<span>{{ $t('detection.list.startTime') }} : </span>
|
||||||
|
<span>{{ dateFormatByAppearance(item.startTime) || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="basic-info__item">
|
||||||
|
<i class="cn-icon cn-icon-Duration"></i>
|
||||||
|
<span>{{ $t('overall.duration') }} : </span>
|
||||||
|
<span>{{ unitConvert(item.durationMs, 'time', null, null, 0).join(' ') || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getMillisecond, getNowTime, getSecond } from '@/utils/date-util'
|
||||||
|
import { eventSeverityColor1 } from '@/utils/constants'
|
||||||
|
import unitConvert from '@/utils/unit-convert'
|
||||||
|
import axios from '_axios@0.21.4@axios'
|
||||||
|
import { api } from '@/utils/api'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import chartMixin from '@/views/charts2/chart-mixin'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SecurityEvent',
|
||||||
|
mixins: [chartMixin],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
eventList: [],
|
||||||
|
showError: false,
|
||||||
|
eventSeverityColor1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup () {
|
||||||
|
const { query } = useRoute()
|
||||||
|
// 获取url携带的range、startTime、endTime
|
||||||
|
const rangeParam = query.range
|
||||||
|
const startTimeParam = query.startTime
|
||||||
|
const endTimeParam = query.endTime
|
||||||
|
// 若url携带了,使用携带的值,否则使用默认值。
|
||||||
|
|
||||||
|
const dateRangeValue = rangeParam ? parseInt(query.range) : 60
|
||||||
|
const timeFilter = ref({ dateRangeValue })
|
||||||
|
if (!startTimeParam || !endTimeParam) {
|
||||||
|
const {
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
} = getNowTime(60)
|
||||||
|
timeFilter.value.startTime = startTime
|
||||||
|
timeFilter.value.endTime = endTime
|
||||||
|
} else {
|
||||||
|
timeFilter.value.startTime = parseInt(startTimeParam)
|
||||||
|
timeFilter.value.endTime = parseInt(endTimeParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timeFilter
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.initData()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pointColor () {
|
||||||
|
return function (item) {
|
||||||
|
let color = '#8FA1BE'
|
||||||
|
if (item.startTime && item.endTime) {
|
||||||
|
if (getMillisecond(item.endTime) - getMillisecond(item.startTime) < 5 * 60 * 1000) {
|
||||||
|
color = '#D84C4C'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { backgroundColor: color }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
unitConvert,
|
||||||
|
initData () {
|
||||||
|
const params = {
|
||||||
|
startTime: getSecond(this.timeFilter.startTime),
|
||||||
|
endTime: getSecond(this.timeFilter.endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggleLoading(true)
|
||||||
|
axios.get(api.entity.securityEvent, { params: params }).then(response => {
|
||||||
|
const res = response.data
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.isNoData = res.data.result.length === 0
|
||||||
|
this.showError = false
|
||||||
|
if (!this.isNoData) {
|
||||||
|
this.eventList = res.data.result
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.httpError(res)
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
this.httpError(e)
|
||||||
|
}).finally(() => {
|
||||||
|
this.toggleLoading(false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
httpError (e) {
|
||||||
|
this.isNoData = false
|
||||||
|
this.showError = true
|
||||||
|
this.errorMsg = this.errorMsgHandler(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.detection-border {
|
||||||
|
border: 1px solid #E2E5EC;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-info__item-duration {
|
||||||
|
display: inline-block;
|
||||||
|
height: 6px;
|
||||||
|
width: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<span>{{dateFormatByAppearance(detection.startTime) || '-'}}</span>
|
<span>{{dateFormatByAppearance(detection.startTime) || '-'}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="basic-info__item">
|
<div class="basic-info__item">
|
||||||
<i class="cn-icon cn-icon-time2"></i>
|
<i class="cn-icon cn-icon-Duration"></i>
|
||||||
<span>{{$t('overall.duration')}} : </span>
|
<span>{{$t('overall.duration')}} : </span>
|
||||||
<span style="display: inline-block;height: 6px;width: 6px;border-radius: 50%;margin-right: 8px;"
|
<span style="display: inline-block;height: 6px;width: 6px;border-radius: 50%;margin-right: 8px;"
|
||||||
:style="pointColor(detection)"></span>
|
:style="pointColor(detection)"></span>
|
||||||
|
|||||||
Reference in New Issue
Block a user