feat: 实现业务视图页面
登录、模型调优、评测、推理、对比、模型管理、数据集、数据处理、工具、系统(硬件/日志/训练日志)等全部业务页面视图。
This commit is contained in:
285
frontend/src/views/fine-tune/FineTuneListView.vue
Normal file
285
frontend/src/views/fine-tune/FineTuneListView.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user