feat: integrate project management into unified policy page

- Add project section: add/remove projects with policy selection
- Each project card shows: policies, spending, monitor toggle, remove
- Replaces separate project management dialog
- All project and policy operations on one page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-03-28 22:53:18 +08:00
parent fab4765e90
commit dacc521c1c
2 changed files with 97 additions and 9 deletions

View File

@ -748,6 +748,8 @@ def iam_user_policies_overview_view(request, pk):
'project_name': proj.project_name,
'display_name': proj.display_name,
'project_id': proj.id,
'monitor_enabled': proj.monitor_enabled,
'current_spending': str(proj.current_spending),
'policies': proj_items,
})
except Exception:
@ -755,6 +757,8 @@ def iam_user_policies_overview_view(request, pk):
'project_name': proj.project_name,
'display_name': proj.display_name,
'project_id': proj.id,
'monitor_enabled': proj.monitor_enabled,
'current_spending': str(proj.current_spending),
'policies': [],
})

View File

@ -1,7 +1,7 @@
<template>
<div style="max-width: 1200px; margin: 0 auto;">
<div class="page-header">
<h2>{{ overview.display_name || overview.username }} 权限总览</h2>
<h2>{{ overview.display_name || overview.username || '...' }} 权限管理</h2>
<el-button @click="$router.push('/iam-users')">返回子账号列表</el-button>
</div>
@ -24,7 +24,7 @@
<el-alert type="info" :closable="false" style="margin-bottom:12px;">
全局策略对所有项目生效一般只放 Deny 策略项目隔离业务权限请加到项目级
</el-alert>
<el-table :data="overview.global_policies || []" stripe empty-text="无全局策略">
<el-table :data="overview.global_policies || []" stripe empty-text="无全局策略" table-layout="auto">
<el-table-column prop="name" label="策略名" min-width="220" />
<el-table-column label="类型" width="100">
<template #default="{ row }">
@ -42,6 +42,29 @@
</el-table>
</el-card>
<!-- Add Project Section -->
<el-card style="margin-bottom: 20px;">
<template #header>
<span style="font-weight:600; font-size:16px;">关联项目</span>
</template>
<div style="display:flex; gap:8px; align-items:flex-start; flex-wrap:wrap;">
<el-select v-model="projectToAdd" placeholder="选择火山项目" filterable style="width:260px;"
:loading="volcProjectsLoading" @focus="loadVolcProjects">
<el-option v-for="p in volcProjects" :key="p.name"
:label="p.display_name || p.name" :value="p.name"
:disabled="(overview.project_policies || []).some(pp => pp.project_name === p.name)" />
</el-select>
<div v-if="projectToAdd" style="display:flex; flex-wrap:wrap; gap:8px; align-items:center;">
<el-checkbox-group v-model="projectPoliciesToAttach" style="display:flex; flex-wrap:wrap; gap:4px;">
<el-checkbox label="ArkFullAccess" size="small">方舟完整</el-checkbox>
<el-checkbox label="TOSFullAccess" size="small">TOS完整</el-checkbox>
<el-checkbox label="ArkReadOnlyAccess" size="small">方舟只读</el-checkbox>
</el-checkbox-group>
<el-button type="primary" size="small" @click="handleAddProject">确认添加</el-button>
</div>
</div>
</el-card>
<!-- Project-level Policies -->
<el-card v-for="proj in (overview.project_policies || [])" :key="proj.project_name" style="margin-bottom: 16px;">
<template #header>
@ -50,21 +73,22 @@
<el-tag type="success" size="small" style="margin-right:8px;">项目</el-tag>
<span style="font-weight:600; font-size:15px;">{{ proj.project_name }}</span>
<span v-if="proj.display_name" style="color:#999; margin-left:8px;">{{ proj.display_name }}</span>
<span style="color:#e6a23c; margin-left:12px; font-size:13px;">消费: ¥{{ Number(proj.current_spending || 0).toLocaleString() }}</span>
<el-switch :model-value="proj.monitor_enabled" @change="val => toggleMonitor(proj, val)"
active-text="监测" inactive-text="" size="small" style="margin-left:12px;" />
</span>
<div style="display:flex; gap:8px;">
<el-select v-model="projectPolicyToAdd[proj.project_name]" placeholder="添加策略" filterable size="small" style="width:280px;">
<el-select v-model="projectPolicyToAdd[proj.project_name]" placeholder="添加策略" filterable size="small" style="width:260px;">
<el-option v-for="opt in policyOptions" :key="opt.value"
:value="opt.value" :label="opt.label"
:disabled="proj.policies?.some(p => p.name === opt.value)" />
</el-select>
<el-button type="primary" size="small" @click="attachProject(proj)" :disabled="!projectPolicyToAdd[proj.project_name]">添加</el-button>
<el-button type="danger" size="small" text @click="removeProject(proj)">移除项目</el-button>
</div>
</div>
</template>
<el-alert type="info" :closable="false" style="margin-bottom:12px;">
以下权限仅在 <strong>{{ proj.project_name }}</strong> 项目范围内生效
</el-alert>
<el-table :data="proj.policies || []" stripe empty-text="无项目级策略">
<el-table :data="proj.policies || []" stripe empty-text="无项目级策略" table-layout="auto">
<el-table-column prop="name" label="策略名" min-width="220" />
<el-table-column label="类型" width="100">
<template #default="{ row }">
@ -82,7 +106,8 @@
</el-table>
</el-card>
<el-empty v-if="!(overview.project_policies || []).length && !loading" description="暂无关联项目,请先在子账号管理中添加项目" />
<el-empty v-if="!(overview.project_policies || []).length && !loading"
description="暂无关联项目,请在上方添加" />
</div>
</div>
</template>
@ -101,6 +126,12 @@ const overview = ref({})
const globalPolicyToAdd = ref('')
const projectPolicyToAdd = reactive({})
// Add project
const projectToAdd = ref('')
const projectPoliciesToAttach = ref([])
const volcProjects = ref([])
const volcProjectsLoading = ref(false)
const policyOptions = [
{ value: 'ArkFullAccess', label: 'ArkFullAccess方舟/Seedance 完整权限)' },
{ value: 'ArkExperienceAccess', label: 'ArkExperienceAccess方舟体验权限' },
@ -122,6 +153,20 @@ async function loadOverview() {
}
}
async function loadVolcProjects() {
if (volcProjects.value.length) return
volcProjectsLoading.value = true
try {
const { data } = await api.get('/api/v1/projects/')
volcProjects.value = data
} catch (e) {
ElMessage.error('获取火山项目列表失败')
} finally {
volcProjectsLoading.value = false
}
}
// === Global policies ===
async function attachGlobal() {
if (!globalPolicyToAdd.value) return
try {
@ -151,11 +196,50 @@ async function detachGlobal(row) {
}
}
// === Project management ===
async function handleAddProject() {
if (!projectToAdd.value) return
try {
await api.post(`/api/v1/iam-users/${userId}/projects/add/`, {
project_name: projectToAdd.value,
policies: projectPoliciesToAttach.value,
})
ElMessage.success(`已关联项目 ${projectToAdd.value}`)
projectToAdd.value = ''
projectPoliciesToAttach.value = []
await loadOverview()
} catch (e) {
ElMessage.error(e.response?.data?.message || '添加失败')
}
}
async function removeProject(proj) {
await ElMessageBox.confirm(`确定移除项目 "${proj.project_name}" 吗?权限将被回收。`, '确认移除', { type: 'warning' })
try {
await api.delete(`/api/v1/iam-users/${userId}/projects/${proj.project_id}/delete/`)
ElMessage.success(`已移除项目 ${proj.project_name}`)
await loadOverview()
} catch (e) {
ElMessage.error(e.response?.data?.message || '移除失败')
}
}
async function toggleMonitor(proj, val) {
try {
await api.put(`/api/v1/iam-users/${userId}/projects/${proj.project_id}/`, {
monitor_enabled: val,
})
await loadOverview()
} catch (e) {
ElMessage.error('切换失败')
}
}
// === Project-level policies ===
async function attachProject(proj) {
const policyName = projectPolicyToAdd[proj.project_name]
if (!policyName) return
try {
// Use the project policies update endpoint
const newPolicies = [...(proj.policies || []).map(p => p.name), policyName]
await api.put(`/api/v1/iam-users/${userId}/projects/${proj.project_id}/policies/`, {
policies: newPolicies,