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