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,145 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import PageCard from '@/components/PageCard.vue'
import { getCompare } from '@/api/modules/compare'
import type { CompareTask, LoadedModel } from '@/types'
const route = useRoute()
const router = useRouter()
const taskId = route.params.id as string
const task = ref<CompareTask | null>(null)
let pollTimer: ReturnType<typeof setInterval> | null = null
const form = reactive({
systemPrompt: '',
question: '',
temperature: 0.7,
topP: 0.9,
topK: 40,
maxTokens: 2048,
})
const loadedModels = computed<LoadedModel[]>(() => {
if (!task.value?.load_status) return []
try {
const ls =
typeof task.value.load_status === 'string'
? JSON.parse(task.value.load_status)
: task.value.load_status
return ls.loaded_models || []
} catch {
return []
}
})
const allReady = computed(
() => loadedModels.value.length > 0 && loadedModels.value.every((m) => m.status === 'ready' || m.status === 'running'),
)
const isStarting = computed(() => loadedModels.value.some((m) => m.status === 'starting'))
async function loadTask() {
try {
task.value = await getCompare(taskId)
} catch {
// ignore
}
}
function handleSubmit() {
if (!form.question.trim()) {
ElMessage.warning('请输入问题')
return
}
if (isStarting.value) {
ElMessage.warning('模型仍在启动中,请稍候')
return
}
// 在新窗口打开结果页(原项目用 window.open
const params = new URLSearchParams({
taskId,
taskName: task.value?.model_name || task.value?.name || '',
question: form.question,
systemPrompt: form.systemPrompt,
temperature: String(form.temperature),
topP: String(form.topP),
topK: String(form.topK),
maxTokens: String(form.maxTokens),
})
const url = router.resolve(`/model-compare/result?${params.toString()}`).href
window.open(url, '_blank')
}
onMounted(() => {
loadTask()
pollTimer = setInterval(loadTask, 5000)
})
onUnmounted(() => {
if (pollTimer) clearInterval(pollTimer)
})
</script>
<template>
<PageCard title="模型对比配置">
<!-- 已启动模型 -->
<el-divider content-position="left">已启动模型</el-divider>
<div class="model-list">
<el-tag
v-for="(m, idx) in loadedModels"
:key="idx"
:type="m.status === 'ready' || m.status === 'running' ? 'success' : m.status === 'starting' ? 'warning' : 'danger'"
size="large"
>
{{ m.model_name }} ({{ m.status }})
</el-tag>
<span v-if="!loadedModels.length" class="empty-hint">暂无已启动模型</span>
</div>
<!-- 输入配置 -->
<el-divider content-position="left">对话配置</el-divider>
<el-form label-width="120px" style="max-width: 700px">
<el-form-item label="系统提示词">
<el-input v-model="form.systemPrompt" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
<el-form-item label="问题">
<el-input v-model="form.question" type="textarea" :rows="4" placeholder="请输入要对比的问题" />
</el-form-item>
<el-form-item label="Temperature">
<el-slider v-model="form.temperature" :min="0" :max="2" :step="0.1" show-input style="max-width: 500px" />
</el-form-item>
<el-form-item label="Top-p">
<el-slider v-model="form.topP" :min="0" :max="1" :step="0.05" show-input style="max-width: 500px" />
</el-form-item>
<el-form-item label="Top-k">
<el-slider v-model="form.topK" :min="1" :max="100" :step="1" show-input style="max-width: 500px" />
</el-form-item>
<el-form-item label="Max Tokens">
<el-slider v-model="form.maxTokens" :min="256" :max="4096" :step="128" show-input style="max-width: 500px" />
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="!allReady || isStarting" @click="handleSubmit">
开始对比
</el-button>
<el-button @click="router.back()">返回</el-button>
</el-form-item>
</el-form>
</PageCard>
</template>
<style scoped>
.model-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
}
.empty-hint {
color: #909399;
}
</style>

View File

