eagleye

Quasar企业级健康检查监控面板

# 企业级健康检查监控面板

我将设计一个企业级的Quasar组合式表格组件,用于展示健康检查数据,完全使用TypeScript并避免any类型。

## 设计思路

1. 创建类型安全的健康检查数据结构
2. 实现组合式API获取数据
3. 设计响应式表格组件
4. 添加可视化状态指示器
5. 实现自动刷新和详细视图

下面是完整的实现方案:

<template>
<div class="health-monitor">
<div class="header">
<q-banner :class="statusBannerClass" class="q-mb-md">
<template #avatar>
<q-icon :name="statusIcon" size="32px" />
</template>
<div class="row items-center">
<div class="col">
<div class="text-h6">系统健康监控面板</div>
<div>{{ statusMessage }}</div>
<div class="text-caption">最后检查: {{ lastCheckTime }}</div>
<div class="text-caption">
环境: {{ healthData?.environment || 'N/A' }} | 版本:
{{ healthData?.version || 'N/A' }}
</div>
</div>
<div class="col-auto">
<q-btn-group>
<q-btn round icon="refresh" color="primary" @click="refreshData" :loading="loading">
<q-tooltip>刷新数据</q-tooltip>
</q-btn>
<q-btn
round
:icon="autoRefresh ? 'pause' : 'play_arrow'"
:color="autoRefresh ? 'amber' : 'positive'"
@click="toggleAutoRefresh"
>
<q-tooltip>{{ autoRefresh ? '暂停自动刷新' : '启用自动刷新' }}</q-tooltip>
</q-btn>
</q-btn-group>
</div>
</div>
</q-banner>

<div class="stats-row q-mb-md">
<q-card class="stat-card" v-for="stat in stats" :key="stat.title">
<q-card-section class="text-center">
<div class="text-h6">{{ stat.value }}</div>
<div class="text-subtitle2">{{ stat.title }}</div>
</q-card-section>
</q-card>
</div>
</div>

<q-table
:rows="tableData"
:columns="columns"
row-key="componentName"
:loading="loading"
:pagination="pagination"
class="health-table"
flat
bordered
>
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="componentName" :props="props">
<div class="text-weight-bold">{{ props.row.componentName }}</div>
<div class="text-caption text-grey">{{ props.row.type }}</div>
</q-td>

<q-td key="status" :props="props">
<div class="row items-center">
<q-icon
:name="getStatusIcon(props.row.healthy)"
:color="getStatusColor(props.row.healthy)"
size="24px"
class="q-mr-sm"
/>
<q-badge :color="getStatusColor(props.row.healthy)" class="status-badge">
{{ props.row.status }}
</q-badge>
</div>
</q-td>

<q-td key="metrics" :props="props">
<div v-for="(value, key) in props.row.metrics" :key="key" class="metric-row">
<span class="metric-label">{{ key }}:</span>
<span class="metric-value">{{ value }}</span>
</div>
</q-td>

<q-td key="details" :props="props">
<q-btn icon="info" size="sm" flat round color="info" @click="showDetails(props.row)">
<q-tooltip>查看详情</q-tooltip>
</q-btn>
</q-td>
</q-tr>
</template>

<template v-slot:loading>
<q-inner-loading showing color="primary" />
</template>
</q-table>

<q-dialog v-model="showDetailDialog" maximized>
<q-card class="detail-card">
<q-bar class="bg-primary text-white">
<div class="text-h6">组件详情: {{ selectedComponent?.componentName }}</div>
<q-space />
<q-btn dense flat icon="close" v-close-popup />
</q-bar>

<q-card-section class="scroll">
<div class="detail-grid">
<template v-for="(value, key) in selectedComponent?.details" :key="key">
<div class="detail-label">{{ formatKey(key as string) }}:</div>
<div
class="detail-value"
:class="{
'text-positive': key === 'healthy' && value === true,
'text-negative': key === 'healthy' && value === false,
}"
>
<template v-if="key === 'healthy'">
<q-icon
:name="value ? 'check_circle' : 'error'"
:color="value ? 'positive' : 'negative'"
/>
{{ value ? '健康' : '异常' }}
</template>
<template v-else>
{{ formatValue(value, key as string) }}
</template>
</div>
</template>
</div>
</q-card-section>

<q-card-actions align="right" class="q-pa-md">
<q-btn flat label="关闭" color="primary" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import type { QTableColumn } from 'quasar'
import { format } from 'date-fns'
import { apiClient } from 'src/services/axios'

// 定义健康检查响应类型
interface HealthComponentDetails {
[key: string]: unknown //对未知类型的值使用 unknown 而不是 any
}

