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.
225 lines
7.1 KiB
225 lines
7.1 KiB
<script setup lang="ts">
|
|
import { request, unwrapApiBody, type ApiResponse } from '../utils/http/factory'
|
|
import { useAuthSession } from '../composables/useAuthSession'
|
|
|
|
withDefaults(
|
|
defineProps<{
|
|
/** @deprecated 保留以兼容旧布局 */
|
|
showAuthActions?: boolean
|
|
}>(),
|
|
{ showAuthActions: false },
|
|
)
|
|
|
|
const route = useRoute()
|
|
const { loggedIn, user, refresh, clear, pending } = useAuthSession()
|
|
const { allowRegister } = useGlobalConfig()
|
|
|
|
const logoutLoading = ref(false)
|
|
|
|
onMounted(() => {
|
|
refresh(true).catch(() => {})
|
|
})
|
|
|
|
const displayName = computed(() => {
|
|
const u = user.value
|
|
if (!u) {
|
|
return ''
|
|
}
|
|
const nick = u.nickname?.trim()
|
|
return nick || u.username
|
|
})
|
|
|
|
const primaryNav = [
|
|
{ label: '首页', to: '/', icon: 'i-lucide-house' },
|
|
{ label: '控制台', to: '/me', icon: 'i-lucide-layout-dashboard' },
|
|
{ label: '资料', to: '/me/profile', icon: 'i-lucide-user-round' },
|
|
{ label: '文章', to: '/me/posts', icon: 'i-lucide-file-text' },
|
|
{ label: '时光机', to: '/me/timeline', icon: 'i-lucide-history' },
|
|
{ label: 'RSS', to: '/me/rss', icon: 'i-lucide-rss' },
|
|
] as const
|
|
|
|
function navActive(to: string) {
|
|
if (to === '/') {
|
|
return route.path === '/'
|
|
}
|
|
return route.path === to || route.path.startsWith(`${to}/`)
|
|
}
|
|
|
|
const accountMenuItems = computed(() => {
|
|
const u = user.value
|
|
if (!u) {
|
|
return [] as { label: string; icon: string; to?: string; target?: string }[][]
|
|
}
|
|
const groups: { label: string; icon: string; to?: string; target?: string }[][] = [
|
|
[
|
|
{ label: '控制台', icon: 'i-lucide-layout-dashboard', to: '/me' },
|
|
{ label: '个人资料', icon: 'i-lucide-user-round', to: '/me/profile' },
|
|
],
|
|
]
|
|
if (u.publicSlug) {
|
|
groups.push([
|
|
{
|
|
label: '公开主页',
|
|
icon: 'i-lucide-external-link',
|
|
to: `/@${u.publicSlug}`,
|
|
target: '_blank',
|
|
},
|
|
])
|
|
}
|
|
if (u.role === 'admin') {
|
|
groups.push([{ label: '用户管理', icon: 'i-lucide-users', to: '/me/admin/users' }])
|
|
}
|
|
return groups
|
|
})
|
|
|
|
async function logout() {
|
|
logoutLoading.value = true
|
|
try {
|
|
unwrapApiBody(await request<ApiResponse<{ success: boolean }>>('/api/auth/logout', { method: 'POST' }))
|
|
clear()
|
|
await navigateTo('/login')
|
|
} finally {
|
|
logoutLoading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<UApp>
|
|
<div class="min-h-screen bg-default text-default flex flex-col">
|
|
<header class="sticky top-0 z-40 border-b border-default/80 bg-default/95 backdrop-blur supports-[backdrop-filter]:bg-default/80">
|
|
<UContainer class="flex h-16 items-center justify-between gap-4">
|
|
<div class="flex min-w-0 flex-1 items-center gap-6">
|
|
<NuxtLink
|
|
to="/"
|
|
class="flex shrink-0 items-center gap-2 font-semibold tracking-tight text-highlighted"
|
|
>
|
|
<span class="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
<UIcon name="i-lucide-orbit" class="size-4" />
|
|
</span>
|
|
<span class="hidden sm:inline">Person Panel</span>
|
|
</NuxtLink>
|
|
|
|
<nav
|
|
v-if="loggedIn"
|
|
class="hidden min-w-0 flex-1 items-center gap-0.5 overflow-x-auto md:flex"
|
|
aria-label="主导航"
|
|
>
|
|
<UButton
|
|
v-for="item in primaryNav"
|
|
:key="item.to"
|
|
:to="item.to"
|
|
:icon="item.icon"
|
|
:label="item.label"
|
|
color="neutral"
|
|
variant="ghost"
|
|
size="sm"
|
|
:class="[
|
|
'shrink-0 rounded-md',
|
|
navActive(item.to) ? 'bg-elevated text-highlighted' : 'text-muted',
|
|
]"
|
|
/>
|
|
</nav>
|
|
|
|
<UDropdownMenu
|
|
v-if="loggedIn"
|
|
class="md:hidden"
|
|
:items="[
|
|
primaryNav.map((i) => ({
|
|
label: i.label,
|
|
icon: i.icon,
|
|
to: i.to,
|
|
})),
|
|
]"
|
|
:content="{ align: 'start' }"
|
|
>
|
|
<UButton color="neutral" variant="outline" icon="i-lucide-menu" size="sm" label="菜单" />
|
|
</UDropdownMenu>
|
|
</div>
|
|
|
|
<div class="flex shrink-0 items-center gap-2">
|
|
<template v-if="pending && !loggedIn">
|
|
<USkeleton class="h-9 w-24 rounded-md" />
|
|
</template>
|
|
|
|
<template v-else-if="loggedIn && user">
|
|
<div class="hidden items-center gap-2 sm:flex">
|
|
<UBadge
|
|
v-if="user.role === 'admin'"
|
|
color="primary"
|
|
variant="subtle"
|
|
size="xs"
|
|
>
|
|
管理员
|
|
</UBadge>
|
|
<UBadge
|
|
v-else
|
|
color="neutral"
|
|
variant="subtle"
|
|
size="xs"
|
|
>
|
|
用户
|
|
</UBadge>
|
|
</div>
|
|
|
|
<UDropdownMenu :items="accountMenuItems" :content="{ align: 'end' }">
|
|
<UButton color="neutral" variant="ghost" class="gap-2 px-2">
|
|
<UAvatar
|
|
:src="user.avatar || undefined"
|
|
:alt="displayName"
|
|
size="sm"
|
|
class="ring-1 ring-default"
|
|
/>
|
|
<span class="max-w-[8rem] truncate text-sm font-medium text-highlighted">
|
|
{{ displayName }}
|
|
</span>
|
|
<span class="hidden text-xs text-muted lg:inline">@{{ user.username }}</span>
|
|
<UIcon name="i-lucide-chevrons-up-down" class="size-4 text-muted" />
|
|
</UButton>
|
|
</UDropdownMenu>
|
|
|
|
<UButton
|
|
color="neutral"
|
|
variant="ghost"
|
|
icon="i-lucide-log-out"
|
|
:loading="logoutLoading"
|
|
class="hidden sm:inline-flex"
|
|
aria-label="退出登录"
|
|
@click="logout"
|
|
/>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<UButton to="/login" color="neutral" variant="ghost" label="登录" />
|
|
<UButton
|
|
v-if="allowRegister"
|
|
to="/register"
|
|
color="neutral"
|
|
variant="outline"
|
|
label="注册"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</UContainer>
|
|
</header>
|
|
|
|
<main class="flex-1">
|
|
<slot />
|
|
</main>
|
|
|
|
<footer class="border-t border-default/80 bg-elevated/30">
|
|
<UContainer class="flex flex-col gap-2 py-6 text-sm text-muted sm:flex-row sm:items-center sm:justify-between">
|
|
<span>Person Panel — 个人资料、文章、时光机与 RSS</span>
|
|
<div class="flex gap-4">
|
|
<NuxtLink to="/" class="hover:text-default">
|
|
首页
|
|
</NuxtLink>
|
|
<NuxtLink v-if="loggedIn" to="/me" class="hover:text-default">
|
|
控制台
|
|
</NuxtLink>
|
|
</div>
|
|
</UContainer>
|
|
</footer>
|
|
</div>
|
|
</UApp>
|
|
</template>
|
|
|