feat: 诊断过程实时进度展示 (SSE流式推送)
- 新增 GET /api/diagnose/stream SSE接口, 每完成一项检查实时推送 - 前端: 诊断中显示步骤进度条+每项检查状态(运行中/完成/问题数) - 解决诊断过程'卡着'无反馈的体验问题 - 保留 POST /api/diagnose 兼容旧接口
This commit is contained in:
+108
-15
@@ -11,10 +11,10 @@
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-input v-model="namespace" placeholder="命名空间 (留空=全部)" clearable style="width: 200px" />
|
||||
<el-button type="primary" :loading="diagnosing" @click="runDiagnosis" :icon="Refresh">
|
||||
<el-button type="primary" :loading="diagnosing" @click="runDiagnosis" :icon="Refresh" :disabled="diagnosing">
|
||||
{{ diagnosing ? '诊断中...' : '一键诊断' }}
|
||||
</el-button>
|
||||
<el-button type="success" :loading="analyzing" @click="runAnalysis" :icon="MagicStick" :disabled="!diagnosis">
|
||||
<el-button type="success" :loading="analyzing" @click="runAnalysis" :icon="MagicStick" :disabled="!diagnosis || diagnosing">
|
||||
{{ analyzing ? 'AI 分析中...' : 'AI 分析' }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -28,10 +28,34 @@
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<!-- 诊断中 -->
|
||||
<div v-if="diagnosing" class="loading-box">
|
||||
<el-icon class="is-loading" :size="48" color="#409eff"><Loading /></el-icon>
|
||||
<p>正在执行集群诊断,请稍候...</p>
|
||||
<!-- 诊断中 - 实时进度 -->
|
||||
<div v-if="diagnosing" class="progress-box">
|
||||
<el-card shadow="never" class="progress-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>🔍 正在执行集群诊断</span>
|
||||
<el-tag size="small" type="info">{{ progressStep }}/{{ progressTotal }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-progress :percentage="progressPercent" :stroke-width="10" :color="'#409eff'" style="margin-bottom: 20px" />
|
||||
<div class="progress-steps">
|
||||
<div v-for="item in progressList" :key="item.name" class="progress-item" :class="'status-' + item.status">
|
||||
<span class="step-icon">
|
||||
<el-icon v-if="item.status === 'running'" class="is-loading" color="#409eff"><Loading /></el-icon>
|
||||
<el-icon v-else-if="item.status === 'done'" color="#67c23a"><CircleCheckFilled /></el-icon>
|
||||
<el-icon v-else color="#c0c4cc"><CircleClose /></el-icon>
|
||||
</span>
|
||||
<span class="step-title">{{ item.title }}</span>
|
||||
<span v-if="item.status === 'done'" class="step-result">
|
||||
<el-tag v-if="item.issues > 0" size="small" :type="item.issues > 0 ? 'warning' : 'success'">
|
||||
{{ item.issues }} 个问题
|
||||
</el-tag>
|
||||
<el-tag v-else size="small" type="success">正常</el-tag>
|
||||
</span>
|
||||
<span v-if="item.status === 'running'" class="step-running">检查中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 诊断结果 -->
|
||||
@@ -195,7 +219,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { Refresh, MagicStick, Loading, WarningFilled, Warning, Promotion, Monitor } from '@element-plus/icons-vue'
|
||||
import { Refresh, MagicStick, Loading, WarningFilled, Warning, Promotion, Monitor, CircleCheckFilled, CircleClose } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const API = '/api'
|
||||
@@ -211,6 +235,16 @@ const chatInput = ref('')
|
||||
const chatLoading = ref(false)
|
||||
const chatBoxRef = ref(null)
|
||||
|
||||
// 诊断进度
|
||||
const progressList = ref([])
|
||||
const progressStep = ref(0)
|
||||
const progressTotal = ref(9)
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (progressTotal.value === 0) return 0
|
||||
return Math.round((progressStep.value / progressTotal.value) * 100)
|
||||
})
|
||||
|
||||
const statusTagType = computed(() => {
|
||||
if (!diagnosis.value) return 'info'
|
||||
const s = diagnosis.value.status
|
||||
@@ -238,14 +272,59 @@ function barColor(pct) {
|
||||
async function runDiagnosis() {
|
||||
diagnosing.value = true
|
||||
aiAnalysis.value = ''
|
||||
diagnosis.value = null
|
||||
progressList.value = []
|
||||
progressStep.value = 0
|
||||
progressTotal.value = 9
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${API}/diagnose`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ namespace: namespace.value }),
|
||||
})
|
||||
diagnosis.value = await resp.json()
|
||||
ElMessage.success(`诊断完成,评分 ${diagnosis.value.score}/100`)
|
||||
const params = new URLSearchParams()
|
||||
if (namespace.value) params.set('namespace', namespace.value)
|
||||
const url = `${API}/diagnose/stream?${params.toString()}`
|
||||
|
||||
const resp = await fetch(url)
|
||||
const reader = resp.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue
|
||||
const jsonStr = line.slice(6)
|
||||
if (!jsonStr) continue
|
||||
|
||||
try {
|
||||
const msg = JSON.parse(jsonStr)
|
||||
|
||||
if (msg.type === 'progress') {
|
||||
progressTotal.value = msg.total
|
||||
progressStep.value = msg.step
|
||||
|
||||
if (msg.status === 'running') {
|
||||
progressList.value.push({ name: msg.name, title: msg.title, status: 'running', issues: 0 })
|
||||
} else if (msg.status === 'done') {
|
||||
const item = progressList.value.find(i => i.name === msg.name)
|
||||
if (item) {
|
||||
item.status = 'done'
|
||||
item.issues = msg.issues
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'complete') {
|
||||
diagnosis.value = msg.diagnosis
|
||||
ElMessage.success(`诊断完成,评分 ${msg.diagnosis.score}/100`)
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore parse errors on partial data
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('诊断失败: ' + e.message)
|
||||
} finally {
|
||||
@@ -322,7 +401,21 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC'
|
||||
.app-main { flex: 1; padding: 20px 24px; max-width: 1400px; margin: 0 auto; width: 100%; }
|
||||
|
||||
.welcome { display: flex; justify-content: center; align-items: center; min-height: 400px; }
|
||||
.loading-box { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 300px; gap: 16px; color: #909399; }
|
||||
|
||||
/* 诊断进度 */
|
||||
.progress-box { display: flex; justify-content: center; padding-top: 40px; }
|
||||
.progress-card { width: 600px; }
|
||||
.progress-steps { display: flex; flex-direction: column; gap: 4px; }
|
||||
.progress-item {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 12px; border-radius: 6px; transition: background .2s;
|
||||
}
|
||||
.progress-item.status-running { background: #ecf5ff; }
|
||||
.progress-item.status-done { background: #f0f9eb; }
|
||||
.step-icon { display: flex; align-items: center; width: 20px; }
|
||||
.step-title { flex: 1; font-size: 14px; color: #303133; }
|
||||
.step-result { font-size: 12px; }
|
||||
.step-running { font-size: 12px; color: #409eff; }
|
||||
|
||||
.summary-row { margin-bottom: 16px; }
|
||||
.score-card { text-align: center; }
|
||||
|
||||
Reference in New Issue
Block a user