This commit is contained in:
aaron 2025-03-10 20:19:04 +08:00
parent 65e828b875
commit 71ab5f512d
2 changed files with 475 additions and 0 deletions

View File

@ -0,0 +1,465 @@
<template>
<div class="system-health-container">
<div class="health-card">
<h1 class="page-title">系统健康状态</h1>
<div class="action-buttons">
<button
class="btn-refresh"
@click="fetchHealthStats"
:disabled="isLoading"
>
{{ isLoading ? '加载中...' : '刷新数据' }}
</button>
<div class="toggle-container">
<label class="toggle-label">
<input type="checkbox" v-model="includeSlowQueries">
包含慢查询
</label>
<label class="toggle-label">
<input type="checkbox" v-model="includeLongSessions">
包含长会话
</label>
</div>
</div>
<div v-if="errorMessage" class="status-message status-error">
{{ errorMessage }}
</div>
<div v-if="isLoading" class="loading-indicator">
<div class="spinner"></div>
<div>加载系统健康数据...</div>
</div>
<div v-if="healthData && !isLoading" class="health-data">
<div class="health-grid">
<!-- 连接池信息 -->
<div class="data-section">
<h2 class="section-title">数据库连接池</h2>
<div class="data-grid">
<div class="data-item">
<div class="data-label">池大小</div>
<div class="data-value">{{ healthData.connection_pool.pool_size }}</div>
</div>
<div class="data-item">
<div class="data-label">已签入</div>
<div class="data-value">{{ healthData.connection_pool.checkedin }}</div>
</div>
<div class="data-item">
<div class="data-label">已签出</div>
<div class="data-value">{{ healthData.connection_pool.checkedout }}</div>
</div>
<div class="data-item">
<div class="data-label">溢出</div>
<div class="data-value" :class="{'value-warning': healthData.connection_pool.overflow > 0}">
{{ healthData.connection_pool.overflow }}
</div>
</div>
</div>
</div>
<!-- 会话信息 -->
<div class="data-section">
<h2 class="section-title">会话信息</h2>
<div class="data-grid">
<div class="data-item">
<div class="data-label">活跃会话</div>
<div class="data-value">{{ healthData.sessions.active_count }}</div>
</div>
<div class="data-item">
<div class="data-label">长时间运行会话</div>
<div class="data-value" :class="{'value-warning': healthData.sessions.long_running_count > 0}">
{{ healthData.sessions.long_running_count }}
</div>
</div>
<div class="data-item">
<div class="data-label">慢查询数量</div>
<div class="data-value" :class="{'value-warning': healthData.performance_stats.slow_queries_count > 0}">
{{ healthData.performance_stats.slow_queries_count }}
</div>
</div>
</div>
</div>
</div>
<!-- 性能统计 -->
<div class="data-section">
<h2 class="section-title">性能统计</h2>
<div class="stats-grid">
<!-- 最慢端点 -->
<div class="subsection">
<h3 class="subsection-title">最慢端点</h3>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>端点</th>
<th>响应时间 ()</th>
</tr>
</thead>
<tbody>
<tr v-for="(endpoint, index) in healthData.performance_stats.top_slow_endpoints" :key="index">
<td>{{ endpoint[0] }}</td>
<td>{{ endpoint[1].toFixed(4) }}</td>
</tr>
<tr v-if="healthData.performance_stats.top_slow_endpoints.length === 0">
<td colspan="2" class="empty-message">无数据</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 每小时请求 -->
<div class="subsection">
<h3 class="subsection-title">每小时请求数</h3>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>时间</th>
<th>请求数</th>
</tr>
</thead>
<tbody>
<tr v-for="(count, hour) in healthData.performance_stats.hourly_requests" :key="hour">
<td>{{ hour }}</td>
<td>{{ count }}</td>
</tr>
<tr v-if="Object.keys(healthData.performance_stats.hourly_requests).length === 0">
<td colspan="2" class="empty-message">无数据</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import apiClient from '../services/api'
export default {
name: 'SystemHealth',
setup() {
const healthData = ref(null)
const isLoading = ref(false)
const errorMessage = ref('')
const includeSlowQueries = ref(false)
const includeLongSessions = ref(false)
//
const fetchHealthStats = async () => {
isLoading.value = true
errorMessage.value = ''
try {
const params = {
include_slow_queries: includeSlowQueries.value,
include_long_sessions: includeLongSessions.value
}
const response = await apiClient.get('/api/health/stats', { params })
if (response.data && response.data.code === 200) {
healthData.value = response.data.data
} else {
errorMessage.value = response.data?.message || '获取健康数据失败'
}
} catch (error) {
console.error('获取健康数据失败:', error)
if (error.code === 'ECONNABORTED') {
errorMessage.value = '请求超时,请检查网络或稍后重试'
} else {
errorMessage.value = '获取健康数据失败,请重试'
}
} finally {
isLoading.value = false
}
}
//
watch([includeSlowQueries, includeLongSessions], () => {
fetchHealthStats()
})
onMounted(() => {
document.title = '系统健康状态 - 蜂快到家'
fetchHealthStats()
})
onBeforeUnmount(() => {
document.title = '蜂快到家'
})
return {
healthData,
isLoading,
errorMessage,
includeSlowQueries,
includeLongSessions,
fetchHealthStats
}
}
}
</script>
<style scoped>
.system-health-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
background: linear-gradient(to bottom, #FFCC00, #FFFFFF);
}
.health-card {
width: 100%;
max-width: 900px;
background-color: #FFFFFF;
border-radius: 15px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
padding: 20px 15px;
margin-bottom: 15px;
}
.page-title {
font-size: 20px;
font-weight: bold;
color: #333333;
text-align: center;
margin-bottom: 15px;
}
.action-buttons {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.btn-refresh {
padding: 8px 15px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
text-align: center;
border: none;
background-color: #FFCC00;
color: #333333;
}
.btn-refresh:hover {
background-color: #FFD633;
}
.btn-refresh:disabled {
background-color: #DDDDDD;
color: #999999;
cursor: not-allowed;
}
.toggle-container {
display: flex;
gap: 12px;
}
.toggle-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #666666;
cursor: pointer;
}
.status-message {
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
margin-bottom: 15px;
text-align: center;
}
.status-error {
background-color: rgba(255, 87, 87, 0.1);
color: #FF5757;
border: 1px solid rgba(255, 87, 87, 0.3);
}
.loading-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
color: #666666;
gap: 10px;
}
.spinner {
width: 30px;
height: 30px;
border: 3px solid rgba(255, 204, 0, 0.3);
border-radius: 50%;
border-top-color: #FFCC00;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.health-data {
display: flex;
flex-direction: column;
gap: 15px;
}
.health-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.data-section {
border: 1px solid #EEEEEE;
border-radius: 8px;
padding: 15px;
background-color: #FAFAFA;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333333;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #EEEEEE;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.subsection {
margin-bottom: 0;
}
.subsection-title {
font-size: 14px;
font-weight: 600;
color: #555555;
margin-bottom: 8px;
}
.data-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.data-item {
background-color: #FFFFFF;
border-radius: 6px;
padding: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.data-label {
font-size: 12px;
color: #666666;
margin-bottom: 3px;
}
.data-value {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.value-warning {
color: #FF5757;
}
.table-container {
overflow-x: auto;
margin-bottom: 0;
max-height: 150px;
overflow-y: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
.data-table th {
background-color: #F2F2F2;
padding: 8px;
text-align: left;
font-weight: 600;
color: #555555;
position: sticky;
top: 0;
z-index: 1;
}
.data-table td {
padding: 6px 8px;
border-top: 1px solid #EEEEEE;
}
.data-table tr:hover td {
background-color: rgba(255, 204, 0, 0.05);
}
.empty-message {
text-align: center;
color: #999999;
padding: 10px;
}
@media (max-width: 768px) {
.health-grid, .stats-grid {
grid-template-columns: 1fr;
}
.data-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.action-buttons {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.toggle-container {
justify-content: space-between;
}
.data-grid {
grid-template-columns: 1fr;
}
.data-value {
font-size: 16px;
}
}
</style>

View File

@ -7,6 +7,7 @@ import HowToUse from '../components/HowToUse.vue'
import CommunityRequest from '../components/CommunityRequest.vue'
import PartnerRequest from '../components/PartnerRequest.vue'
import ImageRecognition from '../components/ImageRecognition.vue'
import SystemHealth from '../components/SystemHealth.vue'
const routes = [
{
@ -69,6 +70,15 @@ const routes = [
meta: {
title: '蜂快AI·快递取件码识别'
}
},
{
path: '/system-health',
name: 'SystemHealth',
component: SystemHealth,
meta: {
title: '系统健康状态',
requiresAuth: true
}
}
]