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.
222 lines
7.2 KiB
222 lines
7.2 KiB
<script setup lang="ts">
|
|
import { useAuthSession } from '../composables/useAuthSession'
|
|
import {
|
|
publicHomeLayoutModeKey,
|
|
type PublicHomeLayoutMode,
|
|
} from '../composables/usePublicHomeLayout'
|
|
import { unwrapApiBody, type ApiResponse } from '../utils/http/factory'
|
|
|
|
const route = useRoute()
|
|
const { loggedIn, user, refresh, clear, initialized } = useAuthSession()
|
|
const { fetchData } = useClientApi()
|
|
const { siteName, allowRegister } = useGlobalConfig()
|
|
const logoutLoading = ref(false)
|
|
|
|
const showPublicLayoutToggle = computed(() => /^\/@[^/]+$/.test(route.path))
|
|
const profileSlug = computed(() => {
|
|
const s = route.params.publicSlug
|
|
return typeof s === 'string' && s.length ? s : ''
|
|
})
|
|
|
|
/** 顶栏「控制台」仅在看自己的 /@slug 时显示;浏览他人主页时不显示控制台也不显示登录 */
|
|
const showPublicConsoleLink = computed(() => {
|
|
if (!loggedIn.value || !user.value) {
|
|
return false
|
|
}
|
|
const slug = profileSlug.value.trim().toLowerCase()
|
|
if (!slug) {
|
|
return true
|
|
}
|
|
const mine = user.value.publicSlug?.trim().toLowerCase() ?? ''
|
|
return mine !== '' && mine === slug
|
|
})
|
|
|
|
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) {
|
|
return siteName.value
|
|
}
|
|
const t = publicHomeHeader.value?.title
|
|
if (typeof t === 'string' && t.trim().length) {
|
|
return t.trim()
|
|
}
|
|
return siteName.value
|
|
})
|
|
const headerBrandIconUrl = computed(() => {
|
|
if (!profileSlug.value) {
|
|
return null
|
|
}
|
|
const u = publicHomeHeader.value?.iconUrl
|
|
if (typeof u !== 'string' || !u.trim()) {
|
|
return null
|
|
}
|
|
return u.trim()
|
|
})
|
|
|
|
const showPublicLogin = computed(() => !loggedIn.value && initialized.value)
|
|
const showPublicRegister = computed(() =>
|
|
!loggedIn.value && initialized.value && allowRegister.value,
|
|
)
|
|
|
|
async function logout() {
|
|
logoutLoading.value = true
|
|
try {
|
|
await fetchData<{ success: boolean }>('/api/auth/logout', { method: 'POST' })
|
|
clear()
|
|
await navigateTo('/')
|
|
} finally {
|
|
logoutLoading.value = false
|
|
}
|
|
}
|
|
|
|
type LayoutBySlug = Record<string, PublicHomeLayoutMode>
|
|
|
|
const layoutBySlug = useCookie<LayoutBySlug>('public_home_layout', {
|
|
default: () => ({}),
|
|
maxAge: 60 * 60 * 24 * 400,
|
|
sameSite: 'lax',
|
|
})
|
|
|
|
function cookieModeForSlug(slug: string): PublicHomeLayoutMode {
|
|
const m = layoutBySlug.value[slug]
|
|
return m === 'detailed' || m === 'showcase' ? m : 'showcase'
|
|
}
|
|
|
|
const publicLayoutMode = ref<PublicHomeLayoutMode>('showcase')
|
|
provide(publicHomeLayoutModeKey, publicLayoutMode)
|
|
|
|
function syncModeFromCookie() {
|
|
if (!showPublicLayoutToggle.value || !profileSlug.value) {
|
|
return
|
|
}
|
|
publicLayoutMode.value = cookieModeForSlug(profileSlug.value)
|
|
}
|
|
|
|
syncModeFromCookie()
|
|
watch([profileSlug, showPublicLayoutToggle, layoutBySlug], syncModeFromCookie)
|
|
|
|
watch(publicLayoutMode, (m) => {
|
|
if (!showPublicLayoutToggle.value || !profileSlug.value) {
|
|
return
|
|
}
|
|
layoutBySlug.value = { ...layoutBySlug.value, [profileSlug.value]: m }
|
|
})
|
|
|
|
onMounted(() => {
|
|
refresh().catch(() => {})
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<UApp>
|
|
<div class="min-h-screen bg-default text-default">
|
|
<header class="sticky top-0 z-40 border-b border-default/70 bg-default/90 backdrop-blur">
|
|
<UContainer class="flex h-12 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-1.5 text-sm sm:gap-2">
|
|
<div
|
|
v-if="showPublicLayoutToggle"
|
|
class="inline-flex rounded-md border border-default/80 bg-elevated/30 p-0.5"
|
|
>
|
|
<UButton
|
|
size="xs"
|
|
:color="publicLayoutMode === 'showcase' ? 'primary' : 'neutral'"
|
|
:variant="publicLayoutMode === 'showcase' ? 'solid' : 'ghost'"
|
|
class="rounded-sm"
|
|
@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-sm"
|
|
@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="!initialized">
|
|
<USkeleton class="h-5 w-12 rounded" />
|
|
</template>
|
|
<template v-else-if="loggedIn">
|
|
<NuxtLink
|
|
v-if="showPublicConsoleLink"
|
|
to="/me"
|
|
class="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted transition-colors hover:text-default"
|
|
>
|
|
<UIcon name="i-lucide-layout-dashboard" class="size-3.5" />
|
|
控制台
|
|
</NuxtLink>
|
|
<UButton
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="xs"
|
|
icon="i-lucide-log-out"
|
|
:loading="logoutLoading"
|
|
@click="logout"
|
|
>
|
|
退出
|
|
</UButton>
|
|
</template>
|
|
<template v-else-if="showPublicLogin">
|
|
<NuxtLink to="/login" class="rounded-md px-2 py-1 text-xs text-muted transition-colors hover:text-default">
|
|
登录
|
|
</NuxtLink>
|
|
<NuxtLink
|
|
v-if="showPublicRegister"
|
|
to="/register"
|
|
class="rounded-md border border-default/80 px-2 py-1 text-xs text-muted transition-colors hover:text-default"
|
|
>
|
|
注册
|
|
</NuxtLink>
|
|
</template>
|
|
</div>
|
|
</UContainer>
|
|
</header>
|
|
<main>
|
|
<slot />
|
|
</main>
|
|
</div>
|
|
</UApp>
|
|
</template>
|
|
|