初始提交:OCI 面板前端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NDataTable, NPopconfirm, useDialog, useMessage, type DataTableColumns } from 'naive-ui'
|
||||
import { h, ref } from 'vue'
|
||||
|
||||
import { clearUserMfa, deleteUser, deleteUserApiKeys, listUsers, resetUserPassword } from '@/api/tenant'
|
||||
import FootNote from '@/components/FootNote.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import UserFormModal from '@/components/tenant/UserFormModal.vue'
|
||||
import { fmtTime } from '@/composables/useFormat'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import type { IamUser } from '@/types/api'
|
||||
|
||||
const props = defineProps<{ cfgId: number }>()
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
const users = useAsync(() => listUsers(props.cfgId))
|
||||
const showForm = ref(false)
|
||||
const editingUser = ref<IamUser | null>(null)
|
||||
|
||||
function openCreate() {
|
||||
editingUser.value = null
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
function openEdit(row: IamUser) {
|
||||
editingUser.value = row
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
async function act(fn: () => Promise<unknown>, ok: string) {
|
||||
try {
|
||||
await fn()
|
||||
message.success(ok)
|
||||
void users.run()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(row: IamUser) {
|
||||
dialog.warning({
|
||||
title: '删除用户',
|
||||
content: () =>
|
||||
h('div', { class: 'text-[13px]' }, [
|
||||
h('p', `确定删除用户「${row.name}」?此操作不可恢复。`),
|
||||
h('p', { class: 'mt-1.5 text-ink-3' }, '删除后该用户的全部 API Key 将一并吊销'),
|
||||
]),
|
||||
positiveText: '确认删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => act(() => deleteUser(props.cfgId, row.id), `已删除用户 ${row.name}`),
|
||||
})
|
||||
}
|
||||
|
||||
async function resetPwd(row: IamUser) {
|
||||
try {
|
||||
const { password } = await resetUserPassword(props.cfgId, row.id)
|
||||
dialog.success({
|
||||
title: '一次性密码',
|
||||
content: () =>
|
||||
h('div', [
|
||||
h('p', { class: 'mb-2 text-[13px]' }, `${row.name} 的新密码(首次登录须修改):`),
|
||||
h('code', { class: 'mono rounded bg-line-soft px-2 py-1 select-all' }, password),
|
||||
]),
|
||||
positiveText: '已保存',
|
||||
})
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '重置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns: DataTableColumns<IamUser> = [
|
||||
{
|
||||
title: '用户',
|
||||
key: 'name',
|
||||
minWidth: 220,
|
||||
render: (row) =>
|
||||
h('div', [
|
||||
h('div', { class: 'flex items-center gap-2' }, [
|
||||
h('span', { class: 'font-medium' }, row.name),
|
||||
row.isCurrentUser
|
||||
? h(StatusBadge, { kind: 'snatch', label: '当前签名用户', dot: false })
|
||||
: null,
|
||||
]),
|
||||
h('div', { class: 'text-xs text-ink-3 mt-px' }, row.email || row.description || '—'),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'lifecycleState',
|
||||
width: 90,
|
||||
render: (row) =>
|
||||
h(StatusBadge, {
|
||||
kind: row.lifecycleState === 'ACTIVE' ? 'run' : 'stop',
|
||||
label: row.lifecycleState === 'ACTIVE' ? '活跃' : row.lifecycleState,
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'MFA',
|
||||
key: 'mfaActivated',
|
||||
width: 70,
|
||||
render: (row) => h('span', { class: 'text-[13px] tabular-nums' }, row.mfaActivated ? '已启用' : '未启用'),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'timeCreated',
|
||||
width: 150,
|
||||
render: (row) => h('span', { class: 'text-[13px] text-ink-2' }, fmtTime(row.timeCreated)),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 290,
|
||||
render: (row) =>
|
||||
h('div', { class: 'flex items-center gap-1' }, [
|
||||
h(NButton, { size: 'tiny', quaternary: true, onClick: () => openEdit(row) }, { default: () => '修改' }),
|
||||
h(NButton, { size: 'tiny', quaternary: true, onClick: () => resetPwd(row) }, { default: () => '重置密码' }),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () =>
|
||||
act(() => clearUserMfa(props.cfgId, row.id), `已清除 ${row.name} 的全部 MFA`),
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '清 MFA' }),
|
||||
default: () => '清除该用户全部 MFA 因子?',
|
||||
},
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () =>
|
||||
act(() => deleteUserApiKeys(props.cfgId, row.id), `已删除 ${row.name} 的 API Key`),
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '删 Key' }),
|
||||
default: () => '删除该用户全部 API Key(跳过当前配置使用中的指纹)?',
|
||||
},
|
||||
),
|
||||
row.isCurrentUser
|
||||
? null
|
||||
: h(
|
||||
NButton,
|
||||
{ size: 'tiny', quaternary: true, type: 'error', onClick: () => confirmDelete(row) },
|
||||
{ default: () => '删除' },
|
||||
),
|
||||
]),
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">IAM 用户</div>
|
||||
<NButton size="small" type="primary" @click="openCreate">添加用户</NButton>
|
||||
</div>
|
||||
<NDataTable
|
||||
:columns="columns"
|
||||
:data="users.data.value ?? []"
|
||||
:loading="users.loading.value"
|
||||
:scroll-x="800"
|
||||
:row-key="(r: IamUser) => r.id"
|
||||
/>
|
||||
<FootNote>
|
||||
重置密码生成一次性密码,用户首次登录须修改;当前配置签名用户不可删除,避免面板失联
|
||||
</FootNote>
|
||||
|
||||
<UserFormModal
|
||||
v-model:show="showForm"
|
||||
:cfg-id="cfgId"
|
||||
:user="editingUser"
|
||||
@created="users.run()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user