feat(web): add task views, file manager routing and sidebar menu

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
dailz
2026-04-20 10:39:58 +08:00
parent c197b9622b
commit 6c19fed2f3
4 changed files with 324 additions and 18 deletions

View File

@@ -6,18 +6,18 @@
HPC 集群管理
</div>
<el-menu :router="true" :default-active="route.path">
<el-menu-item index="/jobs">
<el-icon><List /></el-icon>
<span>任务列表</span>
</el-menu-item>
<el-menu-item index="/jobs/history">
<el-icon><Timer /></el-icon>
<span>任务历史</span>
</el-menu-item>
<el-menu-item index="/jobs/submit">
<el-menu-item index="/tasks/create">
<el-icon><CirclePlus /></el-icon>
<span>提交任务</span>
</el-menu-item>
<el-menu-item index="/tasks">
<el-icon><List /></el-icon>
<span>任务管理</span>
</el-menu-item>
<el-menu-item index="/files">
<el-icon><FolderOpened /></el-icon>
<span>文件管理</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-main>
@@ -29,7 +29,7 @@
<script setup lang="ts">
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { List, Timer, CirclePlus } from '@element-plus/icons-vue'
import { List, CirclePlus, FolderOpened } from '@element-plus/icons-vue'
const route = useRoute()

View File

