Merge branch 'main' of https://git.apinb.com/ops/front
This commit is contained in:
115
.kilo/package-lock.json
generated
Normal file
115
.kilo/package-lock.json
generated
Normal 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
115
.kilocode/package-lock.json
generated
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,7 @@
|
|||||||
<span v-if="r.code" class="text-muted room-option-code">{{ r.code }}</span>
|
<span v-if="r.code" class="text-muted room-option-code">{{ r.code }}</span>
|
||||||
</a-option>
|
</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-button type="outline" :disabled="!selectedRoomId" :loading="loading" @click="reloadRoomData">
|
<a-button type="outline" :disabled="!selectedRoomId" :loading="loading" @click="reloadRoomData">刷新</a-button>
|
||||||
刷新
|
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="statsPartialHint" class="toolbar-meta text-muted">{{ statsPartialHint }}</div>
|
<div v-if="statsPartialHint" class="toolbar-meta text-muted">{{ statsPartialHint }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,60 +86,76 @@
|
|||||||
|
|
||||||
<!-- 第二行:设备卡片 -->
|
<!-- 第二行:设备卡片 -->
|
||||||
<div v-if="selectedRoomId && deviceCardsHint" class="device-cards-hint text-muted">{{ deviceCardsHint }}</div>
|
<div v-if="selectedRoomId && deviceCardsHint" class="device-cards-hint text-muted">{{ deviceCardsHint }}</div>
|
||||||
<a-row v-if="selectedRoomId" :gutter="16" class="device-cards-row">
|
<div v-if="selectedRoomId" class="device-cards-row">
|
||||||
<a-col v-for="d in devicesForCards" :key="d.id" :xs="24" :sm="12" :md="8" :lg="6">
|
<div v-for="d in devicesForCards" :key="d.id" class="device-card-wrapper">
|
||||||
<a-card class="device-card" :bordered="false">
|
<a-card class="device-card" :bordered="false" hoverable>
|
||||||
<div class="device-card-head">
|
<template #title>
|
||||||
<div class="device-card-title">{{ d.name }}</div>
|
<div class="device-card-header">
|
||||||
<a-tag :color="getStatusColor(d.status)" size="small">{{ formatStatusText(d.status) }}</a-tag>
|
<span class="device-card-title">{{ d.name }}</span>
|
||||||
</div>
|
<a-tag :color="getStatusColor(d.status)" size="small">{{ formatStatusText(d.status) }}</a-tag>
|
||||||
<div class="device-card-meta">
|
</div>
|
||||||
<span>{{ DEVICE_CATEGORY_MAP[d.device_category] || d.device_category }}</span>
|
</template>
|
||||||
<span class="text-muted"> · {{ d.type || '-' }}</span>
|
<div class="device-card-body">
|
||||||
</div>
|
<a-descriptions
|
||||||
<div class="device-card-meta text-muted">
|
:column="1"
|
||||||
<a-tag :color="d.collect_method === 'snmp' ? 'purple' : 'arcoblue'" size="small">
|
size="small"
|
||||||
{{ d.collect_method === 'snmp' ? 'SNMP' : 'API' }}
|
:value-style="{ width: '100%', color: 'var(--color-text-2)' }"
|
||||||
</a-tag>
|
:label-style="{ color: 'var(--color-text-3)', fontWeight: 500 }"
|
||||||
<span class="ml-8">{{ formatDateTime(d.last_check_time) }}</span>
|
>
|
||||||
</div>
|
<a-descriptions-item label="分类">{{ DEVICE_CATEGORY_MAP[d.device_category] || d.device_category }}</a-descriptions-item>
|
||||||
<div v-if="d.collect_last_result" class="device-card-result text-muted" :title="d.collect_last_result">
|
<a-descriptions-item label="型号">{{ d.type || '-' }}</a-descriptions-item>
|
||||||
{{ truncate(d.collect_last_result, 80) }}
|
<a-descriptions-item label="采集">
|
||||||
</div>
|
<a-tag :color="d.collect_method === 'snmp' ? 'purple' : 'arcoblue'" size="small">
|
||||||
<a-divider :margin="12" />
|
{{ d.collect_method === 'snmp' ? 'SNMP' : 'API' }}
|
||||||
<div class="device-metrics">
|
</a-tag>
|
||||||
<a-spin :loading="metricsLoading && isMetricsTarget(d)" :size="16">
|
</a-descriptions-item>
|
||||||
<template v-if="metricsForDevice(d).length">
|
<a-descriptions-item label="最近检查">{{ formatDateTime(d.last_check_time) }}</a-descriptions-item>
|
||||||
<div
|
</a-descriptions>
|
||||||
v-for="(m, idx) in metricsForDevice(d).slice(0, 6)"
|
<div v-if="d.collect_last_result" class="device-card-result" :title="d.collect_last_result">
|
||||||
:key="`${m.metric_name}-${idx}`"
|
<a-typography-text :ellipsis="{ rows: 2, showTooltip: true }" class="text-muted">
|
||||||
class="metric-line"
|
{{ d.collect_last_result }}
|
||||||
|
</a-typography-text>
|
||||||
|
</div>
|
||||||
|
<a-divider :margin="12" />
|
||||||
|
<div class="device-metrics">
|
||||||
|
<a-spin :loading="metricsLoading && isMetricsTarget(d)" :size="16">
|
||||||
|
<a-descriptions
|
||||||
|
bordered
|
||||||
|
:column="2"
|
||||||
|
size="small"
|
||||||
|
:label-style="{ color: 'var(--color-text-3)' }"
|
||||||
|
:value-style="{ color: 'var(--color-text-1)', fontWeight: 600, fontFamily: 'SF Mono, Menlo, Monaco, monospace' }"
|
||||||
>
|
>
|
||||||
<span class="metric-name">{{ m.metric_name }}</span>
|
<template v-if="metricsForDevice(d).length">
|
||||||
<span class="metric-val"
|
<a-descriptions-item
|
||||||
>{{ m.metric_value }}{{ m.metric_unit ? ` ${m.metric_unit}` : '' }}</span
|
v-for="(m, idx) in metricsForDevice(d)"
|
||||||
>
|
:key="`${m.metric_name}-${idx}`"
|
||||||
</div>
|
:label="m.metric_name"
|
||||||
</template>
|
:span="1"
|
||||||
<span v-else-if="!isMetricsTarget(d)" class="text-muted">指标见前 {{ METRICS_CARD_LIMIT }} 台</span>
|
>
|
||||||
<span v-else class="text-muted">暂无指标</span>
|
{{ m.metric_value }}
|
||||||
</a-spin>
|
<span v-if="m.metric_unit" class="metric-unit">({{ m.metric_unit }})</span>
|
||||||
|
</a-descriptions-item>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!isMetricsTarget(d)">
|
||||||
|
<a-descriptions-item :span="2" class="text-muted">指标见前 {{ METRICS_CARD_LIMIT }} 台</a-descriptions-item>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-descriptions-item :span="2" class="text-muted">暂无指标</a-descriptions-item>
|
||||||
|
</template>
|
||||||
|
</a-descriptions>
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</div>
|
||||||
</a-row>
|
</div>
|
||||||
<a-empty v-if="selectedRoomId && !devicesAll.length && !loading" description="该机房下暂无设备" />
|
<a-empty v-if="selectedRoomId && !devicesAll.length && !loading" description="该机房下暂无设备" />
|
||||||
|
|
||||||
<!-- 设备列表 -->
|
<!-- 设备列表 -->
|
||||||
<a-card v-if="selectedRoomId" class="table-card" title="设备列表" :bordered="false">
|
<a-card v-if="selectedRoomId" class="table-card" title="设备列表" :bordered="false">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-select
|
<a-select v-model="selectedCategory" placeholder="全部分类" allow-clear style="width: 160px" :options="DEVICE_CATEGORY_OPTIONS" />
|
||||||
v-model="selectedCategory"
|
|
||||||
placeholder="全部分类"
|
|
||||||
allow-clear
|
|
||||||
style="width: 160px"
|
|
||||||
:options="DEVICE_CATEGORY_OPTIONS"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<a-table
|
<a-table
|
||||||
:data="tableRowsPaged"
|
:data="tableRowsPaged"
|
||||||
@@ -176,16 +190,15 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template #collect_last_result="{ record }">
|
<template #collect_last_result="{ record }">
|
||||||
<a-typography-text :ellipsis="{ rows: 2, showTooltip: true }" style="max-width: 240px">
|
<a-typography-text :ellipsis="{ rows: 2, showTooltip: true }">
|
||||||
{{ record.collect_last_result || '-' }}
|
{{ record.collect_last_result || '-' }}
|
||||||
</a-typography-text>
|
</a-typography-text>
|
||||||
</template>
|
</template>
|
||||||
<template #response_time="{ record }">
|
<template #response_time="{ record }">
|
||||||
{{
|
{{ record.response_time != null && record.response_time !== undefined ? Number(record.response_time).toFixed(1) : '-' }}
|
||||||
record.response_time != null && record.response_time !== undefined
|
</template>
|
||||||
? Number(record.response_time).toFixed(1)
|
<template #last_check_time="{ record }">
|
||||||
: '-'
|
{{ formatDateTime(record.last_check_time) }}
|
||||||
}}
|
|
||||||
</template>
|
</template>
|
||||||
<template #continuous_errors="{ record }">
|
<template #continuous_errors="{ record }">
|
||||||
{{ record.continuous_errors != null ? record.continuous_errors : '-' }}
|
{{ record.continuous_errors != null ? record.continuous_errors : '-' }}
|
||||||
@@ -199,12 +212,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import {
|
import { IconBuildingCommunity, IconCircleCheck, IconActivity, IconAlertCircle } from '@tabler/icons-vue'
|
||||||
IconBuildingCommunity,
|
|
||||||
IconCircleCheck,
|
|
||||||
IconActivity,
|
|
||||||
IconAlertCircle,
|
|
||||||
} from '@tabler/icons-vue'
|
|
||||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||||
import { fetchRoomOptions, type RoomOptionItem } from '@/api/ops/room'
|
import { fetchRoomOptions, type RoomOptionItem } from '@/api/ops/room'
|
||||||
import {
|
import {
|
||||||
@@ -241,15 +249,15 @@ const tablePagination = ref({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const columns: TableColumnData[] = [
|
const columns: TableColumnData[] = [
|
||||||
{ title: '名称', dataIndex: 'name', width: 180, ellipsis: true, tooltip: true },
|
{ title: '名称', dataIndex: 'name', minWidth: 180, ellipsis: true, tooltip: true },
|
||||||
{ title: '设备分类', dataIndex: 'device_category', slotName: 'device_category', width: 100 },
|
{ title: '设备分类', dataIndex: 'device_category', slotName: 'device_category', minWidth: 100 },
|
||||||
{ title: '类型/型号', dataIndex: 'type', width: 120, ellipsis: true, tooltip: true },
|
{ title: '类型/型号', dataIndex: 'type', minWidth: 120, ellipsis: true, tooltip: true },
|
||||||
{ title: '采集方式', dataIndex: 'collect_method', slotName: 'collect_method', width: 96 },
|
{ title: '采集方式', dataIndex: 'collect_method', slotName: 'collect_method', minWidth: 96 },
|
||||||
{ title: '启用 / 周期采集', dataIndex: 'enabled', slotName: 'enabled_collect', width: 160 },
|
{ title: '启用 / 周期采集', dataIndex: 'enabled', slotName: 'enabled_collect', minWidth: 160 },
|
||||||
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100, align: 'center' },
|
{ title: '状态', dataIndex: 'status', slotName: 'status', minWidth: 100, align: 'center' },
|
||||||
{ title: '最近检查', dataIndex: 'last_check_time', width: 168, ellipsis: true, tooltip: true },
|
{ title: '最近检查', dataIndex: 'last_check_time', slotName: 'last_check_time', minWidth: 250, ellipsis: true, tooltip: true },
|
||||||
{ title: '响应(ms)', dataIndex: 'response_time', slotName: 'response_time', width: 88 },
|
{ title: '响应(ms)', dataIndex: 'response_time', slotName: 'response_time', minWidth: 88 },
|
||||||
{ title: '连续错误', dataIndex: 'continuous_errors', slotName: 'continuous_errors', width: 88 },
|
{ title: '连续错误', dataIndex: 'continuous_errors', slotName: 'continuous_errors', minWidth: 88 },
|
||||||
{ title: '采集摘要', dataIndex: 'collect_last_result', slotName: 'collect_last_result', minWidth: 200 },
|
{ title: '采集摘要', dataIndex: 'collect_last_result', slotName: 'collect_last_result', minWidth: 200 },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -263,9 +271,7 @@ const statsPartialHint = computed(() => {
|
|||||||
|
|
||||||
const summaryOnline = computed(() => devicesAll.value.filter((d) => d.status === 'online').length)
|
const summaryOnline = computed(() => devicesAll.value.filter((d) => d.status === 'online').length)
|
||||||
const summaryCollectOn = computed(() => devicesAll.value.filter((d) => d.enabled && d.collect_on).length)
|
const summaryCollectOn = computed(() => devicesAll.value.filter((d) => d.enabled && d.collect_on).length)
|
||||||
const summaryOfflineUnknown = computed(
|
const summaryOfflineUnknown = computed(() => devicesAll.value.filter((d) => d.status !== 'online').length)
|
||||||
() => devicesAll.value.filter((d) => d.status !== 'online').length
|
|
||||||
)
|
|
||||||
|
|
||||||
const devicesForCards = computed(() => devicesAll.value)
|
const devicesForCards = computed(() => devicesAll.value)
|
||||||
|
|
||||||
@@ -422,10 +428,7 @@ async function loadMetricsForFirstDevices() {
|
|||||||
batch.map(async (d) => {
|
batch.map(async (d) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetchLatestMetrics(d.service_identity)
|
const res = await fetchLatestMetrics(d.service_identity)
|
||||||
const metrics =
|
const metrics = res?.code === 0 && res.details?.metrics && Array.isArray(res.details.metrics) ? res.details.metrics : []
|
||||||
res?.code === 0 && res.details?.metrics && Array.isArray(res.details.metrics)
|
|
||||||
? res.details.metrics
|
|
||||||
: []
|
|
||||||
metricsByIdentity.value = { ...metricsByIdentity.value, [d.service_identity]: metrics }
|
metricsByIdentity.value = { ...metricsByIdentity.value, [d.service_identity]: metrics }
|
||||||
} catch {
|
} catch {
|
||||||
metricsByIdentity.value = { ...metricsByIdentity.value, [d.service_identity]: [] }
|
metricsByIdentity.value = { ...metricsByIdentity.value, [d.service_identity]: [] }
|
||||||
@@ -519,9 +522,36 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.device-cards-row {
|
.device-cards-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.device-card-wrapper {
|
||||||
|
flex: 1 1 calc(25% - 12px);
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.device-card-wrapper {
|
||||||
|
flex: 1 1 calc(33.33% - 11px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.device-card-wrapper {
|
||||||
|
flex: 1 1 calc(50% - 8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.device-card-wrapper {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.stats-card {
|
.stats-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@@ -592,71 +622,67 @@ export default {
|
|||||||
|
|
||||||
.device-card {
|
.device-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-bottom: 0;
|
border-radius: 12px;
|
||||||
|
background: var(--color-bg-2);
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.06),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
:deep(.arco-card-header) {
|
||||||
|
padding: 16px 16px 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border-1);
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.arco-card-body) {
|
:deep(.arco-card-body) {
|
||||||
padding: 14px;
|
padding: 14px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.08),
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.06);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-card-head {
|
.device-card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-card-title {
|
.device-card-title {
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
line-height: 1.4;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-card-meta {
|
.device-card-body {
|
||||||
font-size: 12px;
|
display: flex;
|
||||||
margin-bottom: 6px;
|
flex-direction: column;
|
||||||
}
|
gap: 10px;
|
||||||
|
|
||||||
.ml-8 {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-card-result {
|
.device-card-result {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.4;
|
line-height: 1.5;
|
||||||
word-break: break-all;
|
padding: 10px 12px;
|
||||||
|
background: var(--color-fill-1);
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 3px solid var(--color-border-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-metrics {
|
.device-metrics {
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-line {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-name {
|
|
||||||
color: var(--color-text-3);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 55%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metric-val {
|
|
||||||
color: var(--color-text-1);
|
|
||||||
font-weight: 500;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-card {
|
.table-card {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user