update
Some checks failed
deploy / deploy (push) Has been cancelled

This commit is contained in:
toom1996
2026-09-01 00:08:05 +08:00
parent 97b023bbbd
commit 50e5775dc8
9 changed files with 434 additions and 141 deletions

View File

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