feat
This commit is contained in:
@@ -140,7 +140,7 @@
|
||||
<a-tag :color="activeStorage.collect_on ? 'green' : 'gray'">
|
||||
{{ activeStorage.collect_on ? '开启' : '关闭' }}
|
||||
</a-tag>
|
||||
<span class="text-muted"> · {{ activeStorage.collect_interval || '-' }}s</span>
|
||||
<span class="text-muted">· {{ activeStorage.collect_interval || '-' }}s</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="最近检查">
|
||||
{{ formatDateTime(activeStorage.last_check_time) }}
|
||||
@@ -162,9 +162,7 @@
|
||||
<template #title>
|
||||
<div class="card-header">
|
||||
<div class="card-title">存储指标趋势</div>
|
||||
<div class="card-subtitle">
|
||||
近 24 小时 · ECharts 折线图(原始样本按小时取 max 聚合展示)
|
||||
</div>
|
||||
<div class="card-subtitle">近 24 小时 · ECharts 折线图(原始样本按小时取 max 聚合展示)</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
@@ -184,58 +182,18 @@
|
||||
</a-space>
|
||||
</template>
|
||||
<a-spin :loading="chartLoading" class="chart-spin chart-spin--fill">
|
||||
<p v-if="chartHint" class="chart-hint text-muted">{{ chartHint }}</p>
|
||||
<div class="chart-container chart-container--line">
|
||||
<!-- Chart 为 ECharts 封装;单序列 type: 'line' 折线 -->
|
||||
<Chart v-if="hasChartSeries" :options="ioChartOptions" height="320px" />
|
||||
<a-empty v-else description="暂无趋势数据或指标未上报" />
|
||||
<div class="chart-content-wrapper">
|
||||
<p v-if="chartHint" class="chart-hint text-muted">{{ chartHint }}</p>
|
||||
<div class="chart-container chart-container--line">
|
||||
<!-- Chart 为 ECharts 封装;单序列 type: 'line' 折线 -->
|
||||
<Chart v-if="hasChartSeries" :options="ioChartOptions" />
|
||||
<a-empty v-else description="暂无趋势数据或指标未上报" />
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-card class="table-card" title="存储设备列表" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<span class="text-muted">共 {{ monitorSummary?.total_devices ?? storageList.length }} 台</span>
|
||||
<a-select
|
||||
v-model="filterType"
|
||||
style="width: 160px"
|
||||
placeholder="全部类型"
|
||||
allow-clear
|
||||
:options="typeFilterOptions"
|
||||
/>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data="filteredDevicesForTable"
|
||||
:loading="tableLoading"
|
||||
:pagination="false"
|
||||
:row-class="rowClassFn"
|
||||
row-key="service_identity"
|
||||
@row-click="onTableRowClick"
|
||||
>
|
||||
<template #status="{ record }">
|
||||
<a-tag v-if="record.status === 'online'" color="green">在线</a-tag>
|
||||
<a-tag v-else-if="record.status === 'error'" color="orange">异常</a-tag>
|
||||
<a-tag v-else-if="record.status === 'offline'" color="red">离线</a-tag>
|
||||
<a-tag v-else color="gray">未知</a-tag>
|
||||
</template>
|
||||
<template #last_check="{ record }">
|
||||
{{ formatDateTime(record.last_check_time) }}
|
||||
</template>
|
||||
<template #latency_col="{ record }">
|
||||
{{ record.response_time ? `${record.response_time.toFixed(1)} ms` : '-' }}
|
||||
</template>
|
||||
<template #collect_on="{ record }">
|
||||
<a-tag :color="record.collect_on ? 'green' : 'gray'">
|
||||
{{ record.collect_on ? '开启' : '关闭' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -244,14 +202,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import {
|
||||
IconDashboard,
|
||||
IconStorage,
|
||||
IconCodeSquare,
|
||||
IconDriveFile,
|
||||
IconFolder,
|
||||
IconCheckCircleFill,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import { IconDashboard, IconStorage, IconCodeSquare, IconDriveFile, IconFolder, IconCheckCircleFill } from '@arco-design/web-vue/es/icon'
|
||||
import Chart from '@/components/chart/index.vue'
|
||||
import {
|
||||
fetchStorageList,
|
||||
@@ -270,19 +221,6 @@ const trendSelectedMetric = ref('')
|
||||
const trendMetricOptions = ref<{ label: string; value: string }[]>([])
|
||||
const metricsOptionsLoading = ref(false)
|
||||
|
||||
interface TableDevice {
|
||||
id: number
|
||||
service_identity: string
|
||||
name: string
|
||||
type: string
|
||||
model: string
|
||||
status: string
|
||||
collect_on: boolean
|
||||
last_check_time: string
|
||||
response_time: number
|
||||
}
|
||||
|
||||
const tableLoading = ref(false)
|
||||
const dropdownLoading = ref(false)
|
||||
const summaryLoading = ref(false)
|
||||
const chartLoading = ref(false)
|
||||
@@ -295,13 +233,9 @@ const monitorSummary = ref<StorageMonitorSummaryPayload | null>(null)
|
||||
|
||||
const chartSeriesPoints = ref<{ time: string; value: number }[]>([])
|
||||
|
||||
const filterType = ref<string | undefined>(undefined)
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const activeStorage = computed(() =>
|
||||
storageList.value.find((s) => s.service_identity === selectedServiceIdentity.value),
|
||||
)
|
||||
const activeStorage = computed(() => storageList.value.find((s) => s.service_identity === selectedServiceIdentity.value))
|
||||
|
||||
const globalHint = computed(() => {
|
||||
const m = monitorSummary.value
|
||||
@@ -317,11 +251,6 @@ const globalHint = computed(() => {
|
||||
return `全量:${n} 台 · 在线 ${online} · 离线 ${offline}`
|
||||
})
|
||||
|
||||
const typeFilterOptions = computed(() => {
|
||||
const set = new Set(storageList.value.map((s) => s.type).filter(Boolean))
|
||||
return [...set].map((t) => ({ label: t, value: t }))
|
||||
})
|
||||
|
||||
function formatDateTime(v: string | undefined) {
|
||||
if (!v) return '-'
|
||||
const d = dayjs(v)
|
||||
@@ -349,7 +278,7 @@ const swapUsage = computed(() => currentHost.value?.swap_usage_percent ?? null)
|
||||
|
||||
const swapHint = computed(() => {
|
||||
const h = currentHost.value
|
||||
if (!h || (h.swap_usage_percent === null || h.swap_usage_percent === undefined)) {
|
||||
if (!h || h.swap_usage_percent === null || h.swap_usage_percent === undefined) {
|
||||
return '无 Swap 或未上报'
|
||||
}
|
||||
return 'Swap 使用率'
|
||||
@@ -391,36 +320,6 @@ const controllerStatusColor = computed(() => {
|
||||
return 'gray'
|
||||
})
|
||||
|
||||
function toTableRow(item: StorageItem): TableDevice {
|
||||
return {
|
||||
id: item.id,
|
||||
service_identity: item.service_identity,
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
model: item.description || '-',
|
||||
status: item.status,
|
||||
collect_on: item.collect_on,
|
||||
last_check_time: item.last_check_time,
|
||||
response_time: item.response_time,
|
||||
}
|
||||
}
|
||||
|
||||
const filteredDevicesForTable = computed<TableDevice[]>(() =>
|
||||
storageList.value
|
||||
.filter((item) => !filterType.value || item.type === filterType.value)
|
||||
.map(toTableRow),
|
||||
)
|
||||
|
||||
function rowClassFn(record: TableDevice) {
|
||||
return record.service_identity === selectedServiceIdentity.value ? 'storage-table-row--active' : ''
|
||||
}
|
||||
|
||||
function onTableRowClick(record: TableDevice) {
|
||||
if (record?.service_identity) {
|
||||
selectedServiceIdentity.value = record.service_identity
|
||||
}
|
||||
}
|
||||
|
||||
const chartHint = computed(() => {
|
||||
if (!selectedServiceIdentity.value) return ''
|
||||
if (!trendMetricOptions.value.length) {
|
||||
@@ -451,11 +350,15 @@ const ioChartOptions = computed(() => {
|
||||
return {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { show: true, data: [name] },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
grid: { left: '3%', right: '4%', bottom: '40px', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: labels,
|
||||
axisLabel: {
|
||||
interval: 'auto',
|
||||
fontSize: 11,
|
||||
},
|
||||
},
|
||||
yAxis: { type: 'value', name: '指标值' },
|
||||
series: [
|
||||
@@ -554,7 +457,6 @@ const onMonitorSelectSearch = useDebounceFn(async (inputValue: string) => {
|
||||
}, 350)
|
||||
|
||||
async function loadStorageList() {
|
||||
tableLoading.value = true
|
||||
try {
|
||||
const res: any = await fetchStorageList({ page: 1, size: 100 })
|
||||
if (res.code !== 0) {
|
||||
@@ -567,10 +469,7 @@ async function loadStorageList() {
|
||||
const list: StorageItem[] = page?.data ?? []
|
||||
storageList.value = list
|
||||
syncOptionsFromList()
|
||||
if (
|
||||
selectedServiceIdentity.value &&
|
||||
!list.some((s) => s.service_identity === selectedServiceIdentity.value)
|
||||
) {
|
||||
if (selectedServiceIdentity.value && !list.some((s) => s.service_identity === selectedServiceIdentity.value)) {
|
||||
selectedServiceIdentity.value = undefined
|
||||
}
|
||||
if (!selectedServiceIdentity.value && list.length > 0) {
|
||||
@@ -580,8 +479,6 @@ async function loadStorageList() {
|
||||
Message.error(e?.message || '加载存储列表失败')
|
||||
storageList.value = []
|
||||
storageOptions.value = []
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,13 +547,9 @@ async function refreshMonitorSummary() {
|
||||
}
|
||||
|
||||
/** 将原始 metrics 按自然小时桶取 metric_value 最大值,供折线图横轴为整点 */
|
||||
function bucketStorageMetricsHourlyMax(
|
||||
metrics: Array<{ timestamp: string; metric_value: number }>,
|
||||
): { time: string; value: number }[] {
|
||||
function bucketStorageMetricsHourlyMax(metrics: Array<{ timestamp: string; metric_value: number }>): { time: string; value: number }[] {
|
||||
if (!metrics?.length) return []
|
||||
const sorted = [...metrics].sort(
|
||||
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
|
||||
)
|
||||
const sorted = [...metrics].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
|
||||
const hourToMax = new Map<string, number>()
|
||||
for (const p of sorted) {
|
||||
const hourKey = dayjs(p.timestamp).startOf('hour').toISOString()
|
||||
@@ -717,17 +610,6 @@ onUnmounted(() => {
|
||||
pollTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '存储名称', dataIndex: 'name', width: 160 },
|
||||
{ title: '类型', dataIndex: 'type', width: 100 },
|
||||
{ title: '标识', dataIndex: 'service_identity', width: 180, ellipsis: true, tooltip: true },
|
||||
{ title: '型号/描述', dataIndex: 'model', width: 160, ellipsis: true, tooltip: true },
|
||||
{ title: '状态', slotName: 'status', width: 90 },
|
||||
{ title: '周期采集', slotName: 'collect_on', width: 100 },
|
||||
{ title: '最近检查', slotName: 'last_check', width: 160 },
|
||||
{ title: '响应耗时', slotName: 'latency_col', width: 100 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -947,6 +829,16 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-content-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-hint {
|
||||
@@ -960,23 +852,23 @@ export default {
|
||||
|
||||
.chart-container--line {
|
||||
flex: 1;
|
||||
min-height: 320px;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-container--line :deep(.chart-container-inner) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chart-container--line :deep(.vue-echarts) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.collect-result {
|
||||
word-break: break-all;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
:deep(.arco-card-header) {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.storage-table-row--active) {
|
||||
background-color: rgba(22, 93, 255, 0.06);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user