- 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>
283 lines
12 KiB
Vue
283 lines
12 KiB
Vue
<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>
|