This commit is contained in:
2026-04-19 20:57:48 +08:00
parent d9a0470ecf
commit 7f209b5fef
3 changed files with 271 additions and 149 deletions

115
.kilo/package-lock.json generated Normal file
View File

@@ -0,0 +1,115 @@
{
"name": ".kilo",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@kilocode/plugin": "7.2.14"
}
},
"node_modules/@kilocode/plugin": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.14.tgz",
"integrity": "sha512-mS+WA9HZIBH2qQ9ARA+v0q4MdQTSdfOvKbe4AOSkjP+P5hVA70OM/UVM9DVcvmjSOxU+wuUxmOy+j/EQIrgFmw==",
"license": "MIT",
"dependencies": {
"@kilocode/sdk": "7.2.14",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
},
"node_modules/@kilocode/sdk": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.14.tgz",
"integrity": "sha512-Naz83lFrsbavuDp6UwxRuglOaSNvRBsZfcRNvb7RpWYAwbuJP0dBdhpXj6uO3ta5qxeQ2JzxKNC9Ffz+LCLLDg==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

115
.kilocode/package-lock.json generated Normal file
View File

@@ -0,0 +1,115 @@
{
"name": ".kilocode",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@kilocode/plugin": "7.2.14"
}
},
"node_modules/@kilocode/plugin": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/plugin/-/plugin-7.2.14.tgz",
"integrity": "sha512-mS+WA9HZIBH2qQ9ARA+v0q4MdQTSdfOvKbe4AOSkjP+P5hVA70OM/UVM9DVcvmjSOxU+wuUxmOy+j/EQIrgFmw==",
"license": "MIT",
"dependencies": {
"@kilocode/sdk": "7.2.14",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
},
"node_modules/@kilocode/sdk": {
"version": "7.2.14",
"resolved": "https://registry.npmjs.org/@kilocode/sdk/-/sdk-7.2.14.tgz",
"integrity": "sha512-Naz83lFrsbavuDp6UwxRuglOaSNvRBsZfcRNvb7RpWYAwbuJP0dBdhpXj6uO3ta5qxeQ2JzxKNC9Ffz+LCLLDg==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@@ -140,7 +140,7 @@
<a-tag :color="activeStorage.collect_on ? 'green' : 'gray'"> <a-tag :color="activeStorage.collect_on ? 'green' : 'gray'">
{{ activeStorage.collect_on ? '开启' : '关闭' }} {{ activeStorage.collect_on ? '开启' : '关闭' }}
</a-tag> </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>
<a-descriptions-item label="最近检查"> <a-descriptions-item label="最近检查">
{{ formatDateTime(activeStorage.last_check_time) }} {{ formatDateTime(activeStorage.last_check_time) }}
@@ -162,9 +162,7 @@
<template #title> <template #title>
<div class="card-header"> <div class="card-header">
<div class="card-title">存储指标趋势</div> <div class="card-title">存储指标趋势</div>
<div class="card-subtitle"> <div class="card-subtitle"> 24 小时 · ECharts 折线图原始样本按小时取 max 聚合展示</div>
24 小时 · ECharts 折线图原始样本按小时取 max 聚合展示
</div>
</div> </div>
</template> </template>
<template #extra> <template #extra>
@@ -184,58 +182,18 @@
</a-space> </a-space>
</template> </template>
<a-spin :loading="chartLoading" class="chart-spin chart-spin--fill"> <a-spin :loading="chartLoading" class="chart-spin chart-spin--fill">
<p v-if="chartHint" class="chart-hint text-muted">{{ chartHint }}</p> <div class="chart-content-wrapper">
<div class="chart-container chart-container--line"> <p v-if="chartHint" class="chart-hint text-muted">{{ chartHint }}</p>
<!-- Chart ECharts 封装单序列 type: 'line' 折线 --> <div class="chart-container chart-container--line">
<Chart v-if="hasChartSeries" :options="ioChartOptions" height="320px" /> <!-- Chart ECharts 封装单序列 type: 'line' 折线 -->
<a-empty v-else description="暂无趋势数据或指标未上报" /> <Chart v-if="hasChartSeries" :options="ioChartOptions" />
<a-empty v-else description="暂无趋势数据或指标未上报" />
</div>
</div> </div>
</a-spin> </a-spin>
</a-card> </a-card>
</a-col> </a-col>
</a-row> </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> </div>
</template> </template>
@@ -244,14 +202,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Message } from '@arco-design/web-vue' import { Message } from '@arco-design/web-vue'
import { useDebounceFn } from '@vueuse/core' import { useDebounceFn } from '@vueuse/core'
import { import { IconDashboard, IconStorage, IconCodeSquare, IconDriveFile, IconFolder, IconCheckCircleFill } from '@arco-design/web-vue/es/icon'
IconDashboard,
IconStorage,
IconCodeSquare,
IconDriveFile,
IconFolder,
IconCheckCircleFill,
} from '@arco-design/web-vue/es/icon'
import Chart from '@/components/chart/index.vue' import Chart from '@/components/chart/index.vue'
import { import {
fetchStorageList, fetchStorageList,
@@ -270,19 +221,6 @@ const trendSelectedMetric = ref('')
const trendMetricOptions = ref<{ label: string; value: string }[]>([]) const trendMetricOptions = ref<{ label: string; value: string }[]>([])
const metricsOptionsLoading = ref(false) 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 dropdownLoading = ref(false)
const summaryLoading = ref(false) const summaryLoading = ref(false)
const chartLoading = ref(false) const chartLoading = ref(false)
@@ -295,13 +233,9 @@ const monitorSummary = ref<StorageMonitorSummaryPayload | null>(null)
const chartSeriesPoints = ref<{ time: string; value: number }[]>([]) const chartSeriesPoints = ref<{ time: string; value: number }[]>([])
const filterType = ref<string | undefined>(undefined)
let pollTimer: ReturnType<typeof setInterval> | null = null let pollTimer: ReturnType<typeof setInterval> | null = null
const activeStorage = computed(() => const activeStorage = computed(() => storageList.value.find((s) => s.service_identity === selectedServiceIdentity.value))
storageList.value.find((s) => s.service_identity === selectedServiceIdentity.value),
)
const globalHint = computed(() => { const globalHint = computed(() => {
const m = monitorSummary.value const m = monitorSummary.value
@@ -317,11 +251,6 @@ const globalHint = computed(() => {
return `全量:${n} 台 · 在线 ${online} · 离线 ${offline}` 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) { function formatDateTime(v: string | undefined) {
if (!v) return '-' if (!v) return '-'
const d = dayjs(v) const d = dayjs(v)
@@ -349,7 +278,7 @@ const swapUsage = computed(() => currentHost.value?.swap_usage_percent ?? null)
const swapHint = computed(() => { const swapHint = computed(() => {
const h = currentHost.value 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 或未上报'
} }
return 'Swap 使用率' return 'Swap 使用率'
@@ -391,36 +320,6 @@ const controllerStatusColor = computed(() => {
return 'gray' 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(() => { const chartHint = computed(() => {
if (!selectedServiceIdentity.value) return '' if (!selectedServiceIdentity.value) return ''
if (!trendMetricOptions.value.length) { if (!trendMetricOptions.value.length) {
@@ -451,11 +350,15 @@ const ioChartOptions = computed(() => {
return { return {
tooltip: { trigger: 'axis' }, tooltip: { trigger: 'axis' },
legend: { show: true, data: [name] }, legend: { show: true, data: [name] },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '40px', containLabel: true },
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
data: labels, data: labels,
axisLabel: {
interval: 'auto',
fontSize: 11,
},
}, },
yAxis: { type: 'value', name: '指标值' }, yAxis: { type: 'value', name: '指标值' },
series: [ series: [
@@ -554,7 +457,6 @@ const onMonitorSelectSearch = useDebounceFn(async (inputValue: string) => {
}, 350) }, 350)
async function loadStorageList() { async function loadStorageList() {
tableLoading.value = true
try { try {
const res: any = await fetchStorageList({ page: 1, size: 100 }) const res: any = await fetchStorageList({ page: 1, size: 100 })
if (res.code !== 0) { if (res.code !== 0) {
@@ -567,10 +469,7 @@ async function loadStorageList() {
const list: StorageItem[] = page?.data ?? [] const list: StorageItem[] = page?.data ?? []
storageList.value = list storageList.value = list
syncOptionsFromList() syncOptionsFromList()
if ( if (selectedServiceIdentity.value && !list.some((s) => s.service_identity === selectedServiceIdentity.value)) {
selectedServiceIdentity.value &&
!list.some((s) => s.service_identity === selectedServiceIdentity.value)
) {
selectedServiceIdentity.value = undefined selectedServiceIdentity.value = undefined
} }
if (!selectedServiceIdentity.value && list.length > 0) { if (!selectedServiceIdentity.value && list.length > 0) {
@@ -580,8 +479,6 @@ async function loadStorageList() {
Message.error(e?.message || '加载存储列表失败') Message.error(e?.message || '加载存储列表失败')
storageList.value = [] storageList.value = []
storageOptions.value = [] storageOptions.value = []
} finally {
tableLoading.value = false
} }
} }
@@ -650,13 +547,9 @@ async function refreshMonitorSummary() {
} }
/** 将原始 metrics 按自然小时桶取 metric_value 最大值,供折线图横轴为整点 */ /** 将原始 metrics 按自然小时桶取 metric_value 最大值,供折线图横轴为整点 */
function bucketStorageMetricsHourlyMax( function bucketStorageMetricsHourlyMax(metrics: Array<{ timestamp: string; metric_value: number }>): { time: string; value: number }[] {
metrics: Array<{ timestamp: string; metric_value: number }>,
): { time: string; value: number }[] {
if (!metrics?.length) return [] if (!metrics?.length) return []
const sorted = [...metrics].sort( const sorted = [...metrics].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
)
const hourToMax = new Map<string, number>() const hourToMax = new Map<string, number>()
for (const p of sorted) { for (const p of sorted) {
const hourKey = dayjs(p.timestamp).startOf('hour').toISOString() const hourKey = dayjs(p.timestamp).startOf('hour').toISOString()
@@ -717,17 +610,6 @@ onUnmounted(() => {
pollTimer = null 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>
<script lang="ts"> <script lang="ts">
@@ -947,6 +829,16 @@ export default {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0; min-height: 0;
overflow: hidden;
}
.chart-content-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
position: relative;
} }
.chart-hint { .chart-hint {
@@ -960,23 +852,23 @@ export default {
.chart-container--line { .chart-container--line {
flex: 1; flex: 1;
min-height: 320px; min-height: 0;
display: flex; display: flex;
flex-direction: column; 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 { .collect-result {
word-break: break-all; word-break: break-all;
font-size: 12px; 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> </style>