- Add volc_login_allowed field to IAMUser model - Add toggle-volc-login API endpoint - Add toggle button in IAMUserList dropdown menu - Sync status on user creation and toggle Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
897 lines
36 KiB
Vue
897 lines
36 KiB
Vue
<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="toggleVolcLogin(row)">
|
||
{{ row.volc_login_allowed ? '🔓 关闭火山登录' : '🔒 开启火山登录' }}
|
||
</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 || '恢复失败')
|
||
}
|
||
}
|
||
|
||
// Toggle Volcengine console login
|
||
async function toggleVolcLogin(row) {
|
||
const action = row.volc_login_allowed ? '关闭' : '开启'
|
||
await ElMessageBox.confirm(
|
||
`确定${action} "${row.username}" 的火山引擎控制台登录?`,
|
||
`${action}火山登录`, { type: 'warning' }
|
||
)
|
||
try {
|
||
const { data } = await api.post(`/api/v1/iam-users/${row.id}/toggle-volc-login/`)
|
||
ElMessage.success(data.message)
|
||
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>
|