feat: 更新后端平台模块、Compute引擎、前端组件及构建产物
- 更新 backend 平台 API、platform_store、compute_gateway sync - 更新 compute agent/engine/adapter 及 API - 更新 Docker 部署配置(app/compute) - 新增 frontend/src/utils/ 工具模块 - 新增 scripts/ops_diagnostics.py 运维诊断脚本 - 新增 docs/2026-07-23-development-summary.md 开发总结 - 重构 frontend/dist 构建产物(新 hash) - 更新前端多个视图组件及 API 模块 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,10 @@ export interface ResourceReplica {
|
||||
local_path: string
|
||||
status: string
|
||||
sync_status: string
|
||||
checksum_sha256?: string
|
||||
byte_size?: number
|
||||
last_checked_at?: string
|
||||
last_error?: string
|
||||
create_time: string
|
||||
}
|
||||
|
||||
@@ -95,3 +99,12 @@ export const getComputeGpus = () => get<ComputeGpu[]>('/compute/gpus')
|
||||
export const getComputeQueue = () => get<ComputeQueueItem[]>('/compute/queue')
|
||||
|
||||
export const getNodeReplicas = (id: string) => get<ResourceReplica[]>(`/compute/nodes/${id}/replicas`)
|
||||
|
||||
export const checkNodeReplicaDrift = (id: string) =>
|
||||
get<{ node_id: string; items: ResourceReplica[]; drifted: number }>(`/compute/nodes/${id}/replicas/drift`)
|
||||
|
||||
export const repairNodeReplicas = (id: string, replicaIds?: string[]) =>
|
||||
post<{ sync: unknown; replicas: ResourceReplica[]; failed: Array<{ replica_id: string; error: string }> }>(
|
||||
`/compute/nodes/${id}/replicas/repair`,
|
||||
replicaIds?.length ? { replica_ids: replicaIds } : {},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,45 @@
|
||||
import { get, post, put, del } from '../request'
|
||||
import type { ModelItem, ModelForm, TrainedModel } from '@/types'
|
||||
|
||||
export interface ModelArtifact {
|
||||
id: string
|
||||
model_id: string
|
||||
model_kind: string
|
||||
artifact_type: string
|
||||
path: string
|
||||
size_bytes: number
|
||||
checksum_sha256?: string
|
||||
metadata?: Record<string, unknown>
|
||||
compute_job_id?: string
|
||||
create_time?: string
|
||||
}
|
||||
|
||||
export interface ModelLineageEdge {
|
||||
id: string
|
||||
child_resource_type: string
|
||||
child_resource_id: string
|
||||
parent_resource_type: string
|
||||
parent_resource_id: string
|
||||
relation_type: string
|
||||
compute_job_id?: string
|
||||
payload?: Record<string, unknown>
|
||||
create_time?: string
|
||||
}
|
||||
|
||||
export interface ModelExportJob {
|
||||
id: string
|
||||
trained_model_id?: string
|
||||
compute_job_id: string
|
||||
node_id?: string
|
||||
export_type: string
|
||||
quantization_bit: number
|
||||
status: string
|
||||
output_dir?: string
|
||||
payload?: Record<string, unknown>
|
||||
create_time?: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
/** 模型列表 */
|
||||
export const getModelList = () => get<ModelItem[]>('/model-manage')
|
||||
|
||||
@@ -18,6 +57,17 @@ export const getLocalModels = () =>
|
||||
export const getTrainedModels = () =>
|
||||
get<{ models: TrainedModel[] }>('/model-manage/trained-models')
|
||||
|
||||
export const getTrainedModelArtifacts = (id: string | number) =>
|
||||
get<ModelArtifact[]>(`/model-manage/trained-models/${id}/artifacts`)
|
||||
|
||||
export const getTrainedModelLineage = (id: string | number) =>
|
||||
get<{ model_id: string; parents: ModelLineageEdge[]; children: ModelLineageEdge[] }>(
|
||||
`/model-manage/trained-models/${id}/lineage`,
|
||||
)
|
||||
|
||||
export const getModelExportJobs = (trainedModelId?: string | number) =>
|
||||
get<ModelExportJob[]>('/model-manage/export-jobs', trainedModelId ? { trained_model_id: trainedModelId } : undefined)
|
||||
|
||||
/** 创建模型 */
|
||||
export const createModel = (data: ModelForm) => post('/model-manage', data)
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ const props = withDefaults(
|
||||
const emit = defineEmits<{
|
||||
create: []
|
||||
refresh: []
|
||||
expandChange: [row: T, expandedRows: T[]]
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
@@ -143,6 +144,10 @@ function handleCreate() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleExpandChange(row: T, expandedRows: T[] | boolean) {
|
||||
emit('expandChange', row, Array.isArray(expandedRows) ? expandedRows : [])
|
||||
}
|
||||
|
||||
// 数据变化时清空选中
|
||||
watch(
|
||||
() => props.data,
|
||||
@@ -214,6 +219,7 @@ defineExpose({ handleDelete, clearSelection })
|
||||
style="width: 100%"
|
||||
:empty-text="emptyText"
|
||||
@selection-change="handleSelectionChange"
|
||||
@expand-change="handleExpandChange"
|
||||
>
|
||||
<el-table-column
|
||||
v-if="multiSelect"
|
||||
|
||||
@@ -1,33 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { statusLabel, statusTagType } from '@/utils/status'
|
||||
|
||||
const props = defineProps<{
|
||||
status?: string
|
||||
}>()
|
||||
|
||||
/** 将任务/模型状态映射为 Element Plus tag 类型 */
|
||||
const tagInfo = computed(() => {
|
||||
const s = props.status || ''
|
||||
switch (s) {
|
||||
case 'running':
|
||||
case 'ready':
|
||||
case 'loaded':
|
||||
return { text: s === 'ready' || s === 'loaded' ? '已就绪' : '运行中', type: 'success' as const }
|
||||
case 'pending':
|
||||
return { text: '等待中', type: 'info' as const }
|
||||
case 'failed':
|
||||
return { text: '失败', type: 'danger' as const }
|
||||
case 'starting':
|
||||
case 'loading':
|
||||
return { text: '加载中', type: 'warning' as const }
|
||||
case 'completed':
|
||||
return { text: '已完成', type: 'success' as const }
|
||||
case 'stopped':
|
||||
return { text: '已停止', type: 'info' as const }
|
||||
default:
|
||||
return { text: s || '未知', type: 'info' as const }
|
||||
}
|
||||
})
|
||||
const tagInfo = computed(() => ({
|
||||
text: statusLabel(props.status),
|
||||
type: statusTagType(props.status),
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
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'
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import PageCard from '@/components/PageCard.vue'
|
||||
import { usePolling } from '@/composables/usePolling'
|
||||
import { getCompare } from '@/api/modules/compare'
|
||||
import type { CompareTask, LoadedModel } from '@/types'
|
||||
import { statusLabel } from '@/utils/status'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -92,7 +93,7 @@ onMounted(async () => {
|
||||
:type="m.status === 'ready' || m.status === 'running' ? 'success' : m.status === 'starting' ? 'warning' : 'danger'"
|
||||
size="large"
|
||||
>
|
||||
{{ m.model_name }} ({{ m.status }})
|
||||
{{ m.model_name }} ({{ statusLabel(m.status) }})
|
||||
</el-tag>
|
||||
<span v-if="!loadedModels.length" class="empty-hint">暂无已启动模型</span>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
checkNodeReplicaDrift,
|
||||
createComputeNode,
|
||||
disableComputeNode,
|
||||
drainComputeNode,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
getComputeNodes,
|
||||
getComputeQueue,
|
||||
getNodeReplicas,
|
||||
repairNodeReplicas,
|
||||
testComputeNode,
|
||||
updateComputeNode,
|
||||
type ComputeGpu,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
type ComputeQueueItem,
|
||||
type ResourceReplica,
|
||||
} from '@/api/modules/compute'
|
||||
import { statusLabel, statusTagType } from '@/utils/status'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -25,6 +28,8 @@ const router = useRouter()
|
||||
const activeTab = ref(String(route.query.tab || 'nodes'))
|
||||
const loading = ref(true)
|
||||
const buttonRefreshing = ref(false)
|
||||
const checkingReplicas = ref(false)
|
||||
const repairingReplicas = ref(false)
|
||||
const nodes = ref<ComputeNode[]>([])
|
||||
const gpus = ref<ComputeGpu[]>([])
|
||||
const queue = ref<ComputeQueueItem[]>([])
|
||||
@@ -58,6 +63,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) => {
|
||||
@@ -68,10 +77,8 @@ watch(
|
||||
async function load(options: { showLoading?: boolean; showButtonLoading?: boolean } = {}) {
|
||||
if (refreshInFlight) return
|
||||
refreshInFlight = true
|
||||
const showLoading = options.showLoading ?? false
|
||||
const showButtonLoading = options.showButtonLoading ?? false
|
||||
if (showLoading) loading.value = true
|
||||
if (showButtonLoading) buttonRefreshing.value = true
|
||||
if (options.showLoading) loading.value = true
|
||||
if (options.showButtonLoading) buttonRefreshing.value = true
|
||||
try {
|
||||
const [nodeList, gpuList, queueList] = await Promise.all([
|
||||
getComputeNodes(),
|
||||
@@ -87,8 +94,8 @@ async function load(options: { showLoading?: boolean; showButtonLoading?: boolea
|
||||
await loadReplicas()
|
||||
lastUpdated.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
|
||||
} finally {
|
||||
if (showLoading) loading.value = false
|
||||
if (showButtonLoading) buttonRefreshing.value = false
|
||||
if (options.showLoading) loading.value = false
|
||||
if (options.showButtonLoading) buttonRefreshing.value = false
|
||||
refreshInFlight = false
|
||||
}
|
||||
}
|
||||
@@ -105,7 +112,7 @@ 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)
|
||||
@@ -121,6 +128,40 @@ async function handleNodeAction(action: 'enable' | 'disable' | 'drain' | 'test',
|
||||
await load()
|
||||
}
|
||||
|
||||
async function handleReplicaDriftCheck() {
|
||||
if (!selectedNodeId.value) return
|
||||
checkingReplicas.value = true
|
||||
try {
|
||||
const result = await checkNodeReplicaDrift(selectedNodeId.value)
|
||||
replicas.value = result.items || []
|
||||
ElMessage.success(result.drifted ? `检测完成,发现 ${result.drifted} 个异常副本` : '检测完成,副本状态正常')
|
||||
} finally {
|
||||
checkingReplicas.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReplicaRepair() {
|
||||
if (!selectedNodeId.value) return
|
||||
const targets = replicas.value.filter((item) => ['drifted', 'failed', 'repair_pending'].includes(item.sync_status))
|
||||
if (!targets.length) {
|
||||
ElMessage.info('暂无需要修复的副本')
|
||||
return
|
||||
}
|
||||
repairingReplicas.value = true
|
||||
try {
|
||||
const result = await repairNodeReplicas(selectedNodeId.value, targets.map((item) => item.id))
|
||||
replicas.value = result.replicas || []
|
||||
if (result.failed?.length) {
|
||||
ElMessage.warning(`修复完成,失败 ${result.failed.length} 个`)
|
||||
} else {
|
||||
ElMessage.success('副本修复完成')
|
||||
}
|
||||
await loadReplicas()
|
||||
} finally {
|
||||
repairingReplicas.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetNodeForm() {
|
||||
Object.assign(nodeForm, {
|
||||
id: '',
|
||||
@@ -146,7 +187,7 @@ function openCreateNodeDialog() {
|
||||
nodeDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditNodeDialog(node: any) {
|
||||
function openEditNodeDialog(node: ComputeNode) {
|
||||
Object.assign(nodeForm, {
|
||||
id: node.id,
|
||||
code: node.code,
|
||||
@@ -212,23 +253,17 @@ async function saveNode() {
|
||||
}
|
||||
}
|
||||
|
||||
function nodeStatusType(status: string) {
|
||||
if (status === 'online') return 'success'
|
||||
if (status === 'draining' || status === 'maintenance') return 'warning'
|
||||
return 'info'
|
||||
function formatBytes(value?: number) {
|
||||
const size = Number(value || 0)
|
||||
if (!size) return '-'
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`
|
||||
return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
function taskStatusType(status: string) {
|
||||
if (status === 'running') return 'success'
|
||||
if (status === 'syncing' || status === 'queued') return 'warning'
|
||||
if (status === 'failed') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function gpuStatusType(status: string) {
|
||||
if (status === 'busy') return 'success'
|
||||
if (status === 'reserved') return 'warning'
|
||||
return 'info'
|
||||
function formatTime(value?: string) {
|
||||
return value ? new Date(value).toLocaleString('zh-CN') : '-'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -256,22 +291,10 @@ onUnmounted(() => {
|
||||
</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">
|
||||
@@ -281,7 +304,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">
|
||||
@@ -308,11 +331,11 @@ onUnmounted(() => {
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="330" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEditNodeDialog(row)">编辑</el-button>
|
||||
<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>
|
||||
@@ -328,13 +351,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 }">
|
||||
@@ -361,19 +382,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>
|
||||
|
||||
@@ -383,14 +404,29 @@ onUnmounted(() => {
|
||||
<el-option v-for="node in nodes" :key="node.id" :label="`${node.code} - ${node.name}`" :value="node.id" />
|
||||
</el-select>
|
||||
<span v-if="selectedNode" class="muted">当前节点 {{ selectedNode.api_base_url }}</span>
|
||||
<el-button :loading="checkingReplicas" @click="handleReplicaDriftCheck">漂移检测</el-button>
|
||||
<el-button type="primary" :loading="repairingReplicas" @click="handleReplicaRepair">修复副本</el-button>
|
||||
</div>
|
||||
<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="110">
|
||||
<template #default="{ row }">{{ formatBytes(row.byte_size) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最后检测" width="190">
|
||||
<template #default="{ row }">{{ formatTime(row.last_checked_at) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -432,21 +468,11 @@ onUnmounted(() => {
|
||||
</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-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>
|
||||
@@ -486,7 +512,8 @@ onUnmounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
.header-actions,
|
||||
.replica-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -499,7 +526,6 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
display: inline-block;
|
||||
min-width: 92px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -549,9 +575,6 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.replica-toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.el-select {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import DataTablePage from '@/components/DataTablePage.vue'
|
||||
import ModelStatusTag from '@/components/ModelStatusTag.vue'
|
||||
import {
|
||||
getEvalList,
|
||||
deleteEval,
|
||||
@@ -84,7 +85,11 @@ onMounted(() => {
|
||||
<el-table-column label="数据集" prop="dataset" align="center" />
|
||||
<el-table-column label="指标" prop="metric" align="center" />
|
||||
<el-table-column label="评分" prop="score" width="100" align="center" />
|
||||
<el-table-column label="状态" prop="status" width="100" align="center" />
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<ModelStatusTag :status="row.status" />
|
||||
</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') : '-' }}
|
||||
|
||||
@@ -99,7 +99,7 @@ export function buildFineTunePayload(
|
||||
|
||||
export function buildFineTuneCommand(form: FineTuneFormModel, gpus: number[]) {
|
||||
const gpuIds = gpus.length ? gpus.join(',') : '0'
|
||||
const stage = form.train_type === 'DPO' ? 'dpo' : form.train_type === 'CPT' ? 'cpt' : 'sft'
|
||||
const stage = form.train_type === 'DPO' ? 'dpo' : form.train_type === 'CPT' ? 'pt' : 'sft'
|
||||
const lines = [
|
||||
`CUDA_VISIBLE_DEVICES=${gpuIds} llamafactory-cli train`,
|
||||
` --stage ${stage}`,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
stopModelByPid,
|
||||
} from '@/api/modules/compare'
|
||||
import type { CompareTask, LoadedModel } from '@/types'
|
||||
import { statusLabel, statusTagType } from '@/utils/status'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -152,7 +153,7 @@ onUnmounted(() => {
|
||||
<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>
|
||||
<el-tag v-else :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status || 'not_started') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="相关模型" align="center" show-overflow-tooltip>
|
||||
|
||||
@@ -1,36 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import DataTablePage from '@/components/DataTablePage.vue'
|
||||
import {
|
||||
getModelList,
|
||||
getTrainedModels,
|
||||
deleteModel,
|
||||
deleteTrainedModel,
|
||||
mergeModel,
|
||||
exportModelUrl,
|
||||
getModelExportJobs,
|
||||
getModelList,
|
||||
getTrainedModelArtifacts,
|
||||
getTrainedModelLineage,
|
||||
getTrainedModels,
|
||||
type ModelArtifact,
|
||||
type ModelExportJob,
|
||||
type ModelLineageEdge,
|
||||
} from '@/api/modules/model'
|
||||
import { MODEL_TYPE_MAP, PURPOSE_MAP, MODEL_SOURCE_MAP } from '@/constants'
|
||||
import { MODEL_SOURCE_MAP, MODEL_TYPE_MAP, PURPOSE_MAP } from '@/constants'
|
||||
import type { ModelItem, TrainedModel } from '@/types'
|
||||
import { mergeStatusLabel, mergeStatusType, statusLabel, statusTagType } from '@/utils/status'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
type TabKey = 'config' | 'trained'
|
||||
const activeTab = ref<TabKey>('config')
|
||||
type TrainedModelRuntime = {
|
||||
artifacts: ModelArtifact[]
|
||||
lineage: { parents: ModelLineageEdge[]; children: ModelLineageEdge[] }
|
||||
exportJobs: ModelExportJob[]
|
||||
loading: boolean
|
||||
loaded: boolean
|
||||
}
|
||||
|
||||
const activeTab = ref<TabKey>('config')
|
||||
const loading = ref(false)
|
||||
const configList = ref<ModelItem[]>([])
|
||||
const trainedList = ref<TrainedModel[]>([])
|
||||
const runtimeMap = reactive<Record<string, TrainedModelRuntime>>({})
|
||||
|
||||
function asModelItem(row: unknown): ModelItem {
|
||||
return row as ModelItem
|
||||
}
|
||||
|
||||
function asTrainedModel(row: unknown): TrainedModel {
|
||||
return row as TrainedModel
|
||||
}
|
||||
|
||||
function runtimeKey(row: TrainedModel) {
|
||||
return String(row.id || row.name)
|
||||
}
|
||||
|
||||
function runtimeOf(row: TrainedModel) {
|
||||
const key = runtimeKey(row)
|
||||
if (!runtimeMap[key]) {
|
||||
runtimeMap[key] = {
|
||||
artifacts: [],
|
||||
lineage: { parents: [], children: [] },
|
||||
exportJobs: [],
|
||||
loading: false,
|
||||
loaded: false,
|
||||
}
|
||||
}
|
||||
return runtimeMap[key]
|
||||
}
|
||||
|
||||
function formatTime(value?: string) {
|
||||
return value ? new Date(value).toLocaleString('zh-CN') : '-'
|
||||
}
|
||||
|
||||
function formatBytes(value?: number) {
|
||||
const size = Number(value || 0)
|
||||
if (size <= 0) return '-'
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`
|
||||
return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
function relationLabel(value?: string) {
|
||||
const map: Record<string, string> = {
|
||||
fine_tuned_from: '由基座模型微调生成',
|
||||
merged_from_adapter: '由 Adapter 合并导出',
|
||||
}
|
||||
return map[value || ''] || value || '-'
|
||||
}
|
||||
|
||||
function artifactTypeLabel(value?: string) {
|
||||
const map: Record<string, string> = {
|
||||
adapter: 'Adapter 权重',
|
||||
merged_model: '合并模型',
|
||||
quantized_model: '量化模型',
|
||||
}
|
||||
return map[value || ''] || value || '-'
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
loading.value = true
|
||||
try {
|
||||
configList.value = (await getModelList()) || []
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -41,8 +106,6 @@ async function loadTrained() {
|
||||
try {
|
||||
const res = await getTrainedModels()
|
||||
trainedList.value = res?.models || []
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -53,32 +116,54 @@ function loadData() {
|
||||
else loadTrained()
|
||||
}
|
||||
|
||||
/** 删除配置模型 */
|
||||
async function handleDeleteConfig(row: any) {
|
||||
async function loadTrainedRuntime(row: TrainedModel, force = false) {
|
||||
const state = runtimeOf(row)
|
||||
if (state.loading || (state.loaded && !force)) return
|
||||
state.loading = true
|
||||
try {
|
||||
const id = row.id || row.name
|
||||
const [artifacts, lineage, exportJobs] = await Promise.all([
|
||||
getTrainedModelArtifacts(id),
|
||||
getTrainedModelLineage(id),
|
||||
getModelExportJobs(id),
|
||||
])
|
||||
state.artifacts = artifacts || []
|
||||
state.lineage = {
|
||||
parents: lineage?.parents || [],
|
||||
children: lineage?.children || [],
|
||||
}
|
||||
state.exportJobs = exportJobs || []
|
||||
state.loaded = true
|
||||
} finally {
|
||||
state.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleExpandChange(row: TrainedModel, expandedRows: TrainedModel[]) {
|
||||
if (expandedRows.some((item) => runtimeKey(item) === runtimeKey(row))) {
|
||||
loadTrainedRuntime(row)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteConfig(row: ModelItem) {
|
||||
await deleteModel(row.id)
|
||||
configList.value = configList.value.filter((item) => item.id !== row.id)
|
||||
await loadConfig()
|
||||
ElMessage.success('删除成功')
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 删除 LoRA 权重 */
|
||||
async function handleDeleteWeight(row: any) {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除模型 "${row.name}" 的权重文件吗?合并模型不受影响。`,
|
||||
'确认删除',
|
||||
{ type: 'warning' },
|
||||
)
|
||||
await deleteTrainedModel(row.name, 'lora')
|
||||
async function handleDeleteWeight(row: TrainedModel) {
|
||||
await ElMessageBox.confirm(`确定要删除模型“${row.name}”的权重文件吗?合并模型不受影响。`, '确认删除', {
|
||||
type: 'warning',
|
||||
})
|
||||
await deleteTrainedModel(row.id || row.name, 'lora')
|
||||
trainedList.value = trainedList.value.filter((item) => item.name !== row.name && item.id !== row.id)
|
||||
await loadTrained()
|
||||
ElMessage.success('权重已删除')
|
||||
}
|
||||
|
||||
/** 合并权重 */
|
||||
async function handleMerge(row: any) {
|
||||
await router.push({
|
||||
function handleMerge(row: TrainedModel) {
|
||||
router.push({
|
||||
path: '/model-manage/merge',
|
||||
query: {
|
||||
model: row.name,
|
||||
@@ -87,12 +172,7 @@ async function handleMerge(row: any) {
|
||||
})
|
||||
}
|
||||
|
||||
/** 导出 */
|
||||
function handleExport(row: any) {
|
||||
window.open(exportModelUrl(row.name), '_blank')
|
||||
}
|
||||
|
||||
function editModel(row: any) {
|
||||
function editModel(row: ModelItem) {
|
||||
router.push(`/model-manage/${row.id}/edit`)
|
||||
}
|
||||
|
||||
@@ -103,34 +183,28 @@ function handleCreateClick() {
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
if (activeTab.value === 'config') {
|
||||
loadConfig()
|
||||
} else {
|
||||
loadTrained()
|
||||
}
|
||||
if (activeTab.value === 'config') loadConfig()
|
||||
else loadTrained()
|
||||
}
|
||||
|
||||
watch(activeTab, () => {
|
||||
loadData()
|
||||
})
|
||||
watch(activeTab, loadData)
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="model-manage-page" style="height: 100%;">
|
||||
<!-- 配置模型列表 -->
|
||||
<div class="model-manage-page">
|
||||
<DataTablePage
|
||||
v-if="activeTab === 'config'"
|
||||
title=""
|
||||
title="模型"
|
||||
:data="configList"
|
||||
:loading="loading"
|
||||
searchable
|
||||
:search-fields="['name', 'description']"
|
||||
create-text="添加模型"
|
||||
@create="handleCreateClick"
|
||||
:delete-fn="handleDeleteConfig"
|
||||
row-key="id"
|
||||
@create="handleCreateClick"
|
||||
@refresh="loadConfig"
|
||||
>
|
||||
<template #title>
|
||||
@@ -140,55 +214,49 @@ onMounted(loadData)
|
||||
</div>
|
||||
</template>
|
||||
<template #columns>
|
||||
<el-table-column label="模型名称" prop="name" align="center" />
|
||||
<el-table-column label="模型名称" prop="name" align="center" min-width="180" />
|
||||
<el-table-column label="模型类型" align="center" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="primary" size="small">
|
||||
{{ MODEL_TYPE_MAP[row.type] || row.type || '-' }}
|
||||
</el-tag>
|
||||
<el-tag type="primary" size="small">{{ MODEL_TYPE_MAP[row.type] || row.type || '-' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用途" align="center" width="100">
|
||||
<el-table-column label="用途" align="center" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="(PURPOSE_MAP[row.purpose]?.type || 'info') as any" size="small">
|
||||
{{ PURPOSE_MAP[row.purpose]?.text || row.purpose || '-' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模型来源" align="center" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ MODEL_SOURCE_MAP[row.model_source] || row.model_source || '-' }}
|
||||
</template>
|
||||
<el-table-column label="模型来源" align="center" width="120">
|
||||
<template #default="{ row }">{{ MODEL_SOURCE_MAP[row.model_source] || row.model_source || '-' }}</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="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.create_time ? new Date(row.create_time).toLocaleString('zh-CN') : '-' }}
|
||||
</template>
|
||||
<template #default="{ row }">{{ formatTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<el-button type="primary" link size="small" @click="editModel(row)">
|
||||
<i class="fa fa-edit" style="margin-right: 4px" />编辑
|
||||
<el-button type="primary" link size="small" @click="editModel(asModelItem(row))">
|
||||
<i class="fa fa-edit" /> 编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="handleDeleteConfig(row)">
|
||||
<i class="fa fa-trash-o" style="margin-right: 4px" />删除
|
||||
<el-button type="danger" link size="small" @click="handleDeleteConfig(asModelItem(row))">
|
||||
<i class="fa fa-trash-o" /> 删除
|
||||
</el-button>
|
||||
</template>
|
||||
</DataTablePage>
|
||||
|
||||
<!-- 训练模型列表 -->
|
||||
<DataTablePage
|
||||
v-else
|
||||
title=""
|
||||
title="训练模型"
|
||||
:data="trainedList"
|
||||
:loading="loading"
|
||||
searchable
|
||||
:search-fields="['name']"
|
||||
row-key="name"
|
||||
@refresh="loadTrained"
|
||||
row-key="id"
|
||||
@expand-change="handleExpandChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #title>
|
||||
<div class="capsule-tabs">
|
||||
@@ -197,62 +265,96 @@ onMounted(loadData)
|
||||
</div>
|
||||
</template>
|
||||
<template #columns>
|
||||
<el-table-column label="模型名称" prop="name" align="center" />
|
||||
<el-table-column label="训练方法" align="center">
|
||||
<el-table-column type="expand" width="48">
|
||||
<template #default="{ row }">
|
||||
{{ row.train_methods?.[0]?.name || '-' }}
|
||||
<div class="runtime-panel" v-loading="runtimeOf(asTrainedModel(row)).loading">
|
||||
<section>
|
||||
<div class="panel-title">
|
||||
<strong>模型产物</strong>
|
||||
<el-button link type="primary" size="small" @click="loadTrainedRuntime(asTrainedModel(row), true)">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="runtimeOf(asTrainedModel(row)).artifacts" size="small" empty-text="暂无产物记录">
|
||||
<el-table-column label="类型" width="120">
|
||||
<template #default="{ row: item }">{{ artifactTypeLabel(item.artifact_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="路径" prop="path" min-width="280" show-overflow-tooltip />
|
||||
<el-table-column label="大小" width="110">
|
||||
<template #default="{ row: item }">{{ formatBytes(item.size_bytes) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Checksum" min-width="220" show-overflow-tooltip>
|
||||
<template #default="{ row: item }">{{ item.checksum_sha256 || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="panel-title"><strong>模型血缘</strong></div>
|
||||
<el-table
|
||||
:data="[...runtimeOf(asTrainedModel(row)).lineage.parents, ...runtimeOf(asTrainedModel(row)).lineage.children]"
|
||||
size="small"
|
||||
empty-text="暂无血缘记录"
|
||||
>
|
||||
<el-table-column label="关系" width="180">
|
||||
<template #default="{ row: item }">{{ relationLabel(item.relation_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="父资源" min-width="220">
|
||||
<template #default="{ row: item }">
|
||||
{{ item.parent_resource_type }} / {{ item.parent_resource_id }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="子资源" min-width="220">
|
||||
<template #default="{ row: item }">
|
||||
{{ item.child_resource_type }} / {{ item.child_resource_id }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Compute Job" prop="compute_job_id" min-width="160" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="panel-title"><strong>导出任务</strong></div>
|
||||
<el-table :data="runtimeOf(asTrainedModel(row)).exportJobs" size="small" empty-text="暂无导出任务">
|
||||
<el-table-column label="任务ID" prop="id" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="类型" prop="export_type" width="100" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row: item }">
|
||||
<el-tag :type="statusTagType(item.status)" size="small">{{ statusLabel(item.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="输出目录" prop="output_dir" min-width="260" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row: item }">{{ formatTime(item.create_time) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模型名称" prop="name" align="center" min-width="180" />
|
||||
<el-table-column label="训练方法" align="center" width="110">
|
||||
<template #default="{ row }">{{ row.train_methods?.[0]?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="基座模型" align="center" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.base_model_path || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="合并状态" align="center" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.merging" type="warning" size="small">合并中</el-tag>
|
||||
<el-tag v-else-if="row.merged" type="success" size="small">已合并</el-tag>
|
||||
<el-tag v-else type="info" size="small">未合并</el-tag>
|
||||
<el-tag :type="mergeStatusType(asTrainedModel(row))" size="small">{{ mergeStatusLabel(asTrainedModel(row)) }}</el-tag>
|
||||
</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>
|
||||
<template #default="{ row }">{{ formatTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
v-if="!row.merged && !row.merging"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleMerge(row)"
|
||||
>
|
||||
<i class="fa fa-code-fork" style="margin-right: 4px" />合并权重
|
||||
<el-button v-if="!row.merged && !row.merging" type="primary" link size="small" @click="handleMerge(asTrainedModel(row))">
|
||||
<i class="fa fa-code-fork" /> 合并权重
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else-if="row.merging"
|
||||
type="info"
|
||||
link
|
||||
size="small"
|
||||
:loading="true"
|
||||
disabled
|
||||
>
|
||||
合并中
|
||||
<el-button v-else-if="row.merging" type="info" link size="small" :loading="true" disabled>合并中</el-button>
|
||||
<el-button type="warning" link size="small" @click="handleDeleteWeight(asTrainedModel(row))">
|
||||
<i class="fa fa-eraser" /> 删除权重
|
||||
</el-button>
|
||||
<el-button type="warning" link size="small" @click="handleDeleteWeight(row)">
|
||||
<i class="fa fa-eraser" style="margin-right: 4px" />删除权重
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.merged"
|
||||
type="success"
|
||||
link
|
||||
size="small"
|
||||
@click="handleExport(row)"
|
||||
>
|
||||
<i class="fa fa-download" style="margin-right: 4px" />导出
|
||||
</el-button>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</DataTablePage>
|
||||
@@ -260,37 +362,33 @@ onMounted(loadData)
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.model-manage-page {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 胶囊切换栏样式 */
|
||||
.capsule-tabs {
|
||||
display: flex;
|
||||
background: #f1f5f9;
|
||||
padding: 3px;
|
||||
border-radius: 8px;
|
||||
gap: 2px;
|
||||
padding: 3px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.capsule-tab-item {
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
padding: 6px 20px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #4f46e5;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@@ -300,4 +398,19 @@ onMounted(loadData)
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.runtime-panel {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 16px 24px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
color: #1f2937;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getUsers } from '@/api/modules/system'
|
||||
import type { SystemUser } from '@/types'
|
||||
import { statusLabel, statusTagType } from '@/utils/status'
|
||||
|
||||
const loading = ref(false)
|
||||
const users = ref<SystemUser[]>([])
|
||||
@@ -32,7 +33,11 @@ onMounted(loadUsers)
|
||||
<el-table-column prop="username" label="账号" min-width="140" />
|
||||
<el-table-column prop="display_name" label="显示名称" min-width="160" />
|
||||
<el-table-column prop="role" label="角色" width="120" />
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column label="状态" width="120">
|
||||
<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 }">{{ row.permissions?.length || 0 }}</template>
|
||||
</el-table-column>
|
||||
|
||||
Reference in New Issue
Block a user