update
This commit is contained in:
@@ -16,7 +16,16 @@ export interface ComputeNode {
|
||||
data_root: string
|
||||
model_root: string
|
||||
log_root: string
|
||||
api_version?: string
|
||||
capabilities?: string[]
|
||||
description?: string
|
||||
last_health_check_at?: string
|
||||
health_detail?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type ComputeNodePayload = Partial<ComputeNode> & {
|
||||
code?: string
|
||||
api_base_url?: string
|
||||
}
|
||||
|
||||
export interface ComputeGpu {
|
||||
@@ -66,11 +75,14 @@ export interface ResourceReplica {
|
||||
|
||||
export const getComputeNodes = () => get<ComputeNode[]>('/compute/nodes')
|
||||
|
||||
export const createComputeNode = (data: ComputeNodePayload) =>
|
||||
post<ComputeNode>('/compute/nodes', data)
|
||||
|
||||
export const updateComputeNode = (id: string, data: Partial<ComputeNode>) =>
|
||||
put<ComputeNode>(`/compute/nodes/${id}`, data)
|
||||
|
||||
export const testComputeNode = (id: string) =>
|
||||
post<{ node_id: string; success: boolean; latency_ms: number }>(`/compute/nodes/${id}/test-connection`)
|
||||
post<{ node_id: string; success: boolean; latency_ms: number; gpu_count: number; error?: string }>(`/compute/nodes/${id}/test-connection`)
|
||||
|
||||
export const enableComputeNode = (id: string) => post<ComputeNode>(`/compute/nodes/${id}/enable`)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface DashboardStats {
|
||||
service_status: ServiceStatusStat[]
|
||||
training_tasks: TrainingTaskStat[]
|
||||
operation_distribution: { name: string; value: number }[]
|
||||
login_duration_rank: { user: string; role: string; duration: string }[]
|
||||
login_duration_rank: { user: string; role: string; duration: number }[]
|
||||
recent_login_users: { user: string; role: string; last_login: string }[]
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ export const getHealth = () => get<HealthMetrics>('/health')
|
||||
export const login = (username: string, password: string) =>
|
||||
post<LoginResponse>('/login', { username, password })
|
||||
|
||||
/** 退出登录,上报会话结束以统计在线时长 */
|
||||
export const logout = (sessionId: string) =>
|
||||
post<null>('/logout', { session_id: sessionId })
|
||||
|
||||
/** 用户列表 */
|
||||
export const getUsers = () => get<SystemUser[]>('/users')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { login as loginApi } from '@/api/modules/system'
|
||||
import { login as loginApi, logout as logoutApi } from '@/api/modules/system'
|
||||
import { SESSION_TIMEOUT } from '@/constants'
|
||||
import type { PermissionCode, SystemUser } from '@/types'
|
||||
|
||||
@@ -61,6 +61,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return '观察员'
|
||||
})
|
||||
const loginTime = ref<number>(parseInt(localStorage.getItem('loginTime') || '0', 10) || 0)
|
||||
const sessionId = ref<string>(localStorage.getItem('sessionId') || '')
|
||||
|
||||
const isLoggedIn = computed(() => {
|
||||
if (!loginTime.value) return false
|
||||
@@ -76,6 +77,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(response.user))
|
||||
localStorage.setItem('loginTime', String(loginTime.value))
|
||||
localStorage.setItem('authToken', response.token)
|
||||
sessionId.value = response.session_id
|
||||
localStorage.setItem('sessionId', response.session_id)
|
||||
}
|
||||
|
||||
/** 检查当前账号是否拥有指定模块权限。 */
|
||||
@@ -93,13 +96,22 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
/** 退出 */
|
||||
function logout() {
|
||||
async function logout() {
|
||||
if (sessionId.value) {
|
||||
try {
|
||||
await logoutApi(sessionId.value)
|
||||
} catch {
|
||||
// 上报失败不影响本地退出
|
||||
}
|
||||
}
|
||||
currentUser.value = null
|
||||
loginTime.value = 0
|
||||
sessionId.value = ''
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem(USER_STORAGE_KEY)
|
||||
localStorage.removeItem('loginTime')
|
||||
localStorage.removeItem('authToken')
|
||||
localStorage.removeItem('sessionId')
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -108,6 +120,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
displayName,
|
||||
roleLabel,
|
||||
loginTime,
|
||||
sessionId,
|
||||
isLoggedIn,
|
||||
hasPermission,
|
||||
login,
|
||||
|
||||
@@ -418,6 +418,7 @@ export interface SystemUser {
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
user: SystemUser
|
||||
session_id: string
|
||||
}
|
||||
|
||||
export interface CreateUserPayload {
|
||||
|
||||
99
frontend/src/utils/status.ts
Normal file
99
frontend/src/utils/status.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
type TagType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
pending: '等待中',
|
||||
syncing: '同步中',
|
||||
queued: '排队中',
|
||||
running: '运行中',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
stopped: '已停止',
|
||||
cancelled: '已取消',
|
||||
starting: '启动中',
|
||||
loading: '加载中',
|
||||
loaded: '已加载',
|
||||
ready: '已就绪',
|
||||
done: '已完成',
|
||||
error: '异常',
|
||||
success: '成功',
|
||||
not_started: '未启动',
|
||||
valid: '有效',
|
||||
modified: '已修改',
|
||||
invalid: '无效',
|
||||
original: '原始',
|
||||
manual: '手动新增',
|
||||
active: '启用',
|
||||
disabled: '停用',
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
draining: '维护中',
|
||||
maintenance: '维护模式',
|
||||
busy: '忙碌',
|
||||
reserved: '已预留',
|
||||
idle: '空闲',
|
||||
warning: '告警',
|
||||
available: '可用',
|
||||
missing: '缺失',
|
||||
synced: '已同步',
|
||||
drifted: '已漂移',
|
||||
repair_pending: '待修复',
|
||||
}
|
||||
|
||||
const STATUS_TYPES: Record<string, TagType> = {
|
||||
completed: 'success',
|
||||
success: 'success',
|
||||
running: 'success',
|
||||
ready: 'success',
|
||||
loaded: 'success',
|
||||
active: 'success',
|
||||
online: 'success',
|
||||
busy: 'success',
|
||||
available: 'success',
|
||||
synced: 'success',
|
||||
valid: 'success',
|
||||
pending: 'info',
|
||||
idle: 'info',
|
||||
offline: 'info',
|
||||
disabled: 'info',
|
||||
original: 'info',
|
||||
stopped: 'info',
|
||||
cancelled: 'info',
|
||||
syncing: 'warning',
|
||||
queued: 'warning',
|
||||
starting: 'warning',
|
||||
loading: 'warning',
|
||||
reserved: 'warning',
|
||||
warning: 'warning',
|
||||
modified: 'warning',
|
||||
draining: 'warning',
|
||||
maintenance: 'warning',
|
||||
repair_pending: 'warning',
|
||||
failed: 'danger',
|
||||
error: 'danger',
|
||||
invalid: 'danger',
|
||||
missing: 'danger',
|
||||
drifted: 'danger',
|
||||
}
|
||||
|
||||
export function statusLabel(status?: string | number | null) {
|
||||
const key = String(status ?? '').trim()
|
||||
if (!key) return '未知'
|
||||
return STATUS_LABELS[key] || key
|
||||
}
|
||||
|
||||
export function statusTagType(status?: string | number | null): TagType {
|
||||
const key = String(status ?? '').trim()
|
||||
return STATUS_TYPES[key] || 'info'
|
||||
}
|
||||
|
||||
export function mergeStatusLabel(row: { merged?: boolean; merging?: boolean }) {
|
||||
if (row.merging) return '合并中'
|
||||
if (row.merged) return '已合并'
|
||||
return '未合并'
|
||||
}
|
||||
|
||||
export function mergeStatusType(row: { merged?: boolean; merging?: boolean }): TagType {
|
||||
if (row.merging) return 'warning'
|
||||
if (row.merged) return 'success'
|
||||
return 'info'
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
createComputeNode,
|
||||
disableComputeNode,
|
||||
drainComputeNode,
|
||||
enableComputeNode,
|
||||
@@ -10,23 +12,45 @@ import {
|
||||
getComputeQueue,
|
||||
getNodeReplicas,
|
||||
testComputeNode,
|
||||
updateComputeNode,
|
||||
type ComputeGpu,
|
||||
type ComputeNode,
|
||||
type ComputeQueueItem,
|
||||
type ResourceReplica,
|
||||
} from '@/api/modules/compute'
|
||||
import { statusLabel, statusTagType } from '@/utils/status'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const activeTab = ref(String(route.query.tab || 'nodes'))
|
||||
const loading = ref(false)
|
||||
const buttonRefreshing = ref(false)
|
||||
const nodes = ref<ComputeNode[]>([])
|
||||
const gpus = ref<ComputeGpu[]>([])
|
||||
const queue = ref<ComputeQueueItem[]>([])
|
||||
const replicas = ref<ResourceReplica[]>([])
|
||||
const selectedNodeId = ref('')
|
||||
const lastUpdated = ref('')
|
||||
const nodeDialogVisible = ref(false)
|
||||
const nodeDialogMode = ref<'create' | 'edit'>('create')
|
||||
const savingNode = ref(false)
|
||||
const nodeForm = reactive({
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
api_base_url: '',
|
||||
file_gateway_url: '',
|
||||
enabled: true,
|
||||
scheduler_status: 'offline',
|
||||
scheduler_weight: 100,
|
||||
tags_text: '',
|
||||
max_parallel_jobs: 1,
|
||||
data_root: '/data/yg-ft',
|
||||
model_root: '/data/yg-ft/models',
|
||||
log_root: '/opt/yg-ft/logs/training',
|
||||
description: '',
|
||||
})
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const selectedNode = computed(() => nodes.value.find((item) => item.id === selectedNodeId.value))
|
||||
@@ -34,6 +58,10 @@ const enabledNodes = computed(() => nodes.value.filter((item) => item.enabled).l
|
||||
const busyGpus = computed(() => gpus.value.filter((item) => item.status === 'busy' || item.status === 'reserved').length)
|
||||
const totalRunningJobs = computed(() => nodes.value.reduce((sum, item) => sum + item.current_running_jobs, 0))
|
||||
|
||||
function asComputeNode(row: unknown): ComputeNode {
|
||||
return row as ComputeNode
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query.tab,
|
||||
(tab) => {
|
||||
@@ -41,8 +69,9 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
async function load(options: { showLoading?: boolean; showButtonLoading?: boolean } = {}) {
|
||||
if (options.showLoading) loading.value = true
|
||||
if (options.showButtonLoading) buttonRefreshing.value = true
|
||||
try {
|
||||
const [nodeList, gpuList, queueList] = await Promise.all([
|
||||
getComputeNodes(),
|
||||
@@ -52,11 +81,14 @@ async function load() {
|
||||
nodes.value = nodeList
|
||||
gpus.value = gpuList
|
||||
queue.value = queueList
|
||||
if (!selectedNodeId.value && nodeList.length) selectedNodeId.value = nodeList[0].id
|
||||
if ((!selectedNodeId.value || !nodeList.some((item) => item.id === selectedNodeId.value)) && nodeList.length) {
|
||||
selectedNodeId.value = nodeList[0].id
|
||||
}
|
||||
await loadReplicas()
|
||||
lastUpdated.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (options.showLoading) loading.value = false
|
||||
if (options.showButtonLoading) buttonRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,37 +104,120 @@ async function changeTab(name: string | number) {
|
||||
await router.replace({ path: '/compute', query: { tab: String(name) } })
|
||||
}
|
||||
|
||||
async function handleNodeAction(action: 'enable' | 'disable' | 'drain' | 'test', node: any) {
|
||||
async function handleNodeAction(action: 'enable' | 'disable' | 'drain' | 'test', node: ComputeNode) {
|
||||
const nodeId = String(node.id)
|
||||
if (action === 'enable') await enableComputeNode(nodeId)
|
||||
if (action === 'disable') await disableComputeNode(nodeId)
|
||||
if (action === 'drain') await drainComputeNode(nodeId)
|
||||
if (action === 'test') await testComputeNode(nodeId)
|
||||
if (action === 'test') {
|
||||
const result = await testComputeNode(nodeId)
|
||||
if (result.success) {
|
||||
ElMessage.success(`连接成功,发现 ${result.gpu_count} 张 GPU,延迟 ${result.latency_ms}ms`)
|
||||
} else {
|
||||
ElMessage.error(result.error || '连接失败')
|
||||
}
|
||||
}
|
||||
await load()
|
||||
}
|
||||
|
||||
function nodeStatusType(status: string) {
|
||||
if (status === 'online') return 'success'
|
||||
if (status === 'draining' || status === 'maintenance') return 'warning'
|
||||
return 'info'
|
||||
function resetNodeForm() {
|
||||
Object.assign(nodeForm, {
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
api_base_url: '',
|
||||
file_gateway_url: '',
|
||||
enabled: true,
|
||||
scheduler_status: 'offline',
|
||||
scheduler_weight: 100,
|
||||
tags_text: '',
|
||||
max_parallel_jobs: 1,
|
||||
data_root: '/data/yg-ft',
|
||||
model_root: '/data/yg-ft/models',
|
||||
log_root: '/opt/yg-ft/logs/training',
|
||||
description: '',
|
||||
})
|
||||
}
|
||||
|
||||
function taskStatusType(status: string) {
|
||||
if (status === 'running') return 'success'
|
||||
if (status === 'syncing' || status === 'queued') return 'warning'
|
||||
if (status === 'failed') return 'danger'
|
||||
return 'info'
|
||||
function openCreateNodeDialog() {
|
||||
resetNodeForm()
|
||||
nodeDialogMode.value = 'create'
|
||||
nodeDialogVisible.value = true
|
||||
}
|
||||
|
||||
function gpuStatusType(status: string) {
|
||||
if (status === 'busy') return 'success'
|
||||
if (status === 'reserved') return 'warning'
|
||||
return 'info'
|
||||
function openEditNodeDialog(node: ComputeNode) {
|
||||
Object.assign(nodeForm, {
|
||||
id: node.id,
|
||||
code: node.code,
|
||||
name: node.name,
|
||||
api_base_url: node.api_base_url,
|
||||
file_gateway_url: node.file_gateway_url,
|
||||
enabled: node.enabled,
|
||||
scheduler_status: node.scheduler_status,
|
||||
scheduler_weight: node.scheduler_weight,
|
||||
tags_text: node.tags?.join(', ') || '',
|
||||
max_parallel_jobs: node.max_parallel_jobs,
|
||||
data_root: node.data_root,
|
||||
model_root: node.model_root,
|
||||
log_root: node.log_root,
|
||||
description: node.description || '',
|
||||
})
|
||||
nodeDialogMode.value = 'edit'
|
||||
nodeDialogVisible.value = true
|
||||
}
|
||||
|
||||
function buildNodePayload() {
|
||||
const tags = nodeForm.tags_text
|
||||
.replace(/,/g, ',')
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
return {
|
||||
code: nodeForm.code.trim(),
|
||||
name: nodeForm.name.trim() || nodeForm.code.trim(),
|
||||
api_base_url: nodeForm.api_base_url.trim().replace(/\/$/, ''),
|
||||
file_gateway_url: (nodeForm.file_gateway_url || nodeForm.api_base_url).trim().replace(/\/$/, ''),
|
||||
enabled: nodeForm.enabled,
|
||||
scheduler_status: nodeForm.scheduler_status,
|
||||
scheduler_weight: Number(nodeForm.scheduler_weight) || 0,
|
||||
tags,
|
||||
max_parallel_jobs: Number(nodeForm.max_parallel_jobs) || 1,
|
||||
data_root: nodeForm.data_root.trim() || '/data/yg-ft',
|
||||
model_root: nodeForm.model_root.trim() || '/data/yg-ft/models',
|
||||
log_root: nodeForm.log_root.trim() || '/opt/yg-ft/logs/training',
|
||||
description: nodeForm.description.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
async function saveNode() {
|
||||
const payload = buildNodePayload()
|
||||
if (!payload.code || !payload.api_base_url) {
|
||||
ElMessage.warning('请填写节点编码和 Compute API 地址')
|
||||
return
|
||||
}
|
||||
savingNode.value = true
|
||||
try {
|
||||
if (nodeDialogMode.value === 'create') {
|
||||
await createComputeNode(payload)
|
||||
ElMessage.success('节点已创建')
|
||||
} else {
|
||||
await updateComputeNode(nodeForm.id, payload)
|
||||
ElMessage.success('节点配置已更新')
|
||||
}
|
||||
nodeDialogVisible.value = false
|
||||
await load()
|
||||
} finally {
|
||||
savingNode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(value?: string) {
|
||||
return value ? new Date(value).toLocaleString('zh-CN') : '-'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
load()
|
||||
timer = setInterval(load, 5000)
|
||||
load({ showLoading: true })
|
||||
timer = setInterval(() => load(), 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -119,27 +234,16 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<span v-if="lastUpdated" class="last-updated">更新 {{ lastUpdated }}</span>
|
||||
<el-button @click="load">刷新</el-button>
|
||||
<el-button type="primary" @click="openCreateNodeDialog">新增节点</el-button>
|
||||
<el-button :loading="buttonRefreshing" @click="load({ showButtonLoading: true })">刷新</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div class="summary-tile">
|
||||
<span>在线节点</span>
|
||||
<strong>{{ enabledNodes }} / {{ nodes.length }}</strong>
|
||||
</div>
|
||||
<div class="summary-tile">
|
||||
<span>GPU 占用</span>
|
||||
<strong>{{ busyGpus }} / {{ gpus.length }}</strong>
|
||||
</div>
|
||||
<div class="summary-tile">
|
||||
<span>运行任务</span>
|
||||
<strong>{{ totalRunningJobs }}</strong>
|
||||
</div>
|
||||
<div class="summary-tile">
|
||||
<span>队列任务</span>
|
||||
<strong>{{ queue.length }}</strong>
|
||||
</div>
|
||||
<div class="summary-tile"><span>启用节点</span><strong>{{ enabledNodes }} / {{ nodes.length }}</strong></div>
|
||||
<div class="summary-tile"><span>GPU 占用</span><strong>{{ busyGpus }} / {{ gpus.length }}</strong></div>
|
||||
<div class="summary-tile"><span>运行任务</span><strong>{{ totalRunningJobs }}</strong></div>
|
||||
<div class="summary-tile"><span>队列任务</span><strong>{{ queue.length }}</strong></div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab" class="compute-tabs" @tab-change="changeTab">
|
||||
@@ -149,7 +253,7 @@ onUnmounted(() => {
|
||||
<el-table-column prop="name" label="节点名称" min-width="150" />
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="nodeStatusType(row.scheduler_status)">{{ row.scheduler_status }}</el-tag>
|
||||
<el-tag :type="statusTagType(row.scheduler_status)">{{ statusLabel(row.scheduler_status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="90">
|
||||
@@ -174,12 +278,13 @@ onUnmounted(() => {
|
||||
<div class="muted mono">{{ row.file_gateway_url }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<el-table-column label="操作" width="330" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleNodeAction('test', row)">测试</el-button>
|
||||
<el-button v-if="row.enabled" size="small" @click="handleNodeAction('disable', row)">停用</el-button>
|
||||
<el-button v-else size="small" type="primary" @click="handleNodeAction('enable', row)">启用</el-button>
|
||||
<el-button size="small" type="warning" plain @click="handleNodeAction('drain', row)">维护</el-button>
|
||||
<el-button size="small" @click="openEditNodeDialog(asComputeNode(row))">编辑</el-button>
|
||||
<el-button size="small" @click="handleNodeAction('test', asComputeNode(row))">测试</el-button>
|
||||
<el-button v-if="row.enabled" size="small" @click="handleNodeAction('disable', asComputeNode(row))">停用</el-button>
|
||||
<el-button v-else size="small" type="primary" @click="handleNodeAction('enable', asComputeNode(row))">启用</el-button>
|
||||
<el-button size="small" type="warning" plain @click="handleNodeAction('drain', asComputeNode(row))">维护</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -195,13 +300,11 @@ onUnmounted(() => {
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="gpuStatusType(row.status)">{{ row.status }}</el-tag>
|
||||
<el-tag :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="利用率" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.gpu_percent" :stroke-width="8" />
|
||||
</template>
|
||||
<template #default="{ row }"><el-progress :percentage="row.gpu_percent" :stroke-width="8" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="显存" min-width="190">
|
||||
<template #default="{ row }">
|
||||
@@ -228,19 +331,19 @@ onUnmounted(() => {
|
||||
<el-table-column prop="name" label="任务名称" min-width="200" />
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="taskStatusType(row.status)">{{ row.status }}</el-tag>
|
||||
<el-tag :type="statusTagType(row.status)">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.progress" :stroke-width="8" />
|
||||
</template>
|
||||
<template #default="{ row }"><el-progress :percentage="row.progress" :stroke-width="8" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="compute_node_id" label="节点" width="140" />
|
||||
<el-table-column label="GPU" width="120">
|
||||
<template #default="{ row }">{{ row.gpus?.join(', ') || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="190" />
|
||||
<el-table-column label="创建时间" width="190">
|
||||
<template #default="{ row }">{{ formatTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -254,13 +357,72 @@ onUnmounted(() => {
|
||||
<el-table :data="replicas" height="100%">
|
||||
<el-table-column prop="resource_type" label="资源类型" width="110" />
|
||||
<el-table-column prop="resource_id" label="资源 ID" min-width="180" />
|
||||
<el-table-column prop="local_path" label="本地路径" min-width="300" />
|
||||
<el-table-column prop="status" label="状态" width="110" />
|
||||
<el-table-column prop="sync_status" label="同步状态" width="120" />
|
||||
<el-table-column prop="create_time" label="创建时间" width="190" />
|
||||
<el-table-column prop="local_path" label="本地路径" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="同步状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.sync_status)" size="small">{{ statusLabel(row.sync_status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="190">
|
||||
<template #default="{ row }">{{ formatTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog
|
||||
v-model="nodeDialogVisible"
|
||||
:title="nodeDialogMode === 'create' ? '新增算力节点' : '编辑算力节点'"
|
||||
width="720px"
|
||||
>
|
||||
<el-form label-width="130px">
|
||||
<div class="node-form-grid">
|
||||
<el-form-item label="节点编码" required>
|
||||
<el-input v-model="nodeForm.code" :disabled="nodeDialogMode === 'edit'" placeholder="gpu-node-01" />
|
||||
</el-form-item>
|
||||
<el-form-item label="节点名称">
|
||||
<el-input v-model="nodeForm.name" placeholder="A800 训练节点 01" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Compute API" required>
|
||||
<el-input v-model="nodeForm.api_base_url" placeholder="http://10.0.0.11:19100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="File Gateway">
|
||||
<el-input v-model="nodeForm.file_gateway_url" placeholder="http://10.0.0.11:19101" />
|
||||
</el-form-item>
|
||||
<el-form-item label="调度权重">
|
||||
<el-input-number v-model="nodeForm.scheduler_weight" :min="0" :max="1000" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最大并行任务">
|
||||
<el-input-number v-model="nodeForm.max_parallel_jobs" :min="1" :max="32" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="nodeForm.enabled" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
<el-form-item label="调度状态">
|
||||
<el-select v-model="nodeForm.scheduler_status">
|
||||
<el-option label="离线" value="offline" />
|
||||
<el-option label="在线" value="online" />
|
||||
<el-option label="维护中" value="draining" />
|
||||
<el-option label="维护模式" value="maintenance" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="标签"><el-input v-model="nodeForm.tags_text" placeholder="a800, lora, beijing" /></el-form-item>
|
||||
<el-form-item label="数据根目录"><el-input v-model="nodeForm.data_root" /></el-form-item>
|
||||
<el-form-item label="模型根目录"><el-input v-model="nodeForm.model_root" /></el-form-item>
|
||||
<el-form-item label="训练日志目录"><el-input v-model="nodeForm.log_root" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="nodeForm.description" type="textarea" :rows="3" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="nodeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="savingNode" @click="saveNode">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -294,7 +456,8 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
.header-actions,
|
||||
.replica-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -306,6 +469,11 @@ onUnmounted(() => {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
min-width: 92px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
@@ -351,9 +519,6 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.replica-toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.el-select {
|
||||
@@ -361,6 +526,17 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.node-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 12px;
|
||||
|
||||
:deep(.el-input-number),
|
||||
:deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.compute-header,
|
||||
.header-actions,
|
||||
@@ -372,5 +548,9 @@ onUnmounted(() => {
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.node-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -246,7 +246,7 @@ const loginDurationChartOption = computed<EChartsOption>(() => ({
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '距上次登录',
|
||||
name: '登录时长',
|
||||
type: 'bar',
|
||||
data: loginDurationStats.value.map((user) => user.duration),
|
||||
barMaxWidth: 18,
|
||||
|
||||
@@ -35,6 +35,7 @@ async function loadData() {
|
||||
async function handleDelete(row: any) {
|
||||
await deleteDataset(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}
|
||||
|
||||
function handlePreview(row: any) {
|
||||
|
||||
Reference in New Issue
Block a user