feat: 支持注销用户并展示普通用户使用情况
- 新增管理员注销用户接口,要求输入用户名二次确认 - 注销时清理用户当前图片、视频、文件、标签、会话和自身日志 - 保留备份任务、管理员审计数据和全局配置 - 用户列表展示图片、视频、文件占用,管理员行隐藏使用情况和操作按钮 - 补充注销流程与用量统计测试
This commit is contained in:
1 parent
dd0005b7ba
commit
ccfdac2e1b
8 files changed
+394
-24
No files matched your search
+138
-15
@@ -1,10 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { formatFileSize } from '~/composables/useFileSize'
|
||||
|
||||
interface AdminUser {
|
||||
id: number
|
||||
username: string
|
||||
role: 'admin' | 'user'
|
||||
created_at: string
|
||||
password_reset: number
|
||||
usage?: {
|
||||
imageBytes: number
|
||||
videoBytes: number
|
||||
fileBytes: number
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -21,6 +28,18 @@ const { t } = useI18n()
|
||||
|
||||
const users = ref<AdminUser[]>([])
|
||||
const loading = ref(false)
|
||||
const deleteTarget = ref<AdminUser | null>(null)
|
||||
const deleteConfirm = ref('')
|
||||
const deleting = ref(false)
|
||||
|
||||
const canDelete = computed(() =>
|
||||
Boolean(
|
||||
deleteTarget.value
|
||||
&& deleteTarget.value.role !== 'admin'
|
||||
&& deleteConfirm.value.trim() === deleteTarget.value.username
|
||||
&& !deleting.value
|
||||
)
|
||||
)
|
||||
|
||||
async function loadUsers() {
|
||||
if (!isAdmin.value) return
|
||||
@@ -57,6 +76,51 @@ async function clearPassword(user: AdminUser) {
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteDialog(user: AdminUser) {
|
||||
if (user.role === 'admin') return
|
||||
deleteTarget.value = user
|
||||
deleteConfirm.value = ''
|
||||
}
|
||||
|
||||
function closeDeleteDialog() {
|
||||
if (deleting.value) return
|
||||
deleteTarget.value = null
|
||||
deleteConfirm.value = ''
|
||||
}
|
||||
|
||||
function onDeleteOpenChange(open: boolean) {
|
||||
if (!open) closeDeleteDialog()
|
||||
}
|
||||
|
||||
async function deleteUser() {
|
||||
const target = deleteTarget.value
|
||||
if (!target || !canDelete.value) return
|
||||
|
||||
deleting.value = true
|
||||
try {
|
||||
await $fetch(`/api/admin/users/${target.id}/delete`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: { confirmation: deleteConfirm.value.trim() }
|
||||
})
|
||||
toast.add({
|
||||
title: t('users.userDeleted', { name: target.username }),
|
||||
color: 'success'
|
||||
})
|
||||
deleteTarget.value = null
|
||||
deleteConfirm.value = ''
|
||||
await loadUsers()
|
||||
} catch (error: unknown) {
|
||||
handleAuthError(error)
|
||||
toast.add({
|
||||
title: getFetchErrorMessage(error, t('users.deleteFailed')),
|
||||
color: 'error'
|
||||
})
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(value: string) {
|
||||
try {
|
||||
return new Date(value).toLocaleString()
|
||||
@@ -150,24 +214,83 @@ onMounted(async () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UBadge
|
||||
v-if="user.password_reset === 1"
|
||||
color="warning"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
<div
|
||||
v-if="user.role !== 'admin'"
|
||||
class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted"
|
||||
>
|
||||
{{ t('users.resetPending') }}
|
||||
</UBadge>
|
||||
<UButton
|
||||
v-else
|
||||
variant="outline"
|
||||
color="warning"
|
||||
size="sm"
|
||||
:label="t('users.clearPassword')"
|
||||
@click="clearPassword(user)"
|
||||
/>
|
||||
<span>{{ t('users.usageImages') }} {{ formatFileSize(user.usage?.imageBytes ?? 0) }}</span>
|
||||
<span>{{ t('users.usageVideos') }} {{ formatFileSize(user.usage?.videoBytes ?? 0) }}</span>
|
||||
<span>{{ t('users.usageFiles') }} {{ formatFileSize(user.usage?.fileBytes ?? 0) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="user.role !== 'admin'"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<UBadge
|
||||
v-if="user.password_reset === 1"
|
||||
color="warning"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
>
|
||||
{{ t('users.resetPending') }}
|
||||
</UBadge>
|
||||
<UButton
|
||||
v-if="user.password_reset !== 1"
|
||||
variant="outline"
|
||||
color="warning"
|
||||
size="sm"
|
||||
:label="t('users.clearPassword')"
|
||||
@click="clearPassword(user)"
|
||||
/>
|
||||
<UButton
|
||||
variant="outline"
|
||||
color="error"
|
||||
size="sm"
|
||||
:label="t('users.deleteUser')"
|
||||
@click="openDeleteDialog(user)"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</AppShell>
|
||||
|
||||
<UModal
|
||||
:open="Boolean(deleteTarget)"
|
||||
:title="t('users.deleteTitle')"
|
||||
:description="t('users.deleteWarning')"
|
||||
@update:open="onDeleteOpenChange"
|
||||
>
|
||||
<template #body>
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm text-muted">
|
||||
{{ t('users.deleteConfirmLabel', { name: deleteTarget?.username ?? '' }) }}
|
||||
</p>
|
||||
<UInput
|
||||
v-model="deleteConfirm"
|
||||
:placeholder="deleteTarget?.username"
|
||||
class="w-full"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<UButton
|
||||
:label="t('common.cancel')"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
:disabled="deleting"
|
||||
@click="closeDeleteDialog"
|
||||
/>
|
||||
<UButton
|
||||
:label="t('users.deleteSubmit')"
|
||||
color="error"
|
||||
:loading="deleting"
|
||||
:disabled="!canDelete"
|
||||
@click="deleteUser"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user