Files
YG_FT/frontend/src/views/inference/InferenceListView.vue
caoxiaozhu ab9e87f948 refactor: 推理对比超时与打字机展示改进
抽取 withTimeout 替代 Promise.race 超时控制,对比结果新增打字机逐字渲染与清理,推理聊天参数与列表类型同步收敛。
2026-07-16 11:03:25 +08:00

195 lines
6.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import DataTablePage from '@/components/DataTablePage.vue'
import { usePolling } from '@/composables/usePolling'
import {
getCompareList,
deleteCompare,
getCompare,
loadCompare,
unloadCompare,
stopModelByPid,
} from '@/api/modules/compare'
import type { CompareTask, LoadedModel } from '@/types'
const router = useRouter()
const loading = ref(false)
const dataList = ref<CompareTask[]>([])
let delayedRefreshTimer: ReturnType<typeof setTimeout> | null = null
async function loadData(silent = false) {
if (!silent) {
loading.value = true
}
try {
dataList.value = (await getCompareList()) || []
} catch {
// ignore
} finally {
if (!silent) {
loading.value = false
}
}
}
/** 解析 load_status 判断就绪状态 */
function isStarting(row: any): boolean {
const models = parseLoadedModels(row)
return models.length > 0 && models.some((m) => m.status === 'starting')
}
function isReady(row: any): boolean {
const models = parseLoadedModels(row)
if (models.length === 0) return row.status === 'loaded' || row.status === 'ready'
return models.every((m) => m.status === 'ready' || m.status === 'running') && !isStarting(row)
}
function parseLoadedModels(row: any): LoadedModel[] {
if (!row.load_status) return []
try {
const parsed =
typeof row.load_status === 'string' ? JSON.parse(row.load_status) : row.load_status
return parsed.loaded_models || []
} catch {
return []
}
}
/** 解析相关模型字段,返回模型名称列表 */
function parseModelNames(row: any): string[] {
if (!row.models) return []
// 数组结构直接取
if (Array.isArray(row.models)) {
return row.models.map((m: any) => m.model_name || m.name).filter(Boolean)
}
// 字符串结构需先 JSON.parse
try {
const parsed = JSON.parse(row.models)
if (Array.isArray(parsed)) {
return parsed.map((m: any) => m.model_name || m.name).filter(Boolean)
}
} catch {
// 非法 JSON忽略
}
return []
}
/** 加载推理任务 */
async function handleLoad(row: any) {
await loadCompare(row.id)
ElMessage.info('正在加载模型,请稍候...')
if (delayedRefreshTimer) clearTimeout(delayedRefreshTimer)
delayedRefreshTimer = setTimeout(loadData, 1000)
}
/** 卸载推理任务 */
async function handleUnload(row: any) {
await ElMessageBox.confirm('确定要停止模型服务吗?', '确认停止', { type: 'warning' })
await unloadCompare(row.id)
ElMessage.success('已停止模型服务')
loadData()
}
/** 删除(先停止进程) */
async function handleDelete(row: any) {
// 先尝试停止已加载的模型进程
const task = await getCompare(row.id).catch(() => null)
if (task?.load_status) {
const models = parseLoadedModels(task as CompareTask)
for (const m of models) {
if (m.pid) {
await stopModelByPid(m.pid).catch(() => {})
}
}
}
await deleteCompare(row.id)
ElMessage.success('删除成功')
}
/** 开始对话 */
function startChat(row: any) {
router.push(`/model-inference/chat/${row.id}`)
}
const { start: startPolling } = usePolling(() => loadData(true), 3000, { immediate: false })
onMounted(async () => {
await loadData()
startPolling()
})
onUnmounted(() => {
if (delayedRefreshTimer) clearTimeout(delayedRefreshTimer)
})
</script>
<template>
<DataTablePage
title="模型推理"
:data="dataList"
:loading="loading"
searchable
:search-fields="['name', 'model_name', 'description']"
create-text="新建推理"
create-to="/model-inference/create"
:delete-fn="handleDelete"
row-key="id"
@refresh="loadData"
>
<template #columns>
<el-table-column label="推理名称" align="center">
<template #default="{ row }">{{ row.model_name || row.name || '-' }}</template>
</el-table-column>
<el-table-column label="描述" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.description || '-' }}</template>
</el-table-column>
<el-table-column label="状态" align="center" width="120">
<template #default="{ row }">
<el-tag v-if="isStarting(row)" type="warning" size="small">启动中</el-tag>
<el-tag v-else-if="isReady(row)" type="success" size="small">已就绪</el-tag>
<el-tag v-else type="info" size="small">{{ row.status || '未启动' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="相关模型" align="center" show-overflow-tooltip>
<template #default="{ row }">
{{ parseModelNames(row).join('') || '-' }}
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" width="180">
<template #default="{ row }">
{{ row.create_time ? new Date(row.create_time).toLocaleString('zh-CN') : '-' }}
</template>
</el-table-column>
</template>
<template #actions="{ row }">
<div class="action-buttons">
<template v-if="isStarting(row)">
<el-button type="warning" link size="small" :loading="true" disabled>
加载中
</el-button>
</template>
<template v-else-if="isReady(row)">
<el-button type="success" link size="small" @click="startChat(row)">
<i class="fa fa-comments-o" style="margin-right: 4px" />对话
</el-button>
<el-button type="warning" link size="small" @click="handleUnload(row)">
<i class="fa fa-stop-circle-o" style="margin-right: 4px" />停止
</el-button>
</template>
<template v-else>
<el-button type="primary" link size="small" @click="handleLoad(row)">
<i class="fa fa-play-circle-o" style="margin-right: 4px" />加载
</el-button>
</template>
<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>