feat: 实现业务视图页面

登录、模型调优、评测、推理、对比、模型管理、数据集、数据处理、工具、系统(硬件/日志/训练日志)等全部业务页面视图。
This commit is contained in:
caoxiaozhu
2026-07-10 16:45:55 +08:00
parent c1893cf82c
commit 6f6609dae5
30 changed files with 9383 additions and 0 deletions

View 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>