2025-06-11 10:18:18 +08:00
|
|
|
|
<template>
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
<!-- 左侧:训练控制 -->
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
<el-card>
|
|
|
|
|
<template #header>
|
|
|
|
|
<span>启动模型训练</span>
|
|
|
|
|
</template>
|
|
|
|
|
<el-form :model="form" label-width="80px">
|
|
|
|
|
<el-form-item label="产品">
|
|
|
|
|
<el-select v-model="form.product_id" placeholder="请选择产品" filterable>
|
|
|
|
|
<el-option v-for="item in products" :key="item.product_id" :label="item.product_name" :value="item.product_id" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="模型类型">
|
|
|
|
|
<el-select v-model="form.model_type" placeholder="请选择模型">
|
2025-06-18 06:39:41 +08:00
|
|
|
|
<el-option v-for="item in modelTypes" :key="item.id" :label="item.name" :value="item.id" />
|
2025-06-11 10:18:18 +08:00
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="训练轮次">
|
|
|
|
|
<el-input-number v-model="form.epochs" :min="1" :max="1000" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item>
|
|
|
|
|
<el-button type="primary" @click="startTraining" :loading="trainingLoading">启动训练</el-button>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
<!-- 右侧:任务状态 -->
|
|
|
|
|
<el-col :span="16">
|
|
|
|
|
<el-card>
|
|
|
|
|
<template #header>
|
|
|
|
|
<span>训练任务队列</span>
|
|
|
|
|
</template>
|
|
|
|
|
<el-table :data="trainingTasks" stripe>
|
|
|
|
|
<el-table-column prop="task_id" label="任务ID" width="120" show-overflow-tooltip></el-table-column>
|
|
|
|
|
<el-table-column prop="product_id" label="产品ID" width="100"></el-table-column>
|
|
|
|
|
<el-table-column prop="model_type" label="模型类型" width="120"></el-table-column>
|
|
|
|
|
<el-table-column prop="status" label="状态" width="100">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-tag :type="statusTag(row.status)">{{ statusText(row.status) }}</el-tag>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="start_time" label="创建时间">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
{{ formatDateTime(row.start_time) }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column label="详情">
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<el-popover placement="left" trigger="hover" width="400">
|
|
|
|
|
<template #reference>
|
|
|
|
|
<el-button type="text" size="small">查看</el-button>
|
|
|
|
|
</template>
|
|
|
|
|
<div v-if="row.status === 'completed'">
|
|
|
|
|
<h4>评估指标</h4>
|
|
|
|
|
<pre>{{ JSON.stringify(row.metrics, null, 2) }}</pre>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="row.status === 'failed'">
|
|
|
|
|
<h4>错误信息</h4>
|
|
|
|
|
<p>{{ row.error }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="row.status === 'running' || row.status === 'pending'">
|
|
|
|
|
<p>任务正在进行中...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</el-popover>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { ref, onMounted, onUnmounted, reactive } from 'vue'
|
|
|
|
|
import axios from 'axios'
|
|
|
|
|
import { ElMessage, ElPopover, ElButton, ElTag } from 'element-plus'
|
|
|
|
|
|
|
|
|
|
const products = ref([])
|
2025-06-18 06:39:41 +08:00
|
|
|
|
const modelTypes = ref([])
|
2025-06-11 10:18:18 +08:00
|
|
|
|
const trainingLoading = ref(false)
|
|
|
|
|
const form = reactive({
|
|
|
|
|
product_id: '',
|
2025-06-18 06:39:41 +08:00
|
|
|
|
model_type: '',
|
2025-06-11 10:18:18 +08:00
|
|
|
|
epochs: 50,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const trainingTasks = ref([])
|
|
|
|
|
let pollInterval = null;
|
|
|
|
|
|
|
|
|
|
const fetchProducts = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.get('/api/products')
|
|
|
|
|
if (response.data.status === 'success') {
|
|
|
|
|
products.value = response.data.data
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('获取产品列表失败')
|
|
|
|
|
console.error(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-18 06:39:41 +08:00
|
|
|
|
const fetchModelTypes = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.get('/api/model_types')
|
|
|
|
|
if (response.data.status === 'success') {
|
|
|
|
|
modelTypes.value = response.data.data
|
|
|
|
|
if (modelTypes.value.length > 0 && !form.model_type) {
|
|
|
|
|
form.model_type = modelTypes.value[0].id
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
ElMessage.error('获取模型类型列表失败')
|
|
|
|
|
console.error(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-11 10:18:18 +08:00
|
|
|
|
const fetchTrainingTasks = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.get('/api/training')
|
|
|
|
|
if (response.data.status === 'success') {
|
|
|
|
|
trainingTasks.value = response.data.data
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// 第一次加载或轮询失败时提示,避免重复提示
|
|
|
|
|
if (!pollInterval) ElMessage.error('获取训练任务列表失败');
|
|
|
|
|
console.error('获取训练任务列表失败', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const startTraining = async () => {
|
|
|
|
|
if (!form.product_id || !form.model_type) {
|
|
|
|
|
ElMessage.warning('请选择产品和模型类型')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
trainingLoading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.post('/api/training', form)
|
|
|
|
|
if (response.data.task_id) {
|
|
|
|
|
ElMessage.success(`训练任务 ${response.data.task_id} 已启动`)
|
|
|
|
|
// 立即刷新一次列表
|
|
|
|
|
fetchTrainingTasks();
|
|
|
|
|
} else {
|
|
|
|
|
ElMessage.error(response.data.error || '启动训练失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const errorMsg = error.response?.data?.error || '启动训练请求失败';
|
|
|
|
|
ElMessage.error(errorMsg);
|
|
|
|
|
console.error(error);
|
|
|
|
|
} finally {
|
|
|
|
|
trainingLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const statusTag = (status) => {
|
|
|
|
|
if (status === 'completed') return 'success'
|
|
|
|
|
if (status === 'running') return 'primary'
|
|
|
|
|
if (status === 'pending') return 'warning'
|
|
|
|
|
if (status === 'failed') return 'danger'
|
|
|
|
|
return 'info'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const statusText = (status) => {
|
|
|
|
|
const map = {
|
|
|
|
|
'pending': '等待中',
|
|
|
|
|
'running': '进行中',
|
|
|
|
|
'completed': '已完成',
|
|
|
|
|
'failed': '失败'
|
|
|
|
|
};
|
|
|
|
|
return map[status] || '未知';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatDateTime = (isoString) => {
|
|
|
|
|
if (!isoString) return 'N/A';
|
|
|
|
|
return new Date(isoString).toLocaleString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
fetchProducts()
|
2025-06-18 06:39:41 +08:00
|
|
|
|
fetchModelTypes()
|
2025-06-11 10:18:18 +08:00
|
|
|
|
fetchTrainingTasks() // 初始加载
|
|
|
|
|
pollInterval = setInterval(fetchTrainingTasks, 5000) // 每5秒轮询
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (pollInterval) {
|
|
|
|
|
clearInterval(pollInterval)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
</script>
|