update
This commit is contained in:
@ -1,54 +0,0 @@
|
||||
---
|
||||
import { getI18n } from "@/lib/i18n";
|
||||
|
||||
interface Crumb {
|
||||
/** i18n key:英文原文,如 'Home' / 'Runway Archive';中文环境自动取字典翻译 */
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** 中间层级的面包屑项(不含末项)。末项由 <slot name="current"> 提供(如动态年份) */
|
||||
items?: Crumb[];
|
||||
/** 可选:显式指定语言;缺省时跟随当前页面语言(Astro.currentLocale) */
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
const {t, locale, getRelativeLocaleUrl} = getI18n(Astro);
|
||||
const { items = [] } = Astro.props;
|
||||
---
|
||||
|
||||
<nav class="crumb" aria-label="Breadcrumb">
|
||||
<Fragment>
|
||||
<a href={getRelativeLocaleUrl(locale, '/')} class="crumb-link">{t('Home')}</a>
|
||||
<span class="crumb-sep" aria-hidden="true">/</span>
|
||||
</Fragment>
|
||||
{
|
||||
items.map((it) => (
|
||||
<Fragment>
|
||||
<a href={it.href} class="crumb-link">{t(it.label)}</a>
|
||||
<span class="crumb-sep" aria-hidden="true">/</span>
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
<span class="crumb-cur" aria-current="page">
|
||||
<slot name="current" />
|
||||
</span>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.crumb {
|
||||
@apply inline-flex items-center gap-[10px] mb-[10px] text-[11px] tracking-[0.18em] uppercase text-[#999] no-underline;
|
||||
}
|
||||
.crumb-link {
|
||||
@apply text-black no-underline transition-colors duration-150 hover:text-[#888];
|
||||
}
|
||||
.crumb-sep {
|
||||
@apply text-[#cfcfcf] select-none;
|
||||
}
|
||||
.crumb-cur {
|
||||
@apply text-[#999];
|
||||
}
|
||||
</style>
|
||||
@ -162,18 +162,6 @@ const thumbAspect = aspect === "3/4" ? "aspect-[3/4]" : "aspect-[2/3]"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute left-0 right-0 bottom-11 z-20 px-6 text-center font-mono text-[11px] uppercase tracking-[0.2em] text-black/60 pointer-events-none"
|
||||
x-text="currentImageName"
|
||||
></div>
|
||||
|
||||
<div
|
||||
x-show="!lbZoomed"
|
||||
class="absolute left-0 right-0 bottom-4 z-20 px-6 text-center font-mono text-[10px] uppercase tracking-[0.16em] text-black/40 pointer-events-none transition-opacity duration-200"
|
||||
>
|
||||
{t('Click image to zoom · Scroll to view · Esc to close')}
|
||||
</div>
|
||||
|
||||
{/* 各页独有的附加层(走秀页注入「当前 look 的细节图簇」;街拍页不传) */}
|
||||
<slot name="details" />
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
---
|
||||
import type { RawStreetSnap } from "@/lib/api"
|
||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||
import GalleryLightbox from "@/components/GalleryLightbox.astro"
|
||||
import { getI18n } from "@/lib/i18n"
|
||||
import { href, ROUTES } from '@/lib/routes'
|
||||
@ -55,10 +54,6 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ")
|
||||
{
|
||||
snap ? (
|
||||
<article class="mx-auto max-w-[1600px] px-5 md:px-10 py-12 md:py-16">
|
||||
<Breadcrumb>
|
||||
<span slot="current" class="max-w-[60vw] truncate">{displayTitle}</span>
|
||||
</Breadcrumb>
|
||||
|
||||
<h1 class="font-black leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8">
|
||||
{displayTitle}
|
||||
</h1>
|
||||
@ -4,9 +4,18 @@
|
||||
import Layout from "@/layouts/Layout.astro"
|
||||
import { getI18n } from "@/lib/i18n"
|
||||
import { href, ROUTES } from "@/lib/routes"
|
||||
import { config } from '@/lib/config'
|
||||
|
||||
const { locale, t } = getI18n(Astro)
|
||||
|
||||
const SITE_NAME = config.siteName ?? "Xisoa"
|
||||
const accountDesc = locale === 'cn'
|
||||
? `管理你在 ${SITE_NAME} 的收藏造型与浏览历史。`
|
||||
: `Manage your saved looks and view history on your ${SITE_NAME} account.`
|
||||
const accountKeywords = locale === 'cn'
|
||||
? `账户,收藏,浏览历史,${SITE_NAME}`
|
||||
: `account,saved looks,history,${SITE_NAME}`
|
||||
|
||||
const titleLabel = t("my account")
|
||||
const pleaseLabel = t("please log in")
|
||||
const loginLabel = t("login")
|
||||
@ -22,7 +31,7 @@ const noSavedLabel = t("no saved looks yet")
|
||||
const noHistoryLabel = t("no history yet")
|
||||
const browseLabel = t("browse the archive")
|
||||
---
|
||||
<Layout title={titleLabel}>
|
||||
<Layout title={titleLabel} description={accountDesc} keywords={accountKeywords}>
|
||||
<!-- 页面版心与 Layout 一致:黑白编辑风,衬线大标题 + 等宽小标 + 粗黑细线 -->
|
||||
<main class="account-root mx-auto w-full max-w-[1400px] px-5 py-14 md:px-10 md:py-20 font-sans" x-data="accountPage" data-locale={locale}>
|
||||
|
||||
@ -96,12 +105,12 @@ const browseLabel = t("browse the archive")
|
||||
<div x-show="favorites.length > 0" class="grid grid-cols-2 gap-x-4 gap-y-10 sm:grid-cols-3 lg:grid-cols-3 xl:grid-cols-4 lg:gap-x-5">
|
||||
<template x-for="f in favorites" :key="f.id">
|
||||
<div class="group">
|
||||
<a :href="itemHref(f)" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[#f1f1f1] transition-colors group-hover:border-black">
|
||||
<a :href="itemHref(f)" target="_blank" rel="noopener" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[#f1f1f1] transition-colors group-hover:border-black">
|
||||
<img :src="coverUrl(f)" :alt="f.title" loading="lazy" class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.04]" />
|
||||
</a>
|
||||
<button @click="removeFav(f.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 font-mono text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
|
||||
<div class="mt-3 min-w-0">
|
||||
<a :href="itemHref(f)" class="block">
|
||||
<a :href="itemHref(f)" target="_blank" rel="noopener" class="block">
|
||||
<p class="truncate text-[13px] font-medium leading-snug" x-text="f.title"></p>
|
||||
</a>
|
||||
<p class="mt-1.5 truncate font-mono text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="f.brand || '—'"></p>
|
||||
@ -144,12 +153,12 @@ const browseLabel = t("browse the archive")
|
||||
<div x-show="histTotal > 0" class="grid grid-cols-2 gap-x-4 gap-y-10 sm:grid-cols-3 lg:grid-cols-3 xl:grid-cols-4 lg:gap-x-5">
|
||||
<template x-for="h in history" :key="h.id">
|
||||
<div class="group">
|
||||
<a :href="itemHref(h)" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[#f1f1f1] transition-colors group-hover:border-black">
|
||||
<a :href="itemHref(h)" target="_blank" rel="noopener" class="relative block aspect-[3/4] overflow-hidden border border-black/10 bg-[#f1f1f1] transition-colors group-hover:border-black">
|
||||
<img :src="coverUrl(h)" :alt="h.data?.title || ''" loading="lazy" class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.04]" />
|
||||
</a>
|
||||
<button @click="removeHist(h.id)" class="card-act absolute right-2 top-2 border border-black/15 bg-white/90 px-2.5 py-1 font-mono text-[9px] uppercase tracking-[0.16em] text-black/70 opacity-0 backdrop-blur transition-opacity hover:bg-black hover:text-white group-hover:opacity-100">{removeLabel}</button>
|
||||
<div class="mt-3 min-w-0">
|
||||
<a :href="itemHref(h)" class="block">
|
||||
<a :href="itemHref(h)" target="_blank" rel="noopener" class="block">
|
||||
<p class="truncate text-[13px] font-medium leading-snug" x-text="h.data?.title || '…'"></p>
|
||||
</a>
|
||||
<p class="mt-1.5 truncate font-mono text-[10px] uppercase tracking-[0.16em] text-zinc-400" x-text="h.data?.brand || '—'"></p>
|
||||
@ -189,7 +198,7 @@ const browseLabel = t("browse the archive")
|
||||
import { getFavorites, removeFavorite, fetchFavoritesPage } from "@/lib/favorites"
|
||||
import { fetchHistoryPage, removeHistory, clearHistory } from "@/lib/history"
|
||||
import { clientHref, ROUTES } from "@/lib/routes"
|
||||
import { buildWindowPages } from "@/lib/looks-grid"
|
||||
import { buildWindowPages } from "@/lib/ui"
|
||||
|
||||
const PLACEHOLDER = "data:image/svg+xml;utf8," + encodeURIComponent(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="300" height="400">' +
|
||||
@ -257,7 +266,7 @@ const browseLabel = t("browse the archive")
|
||||
if (p < 1 || p > this.favLastPage || p === this.favPage) return
|
||||
this.loadFavPage(p)
|
||||
},
|
||||
// 窗口化页码(实现见 looks-grid.ts 的 buildWindowPages,与历史分页共用)
|
||||
// 窗口化页码(实现见 ui.ts 的 buildWindowPages,与历史分页共用)
|
||||
get favPageList() {
|
||||
return buildWindowPages(this.favPage, this.favLastPage)
|
||||
},
|
||||
@ -298,7 +307,7 @@ const browseLabel = t("browse the archive")
|
||||
if (p < 1 || p > this.histLastPage || p === this.histPage) return
|
||||
this.loadHistPage(p)
|
||||
},
|
||||
// 窗口化页码(实现见 looks-grid.ts 的 buildWindowPages,与收藏分页共用)
|
||||
// 窗口化页码(实现见 ui.ts 的 buildWindowPages,与收藏分页共用)
|
||||
get histPageList() {
|
||||
return buildWindowPages(this.histPage, this.histLastPage)
|
||||
},
|
||||
@ -1,9 +1,10 @@
|
||||
---
|
||||
import Layout from "@/layouts/Layout.astro"
|
||||
import Item from "@/components/Item.astro"
|
||||
import StreetSnap from "@/components/StreetSnap.astro"
|
||||
import RunwayLookCard from "@/components/RunwayLookCard.astro"
|
||||
import StreetSnapCard from "@/components/StreetSnapCard.astro"
|
||||
import { getSsrArticle, getSsrStreetSnap } from "@/lib/ssr"
|
||||
import { getI18n } from "@/lib/i18n"
|
||||
import { config } from '@/lib/config'
|
||||
|
||||
// 按需 SSR:走秀(runway)与街拍(street snap)详情统一收口到 /item/[id]。
|
||||
// 对外 id 带类型前缀(r=runway / s=snap,见后端 hashid.EncodeTyped),据此直接查对应表,无歧义、无需回落。
|
||||
@ -18,7 +19,7 @@ interface Props {
|
||||
const { id } = Astro.props
|
||||
const isSnap = !!id && id[0] === "s"
|
||||
|
||||
const { t } = getI18n(Astro)
|
||||
const { locale, t } = getI18n(Astro)
|
||||
|
||||
let article = null
|
||||
let articleErr = null
|
||||
@ -50,12 +51,29 @@ const canonId = String((isSnap ? snap?.id : article?.id) ?? id ?? "")
|
||||
const meta = isSnap
|
||||
? { id: canonId, type: "street", title: snap?.title ?? "", cover: snap?.cover ?? "", brand: "" }
|
||||
: { id: canonId, type: "runway", title: article?.title ?? "", cover: article?.cover ?? "", brand: article?.brand_name ?? "" }
|
||||
|
||||
// 详情页动态 TDK:标题用「品牌 + 系列」,描述/关键词按语言与类型生成(避免嵌套反引号,用拼接)。
|
||||
const SITE_NAME = config.siteName ?? "Xisoa"
|
||||
const runwayName = article ? (article.brand_name + " " + article.title).trim() : ""
|
||||
const baseName = isSnap ? (snap?.title || "") : runwayName
|
||||
const detailTitle = baseName || SITE_NAME
|
||||
const detailDesc = isSnap
|
||||
? (locale === 'cn'
|
||||
? `${baseName || SITE_NAME} —— 来自全球街头的时尚抓拍,收录于 ${SITE_NAME} 时尚档案。`
|
||||
: `${baseName || SITE_NAME} — street style snaps from cities around the world, in the ${SITE_NAME} fashion archive.`)
|
||||
: (locale === 'cn'
|
||||
? `${baseName || SITE_NAME} 走秀造型共 ${imageTotal} 张,查看细节图与系列信息,收录于 ${SITE_NAME} 时尚档案。`
|
||||
: `${baseName || SITE_NAME} — ${imageTotal} runway looks with detail images and collection info, in the ${SITE_NAME} fashion archive.`)
|
||||
const detailKeywords = isSnap
|
||||
? `${snap?.title || ''},street style,street snaps,fashion,${SITE_NAME}`.replace(/^,/, '')
|
||||
: `${article?.brand_name || ''},runway,runway looks,fashion week,${SITE_NAME}`.replace(/^,/, '')
|
||||
|
||||
const saveLabel = t("save")
|
||||
const savedLabel = t("saved")
|
||||
---
|
||||
|
||||
<Layout>
|
||||
{article ? <Item article={article} error={articleErr} preview={preview} imageTotal={imageTotal} /> : <StreetSnap snap={snap} error={snapErr} preview={preview} imageTotal={imageTotal} />}
|
||||
<Layout title={detailTitle} description={detailDesc} keywords={detailKeywords}>
|
||||
{article ? <RunwayLookCard article={article} error={articleErr} preview={preview} imageTotal={imageTotal} /> : <StreetSnapCard snap={snap} error={snapErr} preview={preview} imageTotal={imageTotal} />}
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
@ -5,6 +5,7 @@
|
||||
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';
|
||||
@ -26,7 +27,14 @@ const failLabel = t('login failed')
|
||||
const membersLabel = t('members')
|
||||
const backLabel = t('back to site')
|
||||
const homeUrl = href(locale, ROUTES.runwayLooks)
|
||||
const pageTitle = `${loginLabel} — Portfolio Studio`
|
||||
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'}>
|
||||
@ -35,6 +43,8 @@ const pageTitle = `${loginLabel} — Portfolio Studio`
|
||||
<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">
|
||||
@ -1,5 +1,4 @@
|
||||
---
|
||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||
import BrandModal from "@/components/BrandModal.astro"
|
||||
import { getSsgHotBrands } from "@/lib/ssg"
|
||||
import { getI18n } from '@/lib/i18n'
|
||||
@ -68,10 +67,6 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
<!-- 顶部:极简眉头 -->
|
||||
<header class="showshead">
|
||||
<div>
|
||||
<Breadcrumb>
|
||||
<span slot="current">{t('Runway looks')}</span>
|
||||
</Breadcrumb>
|
||||
|
||||
<h1 class="showsh1">{t('all runway looks')}</h1>
|
||||
</div>
|
||||
</header>
|
||||
@ -212,7 +207,7 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
<!-- Card List -->
|
||||
<template x-for="it in items" :key="it.id">
|
||||
<div class="showscard-wrap relative group">
|
||||
<a class="showscard" :href="itemHref(it)" :aria-label="cardTitle(it)">
|
||||
<a class="showscard" :href="itemHref(it)" :aria-label="cardTitle(it)" target="_blank" rel="noopener">
|
||||
<div class="showscard-left">
|
||||
<div class="showscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
||||
<span class="showsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
||||
@ -269,7 +264,7 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
import { toAbs } from "@/lib/request"
|
||||
import { getUser } from "@/lib/auth"
|
||||
import { clientHref, ROUTES } from "@/lib/routes"
|
||||
import { SPIN_SVG, makeCardSlots, buildPageList } from "@/lib/looks-grid"
|
||||
import { SPIN_SVG, makeCardSlots, buildPageList } from "@/lib/ui"
|
||||
import { toggleFavorite, checkFavorited, getFavorites } from "@/lib/favorites"
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
@ -1,5 +1,4 @@
|
||||
---
|
||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||
import { getI18n } from '@/lib/i18n'
|
||||
import "@/styles/look-grid.css"
|
||||
|
||||
@ -49,9 +48,6 @@ const cityOptions: { value: string; label: string }[] = [
|
||||
<!-- 顶部:极简眉头 -->
|
||||
<header class="snapshead">
|
||||
<div>
|
||||
<Breadcrumb>
|
||||
<span slot="current">{t('Street Style')}</span>
|
||||
</Breadcrumb>
|
||||
<h1 class="snapsh1">{t('All Street Snaps')}</h1>
|
||||
</div>
|
||||
</header>
|
||||
@ -170,7 +166,7 @@ const cityOptions: { value: string; label: string }[] = [
|
||||
<div class="snapsgrid">
|
||||
<template x-for="it in items" :key="it.id">
|
||||
<div class="showscard-wrap relative group">
|
||||
<a class="snapscard" :href="itemHref(it)" :aria-label="cardTitle(it)">
|
||||
<a class="snapscard" :href="itemHref(it)" :aria-label="cardTitle(it)" target="_blank" rel="noopener">
|
||||
<div class="snapscard-left">
|
||||
<div class="snapscard-img" :class="{ 'is-loaded': it._imgLoaded }">
|
||||
<span class="snapsspin" aria-hidden="true" x-html="SPIN_SVG"></span>
|
||||
@ -225,7 +221,7 @@ const cityOptions: { value: string; label: string }[] = [
|
||||
import { toAbs } from "@/lib/request"
|
||||
import { getUser } from "@/lib/auth"
|
||||
import { clientHref, ROUTES } from "@/lib/routes"
|
||||
import { SPIN_SVG, makeCardSlots, buildPageList } from "@/lib/looks-grid"
|
||||
import { SPIN_SVG, makeCardSlots, buildPageList } from "@/lib/ui"
|
||||
import { toggleFavorite, checkFavorited, getFavorites } from "@/lib/favorites"
|
||||
|
||||
const DEFAULT_SORT = "image_count"
|
||||
Reference in New Issue
Block a user