第58天:Element Plus 综合项目实战(三)
学习目标
继续我们的智能企业管理平台开发,今天重点实现数据可视化模块、系统监控功能和性能优化实践。
- 集成 ECharts 实现数据可视化
- 开发系统监控和日志管理
- 实现性能监控和优化
- 构建实时数据更新机制
- 添加错误处理和用户反馈
1. 数据可视化模块
1.1 仪表盘页面
vue
<!-- src/views/dashboard/index.vue -->
<template>
<div class="dashboard-container">
<!-- 统计卡片 -->
<el-row :gutter="20" class="stats-cards">
<el-col :xs="24" :sm="12" :lg="6" v-for="card in statsCards" :key="card.key">
<div class="stats-card" :class="card.type">
<div class="stats-card-content">
<div class="stats-card-icon">
<el-icon :size="40">
<component :is="card.icon" />
</el-icon>
</div>
<div class="stats-card-info">
<div class="stats-card-title">{{ card.title }}</div>
<div class="stats-card-value">
<CountUp :end-val="card.value" :duration="2" />
</div>
<div class="stats-card-trend" :class="card.trend.type">
<el-icon><component :is="card.trend.icon" /></el-icon>
<span>{{ card.trend.value }}%</span>
<span class="trend-text">{{ card.trend.text }}</span>
</div>
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="20" class="charts-section">
<!-- 用户增长趋势 -->
<el-col :xs="24" :lg="12">
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>用户增长趋势</span>
<el-button-group size="small">
<el-button
v-for="period in timePeriods"
:key="period.value"
:type="selectedPeriod === period.value ? 'primary' : 'default'"
@click="changePeriod(period.value)"
>
{{ period.label }}
</el-button>
</el-button-group>
</div>
</template>
<UserGrowthChart
:data="userGrowthData"
:loading="chartsLoading.userGrowth"
height="300px"
/>
</el-card>
</el-col>
<!-- 访问量统计 -->
<el-col :xs="24" :lg="12">
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>访问量统计</span>
<el-tooltip content="实时数据,每5秒更新一次" placement="top">
<el-icon class="info-icon"><InfoFilled /></el-icon>
</el-tooltip>
</div>
</template>
<VisitChart
:data="visitData"
:loading="chartsLoading.visit"
height="300px"
/>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="charts-section">
<!-- 销售数据 -->
<el-col :xs="24" :lg="16">
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>销售数据分析</span>
<el-select
v-model="selectedRegion"
size="small"
style="width: 120px"
@change="updateSalesData"
>
<el-option label="全国" value="all" />
<el-option label="华北" value="north" />
<el-option label="华南" value="south" />
<el-option label="华东" value="east" />
<el-option label="华西" value="west" />
</el-select>
</div>
</template>
<SalesChart
:data="salesData"
:loading="chartsLoading.sales"
height="400px"
/>
</el-card>
</el-col>
<!-- 用户分布 -->
<el-col :xs="24" :lg="8">
<el-card class="chart-card">
<template #header>
<span>用户分布</span>
</template>
<UserDistributionChart
:data="userDistributionData"
:loading="chartsLoading.userDistribution"
height="400px"
/>
</el-card>
</el-col>
</el-row>
<!-- 实时活动 -->
<el-row :gutter="20" class="activity-section">
<el-col :xs="24" :lg="16">
<el-card class="activity-card">
<template #header>
<div class="card-header">
<span>实时活动</span>
<el-badge :value="unreadCount" class="badge">
<el-button size="small" @click="markAllRead">全部已读</el-button>
</el-badge>
</div>
</template>
<ActivityTimeline :activities="activities" :loading="activitiesLoading" />
</el-card>
</el-col>
<!-- 快捷操作 -->
<el-col :xs="24" :lg="8">
<el-card class="quick-actions-card">
<template #header>
<span>快捷操作</span>
</template>
<QuickActions :actions="quickActions" @action="handleQuickAction" />
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
User,
TrendCharts,
Monitor,
InfoFilled,
ArrowUp,
ArrowDown
} from '@element-plus/icons-vue'
import CountUp from '@/components/CountUp/index.vue'
import UserGrowthChart from './components/UserGrowthChart.vue'
import VisitChart from './components/VisitChart.vue'
import SalesChart from './components/SalesChart.vue'
import UserDistributionChart from './components/UserDistributionChart.vue'
import ActivityTimeline from './components/ActivityTimeline.vue'
import QuickActions from './components/QuickActions.vue'
import {
getDashboardStats,
getUserGrowthData,
getVisitData,
getSalesData,
getUserDistributionData,
getRealtimeActivities
} from '@/api/dashboard'
import type {
DashboardStats,
UserGrowthData,
VisitData,
SalesData,
UserDistributionData,
Activity,
QuickAction
} from '@/types/dashboard'
// 响应式数据
const statsCards = ref<DashboardStats[]>([])
const userGrowthData = ref<UserGrowthData[]>([])
const visitData = ref<VisitData[]>([])
const salesData = ref<SalesData[]>([])
const userDistributionData = ref<UserDistributionData[]>([])
const activities = ref<Activity[]>([])
const unreadCount = ref(0)
// 加载状态
const chartsLoading = reactive({
userGrowth: false,
visit: false,
sales: false,
userDistribution: false
})
const activitiesLoading = ref(false)
// 控制参数
const selectedPeriod = ref('7d')
const selectedRegion = ref('all')
// 时间周期选项
const timePeriods = [
{ label: '7天', value: '7d' },
{ label: '30天', value: '30d' },
{ label: '90天', value: '90d' },
{ label: '1年', value: '1y' }
]
// 快捷操作
const quickActions: QuickAction[] = [
{
title: '添加用户',
icon: 'User',
color: '#409eff',
action: 'create-user'
},
{
title: '系统设置',
icon: 'Setting',
color: '#67c23a',
action: 'system-settings'
},
{
title: '数据导出',
icon: 'Download',
color: '#e6a23c',
action: 'export-data'
},
{
title: '系统监控',
icon: 'Monitor',
color: '#f56c6c',
action: 'system-monitor'
}
]
// 定时器
let realtimeTimer: NodeJS.Timeout | null = null
// 生命周期
onMounted(() => {
initDashboard()
startRealtimeUpdate()
})
onUnmounted(() => {
stopRealtimeUpdate()
})
// 初始化仪表盘
async function initDashboard() {
await Promise.all([
loadStatsCards(),
loadUserGrowthData(),
loadVisitData(),
loadSalesData(),
loadUserDistributionData(),
loadActivities()
])
}
// 加载统计卡片数据
async function loadStatsCards() {
try {
const response = await getDashboardStats()
statsCards.value = response.data.map((item: any) => ({
...item,
icon: getIconComponent(item.iconName),
trend: {
...item.trend,
icon: item.trend.type === 'up' ? ArrowUp : ArrowDown
}
}))
} catch (error) {
console.error('Load stats cards failed:', error)
ElMessage.error('加载统计数据失败')
}
}
// 加载用户增长数据
async function loadUserGrowthData() {
chartsLoading.userGrowth = true
try {
const response = await getUserGrowthData({ period: selectedPeriod.value })
userGrowthData.value = response.data
} catch (error) {
console.error('Load user growth data failed:', error)
ElMessage.error('加载用户增长数据失败')
} finally {
chartsLoading.userGrowth = false
}
}
// 加载访问数据
async function loadVisitData() {
chartsLoading.visit = true
try {
const response = await getVisitData()
visitData.value = response.data
} catch (error) {
console.error('Load visit data failed:', error)
ElMessage.error('加载访问数据失败')
} finally {
chartsLoading.visit = false
}
}
// 加载销售数据
async function loadSalesData() {
chartsLoading.sales = true
try {
const response = await getSalesData({ region: selectedRegion.value })
salesData.value = response.data
} catch (error) {
console.error('Load sales data failed:', error)
ElMessage.error('加载销售数据失败')
} finally {
chartsLoading.sales = false
}
}
// 加载用户分布数据
async function loadUserDistributionData() {
chartsLoading.userDistribution = true
try {
const response = await getUserDistributionData()
userDistributionData.value = response.data
} catch (error) {
console.error('Load user distribution data failed:', error)
ElMessage.error('加载用户分布数据失败')
} finally {
chartsLoading.userDistribution = false
}
}
// 加载活动数据
async function loadActivities() {
activitiesLoading.value = true
try {
const response = await getRealtimeActivities()
activities.value = response.data.items
unreadCount.value = response.data.unreadCount
} catch (error) {
console.error('Load activities failed:', error)
ElMessage.error('加载活动数据失败')
} finally {
activitiesLoading.value = false
}
}
// 切换时间周期
function changePeriod(period: string) {
selectedPeriod.value = period
loadUserGrowthData()
}
// 更新销售数据
function updateSalesData() {
loadSalesData()
}
// 标记全部已读
function markAllRead() {
unreadCount.value = 0
activities.value.forEach(activity => {
activity.read = true
})
ElMessage.success('已标记全部为已读')
}
// 处理快捷操作
function handleQuickAction(action: string) {
switch (action) {
case 'create-user':
// 跳转到用户创建页面
break
case 'system-settings':
// 跳转到系统设置页面
break
case 'export-data':
// 执行数据导出
break
case 'system-monitor':
// 跳转到系统监控页面
break
default:
ElMessage.info(`执行操作: ${action}`)
}
}
// 开始实时更新
function startRealtimeUpdate() {
realtimeTimer = setInterval(() => {
loadVisitData()
loadActivities()
}, 5000) // 每5秒更新一次
}
// 停止实时更新
function stopRealtimeUpdate() {
if (realtimeTimer) {
clearInterval(realtimeTimer)
realtimeTimer = null
}
}
// 获取图标组件
function getIconComponent(iconName: string) {
const iconMap: Record<string, any> = {
User,
TrendCharts,
Monitor
}
return iconMap[iconName] || User
}
</script>
<style lang="scss" scoped>
.dashboard-container {
padding: 20px;
background-color: #f5f5f5;
min-height: calc(100vh - 84px);
}
.stats-cards {
margin-bottom: 20px;
}
.stats-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
}
&.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
&.success {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
&.warning {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
&.danger {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
}
}
.stats-card-content {
display: flex;
align-items: center;
}
.stats-card-icon {
margin-right: 20px;
opacity: 0.8;
}
.stats-card-info {
flex: 1;
}
.stats-card-title {
font-size: 14px;
margin-bottom: 8px;
opacity: 0.9;
}
.stats-card-value {
font-size: 28px;
font-weight: bold;
margin-bottom: 8px;
}
.stats-card-trend {
display: flex;
align-items: center;
font-size: 12px;
&.up {
color: #67c23a;
}
&.down {
color: #f56c6c;
}
.el-icon {
margin-right: 4px;
}
.trend-text {
margin-left: 4px;
opacity: 0.8;
}
}
.charts-section {
margin-bottom: 20px;
}
.chart-card {
height: 100%;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.info-icon {
color: #909399;
cursor: help;
}
}
}
.activity-section {
margin-bottom: 20px;
}
.activity-card {
.badge {
.el-button {
border: none;
background: transparent;
color: #409eff;
}
}
}
.quick-actions-card {
height: 100%;
}
// 响应式设计
@media (max-width: 768px) {
.dashboard-container {
padding: 10px;
}
.stats-card {
margin-bottom: 10px;
}
.stats-card-content {
flex-direction: column;
text-align: center;
}
.stats-card-icon {
margin-right: 0;
margin-bottom: 10px;
}
}
</style>
1.2 用户增长图表组件
vue
<!-- src/views/dashboard/components/UserGrowthChart.vue -->
<template>
<div class="chart-container">
<div
ref="chartRef"
:style="{ height: height }"
v-loading="loading"
></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import * as echarts from 'echarts'
import type { ECharts } from 'echarts'
import { useResizeObserver } from '@vueuse/core'
import type { UserGrowthData } from '@/types/dashboard'
// Props
interface Props {
data: UserGrowthData[]
loading?: boolean
height?: string
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
height: '300px'
})
// 响应式数据
const chartRef = ref<HTMLDivElement>()
let chartInstance: ECharts | null = null
// 生命周期
onMounted(() => {
nextTick(() => {
initChart()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
// 监听数据变化
watch(
() => props.data,
() => {
updateChart()
},
{ deep: true }
)
// 监听加载状态
watch(
() => props.loading,
(loading) => {
if (chartInstance) {
if (loading) {
chartInstance.showLoading()
} else {
chartInstance.hideLoading()
}
}
}
)
// 响应式调整
useResizeObserver(chartRef, () => {
if (chartInstance) {
chartInstance.resize()
}
})
// 初始化图表
function initChart() {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: (params: any) => {
let result = `${params[0].axisValue}<br/>`
params.forEach((param: any) => {
result += `${param.marker}${param.seriesName}: ${param.value}<br/>`
})
return result
}
},
legend: {
data: ['新增用户', '活跃用户', '累计用户'],
top: 10
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: []
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}'
}
},
series: [
{
name: '新增用户',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 3
},
areaStyle: {
opacity: 0.3
},
emphasis: {
focus: 'series'
},
data: []
},
{
name: '活跃用户',
type: 'line',
stack: 'Total',
smooth: true,
lineStyle: {
width: 3
},
areaStyle: {
opacity: 0.3
},
emphasis: {
focus: 'series'
},
data: []
},
{
name: '累计用户',
type: 'line',
smooth: true,
lineStyle: {
width: 3,
type: 'dashed'
},
emphasis: {
focus: 'series'
},
data: []
}
]
}
chartInstance.setOption(option)
updateChart()
}
// 更新图表数据
function updateChart() {
if (!chartInstance || !props.data.length) return
const dates = props.data.map(item => item.date)
const newUsers = props.data.map(item => item.newUsers)
const activeUsers = props.data.map(item => item.activeUsers)
const totalUsers = props.data.map(item => item.totalUsers)
chartInstance.setOption({
xAxis: {
data: dates
},
series: [
{
data: newUsers
},
{
data: activeUsers
},
{
data: totalUsers
}
]
})
}
</script>
<style lang="scss" scoped>
.chart-container {
width: 100%;
height: 100%;
}
</style>
1.3 销售数据图表组件
vue
<!-- src/views/dashboard/components/SalesChart.vue -->
<template>
<div class="chart-container">
<div
ref="chartRef"
:style="{ height: height }"
v-loading="loading"
></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import * as echarts from 'echarts'
import type { ECharts } from 'echarts'
import { useResizeObserver } from '@vueuse/core'
import type { SalesData } from '@/types/dashboard'
// Props
interface Props {
data: SalesData[]
loading?: boolean
height?: string
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
height: '400px'
})
// 响应式数据
const chartRef = ref<HTMLDivElement>()
let chartInstance: ECharts | null = null
// 生命周期
onMounted(() => {
nextTick(() => {
initChart()
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
// 监听数据变化
watch(
() => props.data,
() => {
updateChart()
},
{ deep: true }
)
// 监听加载状态
watch(
() => props.loading,
(loading) => {
if (chartInstance) {
if (loading) {
chartInstance.showLoading()
} else {
chartInstance.hideLoading()
}
}
}
)
// 响应式调整
useResizeObserver(chartRef, () => {
if (chartInstance) {
chartInstance.resize()
}
})
// 初始化图表
function initChart() {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params: any) => {
let result = `${params[0].axisValue}<br/>`
params.forEach((param: any) => {
const value = typeof param.value === 'number'
? param.value.toLocaleString()
: param.value
result += `${param.marker}${param.seriesName}: ¥${value}<br/>`
})
return result
}
},
legend: {
data: ['销售额', '利润', '成本'],
top: 10
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value',
axisLabel: {
formatter: (value: number) => {
if (value >= 10000) {
return (value / 10000) + 'w'
}
return value.toString()
}
}
},
series: [
{
name: '销售额',
type: 'bar',
barWidth: '60%',
itemStyle: {
borderRadius: [4, 4, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#2378f7' },
{ offset: 0.7, color: '#2378f7' },
{ offset: 1, color: '#83bff6' }
])
}
},
data: []
},
{
name: '利润',
type: 'bar',
barWidth: '60%',
itemStyle: {
borderRadius: [4, 4, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#67c23a' },
{ offset: 0.5, color: '#5daf34' },
{ offset: 1, color: '#5daf34' }
])
},
data: []
},
{
name: '成本',
type: 'line',
smooth: true,
lineStyle: {
width: 3,
color: '#f56c6c'
},
itemStyle: {
color: '#f56c6c'
},
data: []
}
]
}
chartInstance.setOption(option)
updateChart()
}
// 更新图表数据
function updateChart() {
if (!chartInstance || !props.data.length) return
const months = props.data.map(item => item.month)
const sales = props.data.map(item => item.sales)
const profit = props.data.map(item => item.profit)
const cost = props.data.map(item => item.cost)
chartInstance.setOption({
xAxis: {
data: months
},
series: [
{
data: sales
},
{
data: profit
},
{
data: cost
}
]
})
}
</script>
<style lang="scss" scoped>
.chart-container {
width: 100%;
height: 100%;
}
</style>
2. 系统监控模块
2.1 系统监控页面
vue
<!-- src/views/system/monitor.vue -->
<template>
<div class="monitor-container">
<!-- 系统状态概览 -->
<el-row :gutter="20" class="status-overview">
<el-col :xs="24" :sm="12" :lg="6" v-for="metric in systemMetrics" :key="metric.key">
<el-card class="metric-card" :class="getMetricStatus(metric.value, metric.threshold)">
<div class="metric-content">
<div class="metric-icon">
<el-icon :size="32">
<component :is="metric.icon" />
</el-icon>
</div>
<div class="metric-info">
<div class="metric-title">{{ metric.title }}</div>
<div class="metric-value">
{{ formatMetricValue(metric.value, metric.unit) }}
</div>
<div class="metric-status">
<el-tag :type="getMetricTagType(metric.value, metric.threshold)" size="small">
{{ getMetricStatusText(metric.value, metric.threshold) }}
</el-tag>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 实时监控图表 -->
<el-row :gutter="20" class="monitor-charts">
<!-- CPU 使用率 -->
<el-col :xs="24" :lg="12">
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>CPU 使用率</span>
<el-switch
v-model="autoRefresh"
active-text="自动刷新"
@change="toggleAutoRefresh"
/>
</div>
</template>
<RealtimeChart
:data="cpuData"
:loading="chartsLoading.cpu"
type="line"
color="#409eff"
unit="%"
height="250px"
/>
</el-card>
</el-col>
<!-- 内存使用率 -->
<el-col :xs="24" :lg="12">
<el-card class="chart-card">
<template #header>
<span>内存使用率</span>
</template>
<RealtimeChart
:data="memoryData"
:loading="chartsLoading.memory"
type="area"
color="#67c23a"
unit="%"
height="250px"
/>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="monitor-charts">
<!-- 网络流量 -->
<el-col :xs="24" :lg="12">
<el-card class="chart-card">
<template #header>
<span>网络流量</span>
</template>
<NetworkChart
:data="networkData"
:loading="chartsLoading.network"
height="250px"
/>
</el-card>
</el-col>
<!-- 磁盘使用情况 -->
<el-col :xs="24" :lg="12">
<el-card class="chart-card">
<template #header>
<span>磁盘使用情况</span>
</template>
<DiskChart
:data="diskData"
:loading="chartsLoading.disk"
height="250px"
/>
</el-card>
</el-col>
</el-row>
<!-- 服务状态 -->
<el-row :gutter="20" class="service-status">
<el-col :span="24">
<el-card class="service-card">
<template #header>
<div class="card-header">
<span>服务状态</span>
<el-button size="small" @click="refreshServices">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</template>
<ServiceStatus
:services="services"
:loading="servicesLoading"
@restart="handleRestartService"
@stop="handleStopService"
@start="handleStartService"
/>
</el-card>
</el-col>
</el-row>
<!-- 系统日志 -->
<el-row :gutter="20" class="system-logs">
<el-col :span="24">
<el-card class="logs-card">
<template #header>
<div class="card-header">
<span>系统日志</span>
<div class="log-controls">
<el-select v-model="logLevel" size="small" style="width: 100px">
<el-option label="全部" value="all" />
<el-option label="错误" value="error" />
<el-option label="警告" value="warn" />
<el-option label="信息" value="info" />
</el-select>
<el-button size="small" @click="clearLogs">
<el-icon><Delete /></el-icon>
清空
</el-button>
</div>
</div>
</template>
<SystemLogs
:logs="filteredLogs"
:loading="logsLoading"
height="300px"
/>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Cpu,
MemoryCard,
Monitor,
HardDisk,
Refresh,
Delete
} from '@element-plus/icons-vue'
import RealtimeChart from './components/RealtimeChart.vue'
import NetworkChart from './components/NetworkChart.vue'
import DiskChart from './components/DiskChart.vue'
import ServiceStatus from './components/ServiceStatus.vue'
import SystemLogs from './components/SystemLogs.vue'
import {
getSystemMetrics,
getRealtimeData,
getServiceStatus,
getSystemLogs,
restartService,
stopService,
startService
} from '@/api/monitor'
import type {
SystemMetric,
RealtimeData,
Service,
SystemLog
} from '@/types/monitor'
// 响应式数据
const systemMetrics = ref<SystemMetric[]>([])
const cpuData = ref<RealtimeData[]>([])
const memoryData = ref<RealtimeData[]>([])
const networkData = ref<RealtimeData[]>([])
const diskData = ref<any[]>([])
const services = ref<Service[]>([])
const logs = ref<SystemLog[]>([])
// 控制状态
const autoRefresh = ref(true)
const logLevel = ref('all')
// 加载状态
const chartsLoading = reactive({
cpu: false,
memory: false,
network: false,
disk: false
})
const servicesLoading = ref(false)
const logsLoading = ref(false)
// 定时器
let refreshTimer: NodeJS.Timeout | null = null
// 计算属性
const filteredLogs = computed(() => {
if (logLevel.value === 'all') {
return logs.value
}
return logs.value.filter(log => log.level === logLevel.value)
})
// 生命周期
onMounted(() => {
initMonitor()
if (autoRefresh.value) {
startAutoRefresh()
}
})
onUnmounted(() => {
stopAutoRefresh()
})
// 初始化监控
async function initMonitor() {
await Promise.all([
loadSystemMetrics(),
loadRealtimeData(),
loadServices(),
loadLogs()
])
}
// 加载系统指标
async function loadSystemMetrics() {
try {
const response = await getSystemMetrics()
systemMetrics.value = response.data.map((metric: any) => ({
...metric,
icon: getMetricIcon(metric.key)
}))
} catch (error) {
console.error('Load system metrics failed:', error)
ElMessage.error('加载系统指标失败')
}
}
// 加载实时数据
async function loadRealtimeData() {
chartsLoading.cpu = true
chartsLoading.memory = true
chartsLoading.network = true
chartsLoading.disk = true
try {
const response = await getRealtimeData()
const data = response.data
cpuData.value = data.cpu
memoryData.value = data.memory
networkData.value = data.network
diskData.value = data.disk
} catch (error) {
console.error('Load realtime data failed:', error)
ElMessage.error('加载实时数据失败')
} finally {
chartsLoading.cpu = false
chartsLoading.memory = false
chartsLoading.network = false
chartsLoading.disk = false
}
}
// 加载服务状态
async function loadServices() {
servicesLoading.value = true
try {
const response = await getServiceStatus()
services.value = response.data
} catch (error) {
console.error('Load services failed:', error)
ElMessage.error('加载服务状态失败')
} finally {
servicesLoading.value = false
}
}
// 加载系统日志
async function loadLogs() {
logsLoading.value = true
try {
const response = await getSystemLogs({ limit: 100 })
logs.value = response.data
} catch (error) {
console.error('Load logs failed:', error)
ElMessage.error('加载系统日志失败')
} finally {
logsLoading.value = false
}
}
// 切换自动刷新
function toggleAutoRefresh(enabled: boolean) {
if (enabled) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}
// 开始自动刷新
function startAutoRefresh() {
refreshTimer = setInterval(() => {
loadSystemMetrics()
loadRealtimeData()
}, 5000) // 每5秒刷新一次
}
// 停止自动刷新
function stopAutoRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
// 刷新服务
function refreshServices() {
loadServices()
}
// 清空日志
async function clearLogs() {
try {
await ElMessageBox.confirm('确定要清空所有日志吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
logs.value = []
ElMessage.success('日志已清空')
} catch (error) {
// 用户取消
}
}
// 处理服务操作
async function handleRestartService(serviceName: string) {
try {
await restartService(serviceName)
ElMessage.success(`服务 ${serviceName} 重启成功`)
loadServices()
} catch (error) {
console.error('Restart service failed:', error)
ElMessage.error(`服务 ${serviceName} 重启失败`)
}
}
async function handleStopService(serviceName: string) {
try {
await stopService(serviceName)
ElMessage.success(`服务 ${serviceName} 停止成功`)
loadServices()
} catch (error) {
console.error('Stop service failed:', error)
ElMessage.error(`服务 ${serviceName} 停止失败`)
}
}
async function handleStartService(serviceName: string) {
try {
await startService(serviceName)
ElMessage.success(`服务 ${serviceName} 启动成功`)
loadServices()
} catch (error) {
console.error('Start service failed:', error)
ElMessage.error(`服务 ${serviceName} 启动失败`)
}
}
// 工具函数
function getMetricIcon(key: string) {
const iconMap: Record<string, any> = {
cpu: Cpu,
memory: MemoryCard,
disk: HardDisk,
network: Monitor
}
return iconMap[key] || Monitor
}
function getMetricStatus(value: number, threshold: { warning: number; danger: number }) {
if (value >= threshold.danger) return 'danger'
if (value >= threshold.warning) return 'warning'
return 'normal'
}
function getMetricTagType(value: number, threshold: { warning: number; danger: number }) {
if (value >= threshold.danger) return 'danger'
if (value >= threshold.warning) return 'warning'
return 'success'
}
function getMetricStatusText(value: number, threshold: { warning: number; danger: number }) {
if (value >= threshold.danger) return '危险'
if (value >= threshold.warning) return '警告'
return '正常'
}
function formatMetricValue(value: number, unit: string) {
return `${value.toFixed(1)}${unit}`
}
</script>
<style lang="scss" scoped>
.monitor-container {
padding: 20px;
background-color: #f5f5f5;
min-height: calc(100vh - 84px);
}
.status-overview {
margin-bottom: 20px;
}
.metric-card {
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.15);
}
&.normal {
border-left: 4px solid #67c23a;
}
&.warning {
border-left: 4px solid #e6a23c;
}
&.danger {
border-left: 4px solid #f56c6c;
}
}
.metric-content {
display: flex;
align-items: center;
}
.metric-icon {
margin-right: 16px;
color: #409eff;
}
.metric-info {
flex: 1;
}
.metric-title {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.metric-value {
font-size: 24px;
font-weight: bold;
margin-bottom: 8px;
}
.metric-status {
text-align: right;
}
.monitor-charts {
margin-bottom: 20px;
}
.service-status {
margin-bottom: 20px;
}
.system-logs {
margin-bottom: 20px;
}
.chart-card,
.service-card,
.logs-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.log-controls {
display: flex;
gap: 10px;
align-items: center;
}
// 响应式设计
@media (max-width: 768px) {
.monitor-container {
padding: 10px;
}
.metric-content {
flex-direction: column;
text-align: center;
}
.metric-icon {
margin-right: 0;
margin-bottom: 10px;
}
}
</style>
3. 实践练习
练习 1:完善数据可视化
- 添加更多图表类型(饼图、雷达图等)
- 实现图表数据的实时更新
- 添加图表交互功能
- 实现图表数据导出
练习 2:系统监控功能
- 完善系统指标监控
- 添加告警功能
- 实现日志分析
- 添加性能报告生成
练习 3:性能优化
- 优化图表渲染性能
- 实现数据懒加载
- 添加缓存机制
- 优化内存使用
学习资源
作业
- 完成所有数据可视化组件
- 实现完整的系统监控功能
- 添加性能优化措施
- 编写组件文档和使用说明
总结
今天我们继续了综合项目实战,主要完成了:
- 数据可视化模块:集成 ECharts 实现了多种图表展示
- 系统监控功能:实现了实时监控和状态展示
- 性能优化实践:应用了多种性能优化技术
- 实时数据更新:建立了实时数据更新机制
下一步学习计划
明天我们将完成项目实战的最后部分:
- 错误处理和日志系统
- 单元测试和集成测试
- 项目部署和发布
- 项目总结和优化建议