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

This commit is contained in:
toom1996
2026-08-24 00:55:03 +08:00
parent 52ad7497e8
commit 3814cf40ed
3 changed files with 108 additions and 170 deletions

View File

@ -218,7 +218,7 @@ if (!yearChips.length) yearChips = yearSeed
</div> </div>
<!-- 品牌筛选弹窗:搜索 + A-Z 跳转 + 热门推荐,按需加载,避免一次拉全量 --> <!-- 品牌筛选弹窗:搜索 + A-Z 跳转 + 热门推荐,按需加载,避免一次拉全量 -->
<div class="showsmodal" x-show="modalOpen" x-cloak> <div class="showsmodal" x-show="modalOpen" x-cloak @keydown.escape.window="closeBrandModal()">
<div class="showsmodal-mask" @click="closeBrandModal()"></div> <div class="showsmodal-mask" @click="closeBrandModal()"></div>
<div class="showsmodal-panel"> <div class="showsmodal-panel">
<header class="showsmodal-head"> <header class="showsmodal-head">
@ -342,7 +342,7 @@ if (!yearChips.length) yearChips = yearSeed
} }
.showschecks { .showschecks {
@apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin]; @apply list-none mt-[10px] mb-0 p-0 max-h-[280px] overflow-y-auto [scrollbar-width:thin] overscroll-contain;
} }
.showschecks li { .showschecks li {
@apply flex items-center gap-2 py-[5px] text-[11.5px] uppercase tracking-[0.04em] text-black cursor-pointer select-none hover:bg-[#f6f6f6]; @apply flex items-center gap-2 py-[5px] text-[11.5px] uppercase tracking-[0.04em] text-black cursor-pointer select-none hover:bg-[#f6f6f6];
@ -530,7 +530,7 @@ if (!yearChips.length) yearChips = yearSeed
@apply bg-black text-white border-black; @apply bg-black text-white border-black;
} }
.showsmodal-body { .showsmodal-body {
@apply flex-1 min-h-0 overflow-y-auto px-2 py-1; @apply flex-1 min-h-0 overflow-y-auto px-2 py-1 overscroll-contain;
} }
.showsmodal-hint { .showsmodal-hint {
@apply text-[10px] tracking-[0.22em] uppercase text-[#999] px-3 py-2; @apply text-[10px] tracking-[0.22em] uppercase text-[#999] px-3 py-2;
@ -730,10 +730,12 @@ if (!yearChips.length) yearChips = yearSeed
this.modalKeyword = "" this.modalKeyword = ""
this.modalLetter = "" this.modalLetter = ""
this.modalOpen = true this.modalOpen = true
document.body.style.overflow = "hidden"
await this.modalLoadHot() await this.modalLoadHot()
}, },
closeBrandModal() { closeBrandModal() {
this.modalOpen = false this.modalOpen = false
document.body.style.overflow = ""
}, },
async modalLoadHot() { async modalLoadHot() {
this.modalLoading = true this.modalLoading = true
@ -789,6 +791,7 @@ if (!yearChips.length) yearChips = yearSeed
confirmBrandModal() { confirmBrandModal() {
this.selectedBrands = [...this.modalSelected] this.selectedBrands = [...this.modalSelected]
this.modalOpen = false this.modalOpen = false
document.body.style.overflow = ""
this.refreshFilter() this.refreshFilter()
}, },

View File

@ -8,29 +8,36 @@ import { toCodes } from 'astro:i18n';
import LanguageDetector from '@/components/LanguageDetector.astro' import LanguageDetector from '@/components/LanguageDetector.astro'
const {
title = 'Portfolio Studio',
activeCategory = 'Shows',
activeNavTab,
activeThemePalette = null,
} = Astro.props;
const { locale, t, getRelativeLocaleUrl } = getI18n(Astro) const { locale, t, getRelativeLocaleUrl } = getI18n(Astro)
// 脚本内(打包型 <script>)无法用 {t()} 插值,需经 define:vars 把已翻译字符串传入 // 脚本内(打包型 <script>)无法用 {t()} 插值,需经 define:vars 把已翻译字符串传入
const hiLabel = t('Hi') const hiLabel = t('Hi')
const logoutLabel = t('Logout') const logoutLabel = t('Logout')
// 当前路径(去掉语言前缀),供语言切换器「换前缀、留路径」用,例如 /cn/women → /women
const pathSegments = Astro.url.pathname.split('/');
const firstSegment = pathSegments[1] || '';
const currentPath = (LOCALES as readonly string[]).includes(firstSegment)
? '/' + pathSegments.slice(2).filter(Boolean).join('/') || '/'
: Astro.url.pathname;
const pathName = Astro.url.pathname; const pathName = Astro.url.pathname;
const queryString = Astro.url.search;
const locales = toCodes(i18n!.locales); const locales = toCodes(i18n!.locales);
const pathWithoutLocale = locales.includes(pathName.split('/')[1])
? pathName.replace(`/${locale}`, '')
: pathName;
// 顶部导航数据模型(替代原 Alpine x-data 的 menu/state)。
// 下拉菜单与语言列表均由此驱动渲染,逻辑用 Astro 原生 <script> 控制开合。
const headers = {
// 走秀下拉菜单项(href 统一指向 /shows)
runways: [
{ label: t('全部的 runway'), href: href(locale, ROUTES.runwayLooks), current: true },
{ label: 'Spring / Summer 2026', href: href(locale, ROUTES.shows) },
{ label: 'Fall / Winter 2025', href: href(locale, ROUTES.shows) },
{ label: 'Resort 2026', href: href(locale, ROUTES.shows) },
{ label: 'Pre-Fall 2025', href: href(locale, ROUTES.shows) },
{ label: 'Couture FW25', href: href(locale, ROUTES.shows) },
{ label: 'Menswear SS26', href: href(locale, ROUTES.shows) },
{ label: 'Menswear FW25', href: href(locale, ROUTES.shows) },
],
// 语言列表(code + 本地名称)
languages: locales.map((l) => ({ code: l, label: LOCALE_NAMES[l].nativeName })),
}
--- ---
<!doctype html> <!doctype html>
<html lang={locale === 'cn' ? 'zh-CN' : 'en'}> <html lang={locale === 'cn' ? 'zh-CN' : 'en'}>
@ -41,82 +48,82 @@ const locales = toCodes(i18n!.locales);
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<title>{title}</title> <title>{`111`}</title>
</head> </head>
<body class="bg-white text-black min-h-screen flex flex-col p-[30px_50px] font-sans"> <body class="bg-white text-black min-h-screen flex flex-col p-[30px_50px] font-sans">
<!-- 顶部主导航栏 --> <!-- 顶部主导航栏 -->
<nav class="flex justify-between items-center text-[13px] font-bold tracking-[0.5px] uppercase"> <nav class="flex justify-between items-center text-[13px] font-bold tracking-[0.5px] uppercase">
<div class="flex flex-wrap items-center gap-6 md:gap-8"> <div class="flex flex-wrap items-center gap-6 md:gap-8">
<a href={href(locale, ROUTES.portfolio)} class="relative pb-[2px] cursor-pointer transition-opacity duration-200 flex items-center gap-1.5 hover:opacity-60 opacity-80 border-b-2 border-transparent">{t('Portfolio')}<span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full font-mono tracking-tight leading-none"> <a href={href(locale, ROUTES.portfolio)} class="relative pb-[2px] cursor-pointer transition-opacity duration-200 flex items-center gap-1.5 hover:opacity-60 opacity-80 border-b-2 border-transparent">
2026 {t('Portfolio')}
</span></a> <span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full font-mono tracking-tight leading-none">
<!-- 全部的 runway:header 下拉(假数据预览) --> 2026
<div class="relative" x-data="{ open: false, current: '全部的 runway', menu: ['全部的 runway','Spring / Summer 2026','Fall / Winter 2025','Resort 2026','Pre-Fall 2025','Couture FW25','Menswear SS26','Menswear FW25'] }" @mouseenter="open = true" @mouseleave="open = false" @click.outside="open = false" @keydown.escape.window="open = false"> </span>
<div class="flex items-center gap-1"> </a>
<a href={href(locale, ROUTES.shows)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('全部的 runway')}</a> <!-- 全部的 runway:菜单项用 Astro 渲染(headers.runways.map),开合逻辑用 Alpine.js -->
<button type="button" :aria-expanded="open" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 runway 列表"> <div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
<svg class="w-2.5 h-2.5 transition-transform duration-200" :class="open ? 'rotate-180' : ''" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg> <div class="flex items-center gap-1">
</button> <a href={href(locale, ROUTES.shows)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('全部的 runway')}</a>
</div> <button type="button" @click="open = !open" @click.outside="open = false" @keydown.escape.window="open = false" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 runway 列表" aria-haspopup="true" :aria-expanded="open">
<div x-show="open" style="display:none" class="absolute left-0 top-full mt-2 z-50 min-w-[240px] bg-white border border-black/10 shadow-sm py-1 before:content-[''] before:absolute before:-top-2 before:left-0 before:right-0 before:h-2"> <svg class="w-2.5 h-2.5 transition-transform duration-200" :class="open ? 'rotate-180' : ''" viewBox="0 0 10 6" fill="none"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
<template x-for="item in menu" :key="item"> </button>
<a href={href(locale, ROUTES.shows)} @click="current = item; open = false" class="block px-4 py-2 text-[12px] uppercase tracking-[0.4px] hover:bg-black/5 transition-colors" :class="item === current ? 'font-bold' : ''" x-text="item"></a>
</template>
</div>
</div> </div>
<div x-show="open" x-transition.opacity.duration.150ms class="absolute left-0 top-full mt-2 z-50 min-w-[240px] bg-white border border-black/10 shadow-sm py-1">
{headers.runways.map((item) => (
<a href={item.href} class:list={[
'block px-4 py-2 text-[12px] uppercase tracking-[0.4px] hover:bg-black/5 transition-colors',
item.current ? 'font-bold' : ''
]}>{item.label}</a>
))}
</div>
</div>
<a href={href(locale, ROUTES.latestProjects)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a> <a href={href(locale, ROUTES.latestProjects)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a>
<a href={href(locale, ROUTES.about)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('About')}</a> <a href={href(locale, ROUTES.about)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('About')}</a>
<a href={href(locale, ROUTES.contact)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Contact Us')}</a> <a href={href(locale, ROUTES.contact)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Contact Us')}</a>
</div> </div>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<!-- 语言切换器:Alpine 下拉,hover 展开;独立容器避免被 auth 脚本 innerHTML 重写误伤 --> <!-- 语言切换器:恢复为 Alpine.js 控制开合(x-data + x-show + x-transition) -->
<div <div
class="relative"
id="lang-switch"
x-data="{ open: false }" x-data="{ open: false }"
@mouseenter="open = true" @mouseenter="open = true"
@mouseleave="open = false" @mouseleave="open = false"
@click.outside="open = false"
@keydown.escape.window="open = false"
class="relative"
id="lang-switch"
> >
<button <button
type="button" type="button"
@click="open = !open" @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]" 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-label={t('Language')} aria-label={t('Language')}
aria-haspopup="listbox" aria-haspopup="listbox"
:aria-expanded="open" :aria-expanded="open"
> >
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-[18px] h-[18px]" aria-hidden="true"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="w-[18px] h-[18px] transition-transform duration-200" :class="open ? 'rotate-180' : ''" aria-hidden="true">
<circle cx="12" cy="12" r="10"/> <circle cx="12" cy="12" r="10"/>
<line x1="2" y1="12" x2="22" y2="12"/> <line x1="2" y1="12" x2="22" y2="12"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/> <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg> </svg>
<span>{LOCALE_NAMES[locale]}</span> <span>{LOCALE_NAMES[locale].nativeName}</span>
</button> </button>
<ul <ul
x-show="open" x-show="open"
x-cloak x-transition.opacity.duration.150ms
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 -translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-1"
class="absolute right-0 top-full min-w-[160px] bg-white border border-black/10 shadow-sm z-50 py-2" class="absolute right-0 top-full min-w-[160px] bg-white border border-black/10 shadow-sm z-50 py-2"
> >
{locales.map((l) => ( {headers.languages.map((l) => (
<li> <li>
<a <a
href={getRelativeLocaleUrl(l, pathName)} href={getRelativeLocaleUrl(l.code, pathWithoutLocale)}
@click="open = false" @click="open = false"
class:list={[ class:list={[
'block px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors', 'block px-4 py-1.5 text-[13px] uppercase tracking-[0.5px] hover:bg-black/5 transition-colors',
l === locale ? 'font-bold' : 'opacity-60', l.code === locale ? 'font-bold' : 'opacity-60',
]} ]}
>{LOCALE_NAMES[l].name}</a> >{l.label}</a>
</li> </li>
))} ))}
</ul> </ul>
@ -133,128 +140,54 @@ const locales = toCodes(i18n!.locales);
</div> </div>
<footer class="relative z-[1] bg-white mt-20 pt-10 border-t-2 border-black text-black w-full font-sans"> <footer class="relative z-[1] bg-white mt-20 pt-10 border-t-2 border-black text-black w-full font-sans">
<script define:vars={{ hiLabel, logoutLabel }}> <script define:vars={{ hiLabel, logoutLabel }}>
import { getUser, fetchMe, clearSession } from "@/lib/api" import { getUser, fetchMe, clearSession } from "@/lib/api"
const slot = document.getElementById("auth-slot") const slot = document.getElementById("auth-slot")
function render(user) { function render(user) {
if (!user) return // 未登录:保留 SSR 渲染的 Login/Signup(已带语言前缀,无需重写) 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>` 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") const btn = slot.querySelector("#logout-btn")
if (btn) btn.addEventListener("click", () => { clearSession(); location.reload() }) if (btn) btn.addEventListener("click", () => { clearSession(); location.reload() })
} }
const cached = getUser() const cached = getUser()
if (cached) render(cached) if (cached) render(cached)
fetchMe().then((user) => render(user)).catch(() => {}) fetchMe().then((user) => render(user)).catch(() => {})
</script> </script>
{/* Top Footer Grid */} {/* Top Footer Grid */}
<div class="grid grid-cols-1 md:grid-cols-12 gap-8 mb-10 pb-10 border-b border-zinc-200"> <div class="grid grid-cols-1 md:grid-cols-12 gap-8 mb-10 pb-10 border-b border-zinc-200">
{/* Column 1: Brand & Manifesto */} {/* Column 1: Brand & Manifesto */}
<div class="md:col-span-5 space-y-4"> <div class="md:col-span-5 space-y-4">
<div class="flex items-center gap-2">
<span class="text-base font-black tracking-wider uppercase border-b-2 border-black pb-0.5">
Portfolio Studio
</span>
<span class="text-[10px] font-mono bg-black text-white px-2 py-0.5 font-bold uppercase tracking-widest">
2026 Edition
</span>
</div>
<p class="text-xs text-zinc-600 leading-relaxed max-w-md">
{t('Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.')}
</p>
{/* Location Badge */}
<div class="flex flex-wrap gap-3 text-[11px] font-mono text-zinc-500 uppercase tracking-widest pt-1">
<span>Paris</span>
<span>•</span>
<span>Milan</span>
<span>•</span>
<span>Tokyo</span>
<span>•</span>
<span>New York</span>
</div>
</div>
{/* Column 3: Color Studio & Actions */}
<div class="md:col-span-4 space-y-3">
<span class="text-[11px] font-mono uppercase tracking-widest text-zinc-400 font-bold block mb-2">
{t('Color Studio Tools')}
</span>
<div class="flex flex-col gap-2">
<button
id="btn-ai-generator"
class="w-full bg-black text-white hover:bg-zinc-800 text-xs font-mono font-bold uppercase tracking-wider py-2.5 px-4 flex items-center justify-between transition-colors cursor-pointer"
>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- <Sparkles class="w-3.5 h-3.5 text-amber-300" /> --> <span class="text-base font-black tracking-wider uppercase border-b-2 border-black pb-0.5">
<span>{t('AI Color Trend Generator')}</span> Portfolio Studio
</span>
<span class="text-[10px] font-mono bg-black text-white px-2 py-0.5 font-bold uppercase tracking-widest">
2026 Edition
</span>
</div> </div>
<span class="text-[10px] text-zinc-400 font-normal">Gemini 2026</span>
</button>
<button <p class="text-xs text-zinc-600 leading-relaxed max-w-md">
id="btn-extractor" {t('Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.')}
class="w-full bg-zinc-100 hover:bg-zinc-200 border border-zinc-300 text-black text-xs font-mono font-bold uppercase tracking-wider py-2.5 px-4 flex items-center justify-between transition-colors cursor-pointer" </p>
>
<div class="flex items-center gap-2"> {/* Location Badge */}
<!-- <Layers class="w-3.5 h-3.5 text-zinc-600" /> --> <div class="flex flex-wrap gap-3 text-[11px] font-mono text-zinc-500 uppercase tracking-widest pt-1">
<span>{t('Photo Swatch Extractor')}</span> <span>Paris</span>
<span>•</span>
<span>Milan</span>
<span>•</span>
<span>Tokyo</span>
<span>•</span>
<span>New York</span>
</div> </div>
<span class="text-[10px] text-zinc-500 font-normal">{t('Upload')}</span> </div>
</button>
{activeThemePalette && (
<button
id="btn-reset-theme"
class="w-full bg-red-50 hover:bg-red-100 border border-red-200 text-red-700 text-xs font-mono font-bold uppercase tracking-wider py-2 px-4 flex items-center justify-between transition-colors cursor-pointer"
>
<div class="flex items-center gap-2">
{/* <RefreshCw class="w-3.5 h-3.5" /> */}
<span>{t('Reset B&W Theme')}</span>
</div>
<span class="text-[10px] opacity-80">Default</span>
</button>
)}
</div> </div>
</div>
</div> <div class="mt-6 text-[11px] font-mono text-zinc-400 uppercase tracking-widest">
{t('© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.')}
{/* Bottom Legal & Back To Top */} </div>
<div class="flex flex-col sm:flex-row justify-between items-center text-[12px] font-mono text-zinc-500 gap-4"> </footer>
<div>
<span>{t('© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.')}</span>
</div>
<div class="flex items-center gap-6">
<a
href={href(locale, ROUTES.about)}
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
>
{t('About')}
</a>
<a
href={href(locale, ROUTES.contact)}
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
>
{t('Contact')}
</a>
<button
id="back-to-top"
class="flex items-center gap-1 font-bold text-black hover:opacity-60 transition-opacity cursor-pointer uppercase"
>
<span>{t('Back to top')}</span>
<!-- <ArrowUp class="w-3.5 h-3.5" /> -->
</button>
</div>
</div>
<div class="mt-6 text-[11px] font-mono text-zinc-400 uppercase tracking-widest">
{t('© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.')}
</div>
</footer>
</body> </body>
</html> </html>
@ -263,4 +196,5 @@ const locales = toCodes(i18n!.locales);
body { body {
margin: 0; margin: 0;
} }
</style> </style>

View File

@ -16,11 +16,12 @@ export const ROUTES = {
contact: '/contact', contact: '/contact',
login: '/login', login: '/login',
register: '/register', register: '/register',
runwayLooks: '/runway-looks',
// 动态路由(工厂函数) // 动态路由(工厂函数)
item: (id: string | number) => `/items/${id}`, item: (id: string | number) => `/items/${id}`,
article: (id: string | number) => `/article?id=${id}`, article: (id: string | number) => `/article?id=${id}`,
articlesByBrand: (id: string | number) => `/articles?brand_id=${id}`, articlesByBrand: (id: string | number) => `/articles?brand_id=${id}`,
runwayLooks: (id: string | number) => `/runway-looks/${id}`, // runwayLooks: (id: string | number) => `/runway-looks/${id}`,
} as const; } as const;
/** 服务端:生成带 locale 前缀的完整 URL(依赖 astro:i18n,遵循 astro.config)。 */ /** 服务端:生成带 locale 前缀的完整 URL(依赖 astro:i18n,遵循 astro.config)。 */