|
|
@@ -0,0 +1,219 @@
|
|
|
+<template>
|
|
|
+ <div class="app-container">
|
|
|
+ <el-card>
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>服务器资源监控</span>
|
|
|
+ <div class="actions">
|
|
|
+ <el-button size="small" @click="refresh" :loading="loading">刷新</el-button>
|
|
|
+ <el-switch
|
|
|
+ v-model="autoRefresh"
|
|
|
+ active-text="自动刷新"
|
|
|
+ inactive-text="手动刷新"
|
|
|
+ />
|
|
|
+ <span v-if="lastUpdate" class="last-update">上次更新时间:{{ lastUpdate }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-row :gutter="20">
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card shadow="never" class="stat-card">
|
|
|
+ <template #header>CPU 使用率</template>
|
|
|
+ <div class="stat-body">
|
|
|
+ <el-progress type="dashboard" :percentage="status?.cpu.percent || 0" />
|
|
|
+ <div class="stat-text">核心数:{{ status?.cpu.count ?? '-' }}</div>
|
|
|
+ <div class="stat-text" v-if="status?.cpu.load">
|
|
|
+ Load(1/5/15):{{ status.cpu.load.load1.toFixed(2) }} /
|
|
|
+ {{ status.cpu.load.load5.toFixed(2) }} /
|
|
|
+ {{ status.cpu.load.load15.toFixed(2) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card shadow="never" class="stat-card">
|
|
|
+ <template #header>内存使用情况</template>
|
|
|
+ <div class="stat-body">
|
|
|
+ <el-progress type="circle" :percentage="status?.memory.percent || 0" />
|
|
|
+ <div class="stat-text">总内存:{{ formatBytes(status?.memory.total) }}</div>
|
|
|
+ <div class="stat-text">已使用:{{ formatBytes(status?.memory.used) }}</div>
|
|
|
+ <div class="stat-text">可用:{{ formatBytes(status?.memory.available) }}</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+
|
|
|
+ <el-col :span="8">
|
|
|
+ <el-card shadow="never" class="stat-card">
|
|
|
+ <template #header>系统信息</template>
|
|
|
+ <div class="stat-body">
|
|
|
+ <div class="stat-text">主机名:{{ status?.system.hostname || '-' }}</div>
|
|
|
+ <div class="stat-text">
|
|
|
+ 系统:{{ status?.system.system }} {{ status?.system.release }}
|
|
|
+ </div>
|
|
|
+ <div class="stat-text">版本:{{ status?.system.version }}</div>
|
|
|
+ <div class="stat-text">机器类型:{{ status?.system.machine }}</div>
|
|
|
+ <div class="stat-text">启动时间:{{ formatTimestamp(status?.system.boot_time) }}</div>
|
|
|
+ <div class="stat-text">运行时长:{{ formatDuration(status?.system.uptime_seconds) }}</div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <el-card shadow="never" class="disk-card" style="margin-top: 20px">
|
|
|
+ <template #header>磁盘使用情况</template>
|
|
|
+ <el-table :data="status?.disks || []" v-loading="loading" style="width: 100%">
|
|
|
+ <el-table-column prop="mountpoint" label="挂载点" width="150" />
|
|
|
+ <el-table-column prop="fstype" label="文件系统" width="120" />
|
|
|
+ <el-table-column label="总空间" width="160">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ formatBytes(scope.row.total) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="已使用" width="160">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ formatBytes(scope.row.used) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="可用空间" width="160">
|
|
|
+ <template #default="scope">
|
|
|
+ {{ formatBytes(scope.row.free) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="使用率">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-progress :percentage="scope.row.percent" :stroke-width="14" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </el-card>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, onMounted, onUnmounted, watch } from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import { getSystemStatus, type SystemStatus } from '../../../api/system'
|
|
|
+
|
|
|
+const status = ref<SystemStatus | null>(null)
|
|
|
+const loading = ref(false)
|
|
|
+const autoRefresh = ref(true)
|
|
|
+const lastUpdate = ref<string | null>(null)
|
|
|
+let timer: number | null = null
|
|
|
+
|
|
|
+const fetchStatus = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getSystemStatus()
|
|
|
+ status.value = res.data
|
|
|
+ lastUpdate.value = new Date().toLocaleString()
|
|
|
+ } catch (e) {
|
|
|
+ ElMessage.error('获取系统状态失败')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const refresh = () => {
|
|
|
+ fetchStatus()
|
|
|
+}
|
|
|
+
|
|
|
+const startAutoRefresh = () => {
|
|
|
+ stopAutoRefresh()
|
|
|
+ if (autoRefresh.value) {
|
|
|
+ timer = window.setInterval(fetchStatus, 5000)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const stopAutoRefresh = () => {
|
|
|
+ if (timer !== null) {
|
|
|
+ clearInterval(timer)
|
|
|
+ timer = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchStatus()
|
|
|
+ startAutoRefresh()
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ stopAutoRefresh()
|
|
|
+})
|
|
|
+
|
|
|
+watch(autoRefresh, () => {
|
|
|
+ startAutoRefresh()
|
|
|
+})
|
|
|
+
|
|
|
+const formatBytes = (value?: number) => {
|
|
|
+ if (value === undefined || value === null) return '-'
|
|
|
+ if (value === 0) return '0 B'
|
|
|
+ const k = 1024
|
|
|
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
|
+ const i = Math.floor(Math.log(value) / Math.log(k))
|
|
|
+ return `${(value / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`
|
|
|
+}
|
|
|
+
|
|
|
+const formatTimestamp = (ts?: number) => {
|
|
|
+ if (!ts) return '-'
|
|
|
+ return new Date(ts * 1000).toLocaleString()
|
|
|
+}
|
|
|
+
|
|
|
+const formatDuration = (sec?: number) => {
|
|
|
+ if (sec === undefined || sec === null) return '-'
|
|
|
+ const s = sec
|
|
|
+ const days = Math.floor(s / 86400)
|
|
|
+ const hours = Math.floor((s % 86400) / 3600)
|
|
|
+ const minutes = Math.floor((s % 3600) / 60)
|
|
|
+ const seconds = s % 60
|
|
|
+ const parts: string[] = []
|
|
|
+ if (days) parts.push(`${days}天`)
|
|
|
+ if (hours) parts.push(`${hours}小时`)
|
|
|
+ if (minutes) parts.push(`${minutes}分`)
|
|
|
+ if (seconds || parts.length === 0) parts.push(`${seconds}秒`)
|
|
|
+ return parts.join('')
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.app-container {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.last-update {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-body {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-text {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #666;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|