286 lines
8.3 KiB
Vue
286 lines
8.3 KiB
Vue
|
|
<script setup lang="ts">
|
||
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||
|
|
import { useRouter } from 'vue-router'
|
||
|
|
import { ElMessage } from 'element-plus'
|
||
|
|
import DataTablePage from '@/components/DataTablePage.vue'
|
||
|
|
import ModelStatusTag from '@/components/ModelStatusTag.vue'
|
||
|
|
import { useModelsStore } from '@/stores/models'
|
||
|
|
import {
|
||
|
|
getFineTuneList,
|
||
|
|
deleteFineTune,
|
||
|
|
stopFineTune,
|
||
|
|
getFineTuneProgress,
|
||
|
|
getFineTune,
|
||
|
|
} from '@/api/modules/fineTune'
|
||
|
|
import { TRAIN_TYPE_MAP, TRAIN_METHOD_MAP } from '@/constants'
|
||
|
|
import type { FineTuneTask, TrainingProgress } from '@/types'
|
||
|
|
|
||
|
|
const router = useRouter()
|
||
|
|
const modelsStore = useModelsStore()
|
||
|
|
|
||
|
|
const loading = ref(false)
|
||
|
|
const dataList = ref<FineTuneTask[]>([])
|
||
|
|
const progressCache = ref<Record<string, TrainingProgress>>({})
|
||
|
|
|
||
|
|
// ============ 列头筛选 ============
|
||
|
|
const trainTypeOptions = Object.entries(TRAIN_TYPE_MAP).map(([value, label]) => ({ value, label }))
|
||
|
|
const trainMethodOptions = Object.entries(TRAIN_METHOD_MAP).map(([value, label]) => ({ value, label }))
|
||
|
|
|
||
|
|
const filters = ref({
|
||
|
|
trainType: [] as string[],
|
||
|
|
trainMethod: [] as string[],
|
||
|
|
})
|
||
|
|
|
||
|
|
/** 应用筛选后的列表 */
|
||
|
|
const filteredList = computed(() => {
|
||
|
|
return dataList.value.filter((row) => {
|
||
|
|
if (filters.value.trainType.length && !filters.value.trainType.includes(row.train_type)) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if (filters.value.trainMethod.length && !filters.value.trainMethod.includes(row.train_method)) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
let progressTimer: ReturnType<typeof setInterval> | null = null
|
||
|
|
|
||
|
|
async function loadData() {
|
||
|
|
loading.value = true
|
||
|
|
try {
|
||
|
|
dataList.value = (await getFineTuneList()) || []
|
||
|
|
// 列表加载完成后,立即获取一次运行中任务的进度
|
||
|
|
refreshProgress()
|
||
|
|
} catch {
|
||
|
|
// 拦截器已提示
|
||
|
|
} finally {
|
||
|
|
loading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 刷新训练进度(仅 running/pending 任务) */
|
||
|
|
async function refreshProgress() {
|
||
|
|
const activeTasks = dataList.value.filter(
|
||
|
|
(t) => t.status === 'running' || t.status === 'pending',
|
||
|
|
)
|
||
|
|
for (const task of activeTasks) {
|
||
|
|
try {
|
||
|
|
const [progress, status] = await Promise.all([
|
||
|
|
getFineTuneProgress(task.id),
|
||
|
|
getFineTune(task.id),
|
||
|
|
])
|
||
|
|
progressCache.value[task.id] = progress
|
||
|
|
// 状态变化时更新
|
||
|
|
if (status.status && status.status !== task.status) {
|
||
|
|
task.status = status.status
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
// 静默
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleDelete(row: any) {
|
||
|
|
await deleteFineTune(row.id)
|
||
|
|
ElMessage.success('删除成功')
|
||
|
|
}
|
||
|
|
|
||
|
|
async function handleStop(row: any) {
|
||
|
|
await stopFineTune(row.id)
|
||
|
|
ElMessage.success('训练任务已停止')
|
||
|
|
loadData()
|
||
|
|
}
|
||
|
|
|
||
|
|
function viewLog(row: any) {
|
||
|
|
router.push(`/training-log/${row.id}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatDateTime(value?: string) {
|
||
|
|
if (!value) return '-'
|
||
|
|
return new Date(value).toLocaleString('zh-CN', { hour12: false })
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
modelsStore.load()
|
||
|
|
loadData()
|
||
|
|
progressTimer = setInterval(refreshProgress, 5000)
|
||
|
|
})
|
||
|
|
|
||
|
|
onUnmounted(() => {
|
||
|
|
if (progressTimer) clearInterval(progressTimer)
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<DataTablePage
|
||
|
|
title="模型微调"
|
||
|
|
:data="filteredList"
|
||
|
|
:loading="loading"
|
||
|
|
searchable
|
||
|
|
:search-fields="['name']"
|
||
|
|
create-text="创建训练任务"
|
||
|
|
create-to="/fine-tune/create"
|
||
|
|
row-key="id"
|
||
|
|
:page-size="10"
|
||
|
|
@refresh="loadData"
|
||
|
|
>
|
||
|
|
<template #columns>
|
||
|
|
<el-table-column
|
||
|
|
label="任务名称"
|
||
|
|
prop="name"
|
||
|
|
align="center"
|
||
|
|
width="180"
|
||
|
|
show-overflow-tooltip
|
||
|
|
/>
|
||
|
|
<el-table-column label="任务状态" align="center" width="110">
|
||
|
|
<template #default="{ row }">
|
||
|
|
<ModelStatusTag :status="row.status" />
|
||
|
|
</template>
|
||
|
|
</el-table-column>
|
||
|
|
<el-table-column align="center" width="140">
|
||
|
|
<template #header>
|
||
|
|
<div class="filter-header">
|
||
|
|
<span>训练方式</span>
|
||
|
|
<el-popover trigger="click" placement="bottom" :width="160">
|
||
|
|
<template #reference>
|
||
|
|
<el-badge :is-dot="filters.trainType.length > 0" class="filter-badge">
|
||
|
|
<i
|
||
|
|
class="fa fa-filter filter-icon"
|
||
|
|
:class="{ active: filters.trainType.length > 0 }"
|
||
|
|
/>
|
||
|
|
</el-badge>
|
||
|
|
</template>
|
||
|
|
<el-checkbox-group v-model="filters.trainType" class="filter-options">
|
||
|
|
<el-checkbox v-for="opt in trainTypeOptions" :key="opt.value" :value="opt.value">
|
||
|
|
{{ opt.label }}
|
||
|
|
</el-checkbox>
|
||
|
|
</el-checkbox-group>
|
||
|
|
<div class="filter-actions">
|
||
|
|
<el-button size="small" link @click="filters.trainType = []">清除</el-button>
|
||
|
|
</div>
|
||
|
|
</el-popover>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
<template #default="{ row }">
|
||
|
|
{{ TRAIN_TYPE_MAP[row.train_type] || '-' }}
|
||
|
|
</template>
|
||
|
|
</el-table-column>
|
||
|
|
<el-table-column align="center" width="120">
|
||
|
|
<template #header>
|
||
|
|
<div class="filter-header">
|
||
|
|
<span>训练方法</span>
|
||
|
|
<el-popover trigger="click" placement="bottom" :width="160">
|
||
|
|
<template #reference>
|
||
|
|
<el-badge :is-dot="filters.trainMethod.length > 0" class="filter-badge">
|
||
|
|
<i
|
||
|
|
class="fa fa-filter filter-icon"
|
||
|
|
:class="{ active: filters.trainMethod.length > 0 }"
|
||
|
|
/>
|
||
|
|
</el-badge>
|
||
|
|
</template>
|
||
|
|
<el-checkbox-group v-model="filters.trainMethod" class="filter-options">
|
||
|
|
<el-checkbox v-for="opt in trainMethodOptions" :key="opt.value" :value="opt.value">
|
||
|
|
{{ opt.label }}
|
||
|
|
</el-checkbox>
|
||
|
|
</el-checkbox-group>
|
||
|
|
<div class="filter-actions">
|
||
|
|
<el-button size="small" link @click="filters.trainMethod = []">清除</el-button>
|
||
|
|
</div>
|
||
|
|
</el-popover>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
<template #default="{ row }">{{ row.train_method || '-' }}</template>
|
||
|
|
</el-table-column>
|
||
|
|
<el-table-column label="基座模型" align="center" width="190" show-overflow-tooltip>
|
||
|
|
<template #default="{ row }">{{ modelsStore.getModelName(row.base_model) }}</template>
|
||
|
|
</el-table-column>
|
||
|
|
<el-table-column label="训练开始时间" align="center" width="190">
|
||
|
|
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
|
||
|
|
</el-table-column>
|
||
|
|
<el-table-column label="训练时长" align="center" width="130">
|
||
|
|
<template #default="{ row }">{{ row.train_duration || '-' }}</template>
|
||
|
|
</el-table-column>
|
||
|
|
<el-table-column
|
||
|
|
label="进度"
|
||
|
|
align="center"
|
||
|
|
width="140"
|
||
|
|
>
|
||
|
|
<template #default="{ row }">
|
||
|
|
<span class="progress-value">
|
||
|
|
{{ progressCache[row.id]?.progress ?? row.progress ?? 0 }}%
|
||
|
|
</span>
|
||
|
|
</template>
|
||
|
|
</el-table-column>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<template #actions="{ row }">
|
||
|
|
<div class="action-buttons">
|
||
|
|
<el-button
|
||
|
|
v-if="row.status === 'running'"
|
||
|
|
type="warning"
|
||
|
|
link
|
||
|
|
size="small"
|
||
|
|
@click="handleStop(row)"
|
||
|
|
>
|
||
|
|
<i class="fa fa-stop-circle-o" style="margin-right: 4px" /> 停止
|
||
|
|
</el-button>
|
||
|
|
<el-button type="primary" link size="small" @click="viewLog(row)">
|
||
|
|
<i class="fa fa-file-text-o" style="margin-right: 4px" /> 日志
|
||
|
|
</el-button>
|
||
|
|
<el-button type="danger" link size="small" @click="handleDelete(row)">
|
||
|
|
<i class="fa fa-trash-o" style="margin-right: 4px" /> 删除
|
||
|
|
</el-button>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
</DataTablePage>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.progress-value {
|
||
|
|
color: var(--primary-color);
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 列头筛选 */
|
||
|
|
.filter-header {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-badge {
|
||
|
|
line-height: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-icon {
|
||
|
|
cursor: pointer;
|
||
|
|
font-size: 12px;
|
||
|
|
color: #c0c4cc;
|
||
|
|
transition: color 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-icon:hover {
|
||
|
|
color: #1890ff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-icon.active {
|
||
|
|
color: #1890ff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-options {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 8px;
|
||
|
|
max-height: 240px;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-actions {
|
||
|
|
text-align: right;
|
||
|
|
margin-top: 8px;
|
||
|
|
border-top: 1px solid #ebeef5;
|
||
|
|
padding-top: 8px;
|
||
|
|
}
|
||
|
|
</style>
|