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.
 
 
 
 

160 lines
5.2 KiB

<script setup lang="ts">
import { useAuthSession } from '../composables/useAuthSession'
import {
publicHomeLayoutModeKey,
publicHomeLayoutStorageKey,
type PublicHomeLayoutMode,
} from '../composables/usePublicHomeLayout'
import { unwrapApiBody, type ApiResponse } from '../utils/http/factory'
const route = useRoute()
const { loggedIn, pending, refresh } = useAuthSession()
const { siteName } = useGlobalConfig()
const showPublicLayoutToggle = computed(() => /^\/@[^/]+$/.test(route.path))
const profileSlug = computed(() => {
const s = route.params.publicSlug
return typeof s === 'string' && s.length ? s : ''
})
type PublicHomeHeaderPayload = { title: string; iconUrl: string | null }
const { data: publicHomeHeader } = await useAsyncData(
() => `public-home-header:${profileSlug.value || 'none'}`,
async () => {
const slug = profileSlug.value
if (!slug) {
return null
}
try {
const res = await $fetch<ApiResponse<PublicHomeHeaderPayload>>(
`/api/public/profile/${encodeURIComponent(slug)}/home-header`,
)
return unwrapApiBody(res)
}
catch {
return null
}
},
{ watch: [profileSlug] },
)
const headerBrandTo = computed(() => (profileSlug.value ? `/@${profileSlug.value}` : '/'))
const headerBrandTitle = computed(() => {
if (profileSlug.value && publicHomeHeader.value?.title) {
return publicHomeHeader.value.title
}
return siteName.value
})
const headerBrandIconUrl = computed(() =>
profileSlug.value ? publicHomeHeader.value?.iconUrl ?? null : null,
)
const publicLayoutMode = ref<PublicHomeLayoutMode>('showcase')
provide(publicHomeLayoutModeKey, publicLayoutMode)
function syncModeFromStorage() {
if (!import.meta.client) {
return
}
if (!showPublicLayoutToggle.value || !profileSlug.value) {
return
}
const raw = localStorage.getItem(publicHomeLayoutStorageKey(profileSlug.value))
publicLayoutMode.value = raw === 'detailed' || raw === 'showcase' ? raw : 'showcase'
}
onMounted(() => {
refresh().catch(() => {})
syncModeFromStorage()
})
watch([profileSlug, showPublicLayoutToggle], syncModeFromStorage)
watch(publicLayoutMode, (m) => {
if (!import.meta.client) {
return
}
if (!showPublicLayoutToggle.value || !profileSlug.value) {
return
}
localStorage.setItem(publicHomeLayoutStorageKey(profileSlug.value), m)
})
</script>
<template>
<UApp>
<div class="min-h-screen bg-default text-default">
<header class="sticky top-0 z-40 border-b border-default/80 bg-default/95 backdrop-blur">
<UContainer class="flex h-14 items-center justify-between gap-3 sm:gap-4">
<NuxtLink
:to="headerBrandTo"
class="flex min-w-0 items-center gap-2 font-semibold tracking-tight text-highlighted"
>
<span class="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-primary/10 text-primary">
<img
v-if="headerBrandIconUrl"
:src="headerBrandIconUrl"
alt=""
class="h-full w-full object-cover"
loading="lazy"
>
<UIcon v-else name="i-lucide-orbit" class="size-4" />
</span>
<span class="truncate">{{ headerBrandTitle }}</span>
</NuxtLink>
<div class="flex shrink-0 items-center gap-2 text-sm sm:gap-3">
<NuxtLink to="/" class="text-muted hover:text-default whitespace-nowrap">
站点首页
</NuxtLink>
<div
v-if="showPublicLayoutToggle"
class="inline-flex rounded-lg border border-default bg-elevated/40 p-0.5 shadow-sm"
>
<UButton
size="xs"
:color="publicLayoutMode === 'showcase' ? 'primary' : 'neutral'"
:variant="publicLayoutMode === 'showcase' ? 'solid' : 'ghost'"
class="rounded-md"
@click="publicLayoutMode = 'showcase'"
>
<UIcon name="i-lucide-layout-template" class="size-3.5 sm:mr-1" />
<span class="hidden sm:inline">展示</span>
</UButton>
<UButton
size="xs"
:color="publicLayoutMode === 'detailed' ? 'primary' : 'neutral'"
:variant="publicLayoutMode === 'detailed' ? 'solid' : 'ghost'"
class="rounded-md"
@click="publicLayoutMode = 'detailed'"
>
<UIcon name="i-lucide-book-open" class="size-3.5 sm:mr-1" />
<span class="hidden sm:inline">阅读</span>
</UButton>
</div>
<template v-if="pending && !loggedIn">
<USkeleton class="h-5 w-12 rounded" />
</template>
<NuxtLink
v-else-if="loggedIn"
to="/me"
class="text-muted hover:text-default whitespace-nowrap"
>
控制台
</NuxtLink>
<NuxtLink
v-else
to="/login"
class="text-muted hover:text-default whitespace-nowrap"
>
登录
</NuxtLink>
</div>
</UContainer>
</header>
<main>
<slot />
</main>
</div>
</UApp>
</template>