AirGate/frontend/src/views/iam/UserPoliciesView.vue
seaislee1209 dacc521c1c 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>
2026-03-28 22:53:18 +08:00

283 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div style="max-width: 1200px; margin: 0 auto;">
<div class="page-header">
<h2>{{ overview.display_name || overview.username || '...' }} 权限管理</h2>
<el-button @click="$router.push('/iam-users')">返回子账号列表</el-button>
</div>
<div v-loading="loading">
<!-- Global Policies -->
<el-card style="margin-bottom: 20px;">
<template #header>
<div style="display:flex; justify-content:space-between; align-items:center;">
<span style="font-weight:600; font-size:16px;">全局策略</span>
<div style="display:flex; gap:8px;">
<el-select v-model="globalPolicyToAdd" placeholder="添加全局策略" filterable size="small" style="width:280px;">
<el-option v-for="opt in policyOptions" :key="opt.value"
:value="opt.value" :label="opt.label"
:disabled="overview.global_policies?.some(p => p.name === opt.value)" />
</el-select>
<el-button type="primary" size="small" @click="attachGlobal" :disabled="!globalPolicyToAdd">添加</el-button>
</div>
</div>
</template>
<el-alert type="info" :closable="false" style="margin-bottom:12px;">
全局策略对所有项目生效一般只放 Deny 策略项目隔离业务权限请加到项目级
</el-alert>
<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 }">
<el-tag :type="row.type === 'Custom' ? 'warning' : 'info'" size="small">
{{ row.type === 'Custom' ? '自定义' : '系统' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="250" show-overflow-tooltip />
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button size="small" type="danger" text @click="detachGlobal(row)">移除</el-button>
</template>
</el-table-column>
</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>
<div style="display:flex; justify-content:space-between; align-items:center;">
<span>
<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: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-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 }">
<el-tag :type="row.type === 'Custom' ? 'warning' : 'info'" size="small">
{{ row.type === 'Custom' ? '自定义' : '系统' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="250" show-overflow-tooltip />
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button size="small" type="danger" text @click="detachProject(proj, row)">移除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-empty v-if="!(overview.project_policies || []).length && !loading"
description="暂无关联项目,请在上方添加" />
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import api from '../../api'
const route = useRoute()
const userId = route.params.id
const loading = ref(false)
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方舟体验权限' },
{ value: 'ArkReadOnlyAccess', label: 'ArkReadOnlyAccess方舟只读' },
{ value: 'TOSFullAccess', label: 'TOSFullAccess对象存储完整权限' },
{ value: 'TOSReadOnlyAccess', label: 'TOSReadOnlyAccess对象存储只读' },
{ value: 'AccessKeySelfManageAccess', label: 'AccessKeySelfManageAccess自管理密钥' },
]
async function loadOverview() {
loading.value = true
try {
const { data } = await api.get(`/api/v1/iam-users/${userId}/policies/overview/`)
overview.value = data
} catch (e) {
ElMessage.error(e.response?.data?.message || '加载权限信息失败')
} finally {
loading.value = false
}
}
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 {
await api.post(`/api/v1/iam-users/${userId}/policies/attach/`, {
policy_name: globalPolicyToAdd.value,
policy_type: 'System',
})
ElMessage.success(`已添加全局策略 ${globalPolicyToAdd.value}`)
globalPolicyToAdd.value = ''
await loadOverview()
} catch (e) {
ElMessage.error(e.response?.data?.message || '添加失败')
}
}
async function detachGlobal(row) {
await ElMessageBox.confirm(`确定移除全局策略 "${row.name}" 吗?`, '确认移除', { type: 'warning' })
try {
await api.post(`/api/v1/iam-users/${userId}/policies/detach/`, {
policy_name: row.name,
policy_type: row.type,
})
ElMessage.success(`已移除 ${row.name}`)
await loadOverview()
} catch (e) {
ElMessage.error(e.response?.data?.message || '移除失败')
}
}
// === 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 {
const newPolicies = [...(proj.policies || []).map(p => p.name), policyName]
await api.put(`/api/v1/iam-users/${userId}/projects/${proj.project_id}/policies/`, {
policies: newPolicies,
})
ElMessage.success(`已添加 ${policyName}${proj.project_name}`)
projectPolicyToAdd[proj.project_name] = ''
await loadOverview()
} catch (e) {
ElMessage.error(e.response?.data?.message || '添加失败')
}
}
async function detachProject(proj, row) {
await ElMessageBox.confirm(
`确定从 ${proj.project_name} 移除策略 "${row.name}" 吗?`,
'确认移除', { type: 'warning' }
)
try {
const newPolicies = (proj.policies || []).filter(p => p.name !== row.name).map(p => p.name)
await api.put(`/api/v1/iam-users/${userId}/projects/${proj.project_id}/policies/`, {
policies: newPolicies,
})
ElMessage.success(`已从 ${proj.project_name} 移除 ${row.name}`)
await loadOverview()
} catch (e) {
ElMessage.error(e.response?.data?.message || '移除失败')
}
}
onMounted(loadOverview)
</script>
<style scoped>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
</style>