CN-1481 Subscribe详情页KPI组件开发;CN-1482 Subscribe详情页top app组件开发

This commit is contained in:
hanyuxia
2023-11-24 14:39:33 +08:00
parent f25805ea0a
commit 6450e8e050
10 changed files with 779 additions and 3 deletions

View File

@@ -9,6 +9,8 @@ const DEFAULT_TIME_FILTER_RANGE = {
entity: { entity: {
list: 60, list: 60,
trafficLine: 60, trafficLine: 60,
subscriberKpi: 60,
subscriberTopApp: 60,
informationAggregation: 0, informationAggregation: 0,
relatedEntity: 60 * 24 * 7, relatedEntity: 60 * 24 * 7,
openPort: 60 * 24 * 7, openPort: 60 * 24 * 7,

View File

@@ -83,6 +83,8 @@
@import 'views/setting/knowledgeBase'; @import 'views/setting/knowledgeBase';
@import 'views/charts2/entityDetailLine'; @import 'views/charts2/entityDetailLine';
@import 'views/charts2/EntityDetailSubscriberKpi.scss';
@import 'views/charts2/EntityDetailSubscriberTopApp.scss';
@import 'views/charts2/entityDetailTabs'; @import 'views/charts2/entityDetailTabs';
@import 'views/charts2/digitalCertificate'; @import 'views/charts2/digitalCertificate';
@import 'views/charts2/entityDetailBasicInfo'; @import 'views/charts2/entityDetailBasicInfo';

View File

@@ -0,0 +1,97 @@
.subscriber-kpi {
height: 100%;
.subscriber-kpi-header {
height:34px;
padding-bottom:10px;
font-family: NotoSansHans-Medium;
font-size: 14px;
color: #353636;
font-weight: 500;
display:flex;
align-items: center;
justify-content: space-between;
.subscriber-kpi-title {
height:24px;
overflow: hidden;
}
}
.subscriber-kpi-body {
border: 1px solid #E2E5EC;
border-radius: 4px;
height:calc(100% - 34px);
.subscriber-kpi-content {
height: calc(100% - 36px);
padding: 20px 0 20px 20px;
display: flex;
flex-direction: column;
.panel-chart__no-data {
height: calc(100% - 46px);
}
.kpi-type {
display: flex;
flex-direction: column;
justify-content:space-between;
height: calc(100% - 65px);
.kpi-type-value {
display: flex;
flex-direction: column;
padding-bottom:20px;
.kpi-type-value-name {
line-height: 12px;
margin-bottom: 10px;
font-size: 14px;
color: #575757;
font-weight: 400;
}
.kpi-type-data {
display:flex;
flex-direction: row;
.kpi-type-value-number {
font-family: Helvetica-Bold;
font-size: 20px;
color: #353636;
font-weight: 700;
}
.data-trend {
display: flex;
width: 50%;
.data-total-trend {
display: flex;
justify-content: left;
margin-left: 6px;
font-size: 12px;
justify-content: center;
margin-top: 2px;
border-radius: 10px;
font-weight: 500;
font-size: 12px;
height: 20px;
padding: 0 5px;
}
.data-total-trend-black {
background-color: rgba(113,113,113,0.12);
color: #717171;
width: 36px;
}
.data-total-trend-green {
background-color: rgba(126,159,84,0.12);
color: #7E9F54;
}
.data-total-trend-red {
background-color: rgba(226,97,84,0.12);
color: #E26154;
.cn-icon-rise1{
color: #E44D3E;
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,97 @@
.subscriber-top-app {
height: 100%;
.subscriber-top-app-header {
height:34px;
padding-bottom:10px;
font-family: NotoSansHans-Medium;
font-size: 14px;
color: #353636;
font-weight: 500;
display:flex;
align-items: center;
justify-content: space-between;
.subscriber-top-app-title {
height:24px;
overflow: hidden;
}
}
.subscriber-top-app-body {
border: 1px solid #E2E5EC;
border-radius: 4px;
height:calc(100% - 34px);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 20px 20px 20px;
.panel-chart__no-data {
height: calc(100% - 46px);
}
.top-app-left {
height:100%;
display: flex;
flex-direction: column;
margin-right:15px;
.app-data {
display: flex;
flex-direction: row;
align-items: center;
font-size: 14px;
color: #353636;
font-weight: 400;
height:calc(100%/10);
//padding:5.6px 0 5.6px;
.app-index {
text-align: right;
width:20px;
margin-right:20px;
}
.app-name {
width:50px;
margin-right:30px;
}
.top-app-divider {
height:10px;
background: #717171;
margin-left:10px;
margin-right:8px;
}
.app-trend {
display: flex;
flex-direction: row;
align-items: center;
i {
margin-right:3px;
font-size:12px;
color: #717171;
}
}
.app-up {
font-size: 12px;
color: #717171;
letter-spacing: -0.2px;
text-align: center;
font-weight: 400;
}
.app-down {
font-size: 12px;
color: #717171;
letter-spacing: -0.2px;
text-align: center;
font-weight: 400;
}
}
}
.top-app-right {
height: 100%;
width:calc(100% - 248px);
position: relative;
.chart-content {
height: 100%;
width: 100%;
}
}
}
}

View File

@@ -1887,6 +1887,8 @@ export const chartColor5 = ['#E26154', '#E7B34E', '#88AF65']
export const chartColor6 = ['#E99F67', '#D9C74B'] export const chartColor6 = ['#E99F67', '#D9C74B']
export const chartColorForBehaviorPattern = ['#7acac7', '#b4d38e', '#fee9b9', '#fec396', '#fb9b79', '#e3799c', '#edd5f5', '#868cac', '#a4adde', '#64b4e6'] export const chartColorForBehaviorPattern = ['#7acac7', '#b4d38e', '#fee9b9', '#fec396', '#fb9b79', '#e3799c', '#edd5f5', '#868cac', '#a4adde', '#64b4e6']
export const chartColorForSubscriberTopApp = ['#A7C186', '#AFCC8A', '#BEDCAC', '#80BEA5', '#7BBBBC', '#8CB9C8', '#E6BF88', '#E6D99B', '#E0D1B0', '#ECAE95']
export const iso36112 = { export const iso36112 = {
[storageKey.iso36112Capital]: 'data/countriesWithCapital', [storageKey.iso36112Capital]: 'data/countriesWithCapital',
[storageKey.iso36112WorldLow]: 'worldChinaLow', [storageKey.iso36112WorldLow]: 'worldChinaLow',

View File

@@ -166,6 +166,18 @@
:entity="entity" :entity="entity"
@toggleLoading="toggleLoading" @toggleLoading="toggleLoading"
></entity-detail-basic-info> ></entity-detail-basic-info>
<entity-detail-subscriber-kpi
v-else-if="chart.type === typeMapping.entityDetail.subscriberKpi"
:chart="chart"
:entity="entity"
@toggleLoading="toggleLoading"
></entity-detail-subscriber-kpi>
<entity-detail-subscriber-top-app
v-else-if="chart.type === typeMapping.entityDetail.subscriberTopApp"
:chart="chart"
:entity="entity"
@toggleLoading="toggleLoading"
></entity-detail-subscriber-top-app>
<entity-detail-line <entity-detail-line
v-else-if="chart.type === typeMapping.entityDetail.line" v-else-if="chart.type === typeMapping.entityDetail.line"
:chart="chart" :chart="chart"
@@ -210,6 +222,8 @@ import DnsRecentEvents from '@/views/charts2/charts/dnsInsight/DnsRecentEvents'
import DnsTrafficLine from '@/views/charts2/charts/dnsInsight/DnsTrafficLine' import DnsTrafficLine from '@/views/charts2/charts/dnsInsight/DnsTrafficLine'
import EntityDetailBasicInfo from '@/views/charts2/charts/entityDetail/EntityDetailBasicInfo' import EntityDetailBasicInfo from '@/views/charts2/charts/entityDetail/EntityDetailBasicInfo'
import EntityDetailLine from '@/views/charts2/charts/entityDetail/EntityDetailLine' import EntityDetailLine from '@/views/charts2/charts/entityDetail/EntityDetailLine'
import EntityDetailSubscriberKpi from '@/views/charts2/charts/entityDetail/EntityDetailSubscriberKpi'
import EntityDetailSubscriberTopApp from '@/views/charts2/charts/entityDetail/EntityDetailSubscriberTopApp'
import EntityDetailTabsChart from '@/views/charts2/charts/entityDetail/EntityDetailTabs' import EntityDetailTabsChart from '@/views/charts2/charts/entityDetail/EntityDetailTabs'
import { getNowTime } from '@/utils/date-util' import { getNowTime } from '@/utils/date-util'
@@ -245,6 +259,8 @@ export default {
DnsRecentEvents, DnsRecentEvents,
DnsTrafficLine, DnsTrafficLine,
EntityDetailBasicInfo, EntityDetailBasicInfo,
EntityDetailSubscriberKpi,
EntityDetailSubscriberTopApp,
EntityDetailLine, EntityDetailLine,
EntityDetailTabsChart EntityDetailTabsChart
}, },

View File

@@ -34,6 +34,8 @@ export const typeMapping = {
}, },
entityDetail: { entityDetail: {
basicInfo: 712, basicInfo: 712,
subscriberKpi: 714,
subscriberTopApp: 715,
line: 107, line: 107,
tabsChart: 713 tabsChart: 713
} }

View File

@@ -0,0 +1,212 @@
<template>
<div class="subscriber-kpi">
<div class="subscriber-kpi-header">
<div class="subscriber-kpi-title">{{$t('subscriber.kpi')}}</div>
<date-time-range
class="entity-detail-date-time-range"
:start-time="timeFilter.startTime"
:end-time="timeFilter.endTime"
:date-range="timeFilter.dateRangeValue"
ref="dateTimeRange"
@change="reload"
/>
</div>
<div class="subscriber-kpi-body">
<chart-no-data v-if="isNoData" test-id="noData"></chart-no-data>
<chart-error v-if="showError" :content="errorMsg" />
<div class="subscriber-kpi-content" v-if="!isNoData && !showError">
<div class="kpi-type">
<div class="kpi-type-value">
<div class="kpi-type-value-name">{{$t('subscriber.volume')}}</div>
<div class="kpi-type-data" >
<div class="kpi-type-value-number">
{{unitConvert($_.get(kpiData, 'volume'), unitTypes.number).join(' ')}}
</div>
<div class="data-trend">
<div class="data-total-trend data-total-trend-red">
<i class="cn-icon-rise1 cn-icon"></i>&nbsp;
<span >32%</span>
</div>
</div>
</div>
</div>
<div class="kpi-type-value">
<div class="kpi-type-value-name">{{$t('subscriber.throughput')}}</div>
<div class="kpi-type-data" >
<div class="kpi-type-value-number" >{{unitConvert($_.get(kpiData, 'throughput'), unitTypes.bps).join(' ')}}</div>
<div class="data-trend">
<div class="data-total-trend data-total-trend-green">
<i class="cn-icon-decline cn-icon"></i>&nbsp;
<span >8%</span>
</div>
</div>
</div>
</div>
<div class="kpi-type-value">
<div class="kpi-type-value-name">{{$t('subscriber.latency')}}</div>
<div class="kpi-type-data" >
<div class="kpi-type-value-number" >{{unitConvert($_.get(kpiData, 'latency'), unitTypes.time).join(' ')}}</div>
<div class="data-trend">
<div class="data-total-trend data-total-trend-black">
<i class="cn-icon-constant cn-icon"></i>
</div>
</div>
</div>
</div>
<div class="kpi-type-value">
<div class="kpi-type-value-name">{{$t('subscriber.packetLoss')}}</div>
<div class="kpi-type-data" >
<div class="kpi-type-value-number" >{{unitConvert($_.get(kpiData, 'packetLoss'), unitTypes.percent).join(' ')}}</div>
<div class="data-trend">
<div class="data-total-trend data-total-trend-green">
<i class="cn-icon-decline cn-icon"></i>&nbsp;
<span >66%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { api } from '@/utils/api'
import { ref, shallowRef } from 'vue'
import ChartNoData from '@/views/charts/charts/ChartNoData'
import chartMixin from '@/views/charts2/chart-mixin'
import ChartError from '@/components/common/Error'
import unitConvert from '@/utils/unit-convert'
import { unitTypes } from '@/utils/constants'
import { overwriteUrl, urlParamsHandler } from '@/utils/tools'
import axios from 'axios'
import { useRoute } from 'vue-router'
import { getNowTime, getSecond } from '@/utils/date-util'
export default {
name: 'EntityDetailSubscriberKpi',
components: {
ChartError,
ChartNoData
},
mixins: [chartMixin],
setup () {
const { query } = useRoute()
const queryCondition = ref(query.queryCondition || '')
// 获取url携带的range、startTime、endTime
const rangeParam = query.kpiRange
const startTimeParam = query.kpiStartTime
const endTimeParam = query.kpiEndTime
// 优先级url > config.js > 默认值。
const dateRangeValue = rangeParam ? parseInt(rangeParam) : (DEFAULT_TIME_FILTER_RANGE.entity.subscriberKpi || 60)
const timeFilter = ref({ dateRangeValue })
if (!startTimeParam || !endTimeParam) {
const { startTime, endTime } = getNowTime(dateRangeValue)
timeFilter.value.startTime = startTime
timeFilter.value.endTime = endTime
} else {
timeFilter.value.startTime = parseInt(startTimeParam)
timeFilter.value.endTime = parseInt(endTimeParam)
}
return {
queryCondition,
timeFilter
}
},
data () {
return {
kpiData: {
volume: 5300000000,
throughput: 600000,
latency: 21,
packetLoss: 0.0192
},
unitConvert,
unitTypes,
isNoData: false,
showError: false,
errorMsg: ''
}
},
watch: {
timeFilter: {
handler () {
this.init()
}
}
},
methods: {
init (val, show, active, n) {
const params = {
resource: this.entity.entityName,
startTime: getSecond(this.timeFilter.startTime),
endTime: getSecond(this.timeFilter.endTime)
}
if (this.queryCondition) {
// params.q = this.queryCondition
}
this.toggleLoading(true)
axios.get(`${api.entity.throughput}/${this.entity.entityType}`, { params: params }).then(response => {
const res = response.data
if (response.status === 200) {
this.isNoData = res.data.result.length === 0
this.showError = false
if (!this.isNoData) {
this.kpiData = res.data.result
}
} else {
this.httpError(res)
}
}).catch(e => {
console.error(e)
this.httpError(e)
}).finally(() => {
this.toggleLoading(false)
// 测试代码
// this.isNoData = false
})
},
httpError (e) {
this.isNoData = false
this.showError = true
this.errorMsg = this.errorMsgHandler(e)
},
reload (startTime, endTime, dateRangeValue) {
this.timeFilter = { startTime: getSecond(startTime), endTime: getSecond(endTime) }
const { query } = this.$route
const newUrl = urlParamsHandler(window.location.href, query, {
kpiStartTime: this.timeFilter.startTime,
kpiEndTime: this.timeFilter.endTime,
kpiRange: dateRangeValue.value
})
overwriteUrl(newUrl)
}
},
mounted () {
this.$nextTick(() => {
this.init()
})
},
beforeUnmount () {
this.unitConvert = null
}
}
</script>

View File

@@ -0,0 +1,290 @@
<template>
<div class="subscriber-top-app">
<div class="subscriber-top-app-header">
<div class="subscriber-top-app-title">{{$t('subscriber.topApp')}}</div>
<date-time-range
class="entity-detail-date-time-range"
:start-time="timeFilter.startTime"
:end-time="timeFilter.endTime"
:date-range="timeFilter.dateRangeValue"
ref="dateTimeRange"
@change="reload"
/>
</div>
<div class="subscriber-top-app-body">
<chart-no-data v-if="isNoData" test-id="noData"></chart-no-data>
<chart-error v-if="showError" :content="errorMsg" />
<div class="top-app-left" v-show="!isNoData && !showError">
<div class="app-data" v-for="(app, index) in topAppData">
<div class="app-index">{{++index}}</div>
<div class="app-name">{{app.name}}</div>
<div class="app-trend">
<i class="cn-icon cn-icon-egress"></i>
<div class="app-up">{{app.up}}</div>
</div>
<el-divider direction="vertical" class="top-app-divider"/>
<div class="app-trend">
<i class="cn-icon cn-icon-ingress"></i>
<div class="app-down">{{app.down}}</div>
</div>
</div>
</div>
<div class="top-app-right" >
<div class="chart-content" id="subscriberTopAppChart"></div>
</div>
</div>
</div>
</template>
<script>
import { api } from '@/utils/api'
import { ref, shallowRef } from 'vue'
import ChartNoData from '@/views/charts/charts/ChartNoData'
import chartMixin from '@/views/charts2/chart-mixin'
import ChartError from '@/components/common/Error'
import unitConvert from '@/utils/unit-convert'
import { unitTypes } from '@/utils/constants'
import { overwriteUrl, urlParamsHandler } from '@/utils/tools'
import axios from 'axios'
import { useRoute } from 'vue-router'
import { getNowTime, getSecond } from '@/utils/date-util'
import * as echarts from 'echarts'
import { entityDetailSubscriberTopApp } from '@/views/charts2/charts/options/echartOption'
export default {
name: 'EntityDetailSubscriberTopApp',
components: {
ChartError,
ChartNoData
},
mixins: [chartMixin],
setup () {
const { query } = useRoute()
const queryCondition = ref(query.queryCondition || '')
// 获取url携带的range、startTime、endTime
const rangeParam = query.topAppRange
const startTimeParam = query.topAppStartTime
const endTimeParam = query.topAppEndTime
// 优先级url > config.js > 默认值。
const dateRangeValue = rangeParam ? parseInt(rangeParam) : (DEFAULT_TIME_FILTER_RANGE.entity.subscriberTopApp || 60)
const timeFilter = ref({ dateRangeValue })
if (!startTimeParam || !endTimeParam) {
const { startTime, endTime } = getNowTime(dateRangeValue)
timeFilter.value.startTime = startTime
timeFilter.value.endTime = endTime
} else {
timeFilter.value.startTime = parseInt(startTimeParam)
timeFilter.value.endTime = parseInt(endTimeParam)
}
return {
queryCondition,
timeFilter,
myChart: shallowRef(null)
}
},
data () {
return {
topAppData: [
{
name: 'Wetchat',
up: '1.2GB',
down: '28MB'
},
{
name: 'QQ',
up: '1.2GB',
down: '28MB'
},
{
name: 'Douyin',
up: '1.2GB',
down: '28MB'
},
{
name: 'Weibo',
up: '1.2GB',
down: '28MB'
},
{
name: 'Tecent',
up: '1.2GB',
down: '28MB'
},
{
name: 'Kuaishou',
up: '1.2GB',
down: '28MB'
},
{
name: 'Taobao',
up: '1.2GB',
down: '28MB'
},
{
name: 'Jd',
up: '1.2GB',
down: '28MB'
},
{
name: 'Aiqiyi',
up: '1.2GB',
down: '28MB'
},
{
name: 'Baidu',
up: '1.2GB',
down: '28MB'
}
],
unitConvert,
unitTypes,
isNoData: false,
showError: false,
errorMsg: ''
}
},
watch: {
timeFilter: {
handler () {
this.init()
}
}
},
methods: {
init (val, show, active, n) {
const params = {
resource: this.entity.entityName,
startTime: getSecond(this.timeFilter.startTime),
endTime: getSecond(this.timeFilter.endTime)
}
if (this.queryCondition) {
// params.q = this.queryCondition
}
this.toggleLoading(true)
axios.get(`${api.entity.throughput}/${this.entity.entityType}`, { params: params }).then(response => {
const res = response.data
if (response.status === 200) {
this.isNoData = res.data.result.length === 0
this.showError = false
if (!this.isNoData) {
this.topAppData = res.data.result
this.initEchart()
}
} else {
this.httpError(res)
}
}).catch(e => {
console.error(e)
this.httpError(e)
}).finally(() => {
this.toggleLoading(false)
// 无数据时的测试代码
//this.isNoData = false
//this.initEchart()
})
},
initEchart (data) {
data = [
{
count: 0.307,
app: 1
},
{
count: 0.206,
app: 2
},
{
count: 0.09,
app: 3
},
{
count: 0.082,
app: 4
},
{
count: 0.06,
app: 5
},
{
count: 0.032,
app: 6
},
{
count: 0.024,
app: 7
},
{
count: 0.017,
app: 8
},
{
count: 0.009,
app: 9
},
{
count: 0.006,
app: 10
}
]
if (data && data.length > 0) {
const chartDom = document.getElementById('subscriberTopAppChart')
this.myChart = echarts.getInstanceByDom(chartDom)
if (this.myChart) {
echarts.dispose(this.myChart)
}
this.myChart = echarts.init(chartDom)
const chartOption = this.$_.cloneDeep(entityDetailSubscriberTopApp)
chartOption.series[0].data = data.map(d => {
return [d.count, d.app]
}).reverse()
this.myChart.setOption(chartOption)
}
},
resize () {
if (this.myChart) {
this.myChart.resize()
}
},
httpError (e) {
this.isNoData = false
this.showError = true
this.errorMsg = this.errorMsgHandler(e)
},
reload (startTime, endTime, dateRangeValue) {
this.timeFilter = { startTime: getSecond(startTime), endTime: getSecond(endTime) }
const { query } = this.$route
const newUrl = urlParamsHandler(window.location.href, query, {
topAppStartTime: this.timeFilter.startTime,
topAppEndTime: this.timeFilter.endTime,
topAppRange: dateRangeValue.value
})
overwriteUrl(newUrl)
}
},
mounted () {
this.myChart = null
this.$nextTick(() => {
this.init()
})
window.addEventListener('resize', this.resize)
},
beforeUnmount () {
window.removeEventListener('resize', this.resize)
if (this.myChart) {
echarts.dispose(this.myChart)
}
// 检测时发现该方法占用较大内存,且未被释放
this.unitConvert = null
}
}
</script>

View File

@@ -5,7 +5,8 @@ import {
chartColor5, chartColor5,
chartColor6, chartColor6,
chartColorForBehaviorPattern, chartColorForBehaviorPattern,
unitTypes unitTypes,
chartColorForSubscriberTopApp
} from '@/utils/constants' } from '@/utils/constants'
import unitConvert from '@/utils/unit-convert' import unitConvert from '@/utils/unit-convert'
import { axisFormatter } from '@/views/charts/charts/tools' import { axisFormatter } from '@/views/charts/charts/tools'
@@ -666,3 +667,58 @@ export const stackedBarChartOption = {
} }
] ]
} }
export const entityDetailSubscriberTopApp = {
xAxis: {
axisTick: { show: false },
axisLine: { show: false },
axisLabel: {
show: false
},
splitLine: {
show: false
}
},
grid: {
top: 0,
left: 15,
right: 20,
bottom: 0
},
yAxis: {
type: 'category',
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: {
color: '#E2E5EC'
}
},
axisLabel: {
show: false
},
splitLine: {
show: false
}
},
series: [{
barWidth: 14,
barMaxHeight: 20,
itemStyle: {
color: function (params) {
const colorList = chartColorForSubscriberTopApp
return colorList[params.dataIndex]
}
},
data: [],
type: 'bar',
label: {
show: true,
position: 'right',
valueAnimation: true,
formatter: function (param, index, callback) {
return `${unitConvert(param.value[0], unitTypes.percent, null, null, 1).join(' ')}`
}
}
}]
}