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 CommunityRequest from '../components/CommunityRequest.vue'
|
||||||
import PartnerRequest from '../components/PartnerRequest.vue'
|
import PartnerRequest from '../components/PartnerRequest.vue'
|
||||||
import ImageRecognition from '../components/ImageRecognition.vue'
|
import ImageRecognition from '../components/ImageRecognition.vue'
|
||||||
|
import SystemHealth from '../components/SystemHealth.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -69,6 +70,15 @@ const routes = [
|
|||||||
meta: {
|
meta: {
|
||||||
title: '蜂快AI·快递取件码识别'
|
title: '蜂快AI·快递取件码识别'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/system-health',
|
||||||
|
name: 'SystemHealth',
|
||||||
|
component: SystemHealth,
|
||||||
|
meta: {
|
||||||
|
title: '系统健康状态',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user