feat(监控): 实现服务器监控页面

- 新增服务器监控主页面,展示系统基础信息
- 实现CPU、内存、JVM、磁盘使用情况展示
- 新增StatCard统计卡片组件,支持图标和趋势显示
- 采用响应式网格布局,适配不同屏幕尺寸
- 集成系统信息实时监控和数据刷新功能
This commit is contained in:
Leo 2025-09-28 00:08:16 +08:00
parent a6ce521255
commit 19180e5702
2 changed files with 509 additions and 0 deletions

View File

@ -0,0 +1,182 @@
<template>
<n-card class="stat-card" :style="cardStyle" hoverable>
<!-- 主要内容区域 -->
<n-space justify="space-between" align="center" class="mb-2">
<!-- 左侧数据区域 -->
<div class="stat-content">
<div class="stat-title">
{{ title }}
</div>
<div class="stat-value">
{{ value }}
</div>
<div class="stat-subtitle">
{{ subtitle }}
</div>
</div>
<!-- 右侧图标区域 -->
<div class="stat-icon" :style="iconStyle">
<n-icon :size="28">
<IconParkOutlineServer v-if="icon === 'server'" />
<IconParkOutlineComputer v-else-if="icon === 'system'" />
<IconParkOutlineNetworkTree v-else-if="icon === 'network'" />
<IconParkOutlineChip v-else-if="icon === 'chip'" />
<IconParkOutlineCpu v-else-if="icon === 'cpu'" />
<IconParkOutlineData v-else-if="icon === 'memory'" />
<IconParkOutlineCode v-else-if="icon === 'java'" />
<IconParkOutlineServer v-else />
</n-icon>
</div>
</n-space>
<!-- 底部信息区域 -->
<div v-if="trend || extraInfo" class="stat-footer">
<n-space justify="space-between" align="center">
<div class="stat-extra">
{{ extraInfo }}
</div>
<div v-if="trend" class="stat-trend" :class="trendClass">
<n-icon :size="12" class="trend-icon">
<IconParkOutlineUp v-if="trendUp" />
<IconParkOutlineDown v-else />
</n-icon>
<span class="trend-text">{{ trend }}</span>
</div>
</n-space>
</div>
</n-card>
</template>
<script setup lang="ts">
import { computed } from 'vue'
// Props
interface StatCardProps {
title: string
value: string | number
subtitle: string
icon?: string
color?: string
trend?: string
trendUp?: boolean
extraInfo?: string
}
// Props
const props = withDefaults(defineProps<StatCardProps>(), {
icon: 'server',
color: '#18a058',
trendUp: true,
})
//
const cardStyle = computed(() => ({
background:
'linear-gradient(135deg, var(--card-color) 0%, rgba(255, 255, 255, 0.6) 100%)',
border: '1px solid var(--border-color)',
borderRadius: '12px',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.05)',
transition: 'all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
padding: '18px',
position: 'relative',
overflow: 'hidden',
minHeight: '140px',
}))
//
const iconStyle = computed(() => ({
color: props.color,
background: `${props.color}12`,
borderRadius: '10px',
padding: '10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}))
//
const trendClass = computed(() => ({
'trend-up': props.trendUp,
'trend-down': !props.trendUp,
}))
</script>
<style scoped>
.stat-card {
height: 100%;
transition: transform 0.2s ease-in-out;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-content {
flex: 1;
}
.stat-title {
font-size: 13px;
font-weight: 500;
color: var(--text-color-3);
margin-bottom: 8px;
line-height: 1.4;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: var(--text-color-1);
margin-bottom: 6px;
line-height: 1.2;
word-break: break-all;
}
.stat-subtitle {
font-size: 12px;
color: var(--text-color-3);
line-height: 1.3;
}
.stat-icon {
flex-shrink: 0;
transition: all 0.3s ease;
}
.stat-footer {
margin-top: 12px;
padding-top: 8px;
border-top: 1px solid var(--divider-color);
}
.stat-extra {
font-size: 12px;
color: var(--text-color-3);
}
.stat-trend {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 500;
}
.trend-up {
color: var(--success-color);
}
.trend-down {
color: var(--error-color);
}
.trend-icon {
display: flex;
align-items: center;
}
.trend-text {
line-height: 1;
}
</style>

View File

@ -0,0 +1,327 @@
<template>
<div class="server-monitor-container">
<!-- 页面头部 -->
<div class="page-header mb-6">
<div class="header-left">
<h2 class="page-title">
服务监控
</h2>
<p class="page-subtitle">
实时监控服务器运行状态和系统资源使用情况
</p>
</div>
<div class="header-right">
<n-button type="primary" :loading="loading" @click="refreshData">
<template #icon>
<n-icon><IconParkOutlineRefresh /></n-icon>
</template>
刷新数据
</n-button>
</div>
</div>
<!-- 系统信息统计卡片 -->
<n-grid :x-gap="16" :y-gap="16" class="mb-6">
<n-gi :span="6">
<StatCard
title="服务器名称"
:value="serverData?.sys?.computerName || 'N/A'"
subtitle="主机名"
icon="server"
color="#18a058"
/>
</n-gi>
<n-gi :span="6">
<StatCard
title="操作系统"
:value="serverData?.sys?.osName || 'N/A'"
subtitle="系统版本"
icon="system"
color="#2080f0"
/>
</n-gi>
<n-gi :span="6">
<StatCard
title="服务器IP"
:value="serverData?.sys?.computerIp || 'N/A'"
subtitle="主机地址"
icon="network"
color="#f0a020"
/>
</n-gi>
<n-gi :span="6">
<StatCard
title="系统架构"
:value="serverData?.sys?.osArch || 'N/A'"
subtitle="处理器架构"
icon="chip"
color="#d03050"
/>
</n-gi>
</n-grid>
<!-- CPU和内存信息 -->
<n-grid :x-gap="16" :y-gap="16" class="mb-6">
<n-gi :span="8">
<StatCard
title="CPU使用率"
:value="formatPercentage(serverData?.cpu?.cpuUsage)"
subtitle="处理器负载"
icon="cpu"
color="#18a058"
:trend="formatPercentage(serverData?.cpu?.cpuUsage)"
:trend-up="(serverData?.cpu?.cpuUsage || 0) < 80"
/>
</n-gi>
<n-gi :span="8">
<StatCard
title="内存使用率"
:value="formatPercentage(serverData?.mem?.usage)"
subtitle="内存负载"
icon="memory"
color="#2080f0"
:trend="formatPercentage(serverData?.mem?.usage)"
:trend-up="(serverData?.mem?.usage || 0) < 80"
/>
</n-gi>
<n-gi :span="8">
<StatCard
title="JVM使用率"
:value="formatPercentage(serverData?.jvm?.usage)"
subtitle="Java虚拟机"
icon="java"
color="#f0a020"
:trend="formatPercentage(serverData?.jvm?.usage)"
:trend-up="(serverData?.jvm?.usage || 0) < 80"
/>
</n-gi>
</n-grid>
<!-- Java环境信息 -->
<n-card title="Java环境信息" class="mb-6">
<template #header-extra>
<n-icon><IconParkOutlineCode /></n-icon>
</template>
<n-descriptions :column="3" label-placement="left" :label-style="{ width: '120px' }">
<n-descriptions-item label="Java版本">
{{ serverData?.jvm?.version || 'N/A' }}
</n-descriptions-item>
<n-descriptions-item label="启动时间">
{{ serverData?.jvm?.startTime || 'N/A' }}
</n-descriptions-item>
<n-descriptions-item label="运行时间">
{{ serverData?.jvm?.runTime || 'N/A' }}
</n-descriptions-item>
<n-descriptions-item label="安装路径">
{{ serverData?.jvm?.home || 'N/A' }}
</n-descriptions-item>
<n-descriptions-item label="项目路径">
{{ serverData?.sys?.userDir || 'N/A' }}
</n-descriptions-item>
<n-descriptions-item label="运行参数">
<n-tag type="info" size="small">
-Djava.version={{ serverData?.jvm?.version || 'N/A' }}
</n-tag>
</n-descriptions-item>
</n-descriptions>
<!-- JVM内存详情 -->
<n-divider>JVM内存详情</n-divider>
<n-space>
<n-statistic label="总内存" :value="serverData?.jvm?.totalStr || '0 MB'" />
<n-statistic label="已用内存" :value="serverData?.jvm?.usedStr || '0 MB'" />
<n-statistic label="剩余内存" :value="serverData?.jvm?.freeStr || '0 MB'" />
<n-statistic label="最大内存" :value="serverData?.jvm?.maxStr || '0 MB'" />
</n-space>
</n-card>
<!-- 磁盘状态 -->
<n-card title="磁盘状态">
<template #header-extra>
<n-icon><IconParkOutlineHdd /></n-icon>
</template>
<n-data-table
:columns="diskColumns"
:data="serverData?.sysFiles || []"
:loading="loading"
size="small"
:bordered="false"
/>
</n-card>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import type { DataTableColumns } from 'naive-ui'
import { coiMsgError, coiMsgSuccess } from '@/utils/coi'
import { getServerInformation } from '@/service/api/monitor/server'
import type { ServerVo, SysFileVo } from '@/service/api/monitor/server'
import StatCard from './components/StatCard.vue'
//
const loading = ref(false)
const serverData = ref<ServerVo>()
//
const diskColumns: DataTableColumns<SysFileVo> = [
{
title: '盘符路径',
key: 'dirName',
width: 120,
ellipsis: {
tooltip: true,
},
},
{
title: '文件系统',
key: 'sysTypeName',
width: 100,
},
{
title: '盘符类型',
key: 'typeName',
width: 200,
ellipsis: {
tooltip: true,
},
},
{
title: '总大小',
key: 'total',
width: 100,
align: 'right',
},
{
title: '可用大小',
key: 'free',
width: 100,
align: 'right',
},
{
title: '已用大小',
key: 'used',
width: 100,
align: 'right',
},
{
title: '已用百分比',
key: 'usage',
width: 120,
align: 'center',
render: (row) => {
const usage = row.usage || 0
const color = usage > 80 ? '#d03050' : usage > 60 ? '#f0a020' : '#18a058'
return h('div', { style: { color } }, `${usage.toFixed(1)}%`)
},
},
]
//
function formatPercentage(value?: number): string {
if (value === undefined || value === null)
return '0.0%'
return `${value.toFixed(1)}%`
}
//
async function fetchServerData() {
try {
loading.value = true
const { data, isSuccess } = await getServerInformation()
if (isSuccess && data) {
serverData.value = data
}
else {
coiMsgError('获取服务器监控数据失败')
}
}
catch (error) {
console.error('获取服务器监控数据异常:', error)
coiMsgError('获取服务器监控数据异常')
}
finally {
loading.value = false
}
}
//
async function refreshData() {
await fetchServerData()
coiMsgSuccess('数据刷新成功')
}
//
onMounted(() => {
fetchServerData()
})
</script>
<style scoped>
.server-monitor-container {
padding: 20px;
background-color: var(--body-color);
min-height: 100vh;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.header-left {
flex: 1;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: var(--text-color-1);
margin: 0 0 4px 0;
}
.page-subtitle {
font-size: 14px;
color: var(--text-color-3);
margin: 0;
}
.header-right {
display: flex;
gap: 12px;
}
:deep(.n-card) {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
:deep(.n-card .n-card__header) {
padding: 16px 20px;
border-bottom: 1px solid var(--divider-color);
}
:deep(.n-card .n-card__content) {
padding: 20px;
}
:deep(.n-descriptions .n-descriptions-item-label) {
font-weight: 500;
color: var(--text-color-2);
}
:deep(.n-divider) {
margin: 16px 0;
}
:deep(.n-statistic) {
text-align: center;
}
:deep(.n-data-table) {
border-radius: 8px;
}
</style>