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.
 
 
 
 
 

262 lines
8.3 KiB

<script setup lang="ts">
type PublicPostListItem = {
title?: string | null
excerpt?: string | null
slug?: string | null
publishedAt?: Date | string | null
tags?: string[]
}
type PublicTimelineItem = {
id?: number
title?: string | null
occurredOn?: Date | string | null
bodyMarkdown?: string | null
}
type PublicRssListItem = {
title?: string | null
canonicalUrl?: string | null
canonical_url?: string | null
}
type NormalizedModule<T> = {
items: T[]
total: number
}
type SocialLink = {
label: string
icon?: string
safeHref: string
}
const props = defineProps<{
slug: string
displayName: string
publicSlug: string | null
avatar: string | null
bioPreviewText: string
hasBioPreview: boolean
socialLinks: SocialLink[]
postsModule: NormalizedModule<PublicPostListItem>
timelineModule: NormalizedModule<PublicTimelineItem>
readingModule: NormalizedModule<PublicRssListItem>
}>()
function postTitle(it: PublicPostListItem): string {
const t = it.title
return typeof t === 'string' && t.trim().length ? t : '未命名文章'
}
function postExcerpt(it: PublicPostListItem): string {
return typeof it.excerpt === 'string' ? it.excerpt : ''
}
function postSlug(it: PublicPostListItem): string {
return typeof it.slug === 'string' ? it.slug : ''
}
function timelineTitle(it: PublicTimelineItem): string {
const t = it.title
return typeof t === 'string' && t.trim().length ? t : '未命名动态'
}
function timelineBody(it: PublicTimelineItem): string {
return typeof it.bodyMarkdown === 'string' ? it.bodyMarkdown : ''
}
function timelineKey(it: PublicTimelineItem, index: number): string | number {
return it.id ?? index
}
function rssHref(it: PublicRssListItem): string | undefined {
return it.canonicalUrl ?? it.canonical_url ?? undefined
}
function rssTitle(it: PublicRssListItem): string {
const t = it.title
return typeof t === 'string' && t.trim().length ? t : '未命名'
}
function rssHostname(href: string | undefined): string {
if (!href) {
return ''
}
try {
return new URL(href).hostname
}
catch {
return href
}
}
const hasModuleContent = computed(() =>
props.postsModule.total > 0 || props.timelineModule.total > 0 || props.readingModule.total > 0,
)
</script>
<template>
<div class="mx-auto flex w-full max-w-xl flex-col gap-10 sm:gap-12">
<section
class="w-full rounded-2xl border border-default/80 bg-elevated/25 px-6 py-8 text-center shadow-sm ring-1 ring-black/5 dark:ring-white/10 sm:px-10 sm:py-9"
aria-label="个人资料"
>
<div class="flex flex-col items-center gap-4">
<div v-if="avatar" class="flex justify-center">
<img
:src="avatar"
alt=""
class="h-24 w-24 rounded-full border-2 border-default object-cover shadow-md ring-4 ring-primary/10"
>
</div>
<div class="space-y-1">
<h1 class="text-balance text-2xl font-semibold tracking-tight text-highlighted sm:text-3xl">
{{ displayName }}
</h1>
<p
v-if="publicSlug"
class="text-sm font-medium tabular-nums text-muted"
>
@{{ publicSlug }}
</p>
</div>
<ul
v-if="socialLinks.length"
class="flex flex-wrap items-center justify-center gap-2.5"
aria-label="社交链接"
>
<li v-for="(l, i) in socialLinks" :key="i">
<a
:href="l.safeHref"
target="_blank"
rel="noopener noreferrer"
:title="l.label"
:aria-label="`${l.label}(新窗口打开)`"
class="flex size-10 items-center justify-center rounded-full border border-default/80 bg-default/40 text-primary transition-colors hover:border-primary/35 hover:bg-elevated focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<UIcon
:name="socialLinkIconName({ ...l, url: l.safeHref })"
class="size-5 shrink-0 opacity-90"
/>
</a>
</li>
</ul>
</div>
</section>
<section
v-if="hasBioPreview"
class="w-full"
>
<NuxtLink
:to="`/@${slug}/about`"
class="block rounded-2xl border border-default/70 bg-elevated/20 p-5 transition-colors hover:border-primary/25 hover:bg-elevated/35 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary sm:p-6"
>
<p class="text-sm leading-relaxed text-toned sm:text-base">
{{ bioPreviewText }}
</p>
<p class="mt-3 text-xs font-medium text-primary">
查看完整介绍
</p>
</NuxtLink>
</section>
<template v-if="hasModuleContent">
<PublicHubModuleCard
title="文章"
:total="postsModule.total"
description="最近发布的文章摘要,进入后可查看完整内容。"
:to="`/@${slug}/posts`"
>
<template #preview>
<NuxtLink
v-for="(p, i) in postsModule.items.slice(0, 2)"
:key="`${postSlug(p)}-${i}`"
:to="`/@${slug}/posts/${encodeURIComponent(postSlug(p))}`"
class="block rounded-lg border border-default/80 bg-default/30 px-3 py-2 transition-colors hover:bg-elevated/40"
>
<div class="text-sm font-medium text-highlighted">
{{ postTitle(p) }}
</div>
<time
v-if="p.publishedAt"
class="mt-1 block text-xs tabular-nums text-muted"
:datetime="occurredOnToIsoAttr(p.publishedAt)"
>{{ formatPublishedDateOnly(p.publishedAt) }}</time>
<PostTagsPostTagBadges v-if="p.tags?.length" :tags="p.tags" :max="3" class="mt-1.5" />
<p v-if="postExcerpt(p)" class="mt-1 line-clamp-2 text-xs text-muted">
{{ postExcerpt(p) }}
</p>
</NuxtLink>
</template>
</PublicHubModuleCard>
<PublicHubModuleCard
title="时光机"
:total="timelineModule.total"
description="近期动态与节点,仅展示入口预览。"
:to="`/@${slug}/timeline`"
>
<template #preview>
<div
v-for="(e, i) in timelineModule.items.slice(0, 2)"
:key="timelineKey(e, i)"
class="rounded-lg border border-default/80 bg-default/30 px-3 py-2"
>
<div class="text-sm font-medium text-highlighted">
{{ timelineTitle(e) }}
</div>
<time
v-if="e.occurredOn"
class="mt-1 block text-xs tabular-nums text-muted"
:datetime="occurredOnToIsoAttr(e.occurredOn)"
>{{ formatOccurredOnDisplay(e.occurredOn) }}</time>
<p v-if="timelineBody(e)" class="mt-1 line-clamp-2 text-xs text-muted">
{{ timelineBody(e) }}
</p>
</div>
</template>
</PublicHubModuleCard>
<PublicHubModuleCard
title="阅读"
:total="readingModule.total"
description="最近收藏/阅读条目,进入阅读页获取完整列表。"
:to="`/@${slug}/reading`"
>
<template #preview>
<template v-for="(item, i) in readingModule.items.slice(0, 2)" :key="i">
<a
v-if="rssHref(item)"
:href="rssHref(item)"
target="_blank"
rel="noopener noreferrer"
class="block rounded-lg border border-default/80 bg-default/30 px-3 py-2 transition-colors hover:bg-elevated/40"
>
<div class="text-sm font-medium text-highlighted">
{{ rssTitle(item) }}
</div>
<div class="mt-1 text-xs text-muted">
{{ rssHostname(rssHref(item)) }}
</div>
</a>
<div
v-else
class="rounded-lg border border-default/80 bg-default/30 px-3 py-2"
>
<div class="text-sm font-medium text-highlighted">
{{ rssTitle(item) }}
</div>
</div>
</template>
</template>
</PublicHubModuleCard>
</template>
<UEmpty
v-else
title="这里还没有公开内容"
description="站主尚未发布任何公开文章或动态。"
/>
</div>
</template>