You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
205 lines
5.4 KiB
205 lines
5.4 KiB
<script setup lang="ts">
|
|
import { request, unwrapApiBody, type ApiResponse } from '../../../../utils/http/factory'
|
|
import { useAuthSession } from '../../../../composables/useAuthSession'
|
|
import { buildPublicProfileAbsoluteUrl } from '../../../../utils/public-profile-url'
|
|
|
|
definePageMeta({ title: '用户管理' })
|
|
|
|
const { user, refresh } = useAuthSession()
|
|
const toast = useToast()
|
|
|
|
const rows = ref<
|
|
{
|
|
id: number
|
|
username: string
|
|
role: string
|
|
status: string
|
|
publicSlug: string | null
|
|
postCount: number
|
|
timelineEventCount: number
|
|
rssFeedCount: number
|
|
}[]
|
|
>([])
|
|
const loading = ref(true)
|
|
const form = reactive({ username: '', password: '', email: '' })
|
|
const creating = ref(false)
|
|
|
|
async function copyPublicUrl(publicSlug: string) {
|
|
const href = buildPublicProfileAbsoluteUrl(window.location.origin, publicSlug)
|
|
try {
|
|
await navigator.clipboard.writeText(href)
|
|
toast.add({ title: '已复制公开页链接', color: 'success' })
|
|
} catch {
|
|
toast.add({ title: '复制失败', color: 'error' })
|
|
}
|
|
}
|
|
|
|
async function ensureAdmin() {
|
|
await refresh(true)
|
|
if (user.value?.role !== 'admin') {
|
|
await navigateTo('/me')
|
|
}
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const res = await request<ApiResponse<{ users: typeof rows.value }>>('/api/admin/users')
|
|
rows.value = unwrapApiBody(res).users
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await ensureAdmin()
|
|
await load()
|
|
})
|
|
|
|
async function createUser() {
|
|
creating.value = true
|
|
try {
|
|
await request('/api/admin/users', {
|
|
method: 'POST',
|
|
body: {
|
|
username: form.username,
|
|
password: form.password,
|
|
email: form.email || null,
|
|
},
|
|
})
|
|
form.username = ''
|
|
form.password = ''
|
|
form.email = ''
|
|
await load()
|
|
} finally {
|
|
creating.value = false
|
|
}
|
|
}
|
|
|
|
async function setStatus(id: number, status: 'active' | 'disabled') {
|
|
await request(`/api/admin/users/${id}`, { method: 'PATCH', body: { status } })
|
|
await load()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<UContainer class="py-8 space-y-8 max-w-4xl">
|
|
<h1 class="text-2xl font-semibold">
|
|
用户管理
|
|
</h1>
|
|
|
|
<UCard>
|
|
<template #header>
|
|
新建用户
|
|
</template>
|
|
<div class="grid sm:grid-cols-3 gap-2">
|
|
<UInput v-model="form.username" placeholder="用户名" />
|
|
<UInput v-model="form.password" type="password" placeholder="初始密码" />
|
|
<UInput v-model="form.email" placeholder="邮箱(可选)" />
|
|
</div>
|
|
<UButton class="mt-3" :loading="creating" @click="createUser">
|
|
创建
|
|
</UButton>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<template #header>
|
|
用户列表
|
|
</template>
|
|
<div v-if="loading" class="text-muted">
|
|
加载中…
|
|
</div>
|
|
<table v-else class="w-full text-sm">
|
|
<thead>
|
|
<tr class="text-left text-muted border-b border-default">
|
|
<th class="pb-2">
|
|
id
|
|
</th>
|
|
<th class="pb-2">
|
|
用户名
|
|
</th>
|
|
<th class="pb-2">
|
|
角色
|
|
</th>
|
|
<th class="pb-2">
|
|
状态
|
|
</th>
|
|
<th class="pb-2">
|
|
文章
|
|
</th>
|
|
<th class="pb-2">
|
|
时间线
|
|
</th>
|
|
<th class="pb-2">
|
|
RSS 源
|
|
</th>
|
|
<th class="pb-2">
|
|
公开页
|
|
</th>
|
|
<th class="pb-2" />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="u in rows" :key="u.id" class="border-b border-default/60">
|
|
<td class="py-2">
|
|
{{ u.id }}
|
|
</td>
|
|
<td class="py-2">
|
|
{{ u.username }}
|
|
</td>
|
|
<td class="py-2">
|
|
{{ u.role }}
|
|
</td>
|
|
<td class="py-2">
|
|
{{ u.status }}
|
|
</td>
|
|
<td class="py-2 tabular-nums">
|
|
{{ u.postCount }}
|
|
</td>
|
|
<td class="py-2 tabular-nums">
|
|
{{ u.timelineEventCount }}
|
|
</td>
|
|
<td class="py-2 tabular-nums">
|
|
{{ u.rssFeedCount }}
|
|
</td>
|
|
<td class="py-2">
|
|
<template v-if="u.publicSlug?.trim()">
|
|
<div class="flex items-center gap-1 flex-wrap">
|
|
<NuxtLink
|
|
class="text-primary hover:underline"
|
|
:to="`/@${u.publicSlug.trim()}`"
|
|
>
|
|
@{{ u.publicSlug.trim() }}
|
|
</NuxtLink>
|
|
<UButton size="xs" variant="soft" @click="copyPublicUrl(u.publicSlug.trim())">
|
|
复制链接
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
<span v-else class="text-muted">—</span>
|
|
</td>
|
|
<td class="py-2 text-right">
|
|
<UButton
|
|
v-if="u.status === 'active'"
|
|
size="xs"
|
|
color="error"
|
|
variant="soft"
|
|
@click="setStatus(u.id, 'disabled')"
|
|
>
|
|
禁用
|
|
</UButton>
|
|
<UButton
|
|
v-else
|
|
size="xs"
|
|
variant="soft"
|
|
@click="setStatus(u.id, 'active')"
|
|
>
|
|
启用
|
|
</UButton>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</UCard>
|
|
</UContainer>
|
|
</template>
|
|
|