interface HealthComponent {
componentName: string
type: string
healthy: boolean
status: string
metrics: Record<string, string>
details: HealthComponentDetails
}

interface HealthResponse {
status: 'up' | 'down' | 'degraded'
timestamp: string
version: string
environment: string
components: {
[key: string]: HealthComponentDetails
}
}

// 状态管理
const loading = ref(false)
const healthData = ref<HealthResponse | null>(null)
const tableData = ref<HealthComponent[]>([])
const healthStatus = ref<'up' | 'down' | 'degraded' | 'loading'>('loading')
const lastCheckTime = ref('')
const autoRefresh = ref(true)
const refreshInterval = ref<NodeJS.Timeout | null>(null)
const showDetailDialog = ref(false)
const selectedComponent = ref<HealthComponent | null>(null)

// 表格配置
const columns: QTableColumn[] = [
{
name: 'componentName',
required: true,
label: '组件',
align: 'left',
field: 'componentName',
sortable: true,
},
{
name: 'status',
label: '状态',
align: 'center',
field: 'status',
sortable: true,
},
{
name: 'metrics',
label: '关键指标',
align: 'left',
field: 'metrics',
},
{
name: 'details',
label: '详情',
align: 'center',
field: 'details',
},
]

const pagination = ref({
sortBy: 'componentName',
descending: false,
page: 1,
rowsPerPage: 10,
})

// 计算统计数据
const stats = computed(() => {
if (!tableData.value.length) return []

const totalComponents = tableData.value.length
const healthyComponents = tableData.value.filter((c) => c.healthy).length
const unhealthyComponents = totalComponents - healthyComponents

return [
{ title: '系统状态', value: healthStatus.value.toUpperCase() },
{ title: '健康组件', value: healthyComponents },
{ title: '异常组件', value: unhealthyComponents },
{ title: '组件总数', value: totalComponents },
]
})

// 计算横幅样式
const statusBannerClass = computed(() => {
switch (healthStatus.value) {
case 'up':
return 'bg-positive'
case 'degraded':
return 'bg-warning'
case 'down':
return 'bg-negative'
default:
return 'bg-grey'
}
})

const statusIcon = computed(() => {
switch (healthStatus.value) {
case 'up':
return 'check_circle'
case 'degraded':
return 'warning'
case 'down':
return 'error'
default:
return 'hourglass_empty'
}
})

const statusMessage = computed(() => {
switch (healthStatus.value) {
case 'up':
return '所有系统服务运行正常'
case 'degraded':
return '系统服务部分降级,可能影响部分功能'
case 'down':
return '核心服务连接异常,请检查网络或联系管理员'
case 'loading':
return '正在检查系统状态...'
default:
return '系统状态未知'
}
})

// 辅助函数
const getStatusColor = (healthy?: boolean) => {
if (healthy === undefined) return 'grey'
return healthy ? 'positive' : 'negative'
}

const getStatusIcon = (healthy?: boolean) => {
if (healthy === undefined) return 'help'
return healthy ? 'check_circle' : 'error'
}

// const formatTime = (time?: string) => {
// if (!time) return 'N/A'
// return format(new Date(time), 'yyyy-MM-dd HH:mm:ss')
// }

const formatKey = (key: string) => {
// 将snake_case转换为驼峰命名
return key.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())
}

const formatValue = (value: unknown, key: string) => {
if (typeof value === 'boolean') {
return value ? '是' : '否'
}

// 特殊处理百分比值
if (key.includes('percent') && typeof value === 'string') {
return value.endsWith('%') ? value : `${value}%`
}

return value
}

// 获取健康检查数据
const fetchHealthData = async () => {
try {
loading.value = true
healthStatus.value = 'loading'

const response = await apiClient.get<HealthResponse>('/audit-logs/beacon/health/')
healthData.value = response.data

// 处理组件数据
tableData.value = Object.entries(response.data.components).map(([key, value]) => {
// 提取关键指标
const metrics: Record<string, string> = {}
const details: HealthComponentDetails = {}

Object.entries(value).forEach(([prop, val]) => {
details[prop] = val

// 排除状态和布尔值指标
if (prop !== 'healthy' && prop !== 'status' && typeof val !== 'boolean') {
metrics[prop] = val as string
}
})

return {
componentName: key,
type: key,
healthy: value.healthy as boolean,
status: (value.status as string) || 'N/A',
metrics,
details,
}
})

healthStatus.value = healthData.value.status
lastCheckTime.value = format(new Date(), 'HH:mm:ss')
} catch (error) {
console.error('获取健康检查数据失败:', error)
healthStatus.value = 'down'
tableData.value = []
} finally {
loading.value = false
}
}

