1 changed files with 151 additions and 0 deletions
@ -0,0 +1,151 @@ |
|||
<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> |
|||
Loading…
Reference in new issue