Quota allocation system: - Replace monthly budget with one-time quota allocation (prepaid model) - Support both adding (+) and deducting (-) quota with underflow protection - Stepped alerts at configurable percentages (e.g., 50%/80%/90%) - Auto-disable when quota exhausted (100%), alert state resets on new allocation - Quota allocation history with operator audit trail IAM management: - Create new IAM sub-accounts directly from AirGate (auto-generates API keys) - SecretKey shown once in dialog with copy-to-clipboard - Attach/detach IAM policies via UI (ArkFullAccess, TOSFullAccess, etc.) - Sync existing users from Volcengine - Project list pulled from Volcengine API for dropdown selection Security & auth: - API Key authentication for external systems (AirDrama integration) - SECRET_KEY enforced in production (raises error if missing with DEBUG=False) - APIKeyUser with proper pk/is_staff attributes for DRF compatibility Infrastructure: - Docker + docker-compose for backend and frontend - Nginx reverse proxy for frontend with /api/ forwarding - Entrypoint with auto-migrate and default admin creation - SQLite data persisted via Docker volume at /app/data/ Bug fixes from audit: - Fix frontend referencing non-existent fields (current_month_spending, effective_budget, budget_usage_percent) - Fix scheduler using naive datetime.now() → timezone.now() - Fix scheduler reading interval from settings instead of GlobalConfig DB - Fix docker-compose SQLite volume mounting as directory - Fix CORS origin with explicit port 80 - Remove dead config (VOLC_ACCESS_KEY/SK, MONITOR_INTERVAL from settings) - Remove unused imports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
561 lines
22 KiB
Vue
561 lines
22 KiB
Vue
<template>
|
||
<div>
|
||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
|
||
<h2>子账号管理</h2>
|
||
<div style="display:flex; gap:8px;">
|
||
<el-button type="success" @click="showCreate = true">
|
||
<el-icon><Plus /></el-icon> 创建子账号
|
||
</el-button>
|
||
<el-button type="primary" @click="handleSync" :loading="syncing">
|
||
<el-icon><Refresh /></el-icon> 同步已有用户
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-card>
|
||
<el-table :data="users" stripe v-loading="loading" style="width: 100%;">
|
||
<el-table-column prop="username" label="用户名" width="140" />
|
||
<el-table-column prop="display_name" label="显示名" width="110" />
|
||
<el-table-column prop="status" label="状态" width="80">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 'active' ? 'success' : row.status === 'disabled' ? 'danger' : 'info'" size="small">
|
||
{{ row.status === 'active' ? '正常' : row.status === 'disabled' ? '已停用' : '未知' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="已划拨额度" width="120">
|
||
<template #default="{ row }">¥{{ Number(row.allocated_quota).toLocaleString() }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="累计消费" width="120">
|
||
<template #default="{ row }">
|
||
<span :style="{ color: Number(row.consumed_total) > 0 ? '#e6a23c' : '' }">
|
||
¥{{ Number(row.consumed_total).toLocaleString() }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="剩余额度" width="120">
|
||
<template #default="{ row }">
|
||
<span :style="{ color: Number(row.remaining_quota) <= 0 ? '#f56c6c' : '#67c23a', fontWeight: 600 }">
|
||
¥{{ Number(row.remaining_quota).toLocaleString() }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="使用率" width="140">
|
||
<template #default="{ row }">
|
||
<el-progress v-if="Number(row.allocated_quota) > 0"
|
||
:percentage="Math.min(100, row.usage_percent || 0)"
|
||
:color="row.usage_percent >= 90 ? '#f56c6c' : row.usage_percent >= 50 ? '#e6a23c' : '#67c23a'"
|
||
:stroke-width="10"
|
||
:format="() => `${row.usage_percent || 0}%`"
|
||
/>
|
||
<span v-else style="color:#999;font-size:12px;">未划拨</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="告警阶梯" width="130">
|
||
<template #default="{ row }">
|
||
<el-tag v-for="step in (row.effective_alert_thresholds || [])" :key="step"
|
||
:type="(row.triggered_alerts || []).includes(step) ? 'danger' : 'info'"
|
||
size="small" style="margin:1px 2px;">
|
||
{{ step }}%
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="320" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button size="small" type="warning" @click="openAllocate(row)">划拨</el-button>
|
||
<el-button size="small" @click="openConfig(row)">配置</el-button>
|
||
<el-button v-if="row.status === 'active'" size="small" type="danger" @click="handleDisable(row)">停用</el-button>
|
||
<el-button v-if="row.status === 'disabled'" size="small" type="success" @click="handleEnable(row)">恢复</el-button>
|
||
<el-button size="small" @click="openPolicies(row)">权限</el-button>
|
||
<el-button size="small" @click="openQuotaHistory(row)">记录</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<!-- Allocate Dialog -->
|
||
<el-dialog v-model="allocateVisible" title="额度变更" width="480px">
|
||
<div style="margin-bottom:16px; padding:12px; background:#f5f7fa; border-radius:8px;">
|
||
<div>用户: <b>{{ allocateUser?.username }}</b></div>
|
||
<div>当前额度: ¥{{ Number(allocateUser?.allocated_quota || 0).toLocaleString() }}</div>
|
||
<div>已消费: ¥{{ Number(allocateUser?.consumed_total || 0).toLocaleString() }}</div>
|
||
<div>剩余: <span style="color:#67c23a;font-weight:600;">¥{{ Number(allocateUser?.remaining_quota || 0).toLocaleString() }}</span></div>
|
||
</div>
|
||
<el-form :model="allocateForm" label-width="100px">
|
||
<el-form-item label="操作类型">
|
||
<el-radio-group v-model="allocateForm.mode">
|
||
<el-radio-button value="add">追加额度</el-radio-button>
|
||
<el-radio-button value="deduct">扣减额度</el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item :label="allocateForm.mode === 'add' ? '追加金额(元)' : '扣减金额(元)'">
|
||
<el-input-number v-model="allocateForm.amount" :min="1" :step="10000"
|
||
:max="allocateForm.mode === 'deduct' ? maxDeduct : undefined"
|
||
:precision="2" style="width:100%;" controls-position="right" />
|
||
<div v-if="allocateForm.mode === 'deduct'" class="form-hint">
|
||
最多可扣减: ¥{{ maxDeduct.toLocaleString() }}(不能低于已消费金额)
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="allocateForm.note" :placeholder="allocateForm.mode === 'add' ? '如:3月额度追加' : '如:额度调整'" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<div v-if="allocateForm.amount" style="padding:8px 12px; border-radius:4px; font-size:13px;"
|
||
:style="{ background: allocateForm.mode === 'add' ? '#f0f9eb' : '#fef0f0' }">
|
||
变更后总额度: ¥{{ newTotalAfter.toLocaleString() }}
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="allocateVisible = false">取消</el-button>
|
||
<el-button :type="allocateForm.mode === 'add' ? 'primary' : 'warning'"
|
||
@click="submitAllocate" :loading="allocating">
|
||
{{ allocateForm.mode === 'add' ? '确认追加' : '确认扣减' }}
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- Config Dialog -->
|
||
<el-dialog v-model="configVisible" title="监控配置" width="560px">
|
||
<el-form :model="configForm" label-width="130px">
|
||
<el-form-item label="关联项目">
|
||
<el-select v-model="configForm.project_name" placeholder="选择火山引擎项目"
|
||
filterable clearable style="width:100%;" :loading="projectsLoading">
|
||
<el-option v-for="p in projects" :key="p.name" :label="p.display_name || p.name" :value="p.name" />
|
||
</el-select>
|
||
<div class="form-hint">
|
||
<el-button link type="primary" size="small" @click="loadProjects" :loading="projectsLoading">刷新项目列表</el-button>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<el-divider content-position="left">告警阶梯</el-divider>
|
||
|
||
<el-form-item label="告警阶梯(%)">
|
||
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
|
||
<el-tag v-for="(step, i) in configForm.alert_thresholds" :key="i"
|
||
closable @close="removeStep(i)" size="large">{{ step }}%</el-tag>
|
||
<el-input-number v-model="newStep" :min="1" :max="99" size="small" style="width:100px;" />
|
||
<el-button size="small" @click="addStep">添加</el-button>
|
||
</div>
|
||
<div class="form-hint">达到已划拨额度对应百分比时发送告警</div>
|
||
</el-form-item>
|
||
|
||
<el-divider content-position="left">开关</el-divider>
|
||
|
||
<el-form-item label="消费监控">
|
||
<el-switch v-model="configForm.monitor_enabled" />
|
||
</el-form-item>
|
||
<el-form-item label="额度用尽自动停用">
|
||
<el-switch v-model="configForm.auto_disable_enabled" />
|
||
<span class="switch-hint">{{ configForm.auto_disable_enabled ? '消费达100%额度时自动停用' : '仅通知不停用' }}</span>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="configVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="saveConfig" :loading="saving">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- Quota History Dialog -->
|
||
<el-dialog v-model="historyVisible" :title="`${historyUser?.username} 额度划拨记录`" width="600px">
|
||
<el-table :data="quotaHistory" stripe v-loading="historyLoading" empty-text="暂无划拨记录">
|
||
<el-table-column prop="created_at" label="时间" width="180">
|
||
<template #default="{ row }">{{ new Date(row.created_at).toLocaleString('zh-CN') }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="amount" label="变更金额" width="120">
|
||
<template #default="{ row }">
|
||
<span :style="{ color: Number(row.amount) >= 0 ? '#67c23a' : '#f56c6c' }">
|
||
{{ Number(row.amount) >= 0 ? '+' : '' }}¥{{ Number(row.amount).toLocaleString() }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="total_after" label="划拨后总额度" width="130">
|
||
<template #default="{ row }">¥{{ Number(row.total_after).toLocaleString() }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="note" label="备注" />
|
||
<el-table-column prop="created_by" label="操作人" width="100" />
|
||
</el-table>
|
||
</el-dialog>
|
||
|
||
<!-- Policies Dialog -->
|
||
<el-dialog v-model="policiesVisible" :title="`${policiesUser?.username} 权限策略`" width="650px">
|
||
<div style="margin-bottom:12px; display:flex; gap:8px;">
|
||
<el-select v-model="policyToAttach" placeholder="选择要附加的策略" filterable style="flex:1;">
|
||
<el-option-group label="常用策略">
|
||
<el-option value="ArkFullAccess" label="ArkFullAccess(方舟/Seedance 完整权限)" />
|
||
<el-option value="ArkReadOnlyAccess" label="ArkReadOnlyAccess(方舟只读)" />
|
||
<el-option value="TOSFullAccess" label="TOSFullAccess(对象存储完整权限)" />
|
||
<el-option value="TOSReadOnlyAccess" label="TOSReadOnlyAccess(对象存储只读)" />
|
||
<el-option value="AccessKeySelfManageAccess" label="AccessKeySelfManageAccess(自管理密钥)" />
|
||
</el-option-group>
|
||
</el-select>
|
||
<el-button type="primary" @click="handleAttachPolicy" :disabled="!policyToAttach">附加</el-button>
|
||
</div>
|
||
<el-table :data="policies" stripe v-loading="policiesLoading" empty-text="暂无策略">
|
||
<el-table-column prop="PolicyName" label="策略名" />
|
||
<el-table-column prop="PolicyType" label="类型" width="80" />
|
||
<el-table-column prop="Description" label="说明" />
|
||
<el-table-column label="操作" width="80">
|
||
<template #default="{ row }">
|
||
<el-button size="small" type="danger" text @click="handleDetachPolicy(row)">移除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-dialog>
|
||
|
||
<!-- Create User Dialog -->
|
||
<el-dialog v-model="showCreate" title="创建子账号" width="520px">
|
||
<el-alert type="warning" :closable="false" style="margin-bottom:16px;"
|
||
description="创建后会在火山引擎生成 IAM 用户和 API 密钥。SecretKey 仅显示一次,请务必保存!" />
|
||
<el-form :model="createForm" label-width="110px">
|
||
<el-form-item label="用户名" required>
|
||
<el-input v-model="createForm.username" placeholder="英文字母开头,如 dept_video" />
|
||
</el-form-item>
|
||
<el-form-item label="显示名">
|
||
<el-input v-model="createForm.display_name" placeholder="如:视频部门" />
|
||
</el-form-item>
|
||
<el-form-item label="邮箱">
|
||
<el-input v-model="createForm.email" placeholder="选填" />
|
||
</el-form-item>
|
||
<el-form-item label="手机号">
|
||
<el-input v-model="createForm.phone" placeholder="选填,如 +8618000000000" />
|
||
</el-form-item>
|
||
<el-form-item label="控制台密码">
|
||
<el-input v-model="createForm.password" type="password" show-password
|
||
placeholder="选填,填了才开通控制台登录" />
|
||
</el-form-item>
|
||
<el-form-item label="关联项目">
|
||
<el-select v-model="createForm.project_name" placeholder="选填" filterable clearable
|
||
style="width:100%;" :loading="projectsLoading">
|
||
<el-option v-for="p in projects" :key="p.name" :label="p.display_name || p.name" :value="p.name" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="showCreate = false">取消</el-button>
|
||
<el-button type="primary" @click="handleCreate" :loading="creating">创建</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- Secret Key Display Dialog -->
|
||
<el-dialog v-model="showSecretKey" title="API 密钥已生成" width="520px" :close-on-click-modal="false">
|
||
<el-alert type="error" :closable="false" style="margin-bottom:16px;"
|
||
description="SecretAccessKey 仅此一次显示!关闭后无法再次获取,请立即复制保存。" />
|
||
<el-descriptions :column="1" border>
|
||
<el-descriptions-item label="AccessKey ID">
|
||
<code>{{ createdKeys.access_key_id }}</code>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="SecretAccessKey">
|
||
<code style="word-break:break-all;">{{ createdKeys.secret_access_key }}</code>
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
<template #footer>
|
||
<el-button type="primary" @click="copyKeys">复制到剪贴板</el-button>
|
||
<el-button @click="showSecretKey = false">已保存,关闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import api from '../../api'
|
||
|
||
const users = ref([])
|
||
const loading = ref(false)
|
||
const syncing = ref(false)
|
||
|
||
// Create
|
||
const showCreate = ref(false)
|
||
const creating = ref(false)
|
||
const createForm = ref({ username: '', display_name: '', email: '', phone: '', password: '', project_name: '' })
|
||
const showSecretKey = ref(false)
|
||
const createdKeys = ref({ access_key_id: '', secret_access_key: '' })
|
||
|
||
// Allocate
|
||
const allocateVisible = ref(false)
|
||
const allocateUser = ref(null)
|
||
const allocateForm = ref({ amount: null, note: '' })
|
||
const allocating = ref(false)
|
||
|
||
// Config
|
||
const configVisible = ref(false)
|
||
const configForm = ref({})
|
||
const configUserId = ref(null)
|
||
const saving = ref(false)
|
||
const newStep = ref(null)
|
||
|
||
// Projects
|
||
const projects = ref([])
|
||
const projectsLoading = ref(false)
|
||
|
||
// Quota History
|
||
const historyVisible = ref(false)
|
||
const historyUser = ref(null)
|
||
const quotaHistory = ref([])
|
||
const historyLoading = ref(false)
|
||
|
||
async function loadUsers() {
|
||
loading.value = true
|
||
try {
|
||
const { data } = await api.get('/api/v1/iam-users/')
|
||
users.value = data
|
||
} catch (e) {
|
||
ElMessage.error('加载用户列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleSync() {
|
||
syncing.value = true
|
||
try {
|
||
const { data } = await api.post('/api/v1/iam-users/sync/')
|
||
ElMessage.success(data.message)
|
||
await loadUsers()
|
||
} catch (e) {
|
||
ElMessage.error(e.response?.data?.message || '同步失败')
|
||
} finally {
|
||
syncing.value = false
|
||
}
|
||
}
|
||
|
||
async function handleDisable(row) {
|
||
await ElMessageBox.confirm(
|
||
`确定要停用子账号 "${row.username}" 吗?`, '确认停用', { type: 'warning' }
|
||
)
|
||
try {
|
||
await api.post(`/api/v1/iam-users/${row.id}/disable/`)
|
||
ElMessage.success('已停用')
|
||
await loadUsers()
|
||
} catch (e) {
|
||
ElMessage.error(e.response?.data?.message || '停用失败')
|
||
}
|
||
}
|
||
|
||
async function handleEnable(row) {
|
||
await ElMessageBox.confirm(`确定要恢复子账号 "${row.username}" 吗?`, '确认恢复')
|
||
try {
|
||
await api.post(`/api/v1/iam-users/${row.id}/enable/`)
|
||
ElMessage.success('已恢复')
|
||
await loadUsers()
|
||
} catch (e) {
|
||
ElMessage.error(e.response?.data?.message || '恢复失败')
|
||
}
|
||
}
|
||
|
||
// Policies
|
||
const policiesVisible = ref(false)
|
||
const policiesUser = ref(null)
|
||
const policies = ref([])
|
||
const policiesLoading = ref(false)
|
||
const policyToAttach = ref('')
|
||
|
||
// --- Allocate ---
|
||
const maxDeduct = computed(() => {
|
||
if (!allocateUser.value) return 0
|
||
return Math.max(0, Number(allocateUser.value.allocated_quota || 0) - Number(allocateUser.value.consumed_total || 0))
|
||
})
|
||
|
||
const newTotalAfter = computed(() => {
|
||
const current = Number(allocateUser.value?.allocated_quota || 0)
|
||
const amt = Number(allocateForm.value.amount || 0)
|
||
return allocateForm.value.mode === 'add' ? current + amt : current - amt
|
||
})
|
||
|
||
function openAllocate(row) {
|
||
allocateUser.value = row
|
||
allocateForm.value = { mode: 'add', amount: null, note: '' }
|
||
allocateVisible.value = true
|
||
}
|
||
|
||
async function submitAllocate() {
|
||
if (!allocateForm.value.amount || allocateForm.value.amount <= 0) {
|
||
ElMessage.warning('请输入金额')
|
||
return
|
||
}
|
||
allocating.value = true
|
||
const actualAmount = allocateForm.value.mode === 'deduct'
|
||
? -allocateForm.value.amount
|
||
: allocateForm.value.amount
|
||
try {
|
||
const { data } = await api.post(
|
||
`/api/v1/iam-users/${allocateUser.value.id}/allocate/`,
|
||
{ amount: actualAmount, note: allocateForm.value.note }
|
||
)
|
||
ElMessage.success(data.message)
|
||
allocateVisible.value = false
|
||
await loadUsers()
|
||
} catch (e) {
|
||
ElMessage.error(e.response?.data?.message || '操作失败')
|
||
} finally {
|
||
allocating.value = false
|
||
}
|
||
}
|
||
|
||
// --- Config ---
|
||
async function loadProjects() {
|
||
projectsLoading.value = true
|
||
try {
|
||
const { data } = await api.get('/api/v1/projects/')
|
||
projects.value = data
|
||
} catch (e) {
|
||
ElMessage.error(e.response?.data?.message || '获取项目列表失败')
|
||
} finally {
|
||
projectsLoading.value = false
|
||
}
|
||
}
|
||
|
||
function openConfig(row) {
|
||
configUserId.value = row.id
|
||
configForm.value = {
|
||
project_name: row.project_name || '',
|
||
alert_thresholds: [...(row.alert_thresholds?.length ? row.alert_thresholds : row.effective_alert_thresholds || [50, 80, 90])],
|
||
monitor_enabled: row.monitor_enabled,
|
||
auto_disable_enabled: row.auto_disable_enabled,
|
||
}
|
||
newStep.value = null
|
||
configVisible.value = true
|
||
if (projects.value.length === 0) loadProjects()
|
||
}
|
||
|
||
function addStep() {
|
||
if (!newStep.value || newStep.value < 1 || newStep.value > 99) {
|
||
ElMessage.warning('请输入 1-99 之间的百分比')
|
||
return
|
||
}
|
||
if (configForm.value.alert_thresholds.includes(newStep.value)) {
|
||
ElMessage.warning('该阈值已存在')
|
||
return
|
||
}
|
||
configForm.value.alert_thresholds.push(newStep.value)
|
||
configForm.value.alert_thresholds.sort((a, b) => a - b)
|
||
newStep.value = null
|
||
}
|
||
|
||
function removeStep(index) {
|
||
configForm.value.alert_thresholds.splice(index, 1)
|
||
}
|
||
|
||
async function saveConfig() {
|
||
saving.value = true
|
||
try {
|
||
await api.put(`/api/v1/iam-users/${configUserId.value}/update/`, configForm.value)
|
||
ElMessage.success('配置已保存')
|
||
configVisible.value = false
|
||
await loadUsers()
|
||
} catch (e) {
|
||
ElMessage.error('保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
// --- Quota History ---
|
||
async function openQuotaHistory(row) {
|
||
historyUser.value = row
|
||
historyVisible.value = true
|
||
historyLoading.value = true
|
||
try {
|
||
const { data } = await api.get(`/api/v1/iam-users/${row.id}/quota-history/`)
|
||
quotaHistory.value = data
|
||
} catch (e) {
|
||
ElMessage.error('获取划拨记录失败')
|
||
quotaHistory.value = []
|
||
} finally {
|
||
historyLoading.value = false
|
||
}
|
||
}
|
||
|
||
// --- Policies ---
|
||
async function openPolicies(row) {
|
||
policiesUser.value = row
|
||
policiesVisible.value = true
|
||
policiesLoading.value = true
|
||
policyToAttach.value = ''
|
||
try {
|
||
const { data } = await api.get(`/api/v1/iam-users/${row.id}/policies/`)
|
||
policies.value = data.policies || []
|
||
} catch (e) {
|
||
ElMessage.error(e.response?.data?.message || '获取权限失败')
|
||
policies.value = []
|
||
} finally {
|
||
policiesLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handleAttachPolicy() {
|
||
if (!policyToAttach.value) return
|
||
try {
|
||
await api.post(`/api/v1/iam-users/${policiesUser.value.id}/policies/attach/`, {
|
||
policy_name: policyToAttach.value,
|
||
policy_type: 'System',
|
||
})
|
||
ElMessage.success(`已附加 ${policyToAttach.value}`)
|
||
policyToAttach.value = ''
|
||
await openPolicies(policiesUser.value)
|
||
} catch (e) {
|
||
ElMessage.error(e.response?.data?.message || '附加失败')
|
||
}
|
||
}
|
||
|
||
async function handleDetachPolicy(row) {
|
||
await ElMessageBox.confirm(`确定移除策略 "${row.PolicyName}" 吗?`, '确认移除', { type: 'warning' })
|
||
try {
|
||
await api.post(`/api/v1/iam-users/${policiesUser.value.id}/policies/detach/`, {
|
||
policy_name: row.PolicyName,
|
||
policy_type: row.PolicyType,
|
||
})
|
||
ElMessage.success(`已移除 ${row.PolicyName}`)
|
||
await openPolicies(policiesUser.value)
|
||
} catch (e) {
|
||
ElMessage.error(e.response?.data?.message || '移除失败')
|
||
}
|
||
}
|
||
|
||
// --- Create User ---
|
||
async function handleCreate() {
|
||
if (!createForm.value.username) {
|
||
ElMessage.warning('请输入用户名')
|
||
return
|
||
}
|
||
creating.value = true
|
||
try {
|
||
const { data } = await api.post('/api/v1/iam-users/create/', createForm.value)
|
||
ElMessage.success(data.message)
|
||
showCreate.value = false
|
||
createForm.value = { username: '', display_name: '', email: '', phone: '', password: '', project_name: '' }
|
||
|
||
// Show secret key if generated
|
||
if (data.volcengine?.secret_access_key) {
|
||
createdKeys.value = {
|
||
access_key_id: data.volcengine.access_key_id,
|
||
secret_access_key: data.volcengine.secret_access_key,
|
||
}
|
||
showSecretKey.value = true
|
||
}
|
||
await loadUsers()
|
||
} catch (e) {
|
||
ElMessage.error(e.response?.data?.message || '创建失败')
|
||
} finally {
|
||
creating.value = false
|
||
}
|
||
}
|
||
|
||
async function copyKeys() {
|
||
const text = `AccessKey ID: ${createdKeys.value.access_key_id}\nSecretAccessKey: ${createdKeys.value.secret_access_key}`
|
||
try {
|
||
await navigator.clipboard.writeText(text)
|
||
ElMessage.success('已复制到剪贴板')
|
||
} catch {
|
||
ElMessage.error('复制失败,请手动复制')
|
||
}
|
||
}
|
||
|
||
onMounted(loadUsers)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.form-hint { font-size: 12px; color: #999; margin-top: 4px; }
|
||
.switch-hint { font-size: 12px; color: #999; margin-left: 8px; }
|
||
</style>
|