CN-668 Dashboard - npm - 下钻功能交互开发(未改问题:特殊IP报错,protocol和port的tab参数传递有问题,监视timeFilter)

This commit is contained in:
hyx
2022-08-24 07:29:40 +08:00
parent e3db8caf63
commit 1968baa056
8 changed files with 541 additions and 242 deletions

View File

@@ -12,6 +12,39 @@
line-height: 24px; line-height: 24px;
font-weight: 900; font-weight: 900;
color: #353636; color: #353636;
display:flex;
.score {
.circle-icon {
border-radius: 3px;
width: 6px;
height: 6px;
margin-left: 10px;
margin-right: 4px;
}
.data-score-red {
background: #E26154;
}
.data-score-yellow {
background: #E5A219;
}
.data-score-green {
background: #749F4D;
}
height:24px;
font-family: NotoSansHans-Medium;
font-size: 12px;
color: #353636;
font-weight: 500;
margin-left:8px;
padding-right:13px;
background: #F7F7F7;
border: 1px solid #E2E5EC;
box-shadow: 0 2px 4px 0 rgba(51,51,51,0.02);
border-radius: 4px;
display:flex;
align-items: center;
justify-content: center;
}
} }
.panel__time { .panel__time {
display: flex; display: flex;

View File

@@ -401,7 +401,7 @@ export default {
this.$store.commit('setBreadcrumbColumnValue', columnValue) this.$store.commit('setBreadcrumbColumnValue', columnValue)
this.$store.commit('setBreadcrumbColumnName', columnName) this.$store.commit('setBreadcrumbColumnName', columnName)
const tabObjGroup = networkOverviewTabList.filter(item => item.label == columnName) const tabObjGroup = networkOverviewTabList.filter(item => item.label == columnName)
let type = tabObjGroup&&tabObjGroup[0]?tabObjGroup[0].prop:'' const type = tabObjGroup && tabObjGroup[0] ? tabObjGroup[0].prop : ''
this.$store.commit('setDimensionType', type) this.$store.commit('setDimensionType', type)
this.$store.commit('setPanelName', columnValue) this.$store.commit('setPanelName', columnValue)
} else if (columnName) { // 点击的为列名 } else if (columnName) { // 点击的为列名

File diff suppressed because one or more lines are too long

View File

@@ -231,10 +231,6 @@ export default {
get(requestUrl, this.queryParams).then(response => { get(requestUrl, this.queryParams).then(response => {
if (response.code === 200) { if (response.code === 200) {
this.chartData = response.data.result this.chartData = response.data.result
} else {
if (this.chart.type === 601) {
this.$refs['tab' + this.chart.id].loading = false
}
} }
}) })
} }

View File

@@ -1,7 +1,14 @@
<template> <template>
<div class="panel-box"> <div class="panel-box">
<div class="panel__header"> <div class="panel__header">
<div class="panel__title">{{panelName?panelName:(panel.i18n ? $t(panel.i18n) : panel.name)}}</div> <div class="panel__title">{{panelName?panelName:(panel.i18n ? $t(panel.i18n) : panel.name)}}
<div v-if="score" class="score">
<div class="circle-icon" v-if="score <= 2" :class="{'data-score-red': score <= 2}" ></div>
<div class="circle-icon" v-else-if="score <= 4" :class="{'data-score-yellow': score <= 4}" ></div>
<div class="circle-icon" v-else-if="score <= 6" :class="{'data-score-green': score <= 6}" ></div>
Score:{{score}}
</div>
</div>
<div class="panel__time"> <div class="panel__time">
<date-time-range <date-time-range
class="date-time-range" class="date-time-range"
@@ -33,11 +40,13 @@
<script> <script>
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ref } from 'vue' import { ref } from 'vue'
import { panelTypeAndRouteMapping } from '@/utils/constants' import { panelTypeAndRouteMapping, bytesColumnNameGroupForNpm, scoreUrl, customTableTitlesForAppPerformance } from '@/utils/constants'
import { getPanelList, getChartList } from '@/utils/api' import { getPanelList, getChartList } from '@/utils/api'
import { getNowTime } from '@/utils/date-util' import { getNowTime, getSecond } from '@/utils/date-util'
import { getTypeCategory } from '@/views/charts/charts/tools' import { getTypeCategory } from '@/views/charts/charts/tools'
import { computeScore } from '@/utils/tools'
import ChartList from '@/views/charts2/ChartList' import ChartList from '@/views/charts2/ChartList'
import { get } from '@/utils/http'
export default { export default {
name: 'Panel', name: 'Panel',
@@ -53,11 +62,58 @@ export default {
chartList: [], // 普通panel的chart chartList: [], // 普通panel的chart
panelLock: true, panelLock: true,
extraParams: {}, extraParams: {},
panelName: '' panelName: '',
score: null
} }
}, },
async mounted () { async mounted () {
this.panelName = this.$store.getters.getPanelName this.panelName = this.$store.getters.getPanelName
if (this.panelName && this.$route.path === '/panel/networkAppPerformance') {
// let columnName = this.$store.getters.getBreadcrumbColumnName
const columnValue = this.$store.getters.getBreadcrumbColumnValue
const queryParams = {
startTime: getSecond(this.timeFilter.startTime),
endTime: getSecond(this.timeFilter.endTime),
type: this.$store.getters.getDimensionType,
params: columnValue || ''
}
const requestGroup = []
scoreUrl.forEach(url => {
if (url) {
const request = get(url, queryParams, true)
requestGroup.push(request)
}
})
const scoreGroup = []
let score = 0
Promise.all(requestGroup).then(res => {
res.forEach(t => {
if (t.code === 200) {
const data = t.data.result ? t.data.result[0] : null
if (data) {
customTableTitlesForAppPerformance.forEach(item => {
if (data[bytesColumnNameGroupForNpm[item.prop]]) {
score = computeScore(data, item.scoreType)
scoreGroup.push(score)
} else {
scoreGroup.push(0)
}
})
}
}
})
}).finally(() => {
scoreGroup.forEach(i => {
score = Number(score) + Number(i)
})
score = Math.ceil(score * 6)
if (score > 6) {
score = 6
}
this.score = score || 0
})
}
await this.init() await this.init()
const vm = this const vm = this
this.emitter.on('reloadChartList', async function () { this.emitter.on('reloadChartList', async function () {
@@ -74,9 +130,9 @@ export default {
const panel = ref({}) const panel = ref({})
let panelType = 1 // 取得panel的type let panelType = 1 // 取得panel的type
const { params } = useRoute() const { params } = useRoute()
let router = useRouter() const router = useRouter()
let thirdPanel = router.currentRoute.value.params.thirdPanel const thirdPanel = router.currentRoute.value.params.thirdPanel
let fourthPanel = router.currentRoute.value.params.fourthPanel const fourthPanel = router.currentRoute.value.params.fourthPanel
if (fourthPanel) { if (fourthPanel) {
panelType = Number(fourthPanel) panelType = Number(fourthPanel)
} else if (thirdPanel) { } else if (thirdPanel) {

View File

@@ -37,17 +37,21 @@
<template #default="scope" :column="item"> <template #default="scope" :column="item">
<template v-if="item.columnType === tableColumnType.chainRatio" > <template v-if="item.columnType === tableColumnType.chainRatio" >
<div class="data-total" > <div class="data-total" >
{{scope.row[item.prop] ? unitConvert(scope.row[item.prop], unitTypes.number).join(' ') : '-'}} {{scope.row[item.prop]?((scope.row[item.prop][0]||scope.row[item.prop][0]===0)? unitConvert(scope.row[item.prop][0], unitTypes.number).join(' ') : '-'):'' }}
<div v-if="scope.row['trend'] === 'up'" class="data-total-trend data-total-trend-red"> <template v-if="scope.row[item.prop]">
<i class="cn-icon-rise1 cn-icon"></i><span>{{scope.row['trendValue']}}</span> <div v-if="scope.row[item.prop][1] === 'up'" class="data-total-trend data-total-trend-red">
<i class="cn-icon-rise1 cn-icon"></i><span>{{scope.row[item.prop][2]}}</span>
</div> </div>
<div v-else-if="scope.row['trend'] === 'down'" class="data-total-trend data-total-trend-green"> <div v-else-if="scope.row[item.prop][1] === 'down'" class="data-total-trend data-total-trend-green">
<i class="cn-icon-decline cn-icon"></i><span>{{scope.row['trendValue']}}</span> <i class="cn-icon-decline cn-icon"></i><span>{{scope.row[item.prop][2]}}</span>
</div> </div>
<div v-else-if="scope.row['trend'] === 'noChange'" class="data-total-trend data-total-trend-black"> <div v-else-if="scope.row[item.prop][1] === 'noChange'" class="data-total-trend data-total-trend-black">
<i class="cn-icon-constant cn-icon"></i> <i class="cn-icon-constant cn-icon"></i>
</div> </div>
</template>
<template v-else>
-
</template>
</div> </div>
</template> </template>
<template v-else-if="item.columnType === tableColumnType.dillDown" > <template v-else-if="item.columnType === tableColumnType.dillDown" >
@@ -145,10 +149,11 @@
</template> </template>
<script> <script>
import { ref } from 'vue' import { ref } from 'vue'
import {operationType, unitTypes,networkTable,tableColumnType } from '@/utils/constants' import { operationType, unitTypes, networkTable, tableColumnType, cycle } from '@/utils/constants'
import { get } from '@/utils/http' import { get } from '@/utils/http'
import unitConvert from '@/utils/unit-convert' import unitConvert from '@/utils/unit-convert'
import { getChainRatio } from '@/utils/tools' import { getChainRatio, computeScore } from '@/utils/tools'
import { getSecond } from '@/utils/date-util'
import chartMixin from '@/views/charts2/chart-mixin' import chartMixin from '@/views/charts2/chart-mixin'
export default { export default {
@@ -182,16 +187,10 @@ export default {
showBackground: false, showBackground: false,
tableData: [], tableData: [],
showTabs: true, showTabs: true,
columnNameGroup: { columnNameGroup: {},
total: 'bytesTotal', cycleColumnNameGroup: {},
inbound: 'inboundBitsRate',
outbound: 'outboundBitsRate',
internal: 'internalBitsRate',
through: 'throughBitsRate'
},
metricUnit: 'bytes', metricUnit: 'bytes',
loading: false, loading: false,
drillDown: false,
tableColumnType: tableColumnType, tableColumnType: tableColumnType,
curTable: {}, // 当前的表格类型 curTable: {}, // 当前的表格类型
tableType: '', tableType: '',
@@ -231,6 +230,7 @@ export default {
const label = curTab.label const label = curTab.label
if (this.metric === 'Bits/s') { if (this.metric === 'Bits/s') {
this.columnNameGroup = this.curTable.bytesColumnNameGroup this.columnNameGroup = this.curTable.bytesColumnNameGroup
this.cycleColumnNameGroup = this.curTable.bytesCycleColumnNameGroup
this.orderBy = 'bytesTotal' this.orderBy = 'bytesTotal'
this.metricUnit = 'bytes' this.metricUnit = 'bytes'
const titleGroup = [] const titleGroup = []
@@ -253,9 +253,9 @@ export default {
}) })
} }
this.customTableTitles = titleGroup this.customTableTitles = titleGroup
} else if (this.metric === 'Packets/s') { } else if (this.metric === 'Packets/s') {
this.columnNameGroup = this.curTable.packetsColumnNameGroup this.columnNameGroup = this.curTable.packetsColumnNameGroup
this.cycleColumnNameGroup = this.curTable.packetsCycleColumnNameGroup
this.orderBy = 'packetsTotal' this.orderBy = 'packetsTotal'
this.metricUnit = 'packets' this.metricUnit = 'packets'
const titleGroup = [] const titleGroup = []
@@ -280,6 +280,7 @@ export default {
this.customTableTitles = titleGroup this.customTableTitles = titleGroup
} else if (this.metric === 'Sessions/s') { } else if (this.metric === 'Sessions/s') {
this.columnNameGroup = this.curTable.sessionsColumnNameGroup this.columnNameGroup = this.curTable.sessionsColumnNameGroup
this.cycleColumnNameGroup = this.curTable.sessionsCycleColumnNameGroup
this.orderBy = 'sessions' this.orderBy = 'sessions'
this.metricUnit = 'sessions' this.metricUnit = 'sessions'
let totalChecked = true let totalChecked = true
@@ -288,9 +289,10 @@ export default {
totalChecked = item.checked totalChecked = item.checked
} }
}) })
const totalColumn = this.customTableTitles.filter(item => item.prop === 'total')
this.customTableTitles = [ this.customTableTitles = [
{ label: label, prop: 'tab', checked: true, tabColumn: true, columnType: this.curTable.column[0].columnType }, { label: label, prop: 'tab', checked: true, tabColumn: true, columnType: this.curTable.column[0].columnType },
{ label: 'network.total', prop: 'total', checked: totalChecked, tabColumn: false,columnType:this.curTable.column[1].columnType} { label: 'network.total', prop: 'total', checked: totalChecked, tabColumn: false, columnType: this.curTable.column[1].columnType, cycleDataUrl: totalColumn ? totalColumn.cycleDataUrl : '', isInMainUrl: true }
] ]
} }
let queryParams = { let queryParams = {
@@ -367,7 +369,8 @@ export default {
this.$store.commit('setNetworkOverviewCurrentTab', curTab) this.$store.commit('setNetworkOverviewCurrentTab', curTab)
} }
this.showTabs = true this.showTabs = true
} else if (curOperationType === operationType.secondMenu) { // 点击的为第二级菜单、或者点击菜单进入、 } else if (curOperationType === operationType.secondMenu || curOperationType === operationType.mainMenu) { // 点击的为第二级菜单、或者点击菜单进入、
this.list = this.$_.cloneDeep(this.curTable.tabList)
if (curTab) { // tab切换 if (curTab) { // tab切换
this.activeTab = curTab.label this.activeTab = curTab.label
this.customTableTitles[0].label = curTab.label this.customTableTitles[0].label = curTab.label
@@ -428,20 +431,19 @@ export default {
} }
} }
this.tableData = []
const tabList = [] const tabList = []
const tableDataTmp = this.chartData.map(item => { const tableDataTmp = this.chartData.map(item => {
tabList.push(item[curTab.prop]) tabList.push(item[curTab.prop])
return { const otherData = { tab: item[curTab.prop] }
tab: item[curTab.prop], Object.keys(this.columnNameGroup).forEach(i => {
total: item[this.columnNameGroup.total], otherData[i] = item[this.columnNameGroup[i]]
inbound: item[this.columnNameGroup.inbound],
outbound: item[this.columnNameGroup.outbound],
internal: item[this.columnNameGroup.internal],
through: item[this.columnNameGroup.through]
}
}) })
const queryParams = {} return otherData
})
const queryParams = {
startTime: getSecond(this.timeFilter.startTime),
endTime: getSecond(this.timeFilter.endTime)
}
if (tabList.length > 0) { if (tabList.length > 0) {
const conditionStr = tabList.filter(item => item != '') const conditionStr = tabList.filter(item => item != '')
queryParams.params = conditionStr.toString() queryParams.params = conditionStr.toString()
@@ -454,7 +456,11 @@ export default {
if (condition) { if (condition) {
queryParams.q = condition queryParams.q = condition
} }
get(this.gerCycleUrl(), queryParams).then(response => {
const self = this
this.customTableTitles.forEach(tableColumn => {
if (tableColumn.columnType === tableColumnType.chainRatio && tableColumn.isInMainUrl) {
get(self.gerCycleUrl(), queryParams).then(response => {
if (response.code === 200) { if (response.code === 200) {
cycleTotalList = response.data.result cycleTotalList = response.data.result
tableDataTmp.forEach(item => { tableDataTmp.forEach(item => {
@@ -462,9 +468,11 @@ export default {
let trend = ''// 空无图标up向上的图标down向下的图标noChange横向箭头图标 let trend = ''// 空无图标up向上的图标down向下的图标noChange横向箭头图标
let trendPercent = '' let trendPercent = ''
if (cycle) { if (cycle) {
const totalDiff = item.total - cycle[this.metricUnit] const curVal = Number(item[tableColumn.prop])
if (cycle[this.metricUnit] && cycle[this.metricUnit]!=0) { const preVal = Number(cycle[self.cycleColumnNameGroup[tableColumn.prop]])
let chainRatio = getChainRatio(item.total, cycle[this.metricUnit]) if (preVal && preVal != 0) {
const totalDiff = curVal - preVal
const chainRatio = getChainRatio(curVal, preVal)
if (chainRatio === '-') { if (chainRatio === '-') {
trend = '' trend = ''
trendPercent = '' trendPercent = ''
@@ -486,28 +494,105 @@ export default {
} }
} }
} }
this.tableData.push({ item[tableColumn.prop] = [item[tableColumn.prop], trend, trendPercent]
tab: item.tab,
total: item.total,
trend: trend,
trendValue: trendPercent,
inbound: item.inbound,
outbound: item.outbound,
internal: item.internal,
through: item.through
}) })
})
this.toggleLoading(false)
} else {
this.tableData = tableDataTmp
this.toggleLoading(false)
} }
}).catch(e => { }).catch(e => {
this.toggleLoading(false) console.log(e)
}).finally(e => { }).finally(e => {
this.toggleLoading(false) this.tableData = tableDataTmp
// 查询需要单独查询的,且需要展示环比图标,列的当前周期的数据
this.customTableTitles.forEach(tableColumn => {
if (tableColumn.columnType === tableColumnType.chainRatio && !tableColumn.isInMainUrl) {
get(self.gerColumnUrl(tableColumn), queryParams).then(response => {
if (response.code === 200) {
const columnList = response.data.result
self.tableData.forEach((item, i) => {
const data = columnList.find(i => i[curTab.prop] === item.tab)
if (data) {
item[tableColumn.prop] = data[self.columnNameGroup[tableColumn.prop]]
let score = 0
if (data) {
score = computeScore(data, tableColumn.scoreType)
}
item.scoreGroup = item.scoreGroup ? [...item.scoreGroup, score] : [score]
if (item && item.scoreGroup && item.scoreGroup.length >= 5) {
// self.tableData.forEach(data => {
let score = 0
item.scoreGroup.forEach(i => {
score = Number(score) + Number(i)
}) })
score = Math.ceil(score * 6)
if (score > 6) {
score = 6
}
item.score = score || 0
}
}
})
}
}).catch(e => {
console.log(e)
self.toggleLoading(false)
}).finally(e => {
// 查询需要单独查询的,且需要展示环比图标,列的前一周期的数据
if (tableColumn.cycle) {
const queryCycleParams = {
...queryParams,
cycle: tableColumn.cycle
}
get(self.gerColumnUrl(tableColumn), queryCycleParams).then(response => {
if (response.code === 200) {
cycleTotalList = response.data.result
self.tableData.forEach(item => {
const cycle = cycleTotalList.find(i => i[curTab.prop] === item.tab)
let trend = ''// 空无图标up向上的图标down向下的图标noChange横向箭头图标
let trendPercent = ''
if (cycle) {
const curVal = Number(item[tableColumn.prop])
// let preVal = Number(cycle[self.metricUnit])
const preVal = Number(cycle[self.columnNameGroup[tableColumn.prop]])
if (preVal && preVal != 0) {
const totalDiff = curVal - preVal
const chainRatio = getChainRatio(curVal, preVal)
if (chainRatio === '-') {
trend = ''
trendPercent = ''
} else {
trendPercent = Math.round(Math.abs(chainRatio) * 100)
if (totalDiff > 0) {
trend = 'up'
trendPercent = trendPercent + '%'
} else if (totalDiff < 0) {
trend = 'down'
trendPercent = trendPercent + '%'
} else {
trend = 'noChange'// 横向图标
}
if (trendPercent === '0%') {
trend = 'noChange'
trendPercent = ''
}
}
}
}
item[tableColumn.prop] = [item[tableColumn.prop] ? item[tableColumn.prop] : '', trend, trendPercent]
})
}
}).catch(e => {
console.log(e)
})
}
self.toggleLoading(false)
})
}
})
self.toggleLoading(false)
})
}
})
self.toggleLoading(false)
}, },
tabChange (index) { tabChange (index) {
const beforeType = this.$store.getters.getTabOperationBeforeType const beforeType = this.$store.getters.getTabOperationBeforeType
@@ -598,7 +683,6 @@ export default {
}) })
} }
}) })
console.log(this.$store.getters.getDimensionType)
let toPanel = null let toPanel = null
this.list.forEach((item, index) => { this.list.forEach((item, index) => {
if (item.label === columnName) { if (item.label === columnName) {
@@ -717,6 +801,22 @@ export default {
return this.networkSearchUrl.curUrl return this.networkSearchUrl.curUrl
} }
}, },
gerCycleUrl () {
const condition = this.$store.getters.getQueryCondition
if (condition) {
return this.networkSearchUrl.drilldownCycleUrl
} else {
return this.networkSearchUrl.cycleUrl
}
},
gerColumnUrl (tableColumn) {
const condition = this.$store.getters.getQueryCondition
if (condition) {
return tableColumn.dillDownCycleDataUrl
} else {
return tableColumn.cycleDataUrl
}
},
handleQueryParams (extraParams) { handleQueryParams (extraParams) {
let queryType = '' let queryType = ''
const name = this.$store.getters.getBreadcrumbColumnName const name = this.$store.getters.getBreadcrumbColumnName
@@ -768,9 +868,10 @@ export default {
this.customTableTitles[0].label = curTab.label this.customTableTitles[0].label = curTab.label
this.$store.commit('setNetworkOverviewCurrentTab', curTab) this.$store.commit('setNetworkOverviewCurrentTab', curTab)
} }
} else if (curOperationType === operationType.secondMenu) { // 点击第二级菜单,点击菜单 } else if (curOperationType === operationType.secondMenu || curOperationType === operationType.mainMenu) { // 点击第二级菜单,点击菜单
const list = this.$_.cloneDeep(this.networkTabList) this.list = this.$_.cloneDeep(this.curTable.tabList)
const tabObjGroup = list.filter(item => item.checked) this.$store.commit('setNetworkOverviewTabList', this.list)
const tabObjGroup = this.list.filter(item => item.checked)
if (tabObjGroup && tabObjGroup.length > 0) { if (tabObjGroup && tabObjGroup.length > 0) {
const curTab = tabObjGroup[0] const curTab = tabObjGroup[0]
queryType = curTab.prop queryType = curTab.prop
@@ -809,14 +910,6 @@ export default {
} }
return extraParams return extraParams
}, },
gerCycleUrl () {
const condition = this.$store.getters.getQueryCondition
if (condition) {
return this.networkSearchUrl.drilldownCycleUrl
} else {
return this.networkSearchUrl.cycleUrl
}
},
handleCustomizeClick (tab) { handleCustomizeClick (tab) {
this.activeCustomize = tab.paneName this.activeCustomize = tab.paneName
}, },
@@ -848,12 +941,9 @@ export default {
} }
}, },
mounted () { mounted () {
if (this.chart.params && this.chart.params.drillDown) {
this.drillDown = this.chart.params.drillDown
}
// 当前表格相关数据初始化 // 当前表格相关数据初始化
this.tableType = this.chart.params ? this.chart.params.name : 'networkOverview' this.tableType = this.chart.params ? this.chart.params.name : 'networkOverview'
this.curTable = networkTable[this.tableType]?networkTable[this.tableType]:networkTable[0] this.curTable = networkTable[this.tableType] ? networkTable[this.tableType] : networkTable.networkOverview
this.hasMetricSearch = this.curTable.hasMetricSearch this.hasMetricSearch = this.curTable.hasMetricSearch
this.customTableTitles = this.$_.cloneDeep(this.curTable.column) this.customTableTitles = this.$_.cloneDeep(this.curTable.column)
this.list = this.$_.cloneDeep(this.curTable.tabList) this.list = this.$_.cloneDeep(this.curTable.tabList)
@@ -861,6 +951,8 @@ export default {
this.activeTab = this.networkTabList[0].label this.activeTab = this.networkTabList[0].label
this.activeCustomize = ref('tabs') this.activeCustomize = ref('tabs')
this.networkSearchUrl = this.curTable.url this.networkSearchUrl = this.curTable.url
this.columnNameGroup = this.curTable.bytesColumnNameGroup
this.cycleColumnNameGroup = this.curTable.bytesCycleColumnNameGroup
if (this.$store.getters.getNetworkOverviewTabList.length > 0) { if (this.$store.getters.getNetworkOverviewTabList.length > 0) {
this.list = this.$store.getters.getNetworkOverviewTabList this.list = this.$store.getters.getNetworkOverviewTabList
@@ -889,7 +981,7 @@ export default {
} }
}, },
setup (props) {}, setup (props) {},
destroyed () { unmounted () {
this.$store.commit('setNetworkOverviewTabList', this.list)// 保存状态 this.$store.commit('setNetworkOverviewTabList', this.list)// 保存状态
} }
} }