完善部分平台治理功能,及修改看板缺陷

This commit is contained in:
wangjiming
2026-08-10 11:41:16 +08:00
parent 75cc105ebc
commit f809825a7d
18 changed files with 1873 additions and 362 deletions

View File

@@ -22,6 +22,9 @@ import {
type ResourceReplica,
type ResourceSyncJob,
} from '@/api/modules/compute'
import { getGpuAssignments, assignGpus, unassignGpu, type GpuAssignment } from '@/api/modules/gpu'
import { getUsers, type SystemUser } from '@/api/modules/system'
import { useAuthStore } from '@/stores/auth'
import { statusLabel, statusTagType } from '@/utils/status'
const route = useRoute()
@@ -37,6 +40,52 @@ const gpus = ref<ComputeGpu[]>([])
const queue = ref<ComputeQueueItem[]>([])
const replicas = ref<ResourceReplica[]>([])
const activeSyncJob = ref<ResourceSyncJob | null>(null)
// GPU 分配管理
const auth = useAuthStore()
const gpuAssignments = ref<GpuAssignment[]>([])
const assignmentLoading = ref(false)
const showAssignDialog = ref(false)
const assignForm = ref({ node_id: '', gpu_index: 0, user_id: '' })
const allUsers = ref<SystemUser[]>([])
async function loadGpuAssignments() {
if (!auth.isAdmin) return
assignmentLoading.value = true
try {
gpuAssignments.value = await getGpuAssignments()
} catch {
gpuAssignments.value = []
} finally {
assignmentLoading.value = false
}
}
async function handleAssign() {
if (!assignForm.value.node_id || !assignForm.value.user_id) {
ElMessage.warning('请选择节点和用户')
return
}
try {
await assignGpus([{ node_id: assignForm.value.node_id, gpu_index: assignForm.value.gpu_index, user_id: assignForm.value.user_id }])
ElMessage.success('GPU 分配成功')
showAssignDialog.value = false
await loadGpuAssignments()
} catch {
ElMessage.error('GPU 分配失败')
}
}
async function handleUnassign(id: string) {
try {
await ElMessageBox.confirm('确认撤销此 GPU 分配?', '提示', { type: 'warning' })
await unassignGpu(id)
ElMessage.success('已撤销分配')
await loadGpuAssignments()
} catch {
// 用户取消
}
}
const selectedNodeId = ref('')
const lastUpdated = ref('')
const nodeDialogVisible = ref(false)
@@ -127,6 +176,10 @@ async function loadReplicas() {
async function changeTab(name: string | number) {
await router.replace({ path: '/compute', query: { tab: String(name) } })
if (name === 'assignments') {
loadGpuAssignments()
allUsers.value = await getUsers().catch(() => [])
}
}
async function handleNodeAction(action: 'enable' | 'disable' | 'test', node: ComputeNode) {
@@ -309,6 +362,7 @@ function formatTime(value?: string) {
onMounted(() => {
load({ showLoading: true })
timer = setInterval(() => load(), 5000)
if (auth.isAdmin) loadGpuAssignments()
})
onUnmounted(() => {
@@ -476,6 +530,24 @@ onUnmounted(() => {
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane v-if="auth.isAdmin" label="GPU 分配" name="assignments">
<div style="margin-bottom: 16px">
<el-button type="primary" @click="showAssignDialog = true">分配 GPU</el-button>
</div>
<el-table :data="gpuAssignments" border v-loading="assignmentLoading">
<el-table-column prop="node_name" label="节点" min-width="140" />
<el-table-column prop="gpu_index" label="GPU 序号" width="100" />
<el-table-column prop="gpu_name" label="GPU 名称" min-width="160" />
<el-table-column prop="display_name" label="被分配用户" min-width="140" />
<el-table-column prop="assigned_at" label="分配时间" min-width="180" />
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button type="danger" size="small" @click="handleUnassign(row.id)">撤销</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
<el-dialog
@@ -526,6 +598,29 @@ onUnmounted(() => {
<el-button type="primary" :loading="savingNode" @click="saveNode">保存</el-button>
</template>
</el-dialog>
<!-- GPU 分配对话框 -->
<el-dialog v-model="showAssignDialog" title="分配 GPU" width="480px">
<el-form label-width="100px">
<el-form-item label="算力节点">
<el-select v-model="assignForm.node_id" placeholder="选择节点">
<el-option v-for="n in nodes" :key="n.id" :label="n.name || n.code" :value="n.id" />
</el-select>
</el-form-item>
<el-form-item label="GPU 序号">
<el-input-number v-model="assignForm.gpu_index" :min="0" :max="15" />
</el-form-item>
<el-form-item label="用户">
<el-select v-model="assignForm.user_id" placeholder="选择用户" filterable>
<el-option v-for="u in allUsers" :key="u.id" :label="u.display_name || u.username" :value="u.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAssignDialog = false">取消</el-button>
<el-button type="primary" @click="handleAssign">确认分配</el-button>
</template>
</el-dialog>
</section>
</template>

View File

@@ -16,6 +16,8 @@ import { getModelList } from '@/api/modules/model'
import { getDatasetList } from '@/api/modules/dataset'
import { getSystemInfo } from '@/api/modules/system'
import { getComputeNodes } from '@/api/modules/compute'
import { getMyGpus } from '@/api/modules/gpu'
import { useAuthStore } from '@/stores/auth'
import { TEMPLATE_GROUPS, LR_SCHEDULER_OPTIONS, QUANTIZATION_BIT_OPTIONS, QUANT_METHOD_OPTIONS, GGUF_FORMAT_OPTIONS } from '@/constants'
import {
DEFAULT_TRAINING_PARAMS,
@@ -46,8 +48,21 @@ const availableGpus = computed(() => {
.filter((n) => n.scheduler_status === 'online' || n.scheduler_status === 'draining')
.map((n) => n.id),
)
return gpus.value.filter((gpu) => !gpu.node_id || onlineNodeIds.has(gpu.node_id))
let result = gpus.value.filter((gpu) => !gpu.node_id || onlineNodeIds.has(gpu.node_id))
// 普通用户只能看到被分配的 GPU
if (!auth.isAdmin) {
const assignedKeys = new Set(
myAssignedGpus.value.map((g) => `${g.node_id}:${g.gpu_index}`),
)
result = result.filter((gpu) => {
const key = `${gpu.node_id}:${gpu.id ?? gpu.uuid ?? gpu.name}`
return assignedKeys.has(key) || myAssignedGpus.value.length === 0
})
}
return result
})
const auth = useAuthStore()
const myAssignedGpus = ref<Array<{ node_id: string; gpu_index: number }>>([])
const modelDialogVisible = ref(false)
const form = reactive(createDefaultFineTuneForm())
@@ -207,6 +222,14 @@ async function loadGpus() {
const [sys, nodes] = await Promise.all([getSystemInfo(), getComputeNodes().catch(() => [])])
gpus.value = sys?.gpu || []
computeNodes.value = nodes || []
// 普通用户加载被分配的 GPU
if (!auth.isAdmin) {
try {
myAssignedGpus.value = await getMyGpus()
} catch {
myAssignedGpus.value = []
}
}
const firstIdle = availableGpus.value.find((gpu) => !isGpuUnavailable(gpu) && gpu.id != null)
if (firstIdle) selectedGpuKeys.value = [gpuKey(firstIdle)]
} catch {

View File

@@ -2,6 +2,7 @@
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
changeMyPassword,
deleteUser,
getUsers,
resetUserPassword,
@@ -106,6 +107,34 @@ async function confirmResetPwd() {
}
}
// ---------- 用户自改密码 ----------
const myPwdDialog = reactive({ visible: false, oldPassword: '', newPassword: '', saving: false })
function openChangeMyPwd() {
myPwdDialog.oldPassword = ''
myPwdDialog.newPassword = ''
myPwdDialog.visible = true
}
async function confirmChangeMyPwd() {
if (!myPwdDialog.oldPassword.trim() || !myPwdDialog.newPassword.trim()) {
ElMessage.warning('请填写旧密码和新密码')
return
}
if (myPwdDialog.newPassword.length < 6) {
ElMessage.warning('新密码至少 6 位')
return
}
myPwdDialog.saving = true
try {
await changeMyPassword(myPwdDialog.oldPassword.trim(), myPwdDialog.newPassword.trim())
ElMessage.success('密码修改成功')
myPwdDialog.visible = false
} catch {
ElMessage.error('密码修改失败,请检查旧密码是否正确')
} finally {
myPwdDialog.saving = false
}
}
// ---------- 页面权限 ----------
const permDialog = reactive({
visible: false,
@@ -117,10 +146,24 @@ const permDialog = reactive({
function openPerms(row: SystemUser) {
permDialog.id = row.id
permDialog.name = row.display_name
permDialog.checked = [...(row.permissions || [])]
// admin 用户强制全选且只读
if (row.role === 'admin' || row.protected) {
permDialog.checked = [...ALL_PERMISSIONS]
} else {
// 非 admin 用户去掉 user-settings
permDialog.checked = (row.permissions || []).filter((p) => p !== 'user-settings')
}
permDialog.visible = true
}
async function confirmPerms() {
if (permReadonly.value) {
permDialog.visible = false
return
}
// 双重保险:非 admin 用户不允许勾选 user-settings
if (!isTargetAdmin.value) {
permDialog.checked = permDialog.checked.filter((p) => p !== 'user-settings')
}
permDialog.saving = true
try {
await updateUserAccess(permDialog.id, { permissions: permDialog.checked })
@@ -134,7 +177,17 @@ async function confirmPerms() {
}
}
const permColumns = computed(() => ALL_PERMISSIONS)
/** 权限列admin 用户全选且只读,非 admin 用户不显示 user-settings */
const isTargetAdmin = computed(() => {
const u = users.value.find((u) => u.id === permDialog.id)
return u?.role === 'admin' || u?.protected === true
})
const permColumns = computed(() => {
if (isTargetAdmin.value) return ALL_PERMISSIONS
// 非 admin 用户不能拥有 user-settings 权限
return ALL_PERMISSIONS.filter((c) => c !== 'user-settings')
})
const permReadonly = computed(() => isTargetAdmin.value)
// ---------- 删除 ----------
async function removeUser(row: SystemUser) {
@@ -165,7 +218,10 @@ async function removeUser(row: SystemUser) {
<h1>用户设置</h1>
<p>管理平台账号角色状态登录密码与页面权限</p>
</div>
<el-button type="primary" @click="$router.push('/user-settings/create')">创建用户</el-button>
<div>
<el-button @click="openChangeMyPwd">修改密码</el-button>
<el-button type="primary" @click="$router.push('/user-settings/create')">创建用户</el-button>
</div>
</header>
<el-table :data="users" border>
@@ -236,8 +292,11 @@ async function removeUser(row: SystemUser) {
<!-- 页面权限 -->
<el-dialog v-model="permDialog.visible" title="页面权限" width="540px">
<p class="dlg-tip"> <b>{{ permDialog.name }}</b> 分配可访问的页面模块</p>
<el-checkbox-group v-model="permDialog.checked" class="perm-group">
<p class="dlg-tip">
<b>{{ permDialog.name }}</b> 分配可访问的页面模块
<el-tag v-if="permReadonly" type="warning" size="small" style="margin-left: 8px">管理员权限不可更改</el-tag>
</p>
<el-checkbox-group v-model="permDialog.checked" class="perm-group" :disabled="permReadonly">
<el-checkbox
v-for="code in permColumns"
:key="code"
@@ -246,8 +305,24 @@ async function removeUser(row: SystemUser) {
/>
</el-checkbox-group>
<template #footer>
<el-button @click="permDialog.visible = false">取消</el-button>
<el-button type="primary" :loading="permDialog.saving" @click="confirmPerms">保存</el-button>
<el-button @click="permDialog.visible = false">{{ permReadonly ? '关闭' : '取消' }}</el-button>
<el-button v-if="!permReadonly" type="primary" :loading="permDialog.saving" @click="confirmPerms">保存</el-button>
</template>
</el-dialog>
<!-- 修改自己的密码 -->
<el-dialog v-model="myPwdDialog.visible" title="修改密码" width="420px">
<el-form label-width="80px">
<el-form-item label="旧密码">
<el-input v-model="myPwdDialog.oldPassword" placeholder="请输入当前密码" show-password />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="myPwdDialog.newPassword" placeholder="至少 6 位" show-password />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="myPwdDialog.visible = false">取消</el-button>
<el-button type="primary" :loading="myPwdDialog.saving" @click="confirmChangeMyPwd">确认修改</el-button>
</template>
</el-dialog>
</section>