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

View File

@@ -1,203 +1,168 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus' 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 autoRefresh = ref(true)
const refreshInterval = ref(5)
let refreshTimer = null
const loadMonitorData = async () => { const loadServices = async () => {
try { try {
monitorData.value = await getMonitorData() const result = await invoke('get_all_services_status')
services.value = result
} catch (error) { } catch (error) {
ElMessage.error(`Failed to load monitor data: ${error}`) console.error('Failed to load services:', error)
} }
} }
const startAutoRefresh = () => { const loadStats = async () => {
if (refreshTimer) { try {
clearInterval(refreshTimer) const result = await invoke('get_system_stats')
} stats.value = {
cpu: result.cpu_usage || 0,
refreshTimer = setInterval(async () => { memory: result.memory_usage || 0,
await loadMonitorData() disk: result.disk_usage || 0
}, refreshInterval.value * 1000) }
} } catch (error) {
console.error('Failed to load stats:', error)
const stopAutoRefresh = () => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
} }
} }
const toggleAutoRefresh = () => { const refreshAll = async () => {
if (autoRefresh.value) { loading.value = true
startAutoRefresh() await Promise.all([
} else { loadServices(),
stopAutoRefresh() loadStats()
} ])
loading.value = false
} }
const formatBytes = (bytes) => { const serviceStatusColor = (status) => {
if (bytes === 0) return '0 B' switch (status) {
const k = 1024 case 'running': return 'success'
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] case 'stopped': return 'danger'
const i = Math.floor(Math.log(bytes) / Math.log(k)) case 'error': return 'warning'
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i] default: return 'info'
}
} }
onMounted(async () => { onMounted(async () => {
await loadMonitorData() await refreshAll()
if (autoRefresh.value) { if (autoRefresh.value) {
startAutoRefresh() refreshInterval.value = setInterval(refreshAll, 5000)
} }
}) })
onUnmounted(() => { onUnmounted(() => {
stopAutoRefresh() if (refreshInterval.value) {
clearInterval(refreshInterval.value)
}
}) })
</script> </script>
<template> <template>
<div class="monitor-container"> <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> <template #header>
<div class="card-header"> <div class="card-header">
<h2>Monitor Dashboard</h2> <span>Services Status</span>
<div class="refresh-controls"> <el-tag :type="autoRefresh ? 'success' : 'info'" size="small">
<el-switch v-model="autoRefresh" @change="toggleAutoRefresh" /> {{ autoRefresh ? 'Auto Refresh (5s)' : 'Manual Refresh' }}
<span>Auto Refresh ({{ refreshInterval }}s)</span> </el-tag>
<el-button type="primary" size="small" @click="loadMonitorData">
Refresh Now
</el-button>
</div>
</div> </div>
</template> </template>
<div v-if="monitorData"> <el-table :data="services" v-loading="loading" stripe style="width: 100%">
<el-row :gutter="20" class="system-row"> <el-table-column prop="name" label="Service" min-width="150">
<el-col :span="6"> <template #default="{ row }">
<el-card shadow="hover"> <el-icon style="margin-right: 5px"><Monitor /></el-icon>
<div class="metric-card"> <span>{{ row.name }}</span>
<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>
</template> </template>
<el-table :data="monitorData.services" style="width: 100%"> </el-table-column>
<el-table-column prop="name" label="Service Name" /> <el-table-column prop="status" label="Status" width="120">
<el-table-column prop="status" label="Status"> <template #default="{ row }">
<template #default="scope"> <el-tag :type="serviceStatusColor(row.status)" size="small">
<el-tag :type="scope.row.status === 'Running' ? 'success' : 'danger'"> <el-icon style="margin-right: 5px">
{{ scope.row.status }} <CircleCheck v-if="row.status === 'running'" />
</el-tag> <CircleClose v-else />
</template> </el-icon>
</el-table-column> {{ row.status }}
<el-table-column prop="uptime_seconds" label="Uptime"> </el-tag>
<template #default="scope"> </template>
{{ Math.floor(scope.row.uptime_seconds / 3600) }}h {{ Math.floor(scope.row.uptime_seconds % 3600 / 60) }}m </el-table-column>
</template> <el-table-column prop="port" label="Port" width="100" />
</el-table-column> <el-table-column prop="uptime" label="Uptime" min-width="150" />
<el-table-column prop="last_check" label="Last Check"> <el-table-column prop="connections" label="Connections" width="120" />
<template #default="scope"> </el-table>
{{ 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-card> </el-card>
</div> </div>
</template> </template>
@@ -207,48 +172,83 @@ onUnmounted(() => {
padding: 20px; 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 { .card-header {
display: flex; display: flex;
align-items: center;
justify-content: space-between; 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> </style>

View File

@@ -8,6 +8,7 @@ import {
import DashboardView from './Dashboard.vue' import DashboardView from './Dashboard.vue'
import UsersView from './Users.vue' import UsersView from './Users.vue'
import SharesView from './Shares.vue' import SharesView from './Shares.vue'
import MonitorView from './Monitor.vue'
const activeTab = ref('dashboard') const activeTab = ref('dashboard')
@@ -23,7 +24,7 @@ const currentTab = computed(() => {
case 'dashboard': return DashboardView case 'dashboard': return DashboardView
case 'users': return UsersView case 'users': return UsersView
case 'shares': return SharesView case 'shares': return SharesView
case 'monitor': return null case 'monitor': return MonitorView
default: return DashboardView default: return DashboardView
} }
}) })
@@ -53,11 +54,7 @@ const currentTab = computed(() => {
<div class="webadmin-content"> <div class="webadmin-content">
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<component :is="currentTab" v-if="currentTab" /> <component :is="currentTab" />
<div v-else class="monitor-placeholder">
<el-icon :size="50"><Monitor /></el-icon>
<p>Monitor 功能开发中...</p>
</div>
</transition> </transition>
</div> </div>
</div> </div>
@@ -113,18 +110,4 @@ const currentTab = computed(() => {
.fade-leave-to { .fade-leave-to {
opacity: 0; 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> </style>