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.
151 lines
3.9 KiB
151 lines
3.9 KiB
<script setup lang="ts">
|
|
definePageMeta({ title: '发现' })
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const { fetchData, getApiErrorMessage } = useClientApi()
|
|
|
|
function parsePage(raw: unknown): number {
|
|
const n =
|
|
typeof raw === 'string'
|
|
? Number.parseInt(raw, 10)
|
|
: typeof raw === 'number'
|
|
? raw
|
|
: Number.NaN
|
|
if (!Number.isFinite(n) || n < 1) {
|
|
return 1
|
|
}
|
|
return Math.floor(n)
|
|
}
|
|
|
|
const page = ref(parsePage(route.query.page))
|
|
|
|
watch(
|
|
() => route.query.page,
|
|
() => {
|
|
page.value = parsePage(route.query.page)
|
|
},
|
|
)
|
|
|
|
type DiscoverUser = {
|
|
publicSlug: string
|
|
displayName: string
|
|
avatar: string | null
|
|
location: string | null
|
|
}
|
|
|
|
type Payload = {
|
|
items: DiscoverUser[]
|
|
total: number
|
|
page: number
|
|
pageSize: number
|
|
}
|
|
|
|
const PAGE_SIZE = 10
|
|
|
|
const data = ref<Payload | null>(null)
|
|
const pending = ref(false)
|
|
const loadError = ref('')
|
|
|
|
async function load() {
|
|
pending.value = true
|
|
loadError.value = ''
|
|
try {
|
|
const url = page.value > 1 ? `/api/discover/users?page=${page.value}` : '/api/discover/users'
|
|
data.value = await fetchData<Payload>(url, { notify: false })
|
|
} catch (e: unknown) {
|
|
data.value = null
|
|
loadError.value = getApiErrorMessage(e)
|
|
} finally {
|
|
pending.value = false
|
|
}
|
|
}
|
|
|
|
watch(page, () => {
|
|
void load()
|
|
})
|
|
|
|
function onPageChange(p: number) {
|
|
page.value = p
|
|
const query: Record<string, string | string[] | undefined> = { ...route.query }
|
|
if (p > 1) {
|
|
query.page = String(p)
|
|
}
|
|
else {
|
|
delete query.page
|
|
}
|
|
void router.replace({ query })
|
|
}
|
|
|
|
onMounted(() => {
|
|
void load()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<UContainer class="max-w-6xl py-8">
|
|
<h1 class="mb-6 text-2xl font-semibold text-highlighted">
|
|
发现
|
|
</h1>
|
|
|
|
<div v-if="pending && !data" class="text-muted">
|
|
加载中…
|
|
</div>
|
|
<UAlert
|
|
v-else-if="loadError"
|
|
color="error"
|
|
:title="loadError"
|
|
class="mb-6"
|
|
/>
|
|
<template v-else-if="data">
|
|
<UEmpty
|
|
v-if="data.total === 0"
|
|
title="暂时还没有用户"
|
|
description="尚无用户开启「出现在发现页」。可在个人资料中打开该选项并设置公开主页 slug。"
|
|
/>
|
|
<template v-else>
|
|
<ul class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
<li
|
|
v-for="u in data.items"
|
|
:key="u.publicSlug"
|
|
class="border border-default rounded-xl bg-elevated/20 overflow-hidden transition-shadow hover:shadow-md"
|
|
>
|
|
<NuxtLink
|
|
:to="`/@${encodeURIComponent(u.publicSlug)}`"
|
|
class="flex flex-col gap-3 p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-default rounded-xl"
|
|
>
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<UAvatar
|
|
:src="u.avatar || undefined"
|
|
:alt="u.displayName"
|
|
size="md"
|
|
class="shrink-0 ring-1 ring-default"
|
|
/>
|
|
<div class="min-w-0 flex-1">
|
|
<div class="font-medium text-highlighted truncate">
|
|
{{ u.displayName }}
|
|
</div>
|
|
<div class="text-xs text-muted truncate">
|
|
@{{ u.publicSlug }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p v-if="u.location" class="text-sm text-muted line-clamp-2">
|
|
{{ u.location }}
|
|
</p>
|
|
</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
<div v-if="data.total > PAGE_SIZE" class="flex justify-end mt-8">
|
|
<UPagination
|
|
:page="page"
|
|
:total="data.total"
|
|
:items-per-page="PAGE_SIZE"
|
|
size="sm"
|
|
@update:page="onPageChange"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
</UContainer>
|
|
</template>
|
|
|