181 lines
8.9 KiB
Plaintext
181 lines
8.9 KiB
Plaintext
---
|
||
// 登录页的 en/cn 共享实现(语言由 Astro.currentLocale 运行时决定)。
|
||
// 对应页面:src/pages/{en,cn}/login.astro —— 仅做薄包装。
|
||
// 注意:本页**不走 Layout**,自带完整 <html> 文档(独立极简版式,故保留 <!doctype html>)。
|
||
import '@/styles/global.css'
|
||
import { getI18n } from '@/lib/i18n'
|
||
import { href, ROUTES } from '@/lib/routes'
|
||
import { config } from '@/lib/config'
|
||
import { LOCALE_NAMES } from '@/lib/i18n'
|
||
import { i18n } from 'astro:config/client';
|
||
import { toCodes } from 'astro:i18n';
|
||
|
||
const { locale, t, getRelativeLocaleUrl } = getI18n(Astro)
|
||
|
||
const pathName = Astro.url.pathname
|
||
const locales = toCodes(i18n!.locales)
|
||
const pathWithoutLocale = locales.includes(pathName.split('/')[1])
|
||
? pathName.replace(`/${locale}`, '')
|
||
: pathName
|
||
const languages = locales.map((l) => ({ code: l, label: LOCALE_NAMES[l].nativeName }))
|
||
|
||
const loginLabel = t('login')
|
||
const promptLabel = t('login prompt')
|
||
const accountLabel = t('username or email')
|
||
const passwordLabel = t('password')
|
||
const failLabel = t('login failed')
|
||
const membersLabel = t('members')
|
||
const backLabel = t('back to site')
|
||
const homeUrl = href(locale, ROUTES.runwayLooks)
|
||
const SITE_NAME = config.siteName ?? "Xisoa"
|
||
const pageTitle = `${loginLabel} — ${SITE_NAME}`
|
||
const loginDesc = locale === 'cn'
|
||
? `登录 ${SITE_NAME} 以收藏造型、同步浏览历史。`
|
||
: `Log in to ${SITE_NAME} to save looks and sync your view history.`
|
||
const loginKeywords = locale === 'cn'
|
||
? `登录,账户,${SITE_NAME}`
|
||
: `login,account,${SITE_NAME}`
|
||
---
|
||
<!doctype html>
|
||
<html lang={locale === 'cn' ? 'zh-CN' : 'en'}>
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width" />
|
||
<meta name="generator" content={Astro.generator} />
|
||
<title>{pageTitle}</title>
|
||
<meta name="description" content={loginDesc} />
|
||
<meta name="keywords" content={loginKeywords} />
|
||
</head>
|
||
<!-- 全站一致:白底 + 衬线大标题 + 等宽小标 + 粗黑细线,直角无阴影 -->
|
||
<body class="min-h-screen bg-white text-black antialiased selection:bg-black selection:text-white">
|
||
<main class="mx-auto w-full max-w-[1400px] px-5 py-10 md:px-10 md:py-14 font-sans">
|
||
|
||
<!-- 顶部工具行:返回档案 + 语言切换 -->
|
||
<header class="mb-12 flex items-center justify-between gap-6">
|
||
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.22em] text-black/50 transition-colors hover:text-black">
|
||
<span aria-hidden="true">←</span>{backLabel}
|
||
</a>
|
||
|
||
<div class="relative" x-data="{ open: false }" @keydown.escape.window="open = false" @click.outside="open = false">
|
||
<button type="button" @click="open = !open" class="inline-flex items-center gap-2 border border-black/15 px-3 py-2 text-[11px] uppercase tracking-[0.22em] text-black transition-colors hover:bg-black hover:text-white" aria-label={t('Language')} aria-haspopup="listbox" :aria-expanded="open">
|
||
<span>{LOCALE_NAMES[locale].nativeName}</span>
|
||
<svg viewBox="0 0 12 12" width="11" height="11" class="transition-transform duration-200" :class="open ? 'rotate-180' : ''" aria-hidden="true">
|
||
<path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</button>
|
||
<ul x-show="open" x-cloak class="absolute right-0 top-full z-20 mt-2 min-w-[170px] border border-black/15 bg-white py-1">
|
||
{languages.map((l) => (
|
||
<li>
|
||
<a href={getRelativeLocaleUrl(l.code, pathWithoutLocale)} @click="open = false" class:list={['block px-4 py-2 text-[11px] uppercase tracking-[0.2em] transition-colors hover:bg-black hover:text-white', l.code === locale ? 'font-bold text-black' : 'text-black/55']}>{l.label}</a>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 报头:与账户页同一套 masthead(mono 眉标 + 衬线巨标题 + 档案标签 + 2px 粗黑线) -->
|
||
<section class="border-b-2 border-black pb-10">
|
||
<div class="flex flex-wrap items-end justify-between gap-x-16 gap-y-8">
|
||
<div>
|
||
<p class="mb-4 text-[11px] uppercase tracking-[0.3em] text-zinc-400">{membersLabel}</p>
|
||
<h1 class="text-6xl font-black uppercase leading-[0.88] tracking-tight md:text-8xl">{loginLabel}</h1>
|
||
</div>
|
||
<div class="pb-1 text-right text-[11px] uppercase leading-loose tracking-[0.22em] text-zinc-400">
|
||
<p>Portfolio Studio</p>
|
||
<p class="flex items-center justify-end gap-2 text-zinc-500">Archive
|
||
<span class="bg-black px-1.5 py-0.5 text-[10px] font-bold tracking-widest text-white">2026</span>
|
||
</p>
|
||
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 主体:左表单 / 右档案说明(mobile 单栏堆叠) -->
|
||
<div class="grid gap-12 pt-12 md:grid-cols-[minmax(0,1fr)_320px] md:gap-16 md:pt-16">
|
||
|
||
<div class="w-full max-w-md">
|
||
<p class="mb-10 text-[11px] uppercase tracking-[0.22em] text-zinc-400">{promptLabel}</p>
|
||
|
||
<form x-data="loginForm" data-locale={locale} data-fail-label={failLabel} @submit.prevent="submit()" class="space-y-8" novalidate>
|
||
<div>
|
||
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] text-zinc-500">{accountLabel}</label>
|
||
<input type="text" x-model="account" autocomplete="username" required autofocus
|
||
class="w-full border-0 border-b border-black/30 bg-transparent px-0 py-3 text-sm text-black outline-none transition-colors focus:border-black focus:ring-0" />
|
||
</div>
|
||
|
||
<div>
|
||
<label class="mb-2 block text-[11px] uppercase tracking-[0.22em] 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-3 text-sm text-black outline-none transition-colors focus:border-black focus:ring-0" />
|
||
</div>
|
||
|
||
<p x-show="error" x-text="error" x-cloak class="text-[11px] uppercase tracking-[0.2em] text-red-600"></p>
|
||
|
||
<button type="submit" :disabled="loading"
|
||
class="w-full border border-black bg-black py-4 text-[12px] font-bold uppercase tracking-[0.2em] text-white transition-colors hover:bg-white hover:text-black disabled:cursor-not-allowed disabled:opacity-50">
|
||
<span x-show="!loading">{loginLabel}</span>
|
||
<span x-show="loading" x-cloak>…</span>
|
||
</button>
|
||
</form>
|
||
|
||
<div class="mt-10 border-t border-black/15 pt-5">
|
||
<a href={homeUrl} class="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.2em] text-zinc-400 transition-colors hover:text-black">
|
||
<span aria-hidden="true">←</span>{backLabel}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<aside class="border-t border-black/10 pt-8 md:border-l md:border-t-0 md:pl-8 md:pt-0">
|
||
<p class="mb-5 text-[11px] uppercase tracking-[0.3em] text-zinc-400">Archive note</p>
|
||
<p class="text-lg leading-relaxed text-black/80">{t('minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. integrated with 2026 color forecast intelligence and ai palette generator.')}</p>
|
||
<div class="mt-8 border-t border-black/10 pt-5 text-[10px] uppercase tracking-[0.3em] text-zinc-400">
|
||
<p class="mb-3">Runway · Street · Archive</p>
|
||
<p class="tracking-[0.3em]">Paris · Milan · Tokyo</p>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
import { getUser } from "@/lib/auth"
|
||
import { login } from "@/lib/api"
|
||
import { clientHref, ROUTES } from "@/lib/routes"
|
||
|
||
document.addEventListener("alpine:init", () => {
|
||
Alpine.data("loginForm", () => ({
|
||
account: "",
|
||
password: "",
|
||
error: "",
|
||
loading: false,
|
||
|
||
init() {
|
||
if (getUser()) {
|
||
const loc = this.$el.dataset.locale || "en"
|
||
window.location.href = clientHref(loc, ROUTES.account)
|
||
}
|
||
},
|
||
|
||
submit() {
|
||
this.error = ""
|
||
this.loading = true
|
||
|
||
login({ account: this.account, password: this.password })
|
||
.then(() => {
|
||
const params = new URLSearchParams(window.location.search)
|
||
const redirect = params.get("redirect")
|
||
const loc = this.$el.dataset.locale || "en"
|
||
window.location.href = redirect || clientHref(loc, ROUTES.account)
|
||
})
|
||
.catch(() => {
|
||
this.error = this.$el.dataset.failLabel || "login failed"
|
||
})
|
||
.finally(() => {
|
||
this.loading = false
|
||
})
|
||
},
|
||
}))
|
||
})
|
||
</script>
|
||
</body>
|
||
</html>
|