@@ -0,0 +1,225 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import MarkdownView from '@/components/MarkdownView.vue'
import {
getCompare,
chatWithPort,
batchChat,
} from '@/api/modules/compare'
import { getModelByName } from '@/api/modules/model'
import type { CompareTask, LoadedModel } from '@/types'
const route = useRoute()
const taskId = route.query.taskId as string
const question = decodeURIComponent(route.query.question as string || '')
const systemPrompt = decodeURIComponent(route.query.systemPrompt as string || '')
const temperature = Number(route.query.temperature || 0.7)
const topP = Number(route.query.topP || 0.9)
const topK = Number(route.query.topK || 40)
const maxTokens = Number(route.query.maxTokens || 2048)
const taskName = route.query.taskName as string
interface ModelResult {
name: string
content: string
displayContent: string
status: 'loading' | 'done' | 'error'
stats?: { charsPerSec?: number; totalTime?: number }
}
const results = ref<ModelResult[]>([])
const started = ref(false)
const loadedModels = ref<LoadedModel[]>([])
async function init() {
if (started.value) return
started.value = true
try {
const task: any = await getCompare(taskId)
let models: LoadedModel[] = []
if (task.load_status) {
const ls = typeof task.load_status === 'string' ? JSON.parse(task.load_status) : task.load_status
models = ls.loaded_models || []
}
loadedModels.value = models
// 初始化结果占位
results.value = models.map((m) => ({
name: m.model_name || '模型',
content: '',
displayContent: '',
status: 'loading',
}))
// 并行推理
await Promise.all(models.map((m, idx) => inferOne(m, idx)))
} catch (e) {
// ignore
}
}
/** 单模型推理(带超时) */
async function inferOne(model: LoadedModel, idx: number) {
const startTime = Date.now()
try {
// 尝试通过端口代理调用
const res: any = await Promise.race([
chatWithPort({
port: model.port,
model_name: model.model_name,
messages: [
...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
{ role: 'user', content: question },
],
temperature,
top_p: topP,
top_k: topK,
max_tokens: maxTokens,
}),
new Promise((_, reject) => setTimeout(() => reject(new Error('推理超时')), 300000)),
])
const content = res?.response || res?.content || res?.data || JSON.stringify(res)
const totalTime = (Date.now() - startTime) / 1000
results.value[idx].content = content
results.value[idx].status = 'done'
results.value[idx].stats = {
totalTime,
charsPerSec: totalTime > 0 ? (content.length / totalTime).toFixed(1) as unknown as number : 0,
}
// 模拟打字机效果
typewriterDisplay(idx, content)
} catch (e: any) {
results.value[idx].content = '推理失败: ' + (e.message || '')
results.value[idx].status = 'error'
}
}
/** 打字机效果逐字展示 */
function typewriterDisplay(idx: number, content: string) {
let pos = 0
const interval = setInterval(() => {
pos += 2
results.value[idx].displayContent = content.slice(0, pos)
if (pos >= content.length) {
clearInterval(interval)
results.value[idx].displayContent = content
}
}, 20)
}
const allDone = computed(() => results.value.length > 0 && results.value.every((r) => r.status === 'done' || r.status === 'error'))
onMounted(init)
</script>
<template>
<div class="compare-result">
<div class="result-header">
<h2>对比结果{{ taskName ? ` - ${taskName}` : '' }}</h2>
<div class="header-actions">
<el-button @click="$router.push('/model-inference')">返回列表</el-button>
</div>
</div>
<!-- 问题 -->
<el-alert type="info" :closable="false" show-icon class="question-box">
<template #title>
<strong>问题</strong>{{ question }}
</template>
</el-alert>
<!-- 模型结果网格 -->
<div class="result-grid">
<el-card v-for="(r, idx) in results" :key="idx" shadow="hover" class="result-card">
<template #header>
<div class="card-header">
<span class="model-name">{{ r.name }}</span>
<el-tag v-if="r.status === 'loading'" type="warning" size="small">生成中...</el-tag>
<el-tag v-else-if="r.status === 'done'" type="success" size="small">完成</el-tag>
<el-tag v-else type="danger" size="small">失败</el-tag>
</div>
</template>
<div v-if="r.status === 'error'" class="error-text">{{ r.content }}</div>
<MarkdownView v-else-if="r.displayContent" :content="r.displayContent" />
<div v-else class="loading-text">
<i class="fa fa-spinner fa-spin" /> 正在生成回答...
</div>
<div v-if="r.stats" class="result-stats">
<span>耗时 {{ r.stats.totalTime?.toFixed(1) }}s</span>
<span>速度 {{ r.stats.charsPerSec }} /</span>
</div>
</el-card>
</div>
</div>
</template>
<style scoped lang="scss">
.compare-result {
max-width: 1200px;
margin: 0 auto;
}
.result-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
h2 {
font-size: 18px;
font-weight: 500;
margin: 0;
}
}
.question-box {
margin-bottom: 20px;
}
.result-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
gap: 16px;
}
.result-card {
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.model-name {
font-weight: 500;
color: #303133;
}
}
.loading-text,
.error-text {
color: #909399;
min-height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.error-text {
color: #f56c6c;
}
.result-stats {
display: flex;
gap: 16px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #ebeef5;
font-size: 12px;
color: #909399;
}
}
</style>