feat: 评测任务完成进度收尾与指标维度汇总展示
- platform_store: 评测任务完成时置进度 100% 并落地 completed_time - eval_runner: 新增指标维度汇总,供雷达图等维度展示 - 前端: 评测详情/列表展示优化、模型管理增强
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user