150
src/components/AuthModal.astro
Normal file
150
src/components/AuthModal.astro
Normal file
@ -0,0 +1,150 @@
|
||||
---
|
||||
import { getI18n } from "@/i18n/utils"
|
||||
|
||||
// 全站统一登录门禁,两种语义属性(按使用场景二选一,不要混用):
|
||||
// - login-required:需要登录的「动作」(收藏/翻页/看完整图…)。点击时若未登录 →
|
||||
// 就地弹出本登录层并记住该元素;登录成功后自动重放这次点击(继续原操作)。
|
||||
// - login-redirect:需要登录的「完整入口」(想直接去账户/详情受限区…)。未登录 →
|
||||
// 跳独立登录页(自动带 redirect 回跳当前页),登录完成后回原处。
|
||||
// 已登录时两者都直接放行。本组件挂在 Layout 一次,全站共用同一 store。
|
||||
const { t } = getI18n(Astro)
|
||||
const title = t('login')
|
||||
const prompt = t('login to continue')
|
||||
const accountLabel = t('username or email')
|
||||
const passwordLabel = t('password')
|
||||
const failLabel = t('login failed')
|
||||
const closeLabel = t('close')
|
||||
const fullLoginLabel = t('go to full login')
|
||||
---
|
||||
|
||||
<!-- 全局登录层:fixed 覆盖层,所有页面生效 -->
|
||||
<div
|
||||
x-data="$store.auth"
|
||||
x-show="isOpen"
|
||||
x-cloak
|
||||
data-auth-fail={failLabel}
|
||||
class="fixed inset-0 z-[200] flex items-center justify-center bg-black/40 px-5"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={title}
|
||||
@click.self="close()"
|
||||
@keydown.escape.window="close()"
|
||||
>
|
||||
<div class="w-full max-w-sm border-2 border-black bg-white p-8">
|
||||
<div class="mb-6 flex items-start justify-between border-b-2 border-black pb-4">
|
||||
<h3 class="font-serif text-2xl font-black uppercase leading-none tracking-tight">{title}</h3>
|
||||
<button type="button" @click="close()" class="-mt-1 text-[11px] uppercase tracking-widest opacity-60 transition-opacity hover:opacity-100">{closeLabel}</button>
|
||||
</div>
|
||||
|
||||
<p class="mb-7 font-mono text-[11px] uppercase tracking-widest text-zinc-400">{prompt}</p>
|
||||
|
||||
<form @submit.prevent="submit()" class="space-y-6" novalidate>
|
||||
<div>
|
||||
<label class="mb-2 block font-mono text-[11px] uppercase tracking-widest text-zinc-500">{accountLabel}</label>
|
||||
<input type="text" x-model="account" autocomplete="username" required
|
||||
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block font-mono text-[11px] uppercase tracking-widest text-zinc-500">{passwordLabel}</label>
|
||||
<input type="password" x-model="password" autocomplete="current-password" required
|
||||
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-2 text-sm outline-none transition-colors focus:border-black focus:ring-0" />
|
||||
</div>
|
||||
|
||||
<p x-show="error" x-text="error" x-cloak class="text-xs uppercase tracking-wide text-red-600"></p>
|
||||
|
||||
<button type="submit" :disabled="loading"
|
||||
class="w-full border border-black bg-black py-3.5 font-mono text-[12px] font-bold uppercase tracking-[0.2em] text-white transition-colors hover:bg-white hover:text-black disabled:opacity-40">
|
||||
<span x-show="!loading">{title}</span>
|
||||
<span x-show="loading" x-cloak>…</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 border-t border-black/10 pt-4 text-center">
|
||||
<button type="button" @click="goFullLogin()" class="font-mono text-[10px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">{fullLoginLabel}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { getUser, login } from "@/lib/api"
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
// 全局登录 store:request(el) 返回是否放行;未登录记录 pending 并开层
|
||||
Alpine.store("auth", {
|
||||
isOpen: false,
|
||||
pending: null as HTMLElement | null,
|
||||
account: "",
|
||||
password: "",
|
||||
error: "",
|
||||
loading: false,
|
||||
|
||||
request(el: HTMLElement): boolean {
|
||||
if (getUser()) return true // 已登录:放行,交还原生点击
|
||||
this.pending = el
|
||||
this.error = ""
|
||||
this.isOpen = true
|
||||
document.body.style.overflow = "hidden" // 锁背景滚动
|
||||
return false
|
||||
},
|
||||
|
||||
close() {
|
||||
this.isOpen = false
|
||||
this.pending = null
|
||||
this.error = ""
|
||||
document.body.style.overflow = ""
|
||||
},
|
||||
|
||||
async submit() {
|
||||
this.error = ""
|
||||
this.loading = true
|
||||
try {
|
||||
await login({ account: this.account, password: this.password })
|
||||
const el = this.pending
|
||||
this.pending = null
|
||||
this.isOpen = false
|
||||
this.account = ""
|
||||
this.password = ""
|
||||
document.body.style.overflow = ""
|
||||
// 登录成功后自动重放刚才被拦截的那次点击,让用户操作无缝继续
|
||||
if (el) requestAnimationFrame(() => { try { el.click() } catch { /* 忽略 */ } })
|
||||
} catch {
|
||||
const host = document.querySelector<HTMLElement>("[data-auth-fail]")
|
||||
this.error = host?.dataset.authFail || "login failed"
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// 弹窗内兜底:前往独立完整登录页(带回跳当前页)
|
||||
goFullLogin() {
|
||||
const p = window.location.pathname + window.location.search
|
||||
const loc = p.startsWith("/cn") ? "cn" : "en"
|
||||
this.close()
|
||||
window.location.assign(`/${loc}/login?redirect=${encodeURIComponent(p)}`)
|
||||
},
|
||||
})
|
||||
|
||||
// 全站统一登录门禁:两种语义属性(capture 阶段,先于元素自身 @click 处理)
|
||||
// login-required → 就地弹窗(登录后自动重放原点击)
|
||||
// login-redirect → 跳独立登录页(带回跳)
|
||||
function fullLoginUrl(): string {
|
||||
const p = window.location.pathname + window.location.search
|
||||
const loc = p.startsWith("/cn") ? "cn" : "en"
|
||||
return `/${loc}/login?redirect=${encodeURIComponent(p)}`
|
||||
}
|
||||
document.addEventListener("click", (e) => {
|
||||
const hit = e.target as HTMLElement | null
|
||||
if (!hit) return
|
||||
const gate = hit.closest?.("[login-required], [login-redirect]") as HTMLElement | null
|
||||
if (!gate) return
|
||||
if (getUser()) return // 已登录:放行,交还原生点击
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (gate.hasAttribute("login-redirect")) {
|
||||
window.location.assign(fullLoginUrl())
|
||||
return
|
||||
}
|
||||
;(Alpine.store("auth") as any).request(hit) // 记住实际点击目标,登录后重放
|
||||
}, true)
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user