@@ -5,7 +5,7 @@ const router = createRouter({
routes: [
{
path: '/',
redirect: '/jobs',
redirect: '/tasks',
},
{
path: '/jobs',
@@ -19,21 +19,33 @@ const router = createRouter({
component: () => import('@/views/Jobs/History.vue'),
meta: { title: '任务历史' },
},
{
path: '/jobs/submit',
name: 'JobsSubmit',
component: () => import('@/views/Jobs/Submit.vue'),
meta: { title: '提交任务' },
},
{
path: '/jobs/:id',
name: 'JobsDetail',
component: () => import('@/views/Jobs/Detail.vue'),
meta: { title: '任务详情' },
},
{
path: '/tasks',
name: 'TasksList',
component: () => import('@/views/Tasks/List.vue'),
meta: { title: '任务列表' },
},
{
path: '/tasks/create',
name: 'TasksCreate',
component: () => import('@/views/Tasks/Submit.vue'),
meta: { title: '提交任务' },
},
{
path: '/files',
name: 'FilesList',
component: () => import('@/views/Files/List.vue'),
meta: { title: '文件管理' },
},
{
path: '/:pathMatch(.*)*',
redirect: '/jobs',
redirect: '/tasks',
},
],
})

View File

@@ -0,0 +1,127 @@
<template>
<div class="list-container">
<h2>任务列表</h2>
<div class="filter-bar">
<el-select v-model="statusFilter" placeholder="状态筛选" clearable style="width: 200px">
<el-option label="全部" value="" />
<el-option label="已提交" value="submitted" />
<el-option label="准备中" value="preparing" />
<el-option label="下载中" value="downloading" />
<el-option label="就绪" value="ready" />
<el-option label="排队中" value="queued" />
<el-option label="运行中" value="running" />
<el-option label="已完成" value="completed" />
<el-option label="失败" value="failed" />
</el-select>
</div>
<el-table :data="tasks" v-loading="loading" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="task_name" label="任务名称" min-width="160" />
<el-table-column prop="app_name" label="应用" width="120" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getTaskStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="partition" label="分区" width="100" />
<el-table-column prop="cpus" label="CPU" width="80" />
<el-table-column prop="slurm_job_id" label="Slurm Job ID" width="120" />
<el-table-column label="创建时间" width="180">
<template #default="{ row }">
{{ new Date(row.created_at).toLocaleString('zh-CN') }}
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</template>
<script setup lang="ts">
import type { TaskResponse } from '@/types/tasks'
import { listTasks } from '@/api/tasks'
const loading = ref(false)
const tasks = ref<TaskResponse[]>([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(20)
const statusFilter = ref('')
const getTaskStatusType = (status: string | undefined): 'success' | 'warning' | 'info' | 'danger' | undefined => {
switch (status) {
case 'submitted': return 'info'
case 'preparing': return 'info'
case 'downloading': return 'warning'
case 'ready': return undefined
case 'queued': return 'warning'
case 'running': return 'success'
case 'completed': return 'success'
case 'failed': return 'danger'
default: return 'info'
}
}
const fetchTasks = async () => {
loading.value = true
try {
const resp = await listTasks({
page: currentPage.value,
page_size: pageSize.value,
status: statusFilter.value || undefined
})
tasks.value = resp.data?.items || []
total.value = resp.data?.total || 0
} finally {
loading.value = false
}
}
watch(statusFilter, () => {
currentPage.value = 1
fetchTasks()
})
const handlePageChange = (page: number) => {
currentPage.value = page
fetchTasks()
}
const handleSizeChange = (size: number) => {
pageSize.value = size
currentPage.value = 1
fetchTasks()
}
onMounted(() => {
fetchTasks()
})
</script>
<style scoped>
.list-container {
padding: 20px;
}
.filter-bar {
margin-bottom: 16px;
}
.pagination-container {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -0,0 +1,167 @@
<template>
<div class="submit-container">
<div class="page-header">
<h2>提交任务</h2>
</div>
<el-card>
<el-form label-width="120px">
<el-form-item label="选择应用">
<el-select v-model="selectedAppId" placeholder="请选择应用" clearable>
<el-option v-for="app in appList" :key="app.id" :label="app.name" :value="app.id" />
</el-select>
</el-form-item>
<el-form-item label="任务名称">
<el-input v-model="form.task_name" placeholder="可选" />
</el-form-item>
<el-form-item label="关联文件">
<el-button @click="showFilePicker = true">选择文件</el-button>
<div v-if="selectedFiles.length" style="margin-top: 8px">
<el-tag
v-for="f in selectedFiles"
:key="f.id"
closable
@close="selectedFiles = selectedFiles.filter(x => x.id !== f.id)"
style="margin: 2px"
>
{{ f.name }}
</el-tag>
</div>
</el-form-item>
<FilePicker v-model="showFilePicker" @select="selectedFiles = $event" />
<template v-if="selectedApp">
<el-form-item
v-for="param in (selectedApp.parameters || [])"
:key="param.name"
:label="param.label || param.name"
>
<el-input v-if="param.type === 'string'" v-model="values[param.name]" />
<el-input-number v-else-if="param.type === 'integer'"
:model-value="values[param.name] ? Number(values[param.name]) : undefined"
@update:model-value="(val: number | undefined) => {
if (val != null) values[param.name] = String(val)
else delete values[param.name]
}"
/>
<el-select v-else-if="param.type === 'enum'" v-model="values[param.name]">
<el-option v-for="opt in param.options" :key="opt" :label="opt" :value="opt" />
</el-select>
<el-switch v-else-if="param.type === 'boolean'"
:model-value="values[param.name] === 'true'"
@update:model-value="(val: string | number | boolean) => { values[param.name] = String(val) }"
/>
<el-input v-else-if="param.type === 'file' || param.type === 'directory'" disabled placeholder="文件选择功能开发中" />
</el-form-item>
</template>
<el-divider>调度参数</el-divider>
<el-form-item label="CPU 数量">
<el-input-number v-model="form.cpus" :min="1" />
</el-form-item>
<el-form-item label="内存 (MB)">
<el-input-number v-model="form.memory_per_node" placeholder="MB" />
</el-form-item>
<el-form-item label="节点数">
<el-input v-model="form.nodes" placeholder=": 2 2-4" />
</el-form-item>
<el-form-item label="任务数">
<el-input-number v-model="form.tasks" :min="1" />
</el-form-item>
<el-form-item label="每任务 CPU ">
<el-input-number v-model="form.cpus_per_task" :min="1" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="submitting" @click="handleSubmit">提交</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import type { Application } from '@/types/tasks'
import { createTask, getApplications } from '@/api/tasks'
import FilePicker from '@/views/Files/FilePicker.vue'
import type { FileResponse } from '@/types/files'
const router = useRouter()
const selectedAppId = ref<number | undefined>(undefined)
const appList = ref<Application[]>([])
const values = ref<Record<string, string>>({})
const submitting = ref(false)
const form = reactive({
task_name: '',
partition: 'normal',
cpus: undefined as number | undefined,
memory_per_node: undefined as number | undefined,
nodes: '',
tasks: undefined as number | undefined,
cpus_per_task: undefined as number | undefined,
})
const selectedFiles = ref<FileResponse[]>([])
const showFilePicker = ref(false)
const selectedApp = computed(() => appList.value.find(a => a.id === selectedAppId.value))
watch(selectedAppId, () => { values.value = {} })
onMounted(async () => {
try {
const resp = await getApplications({ page_size: 100 })
appList.value = resp.data?.applications || []
} catch {
// Error already handled by Axios interceptor
}
})
const handleSubmit = async () => {
if (!selectedAppId.value) { ElMessage.warning('请选择应用'); return }
submitting.value = true
try {
const taskName = form.task_name.trim() || `task_${selectedAppId.value}_${Date.now()}`
const resp = await createTask({ ...form, task_name: taskName, job_name: taskName, app_id: selectedAppId.value, values: values.value, file_ids: selectedFiles.value.map(f => f.id) })
if (resp.success) {
ElMessage.success('任务提交成功')
router.push('/tasks')
} else {
ElMessage.error(resp.error || '提交失败')
}
} catch {
// Error already handled by Axios interceptor
} finally {
submitting.value = false
}
}
const handleReset = () => {
selectedAppId.value = undefined
form.task_name = ''
form.partition = 'normal'
form.cpus = undefined
form.memory_per_node = undefined
form.nodes = ''
form.tasks = undefined
form.cpus_per_task = undefined
values.value = {}
selectedFiles.value = []
showFilePicker.value = false
}
</script>
<style scoped>
.submit-container {
padding: 20px;
}
</style>