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,283 @@
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import PageCard from '@/components/PageCard.vue'
import { getSystemInfo } from '@/api/modules/system'
import type { SystemInfo } from '@/types'
const refreshInterval = ref(5000)
let timer: ReturnType<typeof setInterval> | null = null
const info = ref<SystemInfo>({})
async function fetchInfo() {
try {
info.value = await getSystemInfo()
} catch {
// ignore
}
}
function startTimer() {
stopTimer()
timer = setInterval(fetchInfo, refreshInterval.value)
}
function stopTimer() {
if (timer) {
clearInterval(timer)
timer = null
}
}
function changeInterval(val: number) {
refreshInterval.value = val
startTimer()
}
/** 温度颜色 */
function tempColor(t?: number) {
if (t == null) return ''
if (t >= 80) return '#f56c6c'
if (t >= 70) return '#e6a23c'
return '#303133'
}
onMounted(() => {
fetchInfo()
startTimer()
})
onUnmounted(stopTimer)
</script>
<template>
<PageCard title="平台性能">
<template #extra>
<span style="font-size: 13px; color: #909399">刷新频率</span>
<el-select v-model="refreshInterval" size="small" style="width: 90px" @change="changeInterval">
<el-option :value="1000" label="1秒" />
<el-option :value="3000" label="3秒" />
<el-option :value="5000" label="5秒" />
</el-select>
</template>
<!-- CPU / 内存 / 磁盘 -->
<el-row :gutter="16" style="margin-bottom: 16px">
<!-- CPU -->
<el-col :span="8">
<el-card shadow="never">
<div class="metric-header">
<div>
<i class="fa fa-microchip metric-icon cpu-icon" />
<div class="metric-info">
<div class="metric-title">CPU 使用率</div>
<div class="metric-sub">{{ info.cpu?.cores || 0 }} 核心</div>
</div>
</div>
<div class="metric-value">{{ info.cpu?.percent || 0 }}%</div>
</div>
<el-progress :percentage="info.cpu?.percent || 0" :stroke-width="10" :show-text="false" color="#1890ff" />
</el-card>
</el-col>
<!-- 内存 -->
<el-col :span="8">
<el-card shadow="never">
<div class="metric-header">
<div>
<i class="fa fa-database metric-icon mem-icon" />
<div class="metric-info">
<div class="metric-title">内存使用</div>
<div class="metric-sub">总计: {{ info.memory?.total_gb || 0 }} GB</div>
</div>
</div>
<div class="metric-value">{{ info.memory?.percent || 0 }}%</div>
</div>
<el-progress :percentage="info.memory?.percent || 0" :stroke-width="10" :show-text="false" color="#e6a23c" />
<div class="metric-detail">
<span>已用 {{ info.memory?.used_gb || 0 }} GB</span>
<span>可用 {{ info.memory?.available_gb || 0 }} GB</span>
<span>缓存 {{ info.memory?.cached_gb || 0 }} GB</span>
</div>
</el-card>
</el-col>
<!-- 磁盘 -->
<el-col :span="8">
<el-card shadow="never">
<div class="metric-header">
<div>
<i class="fa fa-hdd-o metric-icon disk-icon" />
<div class="metric-info">
<div class="metric-title">磁盘使用</div>
<div class="metric-sub">总计: {{ info.disk?.total_gb || 0 }} GB</div>
</div>
</div>
<div class="metric-value">{{ info.disk?.percent || 0 }}%</div>
</div>
<el-progress :percentage="info.disk?.percent || 0" :stroke-width="10" :show-text="false" color="#67c23a" />
<div class="metric-detail">
<span>已用 {{ info.disk?.used_gb || 0 }} GB</span>
<span>可用 {{ (info.disk?.total_gb || 0) - (info.disk?.used_gb || 0) }} GB</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- GPU -->
<el-card shadow="never" style="margin-bottom: 16px">
<template #header>
<div class="section-header">
<i class="fa fa-microchip" style="color: #f56c6c" />
<span>GPU 监控{{ info.gpu?.length || 0 }} </span>
</div>
</template>
<el-row :gutter="12">
<el-col v-for="(gpu, idx) in info.gpu" :key="idx" :span="6">
<el-card shadow="never" class="gpu-card">
<div class="gpu-name">{{ gpu.name }}</div>
<div class="gpu-usage">{{ gpu.gpu_percent }}%</div>
<el-progress :percentage="gpu.gpu_percent" :stroke-width="8" :show-text="false" />
<div class="gpu-meta">
<div>显存: {{ gpu.memory_used_gb }}/{{ gpu.memory_total_gb }} GB</div>
<div>温度: <span :style="{ color: tempColor(gpu.temperature) }">{{ gpu.temperature }}°C</span></div>
<div>功耗: {{ gpu.power_w }} W</div>
<div>风扇: {{ gpu.fan_speed || 0 }}%</div>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
<!-- 系统信息 -->
<el-row :gutter="16">
<el-col :span="12">
<el-card shadow="never">
<template #header>
<div class="section-header">
<i class="fa fa-globe" style="color: #909399" />
<span>网络流量</span>
</div>
</template>
<div class="metric-detail">
<span>总流入: {{ info.network?.download_mb || 0 }} GB</span>
<span>总流出: {{ info.network?.upload_mb || 0 }} GB</span>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header>
<div class="section-header">
<i class="fa fa-info-circle" style="color: #909399" />
<span>系统信息</span>
</div>
</template>
<div class="sys-info">
<div class="sys-row"><span>操作系统</span><span>{{ info.system?.os || '-' }}</span></div>
<div class="sys-row"><span>进程数</span><span>{{ info.system?.process_count || 0 }}</span></div>
</div>
</el-card>
</el-col>
</el-row>
</PageCard>
</template>
<style scoped lang="scss">
.metric-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
> div:first-child {
display: flex;
align-items: center;
gap: 10px;
}
}
.metric-icon {
font-size: 20px;
}
.cpu-icon {
color: #1890ff;
}
.mem-icon {
color: #e6a23c;
}
.disk-icon {
color: #67c23a;
}
.metric-title {
font-size: 14px;
font-weight: 500;
color: #303133;
}
.metric-sub {
font-size: 12px;
color: #909399;
}
.metric-value {
font-size: 24px;
font-weight: 500;
color: #303133;
}
.metric-detail {
display: flex;
justify-content: space-between;
margin-top: 12px;
font-size: 13px;
color: #606266;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
color: #303133;
}
.gpu-card {
.gpu-name {
font-size: 13px;
font-weight: 500;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gpu-usage {
font-size: 18px;
font-weight: 600;
color: #f56c6c;
margin-bottom: 8px;
}
.gpu-meta {
margin-top: 8px;
font-size: 11px;
color: #909399;
line-height: 1.8;
}
}
.sys-info {
.sys-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #ebeef5;
font-size: 13px;
&:last-child {
border-bottom: none;
}
span:first-child {
color: #909399;
}
}
}
</style>

