@ -2,7 +2,7 @@
|
||||
import '@/styles/global.css'
|
||||
import { href, ROUTES } from '@/lib/routes'
|
||||
import { getI18n } from '@/i18n/utils'
|
||||
import { LOCALES, LOCALE_NAMES } from '@/i18n/config'
|
||||
import { LOCALE_NAMES } from '@/i18n/config'
|
||||
import { setApiLocale } from '@/lib/api'
|
||||
import { i18n } from 'astro:config/client';
|
||||
import { toCodes } from 'astro:i18n';
|
||||
@ -17,8 +17,7 @@ const { locale, t, getRelativeLocaleUrl } = getI18n(Astro)
|
||||
setApiLocale(locale)
|
||||
|
||||
// 经 data-* 属性把已翻译字符串传入打包型 <script>;define:vars 会让脚本变内联 classic,导致 import 报错,故改用 data 属性
|
||||
const hiLabel = t('Hi')
|
||||
const logoutLabel = t('Logout')
|
||||
// (账号区现改为 Alpine 组件,译文直接在模板内用 t() 输出,无需 data 属性中转)
|
||||
|
||||
const pathName = Astro.url.pathname;
|
||||
|
||||
@ -141,8 +140,27 @@ const headers = {
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex items-center gap-3" id="auth-slot" data-hi-label={hiLabel} data-logout-label={logoutLabel}>
|
||||
<a href={href(locale, ROUTES.login)} class="hover:opacity-60 transition-opacity duration-200 uppercase tracking-[0.5px] text-[13px]">{t('Login')}</a>
|
||||
<div class="flex items-center gap-3" x-data="authMenu">
|
||||
<!-- 未登录:Login 链接(Alpine 登录后置为隐藏) -->
|
||||
<a x-show="!user" href={href(locale, ROUTES.login)} class="hover:opacity-60 transition-opacity duration-200 uppercase tracking-[0.5px] text-[13px]">{t('Login')}</a>
|
||||
|
||||
<!-- 已登录:账号下拉(hover 展开,与语言切换器同款交互) -->
|
||||
<div x-show="user" class="relative" @mouseenter="open = true" @mouseleave="open = false">
|
||||
<button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="flex items-center gap-1.5 cursor-pointer hover:opacity-60 transition-opacity duration-200 p-1 -m-1 normal-case text-[13px] tracking-[0.5px]" aria-haspopup="menu" :aria-expanded="open">
|
||||
<span x-text="shortName"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="acct-chevron w-[14px] h-[14px] transition-transform duration-200" :class="open ? 'rotate-180' : ''" aria-hidden="true">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<ul x-show="open" x-transition.opacity.duration.150ms class="absolute right-0 top-full min-w-[160px] bg-white border border-black/10 shadow-sm z-50 py-2" role="menu">
|
||||
<li role="none">
|
||||
<a href={href(locale, ROUTES.account)} @click="open = false" role="menuitem" class="block px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors opacity-60">{t('my account')}</a>
|
||||
</li>
|
||||
<li role="none">
|
||||
<button type="button" @click="doLogout()" role="menuitem" class="block w-full text-left px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors opacity-60 bg-transparent border-0 cursor-pointer font-normal">{t('sign out')}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@ -154,18 +172,35 @@ const headers = {
|
||||
<footer class="relative z-[1] bg-white mt-20 pt-10 border-t-2 border-black text-black w-full font-sans">
|
||||
<script>
|
||||
import { getUser, fetchMe, logout } from "@/lib/api"
|
||||
const slot = document.getElementById("auth-slot")
|
||||
const hiLabel = slot?.dataset.hiLabel ?? "Hi"
|
||||
const logoutLabel = slot?.dataset.logoutLabel ?? "Logout"
|
||||
function render(user) {
|
||||
if (!user) return // 未登录:保留 SSR 渲染的 Login/Signup(已带语言前缀,无需重写)
|
||||
slot.innerHTML = `<span class="opacity-70">${hiLabel}, ${user.username}</span><button id="logout-btn" class="hover:opacity-60 transition-opacity cursor-pointer bg-transparent border-0 p-0 font-bold text-[13px] uppercase tracking-[0.5px]">${logoutLabel}</button>`
|
||||
const btn = slot.querySelector("#logout-btn")
|
||||
if (btn) btn.addEventListener("click", () => { logout().finally(() => location.reload()) })
|
||||
}
|
||||
const cached = getUser()
|
||||
if (cached) render(cached)
|
||||
fetchMe().then((user) => render(user)).catch(() => {})
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("authMenu", () => ({
|
||||
user: null,
|
||||
open: false,
|
||||
|
||||
init() {
|
||||
this.user = getUser() // 立即恢复缓存,避免登录态闪烁
|
||||
|
||||
fetchMe()
|
||||
.then((u) => {
|
||||
this.user = u ?? this.user ?? null
|
||||
})
|
||||
.catch(() => {
|
||||
this.user = getUser() ?? null
|
||||
})
|
||||
},
|
||||
|
||||
// 用户名最多展示前 8 个字符,超出加省略号
|
||||
get shortName() {
|
||||
const n = (this.user && this.user.username) || ""
|
||||
return n.length > 8 ? n.slice(0, 8) + "…" : n
|
||||
},
|
||||
|
||||
doLogout() {
|
||||
logout().finally(() => location.reload())
|
||||
},
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
{/* Top Footer Grid */}
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-8 mb-10 pb-10 border-b border-zinc-200">
|
||||
|
||||
Reference in New Issue
Block a user