perf: entities部分内容、优化带统计图表样式

This commit is contained in:
chenjinsong
2021-07-09 10:10:06 +08:00
parent afec688a1c
commit 8edf4e24c9
9 changed files with 352 additions and 48 deletions

View File

@@ -13,16 +13,32 @@ $--menu-hover-background-color: #000C18; // menu背景色
$--menu-item-font-color: #BEBEBE; // menu字色 $--menu-item-font-color: #BEBEBE; // menu字色
$--menu-item-hover-fill: $--color-primary; // menu鼠标悬浮、激活时背景色 $--menu-item-hover-fill: $--color-primary; // menu鼠标悬浮、激活时背景色
$--collapse-header-height: 42px;
$--collapse-border-color: #EFF2F5;
/** 自定义变量 **/ /** 自定义变量 **/
:root { :root {
/* 自适应变量 */
@media only screen and (min-width : 10px) {
--chart-height-unit: 25px; // chart的单元高度
--entity-width: calc(50% - 5px); // entity列表每个entity框的宽度
--entity-height: 190px; // entity列表每个entity框的高度
}
@media only screen and (min-width : 1224px) { @media only screen and (min-width : 1224px) {
--chart-height-unit: 30px; --chart-height-unit: 30px;
} }
@media only screen and (min-width : 1560px) {
--entity-width: calc(33.3% - 7px);
}
@media only screen and (min-width : 1824px) { @media only screen and (min-width : 1824px) {
--chart-height-unit: 40px; --chart-height-unit: 40px;
--entity-width: calc(33.3% - 7px);
--entity-height: 210px;
} }
@media only screen and (min-width : 2424px) { @media only screen and (min-width : 2424px) {
--chart-height-unit: 55px; --chart-height-unit: 55px;
--entity-width: calc(25% - 8px);
--entity-height: 240px;
} }
} }
@@ -32,7 +48,7 @@ $--border-radius-primary: 2px;
$--right-box-border-color: #E7EAED; $--right-box-border-color: #E7EAED;
/* 按钮 */ /* 按钮 */
$--button-border-radius: $--border-color-primary; // 按钮圆角 $--button-border-radius: $--border-radius-primary; // 按钮圆角
$--button-primary-color: #FFF; // 普通按钮字色 $--button-primary-color: #FFF; // 普通按钮字色
$--button-primary-background-color: $--color-primary; // 普通按钮背景色 $--button-primary-background-color: $--color-primary; // 普通按钮背景色

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="cn-chart cn-chart__echarts"> <div class="cn-chart cn-chart__echarts" :class="{'cn-chart__echarts--statistics': isEchartsWithStatistics}">
<div class="cn-chart__header" v-if="layout.indexOf(layoutConstant.HEADER) > -1"> <div class="cn-chart__header" v-if="layout.indexOf(layoutConstant.HEADER) > -1">
<div class="header__title"> <div class="header__title">
<slot name="title"></slot> <slot name="title"></slot>
@@ -18,7 +18,7 @@
</template> </template>
<script> <script>
import { layoutConstant, isEchartsWithTable } from '@/components/charts/chart-options' import { layoutConstant, isEchartsWithTable, isEchartsWithStatistics } from '@/components/charts/chart-options'
export default { export default {
name: 'EchartsFrame', name: 'EchartsFrame',
props: { props: {
@@ -28,7 +28,8 @@ export default {
setup (props) { setup (props) {
return { return {
layoutConstant, layoutConstant,
isPieWithTable: isEchartsWithTable(props.chartInfo.type) isPieWithTable: isEchartsWithTable(props.chartInfo.type),
isEchartsWithStatistics: isEchartsWithStatistics(props.chartInfo.type)
} }
}, },
mounted () { mounted () {

View File

@@ -7,10 +7,10 @@
<div class="legend__table"> <div class="legend__table">
<table> <table>
<tr v-for="(d, i) in data" :key="i" class="legend__row"> <tr v-for="(d, i) in data" :key="i" class="legend__row">
<td><div :style="{backgroundColor: getChartColor(i)}" class="legend__row-top" :title="getChartColor(i)"></div></td> <td style="width: 40px;"><div :style="{backgroundColor: getChartColor(i)}" class="legend__row-top" :title="getChartColor(i)"></div></td>
<td><div class="text__show" :title="d.legend">{{d.legend}}</div></td> <td><div class="text__show" :title="d.legend">{{d.legend}}</div></td>
<td><div :title="d.aggregation.avg" class="legend__row-left">{{d.aggregation.avg}}</div></td> <td style="width: 70px;"><div :title="d.aggregation.avg">{{d.aggregation.avg}}</div></td>
<td><div :title="d.aggregation.max" class="legend__row-right">{{d.aggregation.max}}</div></td> <td style="width: 70px;"><div :title="d.aggregation.max">{{d.aggregation.max}}</div></td>
</tr> </tr>
</table> </table>
</div> </div>
@@ -38,12 +38,13 @@ export default {
setup () { setup () {
return { return {
getChartColor, getChartColor,
chartColor } chartColor
}
} }
} }
</script> </script>
<style scoped> <style lang="scss" scoped>
.chart__legend{ .chart__legend{
width: 455px; width: 455px;
height: 111px; height: 111px;
@@ -52,8 +53,8 @@ export default {
color:#5f6368; color:#5f6368;
margin: auto; margin: auto;
} }
.text__show{ .text__show {
width: 50px; max-width: 240px;
overflow: hidden; overflow: hidden;
text-overflow:ellipsis; text-overflow:ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -66,21 +67,17 @@ export default {
} }
.location{ .location{
position: absolute; position: absolute;
right: 138px; right: 110px;
top: -9px; top: -9px;
font-family: Roboto-Regular; font-size: 13px;
font-size: 14px;
color: #0091FF; color: #0091FF;
font-weight: 400;
} }
.locations{ .locations{
position: absolute; position: absolute;
right: 29px; right: 25px;
top: -8px; top: -8px;
font-family: Roboto-Regular; font-size: 13px;
font-size: 14px;
color: #0091FF; color: #0091FF;
font-weight: 400;
} }
.legend__row-top{ .legend__row-top{
width: 17px; width: 17px;
@@ -90,7 +87,11 @@ export default {
} }
.legend__table { .legend__table {
overflow: auto; overflow: auto;
height: calc(100% - 30px) height: calc(100% - 30px);
table {
width: 100%;
}
} }
.legend__row{ .legend__row{
font-size: 12px; font-size: 12px;
@@ -98,13 +99,7 @@ export default {
} }
.legend__row:hover { .legend__row:hover {
background-color: #f9f9f9; background-color: #f9f9f9;
border: 0px; border: 0;
color: #383838; color: #383838;
} }
.legend__row-right{
margin-left: 60px;
}
.legend__row-left{
margin-left: 178px;
}
</style> </style>

View File

@@ -25,6 +25,10 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
&>.cn-chart.cn-chart__echarts--statistics {
border: none;
box-shadow: none;
}
&>.cn-chart__echarts, &>.cn-chart__table, &>.cn-chart__map { &>.cn-chart__echarts, &>.cn-chart__table, &>.cn-chart__map {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -0,0 +1,83 @@
<template>
<div class="entity-list">
<div class="entity-list__content">
<div class="cn-entity" v-for="d in listData" :key="d.id">
<div class="cn-entity__header">
<div class="header__icon" :style="{backgroundColor: circleColor}"><i :class="iconClass"></i></div>
<div class="header__content">
<div class="content__title">
<template v-if="entityType === 'ip'">{{d.ip || 'Unknown'}}</template>
<template v-else-if="entityType === 'domain'">{{d.domainName || 'Unknown'}}</template>
<template v-else-if="entityType === 'app'">{{d.appName || 'Unknown'}}</template>
</div>
<div class="content__desc" v-if="entityType !== 'ip'">
<template v-if="entityType === 'domain'">
<span class="desc__label">{{$t('entities.reputationLevel')}}:</span>
<span>{{d.reputationLevel || '-'}}</span>
</template>
<template v-else-if="entityType === 'app'">
<span class="desc__label">{{$t('entities.risk')}}:</span>
<span>{{d.appRisk || '-'}}</span>
</template>
</div>
</div>
</div>
<div class="cn-entity__body"></div>
</div>
</div>
<div class="entity-list__pagination"></div>
</div>
</template>
<script>
export default {
name: 'EntityList',
props: {
listData: Array,
entityType: String
},
computed: {
circleColor () {
let color
switch (this.entityType) {
case ('ip'): {
color = '#E8FBF9'
break
}
case ('domain'): {
color = '#EEF6FE'
break
}
case ('app'): {
color = '#FEF7E7'
break
}
default: break
}
return color
},
iconClass () {
let className
switch (this.entityType) {
case ('ip'): {
className = 'cn-icon cn-icon-ip ip-green'
break
}
case ('domain'): {
className = 'cn-icon cn-icon-domain domain-blue'
break
}
case ('app'): {
className = 'cn-icon cn-icon-app app-orange'
break
}
default: break
}
return className
}
}
}
</script>
<style>
</style>

View File

@@ -2,19 +2,28 @@
<div class="entity-left-filter"> <div class="entity-left-filter">
<div class="filter__header">{{$t('entities.filter')}}</div> <div class="filter__header">{{$t('entities.filter')}}</div>
<div class="filter__body"> <div class="filter__body">
<el-collapse v-model="active"> <el-collapse v-model="active" class="filter__collapse">
<el-collapse-item <el-collapse-item
v-for="(f, i) in filterData" v-for="(f, i) in filterData"
:key="i" :key="i"
:title="f.title" :title="f.title"
:name="`${i}`" :name="`${i}`"
class="filter__collapse"
> >
<el-tree <el-tree
:data="f.data" :data="f.data"
:load="(node, resolve) => f.load(node, resolve, f.filterType, f.childrenKey ? f.childrenKey : f.key)"
:node-key="f.key"
:props="{ isLeaf: 'leaf' }"
:expand-on-click-node="false"
:lazy="i === 0"
:show-checkbox="i === 1"
@node-click="nodeClick"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span>{{data[f.key]}}-{{data.count}}</span> <div class="filter-item">
<span>{{node.level === 1 ? data[f.key] : ''}}{{node.level === 2 ? data[f.childrenKey] : ''}}</span>
<span>{{data.count}}</span>
</div>
</template> </template>
</el-tree> </el-tree>
</el-collapse-item> </el-collapse-item>
@@ -34,8 +43,10 @@ export default {
active: ['1'] active: ['1']
} }
}, },
mounted () { methods: {
console.info(this.filterData) nodeClick (data, node) {
this.$emit('select', data)
}
} }
} }
</script> </script>
@@ -47,6 +58,7 @@ export default {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 1px solid $--right-box-border-color; border: 1px solid $--right-box-border-color;
overflow: auto;
.filter__header { .filter__header {
background-color: #FAFAFA; background-color: #FAFAFA;
@@ -59,12 +71,28 @@ export default {
.filter__body { .filter__body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
overflow: auto;
.filter__collapse { .filter__collapse {
max-height: 50%;
.el-collapse-item__header { .el-collapse-item__header {
padding-left: 10px; padding-left: 10px;
padding-top: 8px;
font-weight: bold;
&.is-active {
border-bottom: 1px solid $--right-box-border-color;
}
}
.el-collapse-item__wrap {
padding-top: 6px !important;
.filter-item {
display: flex;
justify-content: space-between;
padding-right: 6px;
width: 100%;
}
} }
} }
} }

View File

@@ -7,4 +7,83 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
background-color: #fff; background-color: #fff;
}
.entity-list {
height: 100%;
width: 100%;
.entity-list__content {
display: grid;
grid-template-columns: repeat(auto-fill, var(--entity-width));
grid-auto-flow: row;
grid-auto-rows: var(--entity-height);
grid-gap: 10px;
height: calc(100% - 67px);
width: 100%;
overflow: auto;
.cn-entity {
display: grid;
grid-template-rows: 44% 56%;
border: 1px solid $--right-box-border-color;
.cn-entity__header {
display: flex;
align-items: center;
padding: 20px;
.header__icon {
display: flex;
justify-content: center;
justify-items: center;
align-items: center;
width: 52px;
height: 52px;
border-radius: 50%;
i {
font-size: 20px;
}
}
.header__content {
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 10px;
.content__title {
font-size: 22px;
color: #333333;
font-weight: bold;
}
.content__desc {
font-size: 14px;
color: #666666;
padding-top: 3px;
.desc__label {
color: #aaa;
padding-right: 10px;
}
}
}
}
.cn-entity__body {
background-color: lightgoldenrodyellow;
}
}
}
.entity-list__pagination {
height: 67px;
}
}
}
.ip-green {
color: #23BF9A;
}
.domain-blue {
color: #0290FF;
}
.app-orange {
color: #FFA200;
}

View File

@@ -16,7 +16,10 @@ export const api = {
chart: '/visual/chart', chart: '/visual/chart',
entityIpFilter: '/interface/entity/ip/filter', entityIpFilter: '/interface/entity/ip/filter',
entityDomainFilter: '/interface/entity/domain/filter', entityDomainFilter: '/interface/entity/domain/filter',
entityAppFilter: '/interface/entity/app/filter' entityAppFilter: '/interface/entity/app/filter',
entityIpList: '/interface/entity/ip/list',
entityDomainList: '/interface/entity/domain/list',
entityAppList: '/interface/entity/app/list'
} }
/* panel */ /* panel */
export async function getPanelList (params) { export async function getPanelList (params) {
@@ -44,6 +47,18 @@ export async function getEntityDomainFilterList (params) {
export async function getEntityAppFilterList (params) { export async function getEntityAppFilterList (params) {
return await getData(api.entityAppFilter, params, true) return await getData(api.entityAppFilter, params, true)
} }
/* ip类型entity列表 */
export async function getEntityIpList (params) {
return await getData(api.entityIpList, params, true)
}
/* domain类型entity列表 */
export async function getEntityDomainList (params) {
return await getData(api.entityDomainList, params, true)
}
/* app类型entity列表 */
export async function getEntityAppList (params) {
return await getData(api.entityAppList, params, true)
}
/* 字典 */ /* 字典 */
export async function getDictList (params) { export async function getDictList (params) {
return await getData(api.dict, params, true) return await getData(api.dict, params, true)

View File

@@ -21,9 +21,13 @@
<!-- 筛选区域 --> <!-- 筛选区域 -->
<left-filter <left-filter
:filter-data="filterData" :filter-data="filterData"
@select="select"
></left-filter> ></left-filter>
<!-- 内容区域 --> <!-- 内容区域 -->
<div style="background-color: lightcyan"></div> <entity-list
:list-data="listData"
:entity-type="filterType"
></entity-list>
</div> </div>
</div> </div>
</template> </template>
@@ -32,17 +36,32 @@
import { entityType } from '@/utils/constants' import { entityType } from '@/utils/constants'
import { ref } from 'vue' import { ref } from 'vue'
import LeftFilter from '@/components/entities/LeftFilter' import LeftFilter from '@/components/entities/LeftFilter'
import { getEntityIpFilterList, getEntityDomainFilterList, getEntityAppFilterList } from '@/utils/api' import EntityList from '@/components/entities/EntityList'
import { getEntityIpFilterList, getEntityDomainFilterList, getEntityAppFilterList, getEntityIpList, getEntityDomainList, getEntityAppList } from '@/utils/api'
export default { export default {
name: 'EntityExplorer', name: 'EntityExplorer',
data () { data () {
return { return {
searchContent: '', searchContent: '',
filterData: [] filterData: [],
pageObjLeftTop: {
pageNo: 1,
pageSize: 10
},
pageObjLeftBottom: {
pageNo: 1,
pageSize: 10
},
pageObjRight: {
pageNo: 1,
pageSize: 50
},
listData: []
} }
}, },
components: { components: {
LeftFilter LeftFilter,
EntityList
}, },
methods: { methods: {
async loadData (filterType, conditionGroup) { async loadData (filterType, conditionGroup) {
@@ -63,32 +82,60 @@ export default {
default: break default: break
} }
return data return data
},
select (data) {
},
getEntityData (param) {
} }
}, },
watch: { watch: {
filterType (n) { filterType (n) {
this.pageObjLeftTop = {
pageNo: 1,
pageSize: 10
}
this.pageObjLeftBottom = {
pageNo: 1,
pageSize: 10
}
this.pageObjRight = {
pageNo: 1,
pageSize: 50
}
this.listData = []
const requests = [] const requests = []
const data = [] const data = []
switch (n) { switch (n) {
case 'ip': { case 'ip': {
requests.push(this.loadData(n, 'country')) requests.push(this.loadData(n, 'country'))
requests.push(this.loadData(n, 'asn')) requests.push(this.loadData(n, 'asn'))
data.push({ title: this.$t('entities.countryOrRegion'), key: 'country' }) data.push({ title: this.$t('entities.countryOrRegion'), key: 'country', childrenKey: 'region', load })
data.push({ title: this.$t('entities.systemNumber'), key: 'asn' }) data.push({ title: this.$t('entities.systemNumber'), key: 'asn', load })
getEntityIpList({ ...this.pageObjRight }).then(res => {
this.listData = res
})
break break
} }
case 'domain': { case 'domain': {
requests.push(this.loadData(n, 'group')) requests.push(this.loadData(n, 'group'))
requests.push(this.loadData(n, 'level')) requests.push(this.loadData(n, 'level'))
data.push({ title: this.$t('entities.group'), key: 'group' }) data.push({ title: this.$t('entities.group'), key: 'group', childrenKey: 'name', load })
data.push({ title: this.$t('entities.level'), key: 'level' }) data.push({ title: this.$t('entities.level'), key: 'level', load })
getEntityDomainList({ ...this.pageObjRight }).then(res => {
this.listData = res
})
break break
} }
case 'app': { case 'app': {
requests.push(this.loadData(n, 'category')) requests.push(this.loadData(n, 'category'))
requests.push(this.loadData(n, 'risk')) requests.push(this.loadData(n, 'risk'))
data.push({ title: this.$t('entities.category'), key: 'category' }) data.push({ title: this.$t('entities.category'), key: 'category', childrenKey: 'subcategory', load })
data.push({ title: this.$t('entities.risk'), key: 'risk' }) data.push({ title: this.$t('entities.risk'), key: 'risk', load })
getEntityAppList({ ...this.pageObjRight }).then(res => {
this.listData = res
})
break break
} }
default: break default: break
@@ -104,15 +151,21 @@ export default {
async mounted () { async mounted () {
const country = await this.loadData(this.filterType, 'country') const country = await this.loadData(this.filterType, 'country')
const asn = await this.loadData(this.filterType, 'asn') const asn = await this.loadData(this.filterType, 'asn')
this.listData = await getEntityIpList({ ...this.pageObjRight })
this.filterData.push({ this.filterData.push({
title: this.$t('entities.countryOrRegion'), title: this.$t('entities.countryOrRegion'),
key: 'country', key: 'country',
data: country childrenKey: 'region',
data: country,
filterType: this.filterType,
load
}) })
this.filterData.push({ this.filterData.push({
title: this.$t('entities.systemNumber'), title: this.$t('entities.systemNumber'),
key: 'asn', key: 'asn',
data: asn data: asn,
filterType: this.filterType,
load
}) })
}, },
setup () { setup () {
@@ -123,6 +176,36 @@ export default {
} }
} }
} }
const load = (node, resolve, filterType, key) => {
if (node.level === 0) {
resolve(node.data)
} else {
let req = null
switch (filterType) {
case 'ip': {
req = getEntityIpFilterList({ type: key })
break
}
case 'domain': {
req = getEntityDomainFilterList({ type: key })
break
}
case 'app': {
req = getEntityAppFilterList({ type: key })
break
}
default: break
}
if (req !== null) {
req.then(res => {
res = res.map(r => {
return { ...r, leaf: true }
})
resolve(res)
})
}
}
}
</script> </script>
<style lang="scss"> <style lang="scss">