View File

@@ -0,0 +1,250 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import PageCard from '@/components/PageCard.vue'
import { useCountdown } from '@/composables/useCountdown'
import {
getLogFiles,
getLogContent,
getTrainingLogFiles,
getTrainingLogContent,
} from '@/api/modules/log'
import type { LogFile } from '@/types'
const activeTab = ref<'system' | 'training'>('system')
// 系统日志
const sysDate = ref(new Date().toISOString().split('T')[0])
const sysFiles = ref<LogFile[]>([])
const sysSelected = ref('')
const sysContent = ref('')
// 训练日志
const trainFiles = ref<LogFile[]>([])
const trainSelected = ref('')
const trainContent = ref('')
// 搜索
const keyword = ref('')
const fullContent = ref('')
// 自动刷新
const refreshInterval = ref(10)
const { remaining, start: startCountdown, stop: stopCountdown } = useCountdown(10)
let refreshTimer: ReturnType<typeof setInterval> | null = null
const filteredContent = computed(() => {
if (!keyword.value.trim()) return fullContent.value
const kw = keyword.value.toLowerCase().trim()
return fullContent.value
.split('\n')
.filter((line) => line.toLowerCase().includes(kw))
.join('\n')
})
const matchCount = computed(() => {
if (!keyword.value.trim()) return 0
const kw = keyword.value.toLowerCase().trim()
return fullContent.value.split('\n').filter((line) => line.toLowerCase().includes(kw)).length
})
async function loadSysFiles() {
try {
sysFiles.value = (await getLogFiles(sysDate.value)) || []
if (sysFiles.value.length > 0) {
sysSelected.value = sysFiles.value[0].file
loadSysContent()
} else {
sysContent.value = '该日期暂无日志文件'
}
} catch {
sysFiles.value = []
}
}
async function loadSysContent() {
if (!sysSelected.value) return
try {
const res = await getLogContent(sysSelected.value)
fullContent.value = res.content || ''
sysContent.value = fullContent.value
} catch {
// ignore
}
}
async function loadTrainFiles() {
try {
trainFiles.value = (await getTrainingLogFiles()) || []
if (trainFiles.value.length > 0) {
trainSelected.value = trainFiles.value[0].file
loadTrainContent()
} else {
trainContent.value = '暂无训练日志'
}
} catch {
trainFiles.value = []
}
}
async function loadTrainContent() {
if (!trainSelected.value) return
try {
const res = await getTrainingLogContent(trainSelected.value)
fullContent.value = res.content || ''
trainContent.value = fullContent.value
} catch {
// ignore
}
}
function refresh() {
if (activeTab.value === 'system') {
loadSysContent()
} else {
loadTrainContent()
}
}
function startAutoRefresh() {
stopAutoRefresh()
if (refreshInterval.value === 0) {
stopCountdown()
return
}
startCountdown()
refreshTimer = setInterval(refresh, refreshInterval.value * 1000)
}
function stopAutoRefresh() {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
watch(refreshInterval, startAutoRefresh)
watch(activeTab, () => {
keyword.value = ''
if (activeTab.value === 'system') loadSysFiles()
else loadTrainFiles()
})
watch(sysSelected, loadSysContent)
watch(trainSelected, loadTrainContent)
onMounted(() => {
loadSysFiles()
startAutoRefresh()
})
onUnmounted(stopAutoRefresh)
</script>
<template>
<PageCard title="查看日志">
<!-- Tab 切换 -->
<el-tabs v-model="activeTab" style="margin-bottom: 16px">
<el-tab-pane label="系统日志" name="system" />
<el-tab-pane label="训练日志" name="training" />
</el-tabs>
<!-- 系统日志选项 -->
<div v-if="activeTab === 'system'" class="log-options">
<div class="option-group">
<span class="option-label">选择日期:</span>
<el-date-picker v-model="sysDate" type="date" value-format="YYYY-MM-DD" style="width: 160px" @change="loadSysFiles" />
</div>
</div>
<div class="option-group">
<span class="option-label">日志类型:</span>
<el-select v-if="activeTab === 'system'" v-model="sysSelected" placeholder="请选择日志文件" style="width: 320px">
<el-option v-for="f in sysFiles" :key="f.file" :label="`${f.name} (${f.size})`" :value="f.file" />
</el-select>
<el-select v-else v-model="trainSelected" placeholder="请选择训练日志" style="width: 320px">
<el-option v-for="f in trainFiles" :key="f.file" :label="`${f.name} (PID: ${f.pid})`" :value="f.file" />
</el-select>
</div>
<div class="option-group">
<span class="option-label">自动刷新:</span>
<el-select v-model="refreshInterval" style="width: 100px">
<el-option :value="0" label="关闭" />
<el-option :value="5" label="5秒" />
<el-option :value="10" label="10秒" />
<el-option :value="30" label="30秒" />
<el-option :value="60" label="60秒" />
</el-select>
<span v-if="refreshInterval > 0" class="countdown">下次刷新: {{ remaining }}</span>
<el-button @click="refresh">立即刷新</el-button>
</div>
<!-- 日志内容 -->
<div class="log-content-box">
<div class="log-toolbar">
<el-input v-model="keyword" placeholder="搜索日志..." size="small" clearable style="width: 240px">
<template #prefix><i class="fa fa-search" /></template>
</el-input>
<span v-if="keyword" class="match-count">{{ matchCount }} 条匹配</span>
</div>
<pre class="log-pre">{{ filteredContent || (activeTab === 'system' ? sysContent : trainContent) || '日志内容将在这里显示...' }}</pre>
</div>
</PageCard>
</template>
<style scoped lang="scss">
.log-options {
margin-bottom: 16px;
}
.option-group {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
.option-label {
font-size: 13px;
color: #606266;
white-space: nowrap;
}
.countdown {
font-size: 12px;
color: #909399;
}
}
.log-content-box {
border: 1px solid #ebeef5;
border-radius: 6px;
overflow: hidden;
}
.log-toolbar {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
.match-count {
font-size: 12px;
color: #909399;
}
}
.log-pre {
margin: 0;
padding: 16px;
background: #1e1e1e;
color: #d4d4d4;
font-family: 'SFMono-Regular', Consolas, monospace;
font-size: 12px;
line-height: 1.6;
max-height: 600px;
overflow: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>

File diff suppressed because it is too large Load Diff