Add Monitor UI: Service status + performance monitoring with auto-refresh

This commit is contained in:
Warren
2026-06-25 16:54:24 +08:00
parent df0b2f5ff8
commit 820186a48c
3 changed files with 215 additions and 227 deletions

View File

@@ -4,6 +4,7 @@ import WebAdmin from '../views/WebAdmin.vue'
import VirtualFolders from '../views/VirtualFolders.vue'
import Quota from '../views/Quota.vue'
import ACL from '../views/ACL.vue'
import Monitor from '../views/Monitor.vue'
import FilePreview from '../views/FilePreview.vue'
import Home from '../views/Home.vue'
import Dashboard from '../views/Dashboard.vue'
@@ -12,7 +13,6 @@ import Config from '../views/Config.vue'
import Diagnostic from '../views/Diagnostic.vue'
import Management from '../views/Management.vue'
import Health from '../views/Health.vue'
import Monitor from '../views/Monitor.vue'
import Backup from '../views/Backup.vue'
import Users from '../views/Users.vue'
import Shares from '../views/Shares.vue'
@@ -48,6 +48,11 @@ const routes = [
name: 'ACL',
component: ACL
},
{
path: '/monitor',
name: 'Monitor',
component: Monitor
},
{
path: '/filepreview',
name: 'FilePreview',

View File

@@ -1,203 +1,168 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getMonitorData } from '../api/tauri'
import { Monitor, CircleCheck, CircleClose, Loading } from '@element-plus/icons-vue'
import { invoke } from '@tauri-apps/api/core'
const monitorData = ref(null)
const services = ref([])
const stats = ref({
cpu: 0,
memory: 0,
disk: 0
})
const loading = ref(false)
const refreshInterval = ref(null)
const autoRefresh = ref(true)
const refreshInterval = ref(5)
let refreshTimer = null
const loadMonitorData = async () => {
const loadServices = async () => {
try {
monitorData.value = await getMonitorData()
const result = await invoke('get_all_services_status')
services.value = result
} catch (error) {
ElMessage.error(`Failed to load monitor data: ${error}`)
console.error('Failed to load services:', error)
}
}
const startAutoRefresh = () => {
if (refreshTimer) {
clearInterval(refreshTimer)
}
refreshTimer = setInterval(async () => {
await loadMonitorData()
}, refreshInterval.value * 1000)
}
const stopAutoRefresh = () => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
const loadStats = async () => {
try {
const result = await invoke('get_system_stats')
stats.value = {
cpu: result.cpu_usage || 0,
memory: result.memory_usage || 0,
disk: result.disk_usage || 0
}
} catch (error) {
console.error('Failed to load stats:', error)
}
}
const toggleAutoRefresh = () => {
if (autoRefresh.value) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
const refreshAll = async () => {
loading.value = true
await Promise.all([
loadServices(),
loadStats()
])
loading.value = false
}
const formatBytes = (bytes) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
const serviceStatusColor = (status) => {
switch (status) {
case 'running': return 'success'
case 'stopped': return 'danger'
case 'error': return 'warning'
default: return 'info'
}
}
onMounted(async () => {
await loadMonitorData()
await refreshAll()
if (autoRefresh.value) {
startAutoRefresh()
refreshInterval.value = setInterval(refreshAll, 5000)
}
})
onUnmounted(() => {
stopAutoRefresh()
if (refreshInterval.value) {
clearInterval(refreshInterval.value)
}
})
</script>
<template>
<div class="monitor-container">
<el-card>
<div class="monitor-header">
<h2>System Monitor</h2>
<p class="header-subtitle">服务状态 + 性能监控</p>
<div class="header-actions">
<el-switch
v-model="autoRefresh"
@change="(val) => {
if (val) {
refreshInterval = setInterval(refreshAll, 5000)
} else {
clearInterval(refreshInterval)
}
}"
active-text="Auto Refresh"
/>
<el-button @click="refreshAll" :icon="Loading" :loading="loading" size="small">Refresh</el-button>
</div>
</div>
<el-row :gutter="20" style="margin-bottom: 20px">
<el-col :span="8">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon cpu">
<el-icon :size="40"><Monitor /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">CPU Usage</div>
<div class="stat-value">{{ stats.cpu.toFixed(1) }}%</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon memory">
<el-icon :size="40"><Monitor /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">Memory Usage</div>
<div class="stat-value">{{ stats.memory.toFixed(1) }}%</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover">
<div class="stat-card">
<div class="stat-icon disk">
<el-icon :size="40"><Monitor /></el-icon>
</div>
<div class="stat-content">
<div class="stat-label">Disk Usage</div>
<div class="stat-value">{{ stats.disk.toFixed(1) }}%</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<h2>Monitor Dashboard</h2>
<div class="refresh-controls">
<el-switch v-model="autoRefresh" @change="toggleAutoRefresh" />
<span>Auto Refresh ({{ refreshInterval }}s)</span>
<el-button type="primary" size="small" @click="loadMonitorData">
Refresh Now
</el-button>
</div>
<span>Services Status</span>
<el-tag :type="autoRefresh ? 'success' : 'info'" size="small">
{{ autoRefresh ? 'Auto Refresh (5s)' : 'Manual Refresh' }}
</el-tag>
</div>
</template>
<div v-if="monitorData">
<el-row :gutter="20" class="system-row">
<el-col :span="6">
<el-card shadow="hover">
<div class="metric-card">
<h3>CPU Usage</h3>
<el-progress
type="dashboard"
:percentage="monitorData.system.cpu_usage"
:color="monitorData.system.cpu_usage > 80 ? '#F56C6C' : '#67C23A'"
/>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="metric-card">
<h3>Memory Usage</h3>
<el-progress
type="dashboard"
:percentage="monitorData.system.memory_usage"
:color="monitorData.system.memory_usage > 80 ? '#F56C6C' : '#67C23A'"
/>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="metric-card">
<h3>Disk Usage</h3>
<el-progress
type="dashboard"
:percentage="monitorData.system.disk_usage"
:color="monitorData.system.disk_usage > 80 ? '#F56C6C' : '#67C23A'"
/>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover">
<div class="metric-card">
<h3>Network Traffic</h3>
<div class="network-metrics">
<p>In: {{ formatBytes(monitorData.system.network_in) }}</p>
<p>Out: {{ formatBytes(monitorData.system.network_out) }}</p>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="details-row">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<h3>File System</h3>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="Total Files">
{{ monitorData.file_system.total_files }}
</el-descriptions-item>
<el-descriptions-item label="Total Size">
{{ formatBytes(monitorData.file_system.total_size) }}
</el-descriptions-item>
<el-descriptions-item label="File Tree Size">
{{ formatBytes(monitorData.file_system.file_tree_size) }}
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<h3>Database</h3>
</template>
<el-descriptions :column="1" border>
<el-descriptions-item label="Database Size">
{{ formatBytes(monitorData.database.database_size) }}
</el-descriptions-item>
<el-descriptions-item label="Table Rows">
<div v-for="(rows, table) in monitorData.database.table_rows" :key="table">
{{ table }}: {{ rows }} rows
</div>
</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
<el-card shadow="hover" class="services-card">
<template #header>
<h3>Service Status</h3>
<el-table :data="services" v-loading="loading" stripe style="width: 100%">
<el-table-column prop="name" label="Service" min-width="150">
<template #default="{ row }">
<el-icon style="margin-right: 5px"><Monitor /></el-icon>
<span>{{ row.name }}</span>
</template>
<el-table :data="monitorData.services" style="width: 100%">
<el-table-column prop="name" label="Service Name" />
<el-table-column prop="status" label="Status">
<template #default="scope">
<el-tag :type="scope.row.status === 'Running' ? 'success' : 'danger'">
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="uptime_seconds" label="Uptime">
<template #default="scope">
{{ Math.floor(scope.row.uptime_seconds / 3600) }}h {{ Math.floor(scope.row.uptime_seconds % 3600 / 60) }}m
</template>
</el-table-column>
<el-table-column prop="last_check" label="Last Check">
<template #default="scope">
{{ new Date(scope.row.last_check).toLocaleString() }}
</template>
</el-table-column>
</el-table>
</el-card>
</div>
<el-empty v-else description="No monitor data available" />
</el-table-column>
<el-table-column prop="status" label="Status" width="120">
<template #default="{ row }">
<el-tag :type="serviceStatusColor(row.status)" size="small">
<el-icon style="margin-right: 5px">
<CircleCheck v-if="row.status === 'running'" />
<CircleClose v-else />
</el-icon>
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="port" label="Port" width="100" />
<el-table-column prop="uptime" label="Uptime" min-width="150" />
<el-table-column prop="connections" label="Connections" width="120" />
</el-table>
</el-card>
</div>
</template>
@@ -207,48 +172,83 @@ onUnmounted(() => {
padding: 20px;
}
.monitor-header {
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.monitor-header h2 {
margin: 0;
font-size: 24px;
}
.header-subtitle {
margin: 5px 0 0;
font-size: 14px;
opacity: 0.9;
}
.header-actions {
display: flex;
align-items: center;
gap: 15px;
}
.stat-card {
display: flex;
align-items: center;
padding: 10px;
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
}
.stat-icon.cpu {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.stat-icon.memory {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.stat-icon.disk {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.stat-content {
flex: 1;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #303133;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
align-items: center;
}
.card-header h2 {
margin: 0;
}
.refresh-controls {
display: flex;
align-items: center;
gap: 10px;
}
.system-row {
margin-bottom: 20px;
}
.metric-card {
text-align: center;
padding: 20px;
}
.metric-card h3 {
margin-bottom: 10px;
}
.network-metrics {
padding: 20px;
}
.network-metrics p {
margin: 5px 0;
}
.details-row {
margin-bottom: 20px;
}
.services-card {
margin-top: 20px;
}
</style>

View File

@@ -8,6 +8,7 @@ import {
import DashboardView from './Dashboard.vue'
import UsersView from './Users.vue'
import SharesView from './Shares.vue'
import MonitorView from './Monitor.vue'
const activeTab = ref('dashboard')
@@ -23,7 +24,7 @@ const currentTab = computed(() => {
case 'dashboard': return DashboardView
case 'users': return UsersView
case 'shares': return SharesView
case 'monitor': return null
case 'monitor': return MonitorView
default: return DashboardView
}
})
@@ -53,11 +54,7 @@ const currentTab = computed(() => {
<div class="webadmin-content">
<transition name="fade" mode="out-in">
<component :is="currentTab" v-if="currentTab" />
<div v-else class="monitor-placeholder">
<el-icon :size="50"><Monitor /></el-icon>
<p>Monitor 功能开发中...</p>
</div>
<component :is="currentTab" />
</transition>
</div>
</div>
@@ -113,18 +110,4 @@ const currentTab = computed(() => {
.fade-leave-to {
opacity: 0;
}
.monitor-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
}
.monitor-placeholder p {
margin-top: 20px;
font-size: 16px;
}
</style>