AirGate/frontend/src/views/iam/IAMUserList.vue
seaislee1209 f79ae0084d fix: show clear error when Volcengine console password is too weak
- Detect InvalidPassword error and return user-friendly message
- Rollback user creation if password policy fails
- Add password requirements hint in create form

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:27:36 +08:00

878 lines
35 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>
<div class="page-header">
<h2>子账号管理</h2>
<div class="actions">
<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" table-layout="auto">
<el-table-column prop="username" label="用户名" min-width="120" />
<el-table-column prop="display_name" label="显示名" min-width="100" />
<el-table-column prop="status" label="状态" min-width="70" align="center">
<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="已划拨" min-width="100" align="right">
<template #default="{ row }">¥{{ Number(row.allocated_quota).toLocaleString() }}</template>
</el-table-column>
<el-table-column label="已消费" min-width="100" align="right">
<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="剩余" min-width="100" align="right">
<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="使用率" min-width="130">
<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="项目" min-width="80" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openProjectsDialog(row)">
{{ row.monitored_project_count || 0 }} / {{ (row.projects || []).length }}
</el-button>
</template>
</el-table-column>
<el-table-column label="告警" min-width="110" align="center">
<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;">
{{ step }}%
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="warning" @click="openAllocate(row)">划拨</el-button>
<el-dropdown trigger="click" style="margin-left:8px;">
<el-button size="small">
更多 <el-icon style="margin-left:4px;"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openProjectsDialog(row)">项目管理</el-dropdown-item>
<el-dropdown-item @click="openConfig(row)">监控配置</el-dropdown-item>
<el-dropdown-item @click="openEditProfile(row)">编辑信息</el-dropdown-item>
<el-dropdown-item @click="openPolicies(row)">权限策略</el-dropdown-item>
<el-dropdown-item @click="openQuotaHistory(row)">划拨记录</el-dropdown-item>
<el-dropdown-item @click="openSetLogin(row)">登录密码</el-dropdown-item>
<el-dropdown-item v-if="row.status === 'active'" divided
@click="handleDisable(row)" style="color:#f56c6c;">停用账号</el-dropdown-item>
<el-dropdown-item v-if="row.status === 'disabled'" divided
@click="handleEnable(row)" style="color:#67c23a;">恢复账号</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- Allocate Dialog -->
<el-dialog v-model="allocateVisible" title="额度变更" width="90%" style="max-width: 520px;">
<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="90%" style="max-width: 560px;">
<el-form :model="configForm" label-width="140px">
<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-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>
<!-- Projects Dialog -->
<el-dialog v-model="projectsDialogVisible" :title="`${projectsUser?.username} 关联项目`" width="90%" style="max-width: 900px;">
<div style="margin-bottom:12px; display:flex; gap:8px; align-items:center;">
<el-select v-model="projectToAdd" placeholder="选择火山项目" filterable style="flex:1;"
:loading="volcProjectsLoading">
<el-option v-for="p in volcProjects" :key="p.name" :label="p.display_name || p.name" :value="p.name" />
</el-select>
<el-button @click="loadVolcProjects" :loading="volcProjectsLoading" text>
<el-icon><Refresh /></el-icon>
</el-button>
</div>
<div v-if="projectToAdd" style="margin-bottom:12px;">
<div style="margin-bottom:4px; font-size:13px; color:#606266;">授权策略(可多选,不选则仅加入监测不授权):</div>
<el-checkbox-group v-model="projectPoliciesToAttach">
<el-checkbox label="ArkFullAccess">方舟/Seedance 完整权限</el-checkbox>
<el-checkbox label="ArkExperienceAccess">方舟体验权限API Key 管理需要)</el-checkbox>
<el-checkbox label="ArkReadOnlyAccess">方舟只读</el-checkbox>
<el-checkbox label="TOSFullAccess">对象存储完整权限</el-checkbox>
<el-checkbox label="TOSReadOnlyAccess">对象存储只读</el-checkbox>
<el-checkbox label="AccessKeySelfManageAccess">自管理密钥</el-checkbox>
</el-checkbox-group>
<el-button type="primary" @click="handleAddProject" style="margin-top:8px;">确认添加</el-button>
</div>
<div style="margin-bottom:12px;">
<el-button size="small" @click="handleToggleAll(true)">全部开启监测</el-button>
<el-button size="small" @click="handleToggleAll(false)">全部关闭监测</el-button>
</div>
<el-table :data="userProjects" stripe v-loading="projectsDialogLoading" empty-text="暂无关联项目">
<el-table-column prop="project_name" label="项目名" min-width="160" />
<el-table-column prop="display_name" label="显示名" min-width="120" />
<el-table-column label="消费" min-width="100" align="right">
<template #default="{ row }">
<span style="color:#e6a23c;">¥{{ Number(row.current_spending).toLocaleString() }}</span>
</template>
</el-table-column>
<el-table-column label="已授权策略" min-width="180">
<template #default="{ row }">
<el-tag v-for="p in (row.attached_policies || [])" :key="p" size="small"
style="margin:1px 2px;">{{ p }}</el-tag>
<span v-if="!(row.attached_policies || []).length" style="color:#999;font-size:12px;">无</span>
</template>
</el-table-column>
<el-table-column label="监测" min-width="70" align="center">
<template #default="{ row }">
<el-switch :model-value="row.monitor_enabled" @change="val => handleToggleProject(row, val)" />
</template>
</el-table-column>
<el-table-column label="操作" width="140" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" text @click="openProjectPolicies(row)">授权</el-button>
<el-button size="small" type="danger" text @click="handleRemoveProject(row)">移除</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- Project Policies Dialog -->
<el-dialog v-model="projectPolicyVisible"
:title="`${projectPolicyProject?.project_name} 项目授权`"
width="90%" style="max-width: 550px;">
<p style="margin-bottom:12px; color:#606266; font-size:13px;">
子账号 <strong>{{ projectsUser?.username }}</strong> 在此项目下的权限:
</p>
<el-checkbox-group v-model="projectPolicySelected">
<div style="display:flex; flex-direction:column; gap:8px;">
<el-checkbox label="ArkFullAccess">方舟/Seedance 完整权限</el-checkbox>
<el-checkbox label="ArkExperienceAccess">方舟体验权限API Key 管理需要)</el-checkbox>
<el-checkbox label="ArkReadOnlyAccess">方舟只读</el-checkbox>
<el-checkbox label="TOSFullAccess">对象存储完整权限</el-checkbox>
<el-checkbox label="TOSReadOnlyAccess">对象存储只读</el-checkbox>
<el-checkbox label="AccessKeySelfManageAccess">自管理密钥</el-checkbox>
</div>
</el-checkbox-group>
<template #footer>
<el-button @click="projectPolicyVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveProjectPolicies" :loading="projectPolicySaving">保存</el-button>
</template>
</el-dialog>
<!-- Set Login Password Dialog -->
<el-dialog v-model="loginPwdVisible" :title="`设置 ${loginPwdUser?.username} 的 AirGate 登录密码`"
width="90%" style="max-width: 450px;">
<el-form label-width="100px">
<el-form-item label="登录状态">
<el-switch v-model="loginPwdEnabled"
active-text="允许登录" inactive-text="禁止登录" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="loginPwdValue" type="password" show-password
placeholder="至少6位留空则不修改密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="loginPwdVisible = false">取消</el-button>
<el-button type="primary" @click="handleSetLogin" :loading="loginPwdSaving">保存</el-button>
</template>
</el-dialog>
<!-- Quota History Dialog -->
<el-dialog v-model="historyVisible" :title="`${historyUser?.username} 额度划拨记录`" width="90%" style="max-width: 800px;">
<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="90%" style="max-width: 850px;">
<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="ArkExperienceAccess" label="ArkExperienceAccess方舟体验权限" />
<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="90%" style="max-width: 580px;">
<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="选填" />
<div style="font-size:12px;color:#999;margin-top:4px;">
火山引擎网页后台的登录密码。不填则子账号无法登录火山网页后台,仅能通过 API Key 使用服务。
密码需包含大小写字母、数字和特殊字符至少8位如 User@1234
</div>
</el-form-item>
<el-form-item label="关联项目">
<el-select v-model="createForm.project_name" placeholder="选填" filterable clearable
style="width:100%;" :loading="volcProjectsLoading"
@focus="() => { if (!volcProjects.length) loadVolcProjects() }">
<el-option v-for="p in volcProjects" :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>
<!-- Edit Profile Dialog -->
<el-dialog v-model="editProfileVisible" :title="`编辑 ${editProfileUser?.username} 信息`"
width="90%" style="max-width: 500px;">
<el-form :model="editProfileForm" label-width="80px">
<el-form-item label="用户名">
<el-input :model-value="editProfileUser?.username" disabled />
</el-form-item>
<el-form-item label="显示名">
<el-input v-model="editProfileForm.display_name" placeholder="如:视频部门" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="editProfileForm.phone" placeholder="如 13800138000" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="editProfileForm.email" placeholder="选填" />
</el-form-item>
</el-form>
<div style="font-size:12px; color:#999; margin-top:8px;">
修改会同步到火山引擎 IAM
</div>
<template #footer>
<el-button @click="editProfileVisible = false">取消</el-button>
<el-button type="primary" @click="handleEditProfile" :loading="editProfileSaving">保存</el-button>
</template>
</el-dialog>
<!-- Secret Key Display Dialog -->
<el-dialog v-model="showSecretKey" title="API 密钥已生成" width="90%" style="max-width: 580px;" :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)
// 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 || '恢复失败')
}
}
// Edit Profile
const editProfileVisible = ref(false)
const editProfileUser = ref(null)
const editProfileForm = ref({ display_name: '', phone: '', email: '' })
const editProfileSaving = ref(false)
function openEditProfile(row) {
editProfileUser.value = row
editProfileForm.value = {
display_name: row.display_name || '',
phone: row.phone || '',
email: row.email || '',
}
editProfileVisible.value = true
}
async function handleEditProfile() {
editProfileSaving.value = true
try {
const { data } = await api.post(
`/api/v1/iam-users/${editProfileUser.value.id}/edit-profile/`,
editProfileForm.value
)
ElMessage.success(data.message)
editProfileVisible.value = false
await loadUsers()
} catch (e) {
ElMessage.error(e.response?.data?.message || '更新失败')
} finally {
editProfileSaving.value = false
}
}
// Policies
const policiesVisible = ref(false)
const policiesUser = ref(null)
const policies = ref([])
const policiesLoading = ref(false)
const policyToAttach = ref('')
// Projects dialog
const projectsDialogVisible = ref(false)
const projectsUser = ref(null)
const userProjects = ref([])
const projectsDialogLoading = ref(false)
const projectToAdd = ref('')
const projectPoliciesToAttach = ref([])
const volcProjects = ref([])
const volcProjectsLoading = ref(false)
// --- 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 ---
function openConfig(row) {
configUserId.value = row.id
configForm.value = {
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
}
// --- Projects Dialog ---
async function loadVolcProjects() {
volcProjectsLoading.value = true
try {
const { data } = await api.get('/api/v1/projects/')
volcProjects.value = data
} catch (e) {
ElMessage.error(e.response?.data?.message || '获取火山项目列表失败')
} finally {
volcProjectsLoading.value = false
}
}
async function openProjectsDialog(row) {
projectsUser.value = row
projectsDialogVisible.value = true
projectToAdd.value = ''
await loadUserProjects(row.id)
if (volcProjects.value.length === 0) loadVolcProjects()
}
async function loadUserProjects(userId) {
projectsDialogLoading.value = true
try {
const { data } = await api.get(`/api/v1/iam-users/${userId}/projects/`)
userProjects.value = data
} catch (e) {
ElMessage.error('获取项目列表失败')
userProjects.value = []
} finally {
projectsDialogLoading.value = false
}
}
async function handleAddProject() {
if (!projectToAdd.value) return
try {
const { data } = await api.post(`/api/v1/iam-users/${projectsUser.value.id}/projects/add/`, {
project_name: projectToAdd.value,
policies: projectPoliciesToAttach.value,
})
const policyMsg = data.attached_policies?.length
? `,已授权 ${data.attached_policies.length} 个策略`
: ''
ElMessage.success(`已添加${policyMsg}`)
projectToAdd.value = ''
projectPoliciesToAttach.value = []
await loadUserProjects(projectsUser.value.id)
await loadUsers()
} catch (e) {
ElMessage.error(e.response?.data?.message || '添加失败')
}
}
async function handleToggleProject(row, val) {
try {
await api.put(`/api/v1/iam-users/${projectsUser.value.id}/projects/${row.id}/`, {
monitor_enabled: val,
})
await loadUserProjects(projectsUser.value.id)
await loadUsers()
} catch (e) {
ElMessage.error('切换失败')
}
}
// === Project Policies ===
const projectPolicyVisible = ref(false)
const projectPolicyProject = ref(null)
const projectPolicySelected = ref([])
const projectPolicySaving = ref(false)
function openProjectPolicies(row) {
projectPolicyProject.value = row
projectPolicySelected.value = [...(row.attached_policies || [])]
projectPolicyVisible.value = true
}
async function handleSaveProjectPolicies() {
projectPolicySaving.value = true
try {
const { data } = await api.put(
`/api/v1/iam-users/${projectsUser.value.id}/projects/${projectPolicyProject.value.id}/policies/`,
{ policies: projectPolicySelected.value }
)
ElMessage.success(data.message || '已更新')
projectPolicyVisible.value = false
await loadUserProjects(projectsUser.value.id)
} catch (e) {
ElMessage.error(e.response?.data?.message || '更新失败')
} finally {
projectPolicySaving.value = false
}
}
async function handleRemoveProject(row) {
await ElMessageBox.confirm(`确定移除项目 "${row.project_name}" 吗?`, '确认', { type: 'warning' })
try {
await api.delete(`/api/v1/iam-users/${projectsUser.value.id}/projects/${row.id}/delete/`)
ElMessage.success('已移除')
await loadUserProjects(projectsUser.value.id)
await loadUsers()
} catch (e) {
ElMessage.error('移除失败')
}
}
async function handleToggleAll(enable) {
try {
const { data } = await api.post(`/api/v1/iam-users/${projectsUser.value.id}/projects/toggle-all/`, {
monitor_enabled: enable,
})
ElMessage.success(data.message)
await loadUserProjects(projectsUser.value.id)
await loadUsers()
} catch (e) {
ElMessage.error('操作失败')
}
}
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 ---
// === Set Login Password ===
const loginPwdVisible = ref(false)
const loginPwdUser = ref(null)
const loginPwdValue = ref('')
const loginPwdEnabled = ref(false)
const loginPwdSaving = ref(false)
function openSetLogin(row) {
loginPwdUser.value = row
loginPwdValue.value = ''
loginPwdEnabled.value = row.login_enabled || false
loginPwdVisible.value = true
}
async function handleSetLogin() {
const payload = { login_enabled: loginPwdEnabled.value }
if (loginPwdValue.value) {
if (loginPwdValue.value.length < 6) {
ElMessage.warning('密码至少6位')
return
}
payload.password = loginPwdValue.value
}
loginPwdSaving.value = true
try {
const { data } = await api.post(`/api/v1/iam-users/${loginPwdUser.value.id}/set-login/`, payload)
ElMessage.success(data.message)
loginPwdVisible.value = false
await loadUsers()
} catch (e) {
ElMessage.error(e.response?.data?.message || '操作失败')
} finally {
loginPwdSaving.value = false
}
}
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>