feat: 评测任务完成进度收尾与指标维度汇总展示

- platform_store: 评测任务完成时置进度 100% 并落地 completed_time
- eval_runner: 新增指标维度汇总,供雷达图等维度展示
- 前端: 评测详情/列表展示优化、模型管理增强
This commit is contained in:
wuyongtao
2026-08-21 10:48:58 +08:00
parent 03254f8196
commit 0233755859
6 changed files with 151 additions and 41 deletions

View File

@@ -56,7 +56,19 @@ const overallScore = computed(() => formatScore(detail.value?.overall_score, det
const displayModelName = computed(() => detail.value?.model_name || String(detail.value?.model_id || '-'))
const displayMetric = computed(() => detail.value?.metric_label || detail.value?.metric || '-')
const progressDetail = computed(() => detail.value?.progress_detail)
const progressPercentage = computed(() => Math.max(0, Math.min(100, Math.round(Number(progressDetail.value?.percentage ?? completionRate.value)))))
const progressTotal = computed(() => Math.max(
Number(detail.value?.sample_count || 0),
Number(progressDetail.value?.total || 0),
Number(detail.value?.samples?.length || 0),
))
const progressCompleted = computed(() => detail.value?.status === 'completed'
? progressTotal.value
: Math.min(progressTotal.value || Number(detail.value?.completed_count || 0), Number(progressDetail.value?.completed ?? detail.value?.completed_count ?? 0)))
const progressPercentage = computed(() => detail.value?.status === 'completed'
? 100
: detail.value?.status === 'failed' || detail.value?.status === 'stopped'
? Math.max(0, Math.min(100, Math.round(Number(progressDetail.value?.percentage ?? detail.value?.progress ?? completionRate.value))))
: Math.max(0, Math.min(100, Math.round(Number(progressDetail.value?.percentage ?? completionRate.value)))))
const progressStage = computed(() => ({
dataset: '准备数据集',
model_loading: '加载模型',
@@ -64,14 +76,32 @@ const progressStage = computed(() => ({
metrics: '计算指标',
completed: '评测完成',
failed: '评测失败',
}[String(progressDetail.value?.stage || '')] || (detail.value?.status === 'running' ? '任务运行中' : '等待开始')))
}[detail.value?.status === 'completed' ? 'completed' : String(progressDetail.value?.stage || '')] || (detail.value?.status === 'running' ? '任务运行中' : '等待开始')))
const radarDimensions = computed(() => (detail.value?.dimension_summary || [])
.filter((item) => item.available !== false && Number.isFinite(Number(item.score)))
.map((item) => ({
name: item.name,
value: Math.max(0, Math.min(100, Number(item.score) / Math.max(Number(item.max_score) || 100, 1) * 100)),
})))
const radarDimensions = computed(() => {
const dimensions = new Map<string, { name: string; value: number }>()
for (const item of detail.value?.dimension_summary || []) {
if (item.available === false || !Number.isFinite(Number(item.score))) continue
dimensions.set(item.name, {
name: item.name,
value: Math.max(0, Math.min(100, Number(item.score) / Math.max(Number(item.max_score) || 100, 1) * 100)),
})
}
const metricLabels: Record<string, string> = {
bleu: 'BLEU',
rouge: 'ROUGE-L',
cosine: 'Cosine 相似度',
exact_match: '精确匹配',
text_similarity: '文本相似度',
}
for (const [key, metric] of Object.entries(detail.value?.basic_metrics || {})) {
const score = Number(metric?.score)
if (!Number.isFinite(score) || metric?.available === false) continue
const name = metricLabels[key] || key
if (!dimensions.has(name)) dimensions.set(name, { name, value: Math.max(0, Math.min(100, score)) })
}
return [...dimensions.values()]
})
const radarOption = computed<EChartsOption>(() => ({
tooltip: { trigger: 'item' },
@@ -197,7 +227,7 @@ onUnmounted(stopPolling)
</div>
<div class="overview-item">
<span>评测进度</span>
<strong>{{ progressDetail?.completed ?? detail.completed_count }} / {{ progressDetail?.total ?? detail.sample_count }}</strong>
<strong>{{ progressCompleted }} / {{ progressTotal }}</strong>
<el-progress :percentage="progressPercentage" :show-text="false" :stroke-width="5" />
<small>{{ progressStage }}{{ progressDetail?.message ? ' · ' + progressDetail.message : '' }}</small>
</div>

View File

@@ -65,6 +65,20 @@ function displayMetric(row: Partial<EvalTask>) {
return row.metric_label || row.metric || '-'
}
function progressPercentage(row: Partial<EvalTask>) {
if (row.status === 'completed') return 100
return Math.max(0, Math.min(100, Math.round(Number(row.progress_detail?.percentage ?? row.progress ?? 0))))
}
function progressCompleted(row: Partial<EvalTask>) {
if (row.status === 'completed') return row.progress_detail?.total ?? '-'
return row.progress_detail?.completed ?? 0
}
function progressTotal(row: Partial<EvalTask>) {
return row.progress_detail?.total ?? '-'
}
const { start: startPolling, stop: stopPolling } = usePolling(
async () => {
await loadEvalList({ silent: true })
@@ -125,18 +139,15 @@ onUnmounted(() => {
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="评分" prop="score" width="100" align="center" />
<el-table-column label="评分" prop="score" width="100" align="center">
<template #default="{ row }">
{{ row.score == null ? '-' : `${Number(row.score).toFixed(2)} / 100` }}
</template>
</el-table-column>
<el-table-column label="评测进度" width="130" align="center">
<template #default="{ row }">
<template v-if="ACTIVE_STATUSES.has(String(row.status || ''))">
<el-progress
:percentage="Math.max(0, Math.min(100, Math.round(Number(row.progress_detail?.percentage ?? row.progress ?? 0))))"
:stroke-width="6"
:show-text="false"
/>
<small>{{ row.progress_detail?.completed ?? 0 }} / {{ row.progress_detail?.total ?? '-' }}</small>
</template>
<span v-else>{{ row.score == null ? '-' : Number(row.score).toFixed(2) + ' / 100' }}</span>
<el-progress :percentage="progressPercentage(row)" :stroke-width="6" :show-text="false" />
<small>{{ progressCompleted(row) }} / {{ progressTotal(row) }}</small>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">

View File

@@ -56,8 +56,8 @@ async function handleMerge() {
compute_node_id: form.compute_node_id,
output_model_name: `${form.model_name}-merged`,
})
ElMessage.success('合并成功')
router.push('/model-manage')
ElMessage.success('合并任务已提交,完成后会自动更新状态')
router.push({ path: '/model-manage', query: { tab: 'trained' } })
} catch {
// ignore
} finally {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import DataTablePage from '@/components/DataTablePage.vue'
@@ -20,6 +20,7 @@ import { MODEL_SOURCE_MAP, MODEL_TYPE_MAP, PURPOSE_MAP } from '@/constants'
import type { ModelItem, TrainedModel } from '@/types'
import { mergeStatusLabel, mergeStatusType, statusLabel, statusTagType } from '@/utils/status'
import { useAuthStore } from '@/stores/auth'
import { usePolling } from '@/composables/usePolling'
const router = useRouter()
const auth = useAuthStore()
@@ -35,7 +36,7 @@ type TrainedModelRuntime = {
loaded: boolean
}
const activeTab = ref<TabKey>('config')
const activeTab = ref<TabKey>(router.currentRoute.value.query.tab === 'trained' ? 'trained' : 'config')
const loading = ref(false)
const configList = ref<ModelItem[]>([])
const trainedList = ref<TrainedModel[]>([])
@@ -131,19 +132,36 @@ async function handleExport(row: TrainedModel) {
}
}
async function loadTrained() {
loading.value = true
async function loadTrained(silent = false) {
if (!silent) loading.value = true
try {
const res = await getTrainedModels()
trainedList.value = res?.models || []
} finally {
loading.value = false
if (!silent) loading.value = false
}
}
function loadData() {
function hasActiveMerge() {
return trainedList.value.some((item) => Boolean(item.merging))
}
const { start: startTrainedPolling, stop: stopTrainedPolling } = usePolling(
async () => {
await loadTrained(true)
if (!hasActiveMerge()) stopTrainedPolling()
},
3000,
{ immediate: false },
)
async function loadData() {
if (activeTab.value === 'config') loadConfig()
else loadTrained()
else {
await loadTrained()
if (hasActiveMerge()) startTrainedPolling()
else stopTrainedPolling()
}
}
async function loadTrainedRuntime(row: TrainedModel, force = false) {
@@ -217,9 +235,16 @@ function handleRefresh() {
else loadTrained()
}
watch(activeTab, loadData)
watch(activeTab, () => {
stopTrainedPolling()
void loadData()
})
onMounted(loadData)
onMounted(() => {
void loadData()
})
onUnmounted(stopTrainedPolling)
</script>
<template>