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

<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>