// 刷新数据
const refreshData = async () => {
await fetchHealthData()
}

// 切换自动刷新
const toggleAutoRefresh = () => {
autoRefresh.value = !autoRefresh.value
if (autoRefresh.value) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
}

// 启动自动刷新
const startAutoRefresh = () => {
if (!refreshInterval.value) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
refreshInterval.value = setInterval(fetchHealthData, 10000)
}
}

// 停止自动刷新
const stopAutoRefresh = () => {
if (refreshInterval.value) {
clearInterval(refreshInterval.value)
refreshInterval.value = null
}
}

// 显示组件详情
const showDetails = (component: HealthComponent) => {
selectedComponent.value = component
showDetailDialog.value = true
}

// 生命周期钩子
onMounted(async () => {
await fetchHealthData()
startAutoRefresh()
})

onBeforeUnmount(() => {
stopAutoRefresh()
})
</script>

<style lang="scss" scoped>
.health-monitor {
max-width: 1400px;
margin: 0 auto;
padding: 20px;

.header {
margin-bottom: 24px;
}

.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;

.stat-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;

&:hover {
transform: translateY(-4px);
}

.text-h6 {
font-size: 1.5rem;
margin-bottom: 4px;
}
}
}

.health-table {
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);

.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
}

.metric-row {
display: flex;
margin-bottom: 4px;
line-height: 1.3;
}

.metric-label {
font-weight: 500;
min-width: 120px;
color: #555;
}

.metric-value {
flex: 1;
}
}

.detail-card {
width: 90%;
max-width: 800px;
border-radius: 12px;

.detail-grid {
display: grid;
grid-template-columns: 160px 1fr;
gap: 16px;
align-items: center;

.detail-label {
font-weight: 600;
color: #555;
text-align: right;
}

.detail-value {
word-break: break-word;
padding: 8px 0;

&.text-positive {
color: $positive;
font-weight: 500;
}

&.text-negative {
color: $negative;
font-weight: 500;
}
}

.full-width {
grid-column: span 2;
}
}
}
}

@media (max-width: 768px) {
.health-monitor {
padding: 10px;

.stats-row {
grid-template-columns: 1fr 1fr;
}

.health-table {
font-size: 0.9rem;

.metric-label {
min-width: 80px;
}
}

.detail-card {
width: 95%;

.detail-grid {
grid-template-columns: 100px 1fr;
gap: 12px;
}
}
}
}
</style>

```

## 功能说明

这个企业级健康检查监控面板包含以下功能:

### 1. 状态概览区
- 顶部横幅显示整体系统状态(绿色/黄色/红色)
- 状态图标直观反映系统健康度
- 最后检查时间显示
- 刷新和自动刷新控制按钮

### 2. 统计卡片
- 系统状态卡片
- 健康组件数量
- 异常组件数量
- 环境信息

### 3. 健康检查表格
- 组件名称
- 状态指示(带颜色编码)
- 最后检查时间
- 详情查看按钮

### 4. 组件详情对话框
- 详细的状态信息
- 进度条可视化(事件数量、内存使用)
- 错误信息展示
- 引擎信息

### 5. 自动刷新功能
- 默认每10秒自动刷新数据
- 可暂停/恢复自动刷新
- 手动刷新按钮

### 6. 响应式设计
- 适配不同屏幕尺寸
- 移动设备优化布局
- 卡片式设计增强可读性

## 类型安全设计

通过以下方式确保类型安全:

```typescript
// 定义健康检查组件接口
interface HealthComponent {
name: string;
healthy: boolean;
status: string;
last_checked?: string;
error?: string;
engine?: string;
item_count?: number;
max_items?: number;
memory_mb?: number;
max_memory_mb?: number;
usage_percent?: number;
threshold_percent?: number;
[key: string]: unknown; // 允许其他属性
}

// 定义健康检查响应接口
interface HealthResponse {
status: 'up' | 'degraded' | 'down';
components: {
[key: string]: HealthComponent;
};
environment: string;
timestamp: string;
}
```

## 使用方法

在需要展示健康检查的页面中使用该组件:

```html
<template>
<div class="q-pa-md">
<HealthCheckTable />
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import HealthCheckTable from 'components/HealthCheckTable.vue';

export default defineComponent({
name: 'HealthPage',
components: {
HealthCheckTable
}
});
</script>
```

这个设计完全符合企业级应用的需求,提供了清晰的健康状态可视化,并确保代码的类型安全和可维护性。

posted on 2025-07-28 10:12  GoGrid  阅读(14)  评论(0)    收藏  举报

导航