更新平台治理
This commit is contained in:
377
frontend/src/views/governance/ResourceAclView.vue
Normal file
377
frontend/src/views/governance/ResourceAclView.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getDatasetList, type DatasetItem } from '@/api/modules/dataset'
|
||||
import { getTrainedModels } from '@/api/modules/model'
|
||||
import type { TrainedModel } from '@/types'
|
||||
import { getUsers, type SystemUser } from '@/api/modules/system'
|
||||
import { getResourceAcl, setResourceAcl, type AclEntry } from '@/api/modules/resource-acl'
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
|
||||
// 资源类型选项
|
||||
const resourceTypes = [
|
||||
{ value: 'dataset', label: '数据集' },
|
||||
{ value: 'trained_model', label: '训练产物(微调模型)' },
|
||||
]
|
||||
|
||||
// 资源列表
|
||||
const datasets = ref<DatasetItem[]>([])
|
||||
const models = ref<TrainedModel[]>([])
|
||||
const users = ref<SystemUser[]>([])
|
||||
|
||||
// 当前选择
|
||||
const form = reactive({
|
||||
resourceType: 'dataset',
|
||||
resourceIds: [] as string[],
|
||||
resourceNames: '' as string,
|
||||
})
|
||||
|
||||
// ACL 编辑
|
||||
const aclEntries = ref<AclEntry[]>([])
|
||||
const showAddEntry = ref(false)
|
||||
const newEntry = reactive({
|
||||
principal_id: '',
|
||||
permissions: ['read', 'execute'] as string[],
|
||||
})
|
||||
|
||||
const permissionOptions = [
|
||||
{ label: '查看 (read)', value: 'read' },
|
||||
{ label: '编辑 (write)', value: 'write' },
|
||||
{ label: '使用 (execute)', value: 'execute' },
|
||||
{ label: '下载 (download)', value: 'download' },
|
||||
{ label: '删除 (delete)', value: 'delete' },
|
||||
{ label: '全部权限 (admin)', value: 'admin' },
|
||||
]
|
||||
|
||||
// 加载所有数据
|
||||
async function loadAll() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [dsRes, trainedRes, userRes] = await Promise.all([
|
||||
getDatasetList().catch(() => []),
|
||||
getTrainedModels().catch(() => ({ models: [] })),
|
||||
getUsers().catch(() => []),
|
||||
])
|
||||
datasets.value = dsRes || []
|
||||
models.value = trainedRes?.models || []
|
||||
users.value = userRes || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前资源类型的列表
|
||||
function currentResourceList() {
|
||||
if (form.resourceType === 'dataset') {
|
||||
return datasets.value.map((d) => ({ id: d.id, name: d.name || d.id }))
|
||||
}
|
||||
if (form.resourceType === 'trained_model') {
|
||||
return models.value.map((m) => ({ id: String(m.id || m.name || ''), name: m.name || String(m.id) || '' }))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// 获取当前资源的名称
|
||||
function getResourceName(id: string) {
|
||||
return currentResourceList().find((r) => r.id === id)?.name || id
|
||||
}
|
||||
|
||||
// 选择资源后加载 ACL
|
||||
async function onResourceChange() {
|
||||
if (!form.resourceIds || form.resourceIds.length === 0) {
|
||||
aclEntries.value = []
|
||||
form.resourceNames = ''
|
||||
return
|
||||
}
|
||||
// 显示选中的资源名称
|
||||
const names = form.resourceIds.map((id) => getResourceName(id))
|
||||
form.resourceNames = names.join('、')
|
||||
|
||||
// 加载第一个资源的 ACL 作为初始值(多个资源的 ACL 合并显示)
|
||||
try {
|
||||
const res = await getResourceAcl(form.resourceType, form.resourceIds[0])
|
||||
if (Array.isArray(res)) {
|
||||
aclEntries.value = res.map((e: any) => ({
|
||||
...e,
|
||||
principal_type: e.principal_type || 'user',
|
||||
permissions: Array.isArray(e.permissions) ? e.permissions : [],
|
||||
}))
|
||||
} else if (res && Array.isArray((res as any).entries)) {
|
||||
aclEntries.value = (res as any).entries.map((e: any) => ({
|
||||
...e,
|
||||
principal_type: e.principal_type || 'user',
|
||||
permissions: Array.isArray(e.permissions) ? e.permissions : [],
|
||||
}))
|
||||
} else {
|
||||
aclEntries.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载 ACL 失败:', e)
|
||||
aclEntries.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 切换资源类型时重置
|
||||
function onTypeChange() {
|
||||
form.resourceIds = []
|
||||
form.resourceNames = ''
|
||||
aclEntries.value = []
|
||||
}
|
||||
|
||||
// 添加授权条目
|
||||
function addEntry() {
|
||||
const trimmedId = (newEntry.principal_id || '').trim()
|
||||
if (!trimmedId) {
|
||||
ElMessage.warning('请选择要授权的用户')
|
||||
return
|
||||
}
|
||||
if (!newEntry.permissions || newEntry.permissions.length === 0) {
|
||||
ElMessage.warning('请至少选择一个权限')
|
||||
return
|
||||
}
|
||||
const exists = aclEntries.value.some((e) => e.principal_id === trimmedId)
|
||||
if (exists) {
|
||||
ElMessage.warning('该用户已存在,请先删除再重新添加')
|
||||
return
|
||||
}
|
||||
aclEntries.value.push({
|
||||
principal_type: 'user',
|
||||
principal_id: trimmedId,
|
||||
permissions: [...newEntry.permissions],
|
||||
})
|
||||
showAddEntry.value = false
|
||||
newEntry.principal_id = ''
|
||||
newEntry.permissions = ['read', 'execute']
|
||||
}
|
||||
|
||||
// 删除授权条目
|
||||
function removeEntry(index: number) {
|
||||
aclEntries.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 保存 ACL
|
||||
async function saveAcl() {
|
||||
if (!form.resourceIds || form.resourceIds.length === 0) {
|
||||
ElMessage.warning('请先选择资源')
|
||||
return
|
||||
}
|
||||
if (aclEntries.value.length === 0) {
|
||||
ElMessage.warning('请至少添加一个授权用户')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
// 为每个选中的资源保存相同的 ACL 授权
|
||||
for (const resourceId of form.resourceIds) {
|
||||
await setResourceAcl(form.resourceType, resourceId, aclEntries.value)
|
||||
}
|
||||
ElMessage.success(`已为 ${form.resourceIds.length} 个资源保存授权配置`)
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示名称
|
||||
function principalName(entry: AclEntry) {
|
||||
const user = users.value.find((u) => u.id === entry.principal_id)
|
||||
return user ? `${user.display_name || user.username}` : entry.principal_id
|
||||
}
|
||||
|
||||
onMounted(loadAll)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page" v-loading="loading">
|
||||
<h2 class="page-title">资源授权管理</h2>
|
||||
<p class="page-desc">将数据集或微调模型授权给指定用户使用。被授权的用户可以在自己的页面看到并使用该资源。</p>
|
||||
|
||||
<!-- 步骤1:选择资源 -->
|
||||
<el-card class="section-card">
|
||||
<template #header>
|
||||
<span>① 选择要授权的资源</span>
|
||||
</template>
|
||||
<el-form :inline="true" label-width="90px">
|
||||
<el-form-item label="资源类型">
|
||||
<el-select v-model="form.resourceType" @change="onTypeChange" style="width: 200px">
|
||||
<el-option v-for="t in resourceTypes" :key="t.value" :label="t.label" :value="t.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择资源">
|
||||
<el-select
|
||||
v-model="form.resourceIds"
|
||||
multiple
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="搜索并选择多个资源..."
|
||||
style="width: 400px"
|
||||
@change="onResourceChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="r in currentResourceList()"
|
||||
:key="r.id"
|
||||
:label="`${r.name} (${r.id.slice(0, 8)}...)`"
|
||||
:value="r.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 步骤2:当前授权列表 + 添加授权 -->
|
||||
<el-card class="section-card" v-if="form.resourceIds.length > 0">
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span>② 管理授权 — {{ form.resourceNames }}</span>
|
||||
<el-button type="primary" size="small" @click="showAddEntry = true">+ 添加授权</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 已有授权列表 -->
|
||||
<el-table :data="aclEntries" border empty-text="暂无授权,点击上方按钮添加" style="width: 100%">
|
||||
<el-table-column label="用户">
|
||||
<template #default="{ row }">{{ principalName(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="授权资源" min-width="200">
|
||||
<template #default>
|
||||
<el-tag v-for="(id, idx) in form.resourceIds" :key="id" size="small" style="margin: 2px">
|
||||
{{ getResourceName(id) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="权限">
|
||||
<template #default="{ row }">
|
||||
<el-space wrap>
|
||||
<el-tag
|
||||
v-for="p in row.permissions"
|
||||
:key="p"
|
||||
size="small"
|
||||
:type="p === 'admin' ? 'danger' : p === 'delete' || p === 'write' ? 'warning' : ''"
|
||||
>{{ p }}</el-tag>
|
||||
</el-space>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" size="small" link @click="removeEntry($index)">移除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 添加授权表单 -->
|
||||
<el-divider v-if="showAddEntry" content-position="left">添加新授权</el-divider>
|
||||
<div v-if="showAddEntry" class="add-entry-form">
|
||||
<el-form :inline="true" label-width="80px">
|
||||
<el-form-item label="选择用户">
|
||||
<el-select
|
||||
v-model="newEntry.principal_id"
|
||||
filterable
|
||||
placeholder="选择要授权的用户"
|
||||
style="width: 240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="u in users"
|
||||
:key="u.id"
|
||||
:label="`${u.display_name || u.username} (${u.username})`"
|
||||
:value="u.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="权限">
|
||||
<el-checkbox-group v-model="newEntry.permissions">
|
||||
<el-checkbox v-for="p in permissionOptions" :key="p.value" :value="p.value">{{ p.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="addEntry">确认添加</el-button>
|
||||
<el-button @click="showAddEntry = false">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="save-bar">
|
||||
<el-button type="primary" :loading="saving" @click="saveAcl">保存授权配置</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<el-card class="section-card help-card">
|
||||
<template #header><span>使用说明</span></template>
|
||||
<div class="help-content">
|
||||
<p><strong>什么是资源授权?</strong></p>
|
||||
<p>默认情况下,普通用户只能看到自己创建的资源。通过授权,管理员可以让指定用户也能看到和使用别人的资源。</p>
|
||||
<br />
|
||||
<p><strong>典型场景:</strong></p>
|
||||
<ul>
|
||||
<li>zhangsan 训练出了模型 ft_qwen_001,admin 授权给 lisi,lisi 可以用这个模型做推理</li>
|
||||
<li>admin 上传了公共数据集,授权给指定用户,被授权用户可以在训练时使用该数据集</li>
|
||||
</ul>
|
||||
<br />
|
||||
<p><strong>权限说明:</strong></p>
|
||||
<ul>
|
||||
<li><code>read</code> - 在列表中看到该资源</li>
|
||||
<li><code>execute</code> - 用该资源做训练/推理/评测</li>
|
||||
<li><code>write</code> - 修改资源信息</li>
|
||||
<li><code>download</code> - 下载文件</li>
|
||||
<li><code>delete</code> - 删除资源</li>
|
||||
<li><code>admin</code> - 全部权限(含转授权)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page {
|
||||
padding: 20px;
|
||||
max-width: 1100px;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.page-desc {
|
||||
color: #666;
|
||||
margin: 0 0 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.section-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.add-entry-form {
|
||||
background: #f9fafc;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
}
|
||||
.save-bar {
|
||||
text-align: right;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
.help-card {
|
||||
.help-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: #555;
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
margin: 4px 0;
|
||||
li { margin-bottom: 4px; }
|
||||
}
|
||||
code {
|
||||
background: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
changeMyPassword,
|
||||
@@ -8,30 +8,12 @@ import {
|
||||
resetUserPassword,
|
||||
updateUserAccess,
|
||||
} from '@/api/modules/system'
|
||||
import type { PermissionCode, SystemUser, UserStatus } from '@/types'
|
||||
import type { SystemUser, UserStatus } from '@/types'
|
||||
import { statusLabel, statusTagType } from '@/utils/status'
|
||||
|
||||
const loading = ref(false)
|
||||
const users = ref<SystemUser[]>([])
|
||||
|
||||
// 权限码 -> 中文名(与路由模块一一对应)
|
||||
const PERMISSION_LABELS: Record<PermissionCode, string> = {
|
||||
dashboard: '服务看板',
|
||||
'fine-tune': '模型训练',
|
||||
'model-eval': '模型评测',
|
||||
'model-inference': '模型推理',
|
||||
'model-manage': '模型管理',
|
||||
dataset: '数据集管理',
|
||||
'data-process': '数据处理',
|
||||
'data-convert': '数据转换',
|
||||
compute: '计算资源',
|
||||
hardware: '硬件监控',
|
||||
logs: '日志中心',
|
||||
'user-settings': '用户与权限',
|
||||
}
|
||||
|
||||
const ALL_PERMISSIONS = Object.keys(PERMISSION_LABELS) as PermissionCode[]
|
||||
|
||||
async function loadUsers() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -59,14 +41,6 @@ function asSystemUser(row: unknown): SystemUser {
|
||||
return row as SystemUser
|
||||
}
|
||||
|
||||
function userPermissions(row: unknown): PermissionCode[] {
|
||||
return (asSystemUser(row).permissions || []) as PermissionCode[]
|
||||
}
|
||||
|
||||
function permissionLabel(code: PermissionCode): string {
|
||||
return PERMISSION_LABELS[code] || code
|
||||
}
|
||||
|
||||
// ---------- 启停 ----------
|
||||
async function toggleStatus(row: SystemUser, next: boolean) {
|
||||
const nextStatus: UserStatus = next ? 'active' : 'disabled'
|
||||
@@ -135,60 +109,6 @@ async function confirmChangeMyPwd() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 页面权限 ----------
|
||||
const permDialog = reactive({
|
||||
visible: false,
|
||||
id: '',
|
||||
name: '',
|
||||
checked: [] as PermissionCode[],
|
||||
saving: false,
|
||||
})
|
||||
function openPerms(row: SystemUser) {
|
||||
permDialog.id = row.id
|
||||
permDialog.name = row.display_name
|
||||
// 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 })
|
||||
ElMessage.success(`已更新 ${permDialog.name} 的页面权限`)
|
||||
permDialog.visible = false
|
||||
await loadUsers()
|
||||
} catch {
|
||||
ElMessage.error('权限更新失败')
|
||||
} finally {
|
||||
permDialog.saving = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 权限列: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) {
|
||||
try {
|
||||
@@ -216,7 +136,7 @@ async function removeUser(row: SystemUser) {
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>用户设置</h1>
|
||||
<p>管理平台账号、角色状态、登录密码与页面权限。</p>
|
||||
<p>管理平台账号、角色状态与登录密码。</p>
|
||||
</div>
|
||||
<div>
|
||||
<el-button @click="openChangeMyPwd">修改密码</el-button>
|
||||
@@ -233,21 +153,6 @@ async function removeUser(row: SystemUser) {
|
||||
<el-tag :type="statusTagType(asSystemUser(row).status)" size="small">{{ statusLabel(asSystemUser(row).status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="页面权限" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-for="p in userPermissions(row).slice(0, 3)"
|
||||
:key="p"
|
||||
size="small"
|
||||
type="info"
|
||||
class="perm-tag"
|
||||
>{{ permissionLabel(p) }}</el-tag>
|
||||
<span v-if="userPermissions(row).length > 3" class="perm-more">
|
||||
+{{ userPermissions(row).length - 3 }}
|
||||
</span>
|
||||
<span v-if="!userPermissions(row).length" class="perm-more">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" min-width="180" />
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
@@ -265,11 +170,6 @@ async function removeUser(row: SystemUser) {
|
||||
:disabled="asSystemUser(row).protected"
|
||||
@click="openResetPwd(asSystemUser(row))"
|
||||
>重置密码</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openPerms(asSystemUser(row))"
|
||||
>页面权限</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@@ -290,26 +190,6 @@ async function removeUser(row: SystemUser) {
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 页面权限 -->
|
||||
<el-dialog v-model="permDialog.visible" title="页面权限" width="540px">
|
||||
<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"
|
||||
:value="code"
|
||||
:label="PERMISSION_LABELS[code]"
|
||||
/>
|
||||
</el-checkbox-group>
|
||||
<template #footer>
|
||||
<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">
|
||||
@@ -350,24 +230,8 @@ async function removeUser(row: SystemUser) {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.perm-tag {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.perm-more {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dlg-tip {
|
||||
margin: 0 0 12px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.perm-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,8 +10,18 @@ const tenant = ref<Tenant | null>(null)
|
||||
const loading = ref(false)
|
||||
const quotaForm = reactive({ gpu: 0, storage: 0, maxProjects: 0 })
|
||||
|
||||
function parseQuota(quota: Record<string, unknown> | undefined | null) {
|
||||
const q = quota || {}
|
||||
function parseQuota(quota: Record<string, unknown> | undefined | null | string) {
|
||||
// 兼容处理:quota 可能是 JSON 字符串或嵌套 { quota: {...} } 格式
|
||||
let q: Record<string, unknown> = {}
|
||||
if (typeof quota === 'string') {
|
||||
try { q = JSON.parse(quota) || {} } catch { q = {} }
|
||||
} else if (quota && typeof quota === 'object') {
|
||||
q = quota
|
||||
}
|
||||
// 如果是嵌套格式 { quota: { gpu: 4, ... } },提取内层
|
||||
if (q.quota && typeof q.quota === 'object' && !q.gpu) {
|
||||
q = q.quota as Record<string, unknown>
|
||||
}
|
||||
return {
|
||||
gpu: Number(q.gpu || q.gpu_quota || 0),
|
||||
storage: Number(q.storage || q.storage_quota || 0),
|
||||
|
||||
@@ -15,8 +15,17 @@ const currentTenant = ref<Tenant | null>(null)
|
||||
const form = ref({ name: '', code: '', gpu: 0, storage: 0, maxProjects: 0 })
|
||||
const quotaForm = reactive({ gpu: 0, storage: 0, maxProjects: 0 })
|
||||
|
||||
function parseQuota(quota: Record<string, unknown> | undefined | null) {
|
||||
const q = quota || {}
|
||||
function parseQuota(quota: Record<string, unknown> | undefined | null | string) {
|
||||
// 兼容处理:quota 可能是 JSON 字符串或嵌套 { quota: {...} } 格式
|
||||
let q: Record<string, unknown> = {}
|
||||
if (typeof quota === 'string') {
|
||||
try { q = JSON.parse(quota) || {} } catch { q = {} }
|
||||
} else if (quota && typeof quota === 'object') {
|
||||
q = quota
|
||||
}
|
||||
if (q.quota && typeof q.quota === 'object' && !q.gpu) {
|
||||
q = q.quota as Record<string, unknown>
|
||||
}
|
||||
return {
|
||||
gpu: Number(q.gpu || q.gpu_quota || 0),
|
||||
storage: Number(q.storage || q.storage_quota || 0),
|
||||
|
||||
Reference in New Issue
Block a user