feat: 实现业务视图页面
登录、模型调优、评测、推理、对比、模型管理、数据集、数据处理、工具、系统(硬件/日志/训练日志)等全部业务页面视图。
This commit is contained in:
283
frontend/src/views/system/HardwareView.vue
Normal file
283
frontend/src/views/system/HardwareView.vue
Normal 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>
|
||||
250
frontend/src/views/system/LogsView.vue
Normal file
250
frontend/src/views/system/LogsView.vue
Normal 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>
|
||||
1026
frontend/src/views/system/TrainingLogView.vue
Normal file
1026
frontend/src/views/system/TrainingLogView.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user