update
This commit is contained in:
parent
65e828b875
commit
71ab5f512d
465
src/components/SystemHealth.vue
Normal file
465
src/components/SystemHealth.vue
Normal 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>
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user