perf: entities部分内容、优化带统计图表样式
This commit is contained in:
@@ -13,16 +13,32 @@ $--menu-hover-background-color: #000C18; // menu背景色
|
||||
$--menu-item-font-color: #BEBEBE; // menu字色
|
||||
$--menu-item-hover-fill: $--color-primary; // menu鼠标悬浮、激活时背景色
|
||||
|
||||
$--collapse-header-height: 42px;
|
||||
$--collapse-border-color: #EFF2F5;
|
||||
|
||||
/** 自定义变量 **/
|
||||
: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) {
|
||||
--chart-height-unit: 30px;
|
||||
}
|
||||
@media only screen and (min-width : 1560px) {
|
||||
--entity-width: calc(33.3% - 7px);
|
||||
}
|
||||
@media only screen and (min-width : 1824px) {
|
||||
--chart-height-unit: 40px;
|
||||
--entity-width: calc(33.3% - 7px);
|
||||
--entity-height: 210px;
|
||||
}
|
||||
@media only screen and (min-width : 2424px) {
|
||||
--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;
|
||||
|
||||
/* 按钮 */
|
||||
$--button-border-radius: $--border-color-primary; // 按钮圆角
|
||||
$--button-border-radius: $--border-radius-primary; // 按钮圆角
|
||||
|
||||
$--button-primary-color: #FFF; // 普通按钮字色
|
||||
$--button-primary-background-color: $--color-primary; // 普通按钮背景色
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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="header__title">
|
||||
<slot name="title"></slot>
|
||||
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { layoutConstant, isEchartsWithTable } from '@/components/charts/chart-options'
|
||||
import { layoutConstant, isEchartsWithTable, isEchartsWithStatistics } from '@/components/charts/chart-options'
|
||||
export default {
|
||||
name: 'EchartsFrame',
|
||||
props: {
|
||||
@@ -28,7 +28,8 @@ export default {
|
||||
setup (props) {
|
||||
return {
|
||||
layoutConstant,
|
||||
isPieWithTable: isEchartsWithTable(props.chartInfo.type)
|
||||
isPieWithTable: isEchartsWithTable(props.chartInfo.type),
|
||||
isEchartsWithStatistics: isEchartsWithStatistics(props.chartInfo.type)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<div class="legend__table">
|
||||
<table>
|
||||
<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 :title="d.aggregation.avg" class="legend__row-left">{{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.avg">{{d.aggregation.avg}}</div></td>
|
||||
<td style="width: 70px;"><div :title="d.aggregation.max">{{d.aggregation.max}}</div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -38,12 +38,13 @@ export default {
|
||||
setup () {
|
||||
return {
|
||||
getChartColor,
|
||||
chartColor }
|
||||
chartColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.chart__legend{
|
||||
width: 455px;
|
||||
height: 111px;
|
||||
@@ -52,8 +53,8 @@ export default {
|
||||
color:#5f6368;
|
||||
margin: auto;
|
||||
}
|
||||
.text__show{
|
||||
width: 50px;
|
||||
.text__show {
|
||||
max-width: 240px;
|
||||
overflow: hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -66,21 +67,17 @@ export default {
|
||||
}
|
||||
.location{
|
||||
position: absolute;
|
||||
right: 138px;
|
||||
right: 110px;
|
||||
top: -9px;
|
||||
font-family: Roboto-Regular;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
color: #0091FF;
|
||||
font-weight: 400;
|
||||
}
|
||||
.locations{
|
||||
position: absolute;
|
||||
right: 29px;
|
||||
right: 25px;
|
||||
top: -8px;
|
||||
font-family: Roboto-Regular;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
color: #0091FF;
|
||||
font-weight: 400;
|
||||
}
|
||||
.legend__row-top{
|
||||
width: 17px;
|
||||
@@ -90,7 +87,11 @@ export default {
|
||||
}
|
||||
.legend__table {
|
||||
overflow: auto;
|
||||
height: calc(100% - 30px)
|
||||
height: calc(100% - 30px);
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.legend__row{
|
||||
font-size: 12px;
|
||||
@@ -98,13 +99,7 @@ export default {
|
||||
}
|
||||
.legend__row:hover {
|
||||
background-color: #f9f9f9;
|
||||
border: 0px;
|
||||
border: 0;
|
||||
color: #383838;
|
||||
}
|
||||
.legend__row-right{
|
||||
margin-left: 60px;
|
||||
}
|
||||
.legend__row-left{
|
||||
margin-left: 178px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
&>.cn-chart.cn-chart__echarts--statistics {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
&>.cn-chart__echarts, &>.cn-chart__table, &>.cn-chart__map {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
83
src/components/entities/EntityList.vue
Normal file
83
src/components/entities/EntityList.vue
Normal 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>
|
||||
@@ -2,19 +2,28 @@
|
||||
<div class="entity-left-filter">
|
||||
<div class="filter__header">{{$t('entities.filter')}}</div>
|
||||
<div class="filter__body">
|
||||
<el-collapse v-model="active">
|
||||
<el-collapse v-model="active" class="filter__collapse">
|
||||
<el-collapse-item
|
||||
v-for="(f, i) in filterData"
|
||||
:key="i"
|
||||
:title="f.title"
|
||||
:name="`${i}`"
|
||||
class="filter__collapse"
|
||||
>
|
||||
<el-tree
|
||||
: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 }">
|
||||
<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>
|
||||
</el-tree>
|
||||
</el-collapse-item>
|
||||
@@ -34,8 +43,10 @@ export default {
|
||||
active: ['1']
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
console.info(this.filterData)
|
||||
methods: {
|
||||
nodeClick (data, node) {
|
||||
this.$emit('select', data)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -47,6 +58,7 @@ export default {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid $--right-box-border-color;
|
||||
overflow: auto;
|
||||
|
||||
.filter__header {
|
||||
background-color: #FAFAFA;
|
||||
@@ -59,12 +71,28 @@ export default {
|
||||
.filter__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.filter__collapse {
|
||||
max-height: 50%;
|
||||
|
||||
.el-collapse-item__header {
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,83 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
@@ -16,7 +16,10 @@ export const api = {
|
||||
chart: '/visual/chart',
|
||||
entityIpFilter: '/interface/entity/ip/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 */
|
||||
export async function getPanelList (params) {
|
||||
@@ -44,6 +47,18 @@ export async function getEntityDomainFilterList (params) {
|
||||
export async function getEntityAppFilterList (params) {
|
||||
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) {
|
||||
return await getData(api.dict, params, true)
|
||||
|
||||
@@ -21,9 +21,13 @@
|
||||
<!-- 筛选区域 -->
|
||||
<left-filter
|
||||
:filter-data="filterData"
|
||||
@select="select"
|
||||
></left-filter>
|
||||
<!-- 内容区域 -->
|
||||
<div style="background-color: lightcyan"></div>
|
||||
<entity-list
|
||||
:list-data="listData"
|
||||
:entity-type="filterType"
|
||||
></entity-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -32,17 +36,32 @@
|
||||
import { entityType } from '@/utils/constants'
|
||||
import { ref } from 'vue'
|
||||
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 {
|
||||
name: 'EntityExplorer',
|
||||
data () {
|
||||
return {
|
||||
searchContent: '',
|
||||
filterData: []
|
||||
filterData: [],
|
||||
pageObjLeftTop: {
|
||||
pageNo: 1,
|
||||
pageSize: 10
|
||||
},
|
||||
pageObjLeftBottom: {
|
||||
pageNo: 1,
|
||||
pageSize: 10
|
||||
},
|
||||
pageObjRight: {
|
||||
pageNo: 1,
|
||||
pageSize: 50
|
||||
},
|
||||
listData: []
|
||||
}
|
||||
},
|
||||
components: {
|
||||
LeftFilter
|
||||
LeftFilter,
|
||||
EntityList
|
||||
},
|
||||
methods: {
|
||||
async loadData (filterType, conditionGroup) {
|
||||
@@ -63,32 +82,60 @@ export default {
|
||||
default: break
|
||||
}
|
||||
return data
|
||||
},
|
||||
select (data) {
|
||||
|
||||
},
|
||||
getEntityData (param) {
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
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 data = []
|
||||
switch (n) {
|
||||
case 'ip': {
|
||||
requests.push(this.loadData(n, 'country'))
|
||||
requests.push(this.loadData(n, 'asn'))
|
||||
data.push({ title: this.$t('entities.countryOrRegion'), key: 'country' })
|
||||
data.push({ title: this.$t('entities.systemNumber'), key: 'asn' })
|
||||
data.push({ title: this.$t('entities.countryOrRegion'), key: 'country', childrenKey: 'region', load })
|
||||
data.push({ title: this.$t('entities.systemNumber'), key: 'asn', load })
|
||||
getEntityIpList({ ...this.pageObjRight }).then(res => {
|
||||
this.listData = res
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'domain': {
|
||||
requests.push(this.loadData(n, 'group'))
|
||||
requests.push(this.loadData(n, 'level'))
|
||||
data.push({ title: this.$t('entities.group'), key: 'group' })
|
||||
data.push({ title: this.$t('entities.level'), key: 'level' })
|
||||
data.push({ title: this.$t('entities.group'), key: 'group', childrenKey: 'name', load })
|
||||
data.push({ title: this.$t('entities.level'), key: 'level', load })
|
||||
getEntityDomainList({ ...this.pageObjRight }).then(res => {
|
||||
this.listData = res
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'app': {
|
||||
requests.push(this.loadData(n, 'category'))
|
||||
requests.push(this.loadData(n, 'risk'))
|
||||
data.push({ title: this.$t('entities.category'), key: 'category' })
|
||||
data.push({ title: this.$t('entities.risk'), key: 'risk' })
|
||||
data.push({ title: this.$t('entities.category'), key: 'category', childrenKey: 'subcategory', load })
|
||||
data.push({ title: this.$t('entities.risk'), key: 'risk', load })
|
||||
getEntityAppList({ ...this.pageObjRight }).then(res => {
|
||||
this.listData = res
|
||||
})
|
||||
break
|
||||
}
|
||||
default: break
|
||||
@@ -104,15 +151,21 @@ export default {
|
||||
async mounted () {
|
||||
const country = await this.loadData(this.filterType, 'country')
|
||||
const asn = await this.loadData(this.filterType, 'asn')
|
||||
this.listData = await getEntityIpList({ ...this.pageObjRight })
|
||||
this.filterData.push({
|
||||
title: this.$t('entities.countryOrRegion'),
|
||||
key: 'country',
|
||||
data: country
|
||||
childrenKey: 'region',
|
||||
data: country,
|
||||
filterType: this.filterType,
|
||||
load
|
||||
})
|
||||
this.filterData.push({
|
||||
title: this.$t('entities.systemNumber'),
|
||||
key: 'asn',
|
||||
data: asn
|
||||
data: asn,
|
||||
filterType: this.filterType,
|
||||
load
|
||||
})
|
||||
},
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user