53
.workbuddy/memory/2026-08-23.md
Normal file
53
.workbuddy/memory/2026-08-23.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# 2026-08-23 工作日志
|
||||||
|
|
||||||
|
## routes.ts 命名优化:PATH → ROUTES
|
||||||
|
- 用户觉得 `PATH` 命名不合适(问该叫 `router` 还是 `route`)。
|
||||||
|
- 决策:`router` 语义错误(无路由匹配/分发能力);`route` 单数不准(它是路由集合);最终改为 **`ROUTES`**(复数,最准确,且契合 `routes.ts` 文件名)。
|
||||||
|
- 落地:大小写敏感地把全站大写 `PATH` 改为 `ROUTES`(共 10 个文件、~28 处),**未触碰** `<path>` SVG 标签(小写)。另把 `routes.ts` 内 `href`/`clientHref` 的参数 `path` → `route`,注释"路径"措辞统一为"路由"。
|
||||||
|
- 涉及文件:src/lib/routes.ts、src/layouts/Layout.astro、src/components/{ComingSoon,Index,Item,RunwayLooks}.astro、src/components/pages/{brands,articles}.astro、src/pages/{shows,cn/shows}.astro。
|
||||||
|
- 验证:`npm run build` 通过(BUILD_EXIT=0);全站 `\bPATH\b` 残留 0。
|
||||||
|
|
||||||
|
## ⚠️ 重要:上一轮"ArticleArchive 重构"在磁盘上未落地
|
||||||
|
- 本会话核对发现:`src/components/ArticleArchive.astro`、`src/components/pages/BrandIntro.astro`、`src/pages/en/brands/[name].astro` 均**不存在**;`src/pages/en/runway-looks/index.astro` 仍用 `<RunwayLooks />`,`RunwayLooks.astro` 仍在。
|
||||||
|
- git status 仅 `src/components/Item.astro` 为未提交修改,无上述新文件(untracked 应出现却无)。
|
||||||
|
- 结论:上轮对话的重构改动(含记忆写入)都未持久化到磁盘,疑似会话沙箱重置。后续若要继续品牌页重构,需从现状(`RunwayLooks` 仍为主组件)重新执行,不能假设 ArticleArchive 已存在。
|
||||||
|
- **用户已手动删除**上一轮品牌页相关尝试,原话"感觉设计的不太好"。**结论:放弃 ArticleArchive 那套重构方向,不再主动重做。** 若日后重提品牌页,需先明确用户想要的设计方向(旧方案已被否定),不要默认沿用 ArticleArchive 方案。
|
||||||
|
|
||||||
|
## 「All Shows」改为「全部的 runway」+ 品牌筛选弹窗改语言切换器风格
|
||||||
|
- 用户给的截图是白底小三角箭头朝上的下拉列表(类语言切换器),要求:
|
||||||
|
1. 把 "All Shows" 改成 "全部的 runway"(中英混排,按用户原话,en/cn 都用同一句)。
|
||||||
|
2. 弹窗按图里风格重做。
|
||||||
|
- **文本改动**(共 6 处):
|
||||||
|
- `src/i18n/dictionary.ts` key 从 `'All Shows': { cn: '全部走秀' }` 改成 `'全部的 runway': { cn: '全部的 runway' }`(en/cn 都显示同一文案)。
|
||||||
|
- `src/i18n/utils.ts` 注释里的示例 key 同步。
|
||||||
|
- `src/layouts/Layout.astro` 导航链接 `t('All Shows')` → `t('全部的 runway')`。
|
||||||
|
- `src/pages/shows.astro` / `src/pages/cn/shows.astro`:`<Layout title="All Shows — Portfolio Studio">` → `title="全部的 runway — Portfolio Studio"`,h1 同步。
|
||||||
|
- `src/components/RunwayLooks.astro` h1 同步。
|
||||||
|
- **弹窗重做**(`.showsmodal` 块):
|
||||||
|
- 范围:`src/pages/cn/shows.astro` 与 `src/components/RunwayLooks.astro`(**注意:`src/pages/shows.astro` 没有弹窗**,是内联展开列表,不动)。
|
||||||
|
- 视觉:从「重型居中弹窗 + 黑边 + 黑底 mask」改成「轻量顶部下拉(pt-[14vh] 居中靠上)+ 透明 mask(仍保留点击关闭)+ 浅边框(border-black/10)+ 顶部小三角箭头(`::before` + `rotate(45deg)`)+ 软投影」。
|
||||||
|
- 输入框/A-Z 按钮/列表行统一更轻量(移除重边框、改 hover 灰色背景 `hover:bg-black/5`)。
|
||||||
|
- footer 改为 `justify-between`:左小灰字「已选 X / 最多 5 个」+ 右确认按钮。
|
||||||
|
- 保留 `max-w-[460px]`、`max-h-[72vh]`、`pointer-events-none/auto` 控制层级(mask 和 panel 可点击,root 透传)。
|
||||||
|
- **弹窗功能(未改)**:搜索、A-Z、多选 5 个、确认按钮逻辑全部保留。
|
||||||
|
- 验证:`npm run build` BUILD_EXIT=0;curl 页面验证包含新文案与 `rotate(45deg)` 编译进 CSS。
|
||||||
|
- **重要陷阱**:PowerShell `.Contains()` 对 UTF-8 中文匹配会返回 False(假阴性),以后验收 HTML 含中文应改用 `curl | Select-String` 或 `Select-Object -First 1 -ExpandProperty Matches`。
|
||||||
|
|
||||||
|
## 「全部的 runway」header 导航下拉菜单(占位假数据,仅预览)
|
||||||
|
- 用户最初说"加个下拉菜单看效果",我误加在了页面 h1 大标题上;**用户纠正:是要加在 header 导航栏的「全部的 runway」链接上**。
|
||||||
|
- 改动:
|
||||||
|
- `src/layouts/Layout.astro`:把 header 导航里的 `<a>`「全部的 runway」包成 `relative` 容器 + Alpine `x-data="{open,current,menu}"`(menu 8 条假 runway 季:全部的 runway / Spring-Summer 2026 / Fall-Winter 2025 / Resort 2026 / Pre-Fall 2025 / Couture FW25 / Menswear SS26 / Menswear FW25)。**触发方式为 hover**(用户对齐语言切换器要求改的):外层容器 `@mouseenter/@mouseleave` 控制显隐、按钮只保留 `:aria-expanded`;并给菜单加 `before:` 透明桥接层(`before:-top-2` 覆盖 `mt-2` 缝隙)避免 hover 在箭头与菜单间断开;`@click.outside`/`@keydown.escape.window` 仍保留关闭。caret 旋转 180°,选中项 `font-bold`。菜单项目前 `<a href=ROUTES.shows>`(占位,点了只跳 /shows,不筛选)。
|
||||||
|
- 三处 h1 下拉(之前误加的)已全部撤销并删除 `.runwaydd*` 样式:`src/pages/cn/shows.astro`、`src/pages/shows.astro`、`src/components/RunwayLooks.astro`。
|
||||||
|
- 依赖:header 下拉用 Alpine,靠 `@astrojs/alpinejs` 集成全局自动 start(语言切换器同机制),故全站页面都能用。
|
||||||
|
- 验证:`npm run build` BUILD_EXIT=0;产物 `dist/client/cn/shows/index.html` 含 `展开 runway 列表` 与 `Menswear`(假数据),`runwaydd` 为 0(h1 下拉已干净移除)。预览服务器指向 `dist/client`(`python -m http.server 8090 --directory dist/client`)。
|
||||||
|
- **注意**:用户明确"先假数据看效果",所以下拉**尚未接通真实筛选**。下一步若生效,需把 menu 接到真实季/年份维度或直接调 `loadPage`。
|
||||||
|
|
||||||
|
## Layout.astro 用 getI18n(t) 补全 footer / 脚本翻译
|
||||||
|
- 用户要求"看看 layout 里有没有能用 getI18n 优化的地方"。排查发现 header 导航已全用 `t()`,但 footer 与一处脚本大量硬编码英文未走翻译。
|
||||||
|
- 字典 `src/i18n/dictionary.ts` 新增:`Color Studio Tools / AI Color Trend Generator / Photo Swatch Extractor / Upload / Reset B&W Theme / Default`、manifesto 长文(key=英文原文)、`Hi / Logout / Language`。
|
||||||
|
- Layout.astro 改动:
|
||||||
|
- footer 模板区包 `t()`:Color Studio Tools、AI Color Trend Generator、Photo Swatch Extractor、Upload、Reset B&W Theme、Default、manifesto 长文、重复版权行(line 219,与 line 246 的 `t()` 一致化)、语言切换器 `aria-label={t('Language')}`。
|
||||||
|
- **脚本区** `Hi`/`Logout`:最初误用 `{t('Hi')}` 写进打包型 `<script>`,构建后原样留存导致运行时 `t is not defined`。已改用 frontmatter 预翻译 + `<script define:vars={{ hiLabel, logoutLabel }}>` 注入(en→Hi/Logout,cn→你好/退出),脚本内 `${hiLabel}`/`${logoutLabel}` 引用。
|
||||||
|
- **故意不翻**:`Portfolio Studio` 品牌名、`2026 Edition`、`Gemini 2026`、Paris/Milan/Tokyo/New York 城市名(专有名词/品牌)。
|
||||||
|
- 另:`import LanguageDetector` 在 Layout 顶部但从未渲染(仅 `<!-- TODO -->` 占位),属死代码,本次未动,已告知用户。
|
||||||
|
- 验证:`npm run build` BUILD_EXIT=0;cn 产物 HTML 含 `色彩工作室工具/AI 色彩趋势生成器/图片色板提取器/极简编辑风格作品集/你好/退出/语言`,en 产物 JS 含 `const logoutLabel="Logout"`,且 `t('Hi')` 残留为 0。
|
||||||
@ -28,6 +28,13 @@
|
|||||||
- **正确做法**:用 `w-full`(强制 100% 父容器,常配 `max-w-[xxx]`),或 `w-full mx-auto`(保留大屏居中)。
|
- **正确做法**:用 `w-full`(强制 100% 父容器,常配 `max-w-[xxx]`),或 `w-full mx-auto`(保留大屏居中)。
|
||||||
- 适用范围:所有 Layout 内的页内根容器(包括 showsroot / blogs / about 等),**避免 `m-auto` 单独使用**。
|
- 适用范围:所有 Layout 内的页内根容器(包括 showsroot / blogs / about 等),**避免 `m-auto` 单独使用**。
|
||||||
|
|
||||||
|
## i18n 脚本内翻译陷阱(2026-08-23 踩坑)
|
||||||
|
- **Astro 对「打包型 `<script>`(module)不做 `{...}` 模板插值**。若在前端 `<script>` 里写 `slot.innerHTML = \`...{t('Hi')}...\``,构建后 `t('Hi')` 会原样留存在打包 JS 中 → 运行时 `t is not defined` 报错、脚本中断。
|
||||||
|
- **正确做法**:在 frontmatter 用 `const hiLabel = t('Hi')` 预翻译,再用 `<script define:vars={{ hiLabel, logoutLabel }}>` 把字符串注入脚本,脚本内用 `${hiLabel}` 引用。define:vars 会把 `const hiLabel="..."` 注入到该 page 的脚本作用域(en 得英文、cn 得中文)。
|
||||||
|
- 模板(`<template>` HTML 区,非 `<script>`)里的 `{t('...')}` 插值正常可用,仅 `<script>` 受限。
|
||||||
|
- 验证坑:`Select-String -Recurse` 在 PowerShell 不存在(参数名不符);查 dist 产物改用 ripgrep 工具(search_content,递归默认开启)或 `Get-ChildItem -Recurse | Select-String`。
|
||||||
|
- 注意:打包后 `<script>` 内容不在 HTML 内联,而在 `_astro/*.js`;用 `--` 类 UX 标志(如 `activeThemePalette` 控制的 Reset B&W Theme)在对应 prop 为空时不会渲染,静态 HTML 搜不到属正常。
|
||||||
|
|
||||||
## 部署(Docker + Gitea Actions)
|
## 部署(Docker + Gitea Actions)
|
||||||
- 仅 brands.astro 构建期 SSR 取数(用 API_SSR);其余页面运行时相对 /api 拉取(nginx 反代 backend:8090)。
|
- 仅 brands.astro 构建期 SSR 取数(用 API_SSR);其余页面运行时相对 /api 拉取(nginx 反代 backend:8090)。
|
||||||
- 交付文件:前端 Dockerfile / nginx.conf / docker-compose.yml / deploy.sh / .gitea/workflows/deploy.yml / .env.example / DEPLOY.md;后端 Dockerfile。
|
- 交付文件:前端 Dockerfile / nginx.conf / docker-compose.yml / deploy.sh / .gitea/workflows/deploy.yml / .env.example / DEPLOY.md;后端 Dockerfile。
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { getI18n } from '@/i18n/utils'
|
import { getI18n } from '@/i18n/utils'
|
||||||
import { href, PATH } from '@/lib/routes'
|
import { href, ROUTES } from '@/lib/routes'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
module?: string
|
module?: string
|
||||||
@ -22,8 +22,8 @@ const { locale } = getI18n(Astro)
|
|||||||
{note || "该模块正在筹备中,尚未对外开放。站点正按模块逐个完善,你可以先浏览已上线的走秀档案。"}
|
{note || "该模块正在筹备中,尚未对外开放。站点正按模块逐个完善,你可以先浏览已上线的走秀档案。"}
|
||||||
</p>
|
</p>
|
||||||
<div class="cs-actions">
|
<div class="cs-actions">
|
||||||
<a class="cs-btn" href={href(locale, PATH.articles)}>浏览走秀档案 →</a>
|
<a class="cs-btn" href={href(locale, ROUTES.articles)}>浏览走秀档案 →</a>
|
||||||
<a class="cs-btn-ghost" href={href(locale, PATH.home)}>返回首页</a>
|
<a class="cs-btn-ghost" href={href(locale, ROUTES.home)}>返回首页</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import ComingSoon from "@/components/ComingSoon.astro"
|
import ComingSoon from "@/components/ComingSoon.astro"
|
||||||
import { toAbs } from "@/lib/api"
|
import { toAbs } from "@/lib/api"
|
||||||
import { getI18n } from "@/i18n/utils"
|
import { getI18n } from "@/i18n/utils"
|
||||||
import { href, PATH } from '@/lib/routes'
|
import { href, ROUTES } from '@/lib/routes'
|
||||||
import { getSsgPopularBrands } from "@/lib/api"
|
import { getSsgPopularBrands } from "@/lib/api"
|
||||||
|
|
||||||
// 国际化:一行取当前语言 + 翻译函数
|
// 国际化:一行取当前语言 + 翻译函数
|
||||||
@ -39,34 +39,31 @@ const galleryItems: GalleryItem[] = popular
|
|||||||
:class="{ 'is-active': activeTab === 'backstage' }"
|
:class="{ 'is-active': activeTab === 'backstage' }"
|
||||||
@click="activeTab = 'backstage'"
|
@click="activeTab = 'backstage'"
|
||||||
>
|
>
|
||||||
A
|
Runway
|
||||||
<span class="text-xs uppercase tracking-widest bg-black text-white px-2 py-0.5 rounded-full font-mono font-bold align-super">Live</span>
|
<!-- <span class="text-xs uppercase tracking-widest bg-black text-white px-2 py-0.5 rounded-full font-mono font-bold align-super">Live</span> -->
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap"
|
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap"
|
||||||
:class="{ 'is-active': activeTab === 'shows' }"
|
:class="{ 'is-active': activeTab === 'shows' }"
|
||||||
@click="activeTab = 'shows'"
|
@click="activeTab = 'shows'"
|
||||||
>
|
>
|
||||||
B
|
Street Style
|
||||||
<span class="text-xs uppercase tracking-widest bg-zinc-200 text-zinc-600 px-2 py-0.5 rounded-full font-mono font-bold align-super">Soon</span>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap"
|
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap"
|
||||||
:class="{ 'is-active': activeTab === 'events' }"
|
:class="{ 'is-active': activeTab === 'events' }"
|
||||||
@click="activeTab = 'events'"
|
@click="activeTab = 'events'"
|
||||||
>
|
>
|
||||||
C
|
Social Media
|
||||||
<span class="text-xs uppercase tracking-widest bg-zinc-200 text-zinc-600 px-2 py-0.5 rounded-full font-mono font-bold align-super">Soon</span>
|
|
||||||
</li>
|
</li>
|
||||||
<li
|
<!-- <li
|
||||||
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap"
|
class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap"
|
||||||
:class="{ 'is-active': activeTab === 'street' }"
|
:class="{ 'is-active': activeTab === 'street' }"
|
||||||
@click="activeTab = 'street'"
|
@click="activeTab = 'street'"
|
||||||
>
|
>
|
||||||
D
|
D
|
||||||
<span class="text-xs uppercase tracking-widest bg-zinc-200 text-zinc-600 px-2 py-0.5 rounded-full font-mono font-bold align-super">Soon</span>
|
<span class="text-xs uppercase tracking-widest bg-zinc-200 text-zinc-600 px-2 py-0.5 rounded-full font-mono font-bold align-super">Soon</span>
|
||||||
</li>
|
</li> -->
|
||||||
<a href={href(locale, PATH.brands)} class="tab text-[42px] font-extrabold cursor-pointer whitespace-nowrap hover:opacity-80">E</a>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -86,7 +83,7 @@ const galleryItems: GalleryItem[] = popular
|
|||||||
{
|
{
|
||||||
galleryItems.map((g, i) => (
|
galleryItems.map((g, i) => (
|
||||||
<div class="swiper-slide hs-slide">
|
<div class="swiper-slide hs-slide">
|
||||||
<a class="hs-item" href={href(locale, PATH.item(g.articleId))} data-idx={i}>
|
<a class="hs-item" href={href(locale, ROUTES.item(g.articleId))} data-idx={i}>
|
||||||
<!-- Alpine 管理单张图片的加载/失败状态 -->
|
<!-- Alpine 管理单张图片的加载/失败状态 -->
|
||||||
<div
|
<div
|
||||||
class="hs-img-box"
|
class="hs-img-box"
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import type { RawArticle } from "@/lib/data"
|
import type { RawArticle } from "@/lib/data"
|
||||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||||
import { getI18n } from "@/i18n/utils"
|
import { getI18n } from "@/i18n/utils"
|
||||||
import { href, PATH } from '@/lib/routes'
|
import { href, ROUTES } from '@/lib/routes'
|
||||||
import { ssrBase } from "@/lib/api"
|
import { ssrBase } from "@/lib/api"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -32,8 +32,8 @@ const descParas = (article?.description || "")
|
|||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
const homeHref = href(locale, PATH.home)
|
const homeHref = href(locale, ROUTES.home)
|
||||||
const archiveHref = href(locale, PATH.shows)
|
const archiveHref = href(locale, ROUTES.shows)
|
||||||
|
|
||||||
const crumbs: { label: string; href: string }[] = [
|
const crumbs: { label: string; href: string }[] = [
|
||||||
// {
|
// {
|
||||||
@ -58,11 +58,11 @@ if (article?.brand_id) {
|
|||||||
{
|
{
|
||||||
article ? (
|
article ? (
|
||||||
<article class="mx-auto max-w-[1600px] px-5 md:px-10 py-12 md:py-16">
|
<article class="mx-auto max-w-[1600px] px-5 md:px-10 py-12 md:py-16">
|
||||||
<Breadcrumb items={crumbs}>
|
{/* <Breadcrumb items={crumbs}>
|
||||||
<span slot="current" class="max-w-[60vw] truncate">
|
<span slot="current" class="max-w-[60vw] truncate">
|
||||||
{article.title}
|
{article.title}
|
||||||
</span>
|
</span>
|
||||||
</Breadcrumb>
|
</Breadcrumb> */}
|
||||||
|
|
||||||
<h1 class="font-black leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8">
|
<h1 class="font-black leading-[0.95] tracking-tight text-[10vw] md:text-[4.5rem] mt-8">
|
||||||
{article.title}
|
{article.title}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||||
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
||||||
import { clientHref, PATH } from '@/lib/routes'
|
import { clientHref, ROUTES } from '@/lib/routes'
|
||||||
import { getI18n } from '@/i18n/utils'
|
import { getI18n } from '@/i18n/utils'
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +33,8 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<span slot="current">{t('Runway looks')}</span>
|
<span slot="current">{t('Runway looks')}</span>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<h1 class="showsh1">All Shows</h1>
|
|
||||||
|
<h1 class="showsh1">全部的 runway</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -479,57 +480,91 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
@keyframes hbxProg { 0% { left: -38%; } 100% { left: 100%; } }
|
@keyframes hbxProg { 0% { left: -38%; } 100% { left: 100%; } }
|
||||||
@keyframes hbxShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
@keyframes hbxShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
||||||
/* ===== 品牌筛选弹窗 ===== */
|
/* ===== 品牌筛选弹窗(语言切换器风格:无 mask、顶部小箭头、干净白底列表) ===== */
|
||||||
.showsmodal {
|
.showsmodal {
|
||||||
@apply fixed inset-0 z-[60] flex items-center justify-center p-[16px];
|
@apply fixed inset-0 z-[60] flex items-start justify-center pt-[14vh] pointer-events-none;
|
||||||
}
|
}
|
||||||
.showsmodal-mask {
|
.showsmodal-mask {
|
||||||
@apply absolute inset-0 bg-black/45;
|
@apply absolute inset-0 pointer-events-auto cursor-default;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
.showsmodal-panel {
|
.showsmodal-panel {
|
||||||
@apply relative w-full max-w-[680px] max-h-[86vh] bg-white flex flex-col border border-black;
|
@apply relative w-full max-w-[460px] max-h-[72vh] bg-white flex flex-col border border-black/10 pointer-events-auto;
|
||||||
|
box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
/* 顶部小三角箭头(语言切换器同款:白底 + 浅边框) */
|
||||||
|
.showsmodal-panel::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: 50%;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: white;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateX(-50%) rotate(45deg);
|
||||||
}
|
}
|
||||||
.showsmodal-head {
|
.showsmodal-head {
|
||||||
@apply flex items-center justify-between px-[16px] py-[13px] border-b border-black shrink-0;
|
@apply flex items-center justify-between px-4 pt-4 pb-2 shrink-0;
|
||||||
}
|
}
|
||||||
.showsmodal-head h3 {
|
.showsmodal-head h3 {
|
||||||
@apply m-0 text-[13px] tracking-[0.2em] uppercase font-bold;
|
@apply m-0 text-[12px] tracking-[0.22em] uppercase font-bold;
|
||||||
}
|
}
|
||||||
.showsmodal-x {
|
.showsmodal-x {
|
||||||
@apply appearance-none bg-white border border-black w-[24px] h-[24px] inline-flex items-center justify-center cursor-pointer text-[12px] leading-none;
|
@apply appearance-none bg-transparent border-0 w-6 h-6 inline-flex items-center justify-center cursor-pointer text-[14px] leading-none text-[#999] transition-colors hover:text-black;
|
||||||
|
}
|
||||||
|
.showsmodal-search {
|
||||||
|
@apply px-4 pt-1 pb-3 shrink-0;
|
||||||
}
|
}
|
||||||
.showsmodal-x:hover { @apply bg-black text-white; }
|
|
||||||
.showsmodal-search { @apply px-[16px] pt-[12px] pb-[8px] shrink-0; }
|
|
||||||
.showsmodal-input {
|
.showsmodal-input {
|
||||||
@apply w-full border border-black px-[10px] py-[8px] text-[13px] outline-none;
|
@apply w-full border border-black/15 px-3 py-2 text-[13px] outline-none bg-[#fafafa] focus:bg-white focus:border-black;
|
||||||
}
|
}
|
||||||
.showsmodal-input:focus { @apply ring-1 ring-black; }
|
|
||||||
.showsmodal-az {
|
.showsmodal-az {
|
||||||
@apply flex flex-wrap gap-[4px] px-[16px] pb-[10px] border-b border-[#e6e6e6] shrink-0;
|
@apply flex flex-wrap gap-1 px-4 pb-3 shrink-0;
|
||||||
}
|
}
|
||||||
.showsmodal-az .az {
|
.showsmodal-az .az {
|
||||||
@apply appearance-none bg-white border border-[#ccc] w-[22px] h-[22px] text-[11px] cursor-pointer leading-none;
|
@apply appearance-none bg-white border border-black/10 w-6 h-6 text-[10px] cursor-pointer leading-none font-mono transition-colors hover:bg-black/5;
|
||||||
|
}
|
||||||
|
.showsmodal-az .az.active {
|
||||||
|
@apply bg-black text-white border-black;
|
||||||
|
}
|
||||||
|
.showsmodal-body {
|
||||||
|
@apply flex-1 min-h-0 overflow-y-auto px-2 py-1;
|
||||||
}
|
}
|
||||||
.showsmodal-az .az.active { @apply bg-black text-white border-black; }
|
|
||||||
.showsmodal-body { @apply flex-1 min-h-0 overflow-y-auto px-[16px] py-[8px]; }
|
|
||||||
.showsmodal-hint {
|
.showsmodal-hint {
|
||||||
@apply text-[11px] tracking-[0.2em] uppercase text-[#888] py-[6px] pb-[8px];
|
@apply text-[10px] tracking-[0.22em] uppercase text-[#999] px-3 py-2;
|
||||||
|
}
|
||||||
|
.showsmodal-loading {
|
||||||
|
@apply flex justify-center py-6 text-[#bdbdbd];
|
||||||
|
}
|
||||||
|
.showsmodal-list {
|
||||||
|
@apply list-none m-0 p-0;
|
||||||
}
|
}
|
||||||
.showsmodal-loading { @apply flex justify-center py-[28px] text-[#bdbdbd]; }
|
|
||||||
.showsmodal-list { @apply list-none m-0 p-0; }
|
|
||||||
.showsmodal-row {
|
.showsmodal-row {
|
||||||
@apply flex items-center gap-[8px] w-full cursor-pointer py-[7px] border-b border-[#f0f0f0] text-[13px];
|
@apply flex items-center gap-2 w-full cursor-pointer px-3 py-2 text-[13px] transition-colors hover:bg-black/5;
|
||||||
|
}
|
||||||
|
.showsmodal-name {
|
||||||
|
@apply flex-1 truncate;
|
||||||
|
}
|
||||||
|
.showsmodal-ct {
|
||||||
|
@apply text-[#999] text-[10px] ml-auto;
|
||||||
|
}
|
||||||
|
.showsmodal-empty {
|
||||||
|
@apply py-6 text-center text-[#999] text-[13px];
|
||||||
}
|
}
|
||||||
.showsmodal-name { @apply flex-1 truncate; }
|
|
||||||
.showsmodal-ct { @apply text-[#999] text-[11px] ml-auto; }
|
|
||||||
.showsmodal-empty { @apply py-[28px] text-center text-[#999] text-[13px]; }
|
|
||||||
.showsmodal-foot {
|
.showsmodal-foot {
|
||||||
@apply flex items-center justify-between px-[16px] py-[12px] border-t border-black shrink-0 text-[12px];
|
@apply flex items-center justify-between px-4 py-3 border-t border-black/5 shrink-0 text-[11px] text-[#888];
|
||||||
|
}
|
||||||
|
.showsmodal-counter {
|
||||||
|
@apply tracking-[0.06em];
|
||||||
}
|
}
|
||||||
.showsmodal-ok {
|
.showsmodal-ok {
|
||||||
@apply appearance-none bg-black text-white border border-black px-[22px] py-[8px] cursor-pointer text-[12px] tracking-[0.1em];
|
@apply appearance-none bg-black text-white border border-black px-4 py-1.5 cursor-pointer text-[12px] tracking-[0.1em] transition-colors hover:bg-black/85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -809,7 +844,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
// 辅助辅助工具函数
|
// 辅助辅助工具函数
|
||||||
toAbsUrl(url: string) { return toAbs(url) },
|
toAbsUrl(url: string) { return toAbs(url) },
|
||||||
itemHref(it: { id: string | number }) { return clientHref(this.locale, PATH.runwayLooks(it.id)) },
|
itemHref(it: { id: string | number }) { return clientHref(this.locale, ROUTES.runwayLooks(it.id)) },
|
||||||
ctLabel(ct?: string) {
|
ctLabel(ct?: string) {
|
||||||
const map: Record<string, string> = { rtw: "Ready-to-Wear", menswear: "Menswear", couture: "Couture", resort: "Resort", pre_fall: "Pre-Fall" }
|
const map: Record<string, string> = { rtw: "Ready-to-Wear", menswear: "Menswear", couture: "Couture", resort: "Resort", pre_fall: "Pre-Fall" }
|
||||||
return ct && map[ct] ? map[ct] : (ct || "")
|
return ct && map[ct] ? map[ct] : (ct || "")
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { getReports, fmtDate } from "@/lib/data"
|
import { getReports, fmtDate } from "@/lib/data"
|
||||||
import { href, PATH } from '@/lib/routes'
|
import { href, ROUTES } from '@/lib/routes'
|
||||||
import { getI18n } from "@/i18n/utils"
|
import { getI18n } from "@/i18n/utils"
|
||||||
|
|
||||||
// 文章展示创意布局页:复用你现有的 Layout 外壳。
|
// 文章展示创意布局页:复用你现有的 Layout 外壳。
|
||||||
@ -25,7 +25,7 @@ const itemBase = href(locale, "/items")
|
|||||||
<p>
|
<p>
|
||||||
走秀趋势报告档案库。支持网格 / 杂志 / 时间轴三种创意视图自由切换,彩色配图由后端接口动态渲染。
|
走秀趋势报告档案库。支持网格 / 杂志 / 时间轴三种创意视图自由切换,彩色配图由后端接口动态渲染。
|
||||||
</p>
|
</p>
|
||||||
<a href={href(locale, PATH.home)} class="archive-back">← Index</a>
|
<a href={href(locale, ROUTES.home)} class="archive-back">← Index</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ const itemBase = href(locale, "/items")
|
|||||||
style={`--ratio:${s.ratio}`}
|
style={`--ratio:${s.ratio}`}
|
||||||
>
|
>
|
||||||
<div class="card-datecol"><span class="card-date">{fmt(s.created_at)}</span></div>
|
<div class="card-datecol"><span class="card-date">{fmt(s.created_at)}</span></div>
|
||||||
<a class="card-media" href={href(locale, PATH.item(s.id))} aria-label={s.title}>
|
<a class="card-media" href={href(locale, ROUTES.item(s.id))} aria-label={s.title}>
|
||||||
<img src={s.cover} alt={s.title} loading="lazy" />
|
<img src={s.cover} alt={s.title} loading="lazy" />
|
||||||
{s.featured && <span class="card-flag">Featured</span>}
|
{s.featured && <span class="card-flag">Featured</span>}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { getSsgBrands, type SsgBrand } from "@/lib/api"
|
import { getSsgBrands, type SsgBrand } from "@/lib/api"
|
||||||
import { getI18n } from "@/i18n/utils"
|
import { getI18n } from "@/i18n/utils"
|
||||||
import { href, PATH } from "@/lib/routes"
|
import { href, ROUTES } from "@/lib/routes"
|
||||||
|
|
||||||
type Brand = SsgBrand
|
type Brand = SsgBrand
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ const total = orderedColumns.reduce((s, c) => s + c.items.length, 0)
|
|||||||
<ul class="brow-items">
|
<ul class="brow-items">
|
||||||
{col.items.map((b) => (
|
{col.items.map((b) => (
|
||||||
<li>
|
<li>
|
||||||
<a href={href(locale, PATH.articlesByBrand(b.id))}>{primaryName(b)}</a>
|
<a href={href(locale, ROUTES.articlesByBrand(b.id))}>{primaryName(b)}</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -9,10 +9,16 @@ export const LOCALES = ['en', 'cn'] as const;
|
|||||||
export type Locale = (typeof LOCALES)[number];
|
export type Locale = (typeof LOCALES)[number];
|
||||||
|
|
||||||
/** 语言显示名(用于语言切换器等 UI)。 */
|
/** 语言显示名(用于语言切换器等 UI)。 */
|
||||||
export const LOCALE_NAMES: Record<Locale, string> = {
|
export const LOCALE_NAMES: Record<string, { name: string; nativeName: string }> = {
|
||||||
en: 'English',
|
cn: {
|
||||||
cn: '中文',
|
name: '中文',
|
||||||
};
|
nativeName: '中文',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
name: 'English',
|
||||||
|
nativeName: 'English',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
/** 浏览器 Accept-Language / navigator.language → 本项目支持的语言。 */
|
/** 浏览器 Accept-Language / navigator.language → 本项目支持的语言。 */
|
||||||
export const BROWSER_LOCALE_MAP: Record<string, Locale> = {
|
export const BROWSER_LOCALE_MAP: Record<string, Locale> = {
|
||||||
|
|||||||
@ -20,7 +20,7 @@ type TranslationEntry = Partial<Record<OtherLocale, string>>;
|
|||||||
export const dict: Record<string, TranslationEntry> = {
|
export const dict: Record<string, TranslationEntry> = {
|
||||||
// ── 导航 / Navigation ──
|
// ── 导航 / Navigation ──
|
||||||
'About': { cn: '关于' },
|
'About': { cn: '关于' },
|
||||||
'All Shows': { cn: '全部走秀' },
|
'全部的 runway': { cn: '全部的 runway' },
|
||||||
'Contact': { cn: '联系' },
|
'Contact': { cn: '联系' },
|
||||||
'Contact Us': { cn: '联系我们' },
|
'Contact Us': { cn: '联系我们' },
|
||||||
'Home': { cn: '首页' },
|
'Home': { cn: '首页' },
|
||||||
@ -47,6 +47,23 @@ export const dict: Record<string, TranslationEntry> = {
|
|||||||
// ── 页脚 / Footer ──
|
// ── 页脚 / Footer ──
|
||||||
'© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.': { cn: '© 2026 PORTFOLIO STUDIO. 保留所有权利。' },
|
'© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.': { cn: '© 2026 PORTFOLIO STUDIO. 保留所有权利。' },
|
||||||
|
|
||||||
|
// ── 页脚工具 / Footer tools ──
|
||||||
|
'Color Studio Tools': { cn: '色彩工作室工具' },
|
||||||
|
'AI Color Trend Generator': { cn: 'AI 色彩趋势生成器' },
|
||||||
|
'Photo Swatch Extractor': { cn: '图片色板提取器' },
|
||||||
|
'Upload': { cn: '上传' },
|
||||||
|
'Reset B&W Theme': { cn: '重置黑白主题' },
|
||||||
|
'Default': { cn: '默认' },
|
||||||
|
|
||||||
|
// ── 页脚简介 / Manifesto ──
|
||||||
|
'Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.':
|
||||||
|
{ cn: '极简编辑风格作品集,记录高级定制秀场、后台试装与街头文化。集成 2026 色彩预测情报与 AI 调色板生成器。' },
|
||||||
|
|
||||||
|
// ── 登录态 / Auth ──
|
||||||
|
'Hi': { cn: '你好' },
|
||||||
|
'Logout': { cn: '退出' },
|
||||||
|
'Language': { cn: '语言' },
|
||||||
|
|
||||||
'Runway looks': {cn: '秀场造型'}
|
'Runway looks': {cn: '秀场造型'}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -18,8 +18,8 @@ export function getLocaleFromUrl(url: URL | string): Locale {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 取翻译函数 t():key 即英文原文,按当前语言自动匹配翻译。
|
* 取翻译函数 t():key 即英文原文,按当前语言自动匹配翻译。
|
||||||
* - 英文环境:t('All Shows') 直接返回 'All Shows'(key 本身)
|
* - 英文环境:t('全部的 runway') 直接返回 '全部的 runway'(key 本身)
|
||||||
* - 中文环境:t('All Shows') 返回字典里的 '全部走秀'(取不到则回退英文)
|
* - 中文环境:t('全部的 runway') 返回字典里的 '全部的 runway'(取不到则回退英文)
|
||||||
* - 支持 {name} 形式的插值参数。
|
* - 支持 {name} 形式的插值参数。
|
||||||
*/
|
*/
|
||||||
export function useTranslations(locale: Locale) {
|
export function useTranslations(locale: Locale) {
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
---
|
---
|
||||||
import '@/styles/global.css'
|
import '@/styles/global.css'
|
||||||
import { href, PATH } from '@/lib/routes'
|
import { href, ROUTES } from '@/lib/routes'
|
||||||
import { getI18n } from '@/i18n/utils'
|
import { getI18n } from '@/i18n/utils'
|
||||||
import { LOCALES, LOCALE_NAMES } from '@/i18n/config'
|
import { LOCALES, LOCALE_NAMES } from '@/i18n/config'
|
||||||
|
import { i18n } from 'astro:config/client';
|
||||||
|
import { toCodes } from 'astro:i18n';
|
||||||
|
|
||||||
import LanguageDetector from '@/components/LanguageDetector.astro'
|
import LanguageDetector from '@/components/LanguageDetector.astro'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -12,7 +15,11 @@ const {
|
|||||||
activeThemePalette = null,
|
activeThemePalette = null,
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const { locale, t } = getI18n(Astro)
|
const { locale, t, getRelativeLocaleUrl } = getI18n(Astro)
|
||||||
|
|
||||||
|
// 脚本内(打包型 <script>)无法用 {t()} 插值,需经 define:vars 把已翻译字符串传入
|
||||||
|
const hiLabel = t('Hi')
|
||||||
|
const logoutLabel = t('Logout')
|
||||||
|
|
||||||
// 当前路径(去掉语言前缀),供语言切换器「换前缀、留路径」用,例如 /cn/women → /women
|
// 当前路径(去掉语言前缀),供语言切换器「换前缀、留路径」用,例如 /cn/women → /women
|
||||||
const pathSegments = Astro.url.pathname.split('/');
|
const pathSegments = Astro.url.pathname.split('/');
|
||||||
@ -20,8 +27,10 @@ const firstSegment = pathSegments[1] || '';
|
|||||||
const currentPath = (LOCALES as readonly string[]).includes(firstSegment)
|
const currentPath = (LOCALES as readonly string[]).includes(firstSegment)
|
||||||
? '/' + pathSegments.slice(2).filter(Boolean).join('/') || '/'
|
? '/' + pathSegments.slice(2).filter(Boolean).join('/') || '/'
|
||||||
: Astro.url.pathname;
|
: Astro.url.pathname;
|
||||||
const queryString = Astro.url.search;
|
|
||||||
|
|
||||||
|
const pathName = Astro.url.pathname;
|
||||||
|
const queryString = Astro.url.search;
|
||||||
|
const locales = toCodes(i18n!.locales);
|
||||||
---
|
---
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={locale === 'cn' ? 'zh-CN' : 'en'}>
|
<html lang={locale === 'cn' ? 'zh-CN' : 'en'}>
|
||||||
@ -39,13 +48,26 @@ const queryString = Astro.url.search;
|
|||||||
<!-- 顶部主导航栏 -->
|
<!-- 顶部主导航栏 -->
|
||||||
<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, PATH.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">{t('Portfolio')}<span class="text-[10px] bg-black text-white px-1.5 py-0.5 rounded-full font-mono tracking-tight leading-none">
|
||||||
2026
|
2026
|
||||||
</span></a>
|
</span></a>
|
||||||
<a href={href(locale, PATH.shows)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('All Shows')}</a>
|
<!-- 全部的 runway:header 下拉(假数据预览) -->
|
||||||
<a href={href(locale, PATH.latestProjects)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Latest Projects')}</a>
|
<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">
|
||||||
<a href={href(locale, PATH.about)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('About')}</a>
|
<div class="flex items-center gap-1">
|
||||||
<a href={href(locale, PATH.contact)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Contact Us')}</a>
|
<a href={href(locale, ROUTES.shows)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('全部的 runway')}</a>
|
||||||
|
<button type="button" :aria-expanded="open" class="pb-[2px] hover:opacity-60 transition-opacity duration-200 flex items-center" aria-label="展开 runway 列表">
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<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">
|
||||||
|
<template x-for="item in menu" :key="item">
|
||||||
|
<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>
|
||||||
|
<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.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">
|
||||||
@ -63,7 +85,7 @@ const queryString = Astro.url.search;
|
|||||||
type="button"
|
type="button"
|
||||||
@click="open = !open"
|
@click="open = !open"
|
||||||
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="Language"
|
aria-label={t('Language')}
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
:aria-expanded="open"
|
:aria-expanded="open"
|
||||||
>
|
>
|
||||||
@ -85,23 +107,23 @@ const queryString = Astro.url.search;
|
|||||||
x-transition:leave-end="opacity-0 -translate-y-1"
|
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) => (
|
{locales.map((l) => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={href(l, currentPath) + queryString}
|
href={getRelativeLocaleUrl(l, pathName)}
|
||||||
@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 === locale ? 'font-bold' : 'opacity-60',
|
||||||
]}
|
]}
|
||||||
>{LOCALE_NAMES[l]}</a>
|
>{LOCALE_NAMES[l].name}</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2" id="auth-slot">
|
<div class="flex items-center gap-2" id="auth-slot">
|
||||||
<a href={href(locale, PATH.login)} class="hover:opacity-60 transition-opacity duration-200">{t('Login')}</a>
|
<a href={href(locale, ROUTES.login)} class="hover:opacity-60 transition-opacity duration-200">{t('Login')}</a>
|
||||||
<a href={href(locale, PATH.register)} class="hover:opacity-60 transition-opacity duration-200">{t('Signup')}</a>
|
<a href={href(locale, ROUTES.register)} class="hover:opacity-60 transition-opacity duration-200">{t('Signup')}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -111,12 +133,12 @@ const queryString = Astro.url.search;
|
|||||||
</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>
|
<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">Hi, ${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]">Logout</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() })
|
||||||
}
|
}
|
||||||
@ -139,7 +161,7 @@ const queryString = Astro.url.search;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-xs text-zinc-600 leading-relaxed max-w-md">
|
<p class="text-xs text-zinc-600 leading-relaxed max-w-md">
|
||||||
Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.
|
{t('Minimalist editorial portfolio documenting haute couture shows, backstage fittings, and street culture. Integrated with 2026 color forecast intelligence and AI palette generator.')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Location Badge */}
|
{/* Location Badge */}
|
||||||
@ -157,7 +179,7 @@ const queryString = Astro.url.search;
|
|||||||
{/* Column 3: Color Studio & Actions */}
|
{/* Column 3: Color Studio & Actions */}
|
||||||
<div class="md:col-span-4 space-y-3">
|
<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">
|
<span class="text-[11px] font-mono uppercase tracking-widest text-zinc-400 font-bold block mb-2">
|
||||||
Color Studio Tools
|
{t('Color Studio Tools')}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
@ -167,7 +189,7 @@ const queryString = Astro.url.search;
|
|||||||
>
|
>
|
||||||
<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" /> -->
|
<!-- <Sparkles class="w-3.5 h-3.5 text-amber-300" /> -->
|
||||||
<span>AI Color Trend Generator</span>
|
<span>{t('AI Color Trend Generator')}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[10px] text-zinc-400 font-normal">Gemini 2026</span>
|
<span class="text-[10px] text-zinc-400 font-normal">Gemini 2026</span>
|
||||||
</button>
|
</button>
|
||||||
@ -178,9 +200,9 @@ const queryString = Astro.url.search;
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- <Layers class="w-3.5 h-3.5 text-zinc-600" /> -->
|
<!-- <Layers class="w-3.5 h-3.5 text-zinc-600" /> -->
|
||||||
<span>Photo Swatch Extractor</span>
|
<span>{t('Photo Swatch Extractor')}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[10px] text-zinc-500 font-normal">Upload</span>
|
<span class="text-[10px] text-zinc-500 font-normal">{t('Upload')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{activeThemePalette && (
|
{activeThemePalette && (
|
||||||
@ -190,7 +212,7 @@ const queryString = Astro.url.search;
|
|||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{/* <RefreshCw class="w-3.5 h-3.5" /> */}
|
{/* <RefreshCw class="w-3.5 h-3.5" /> */}
|
||||||
<span>Reset B&W Theme</span>
|
<span>{t('Reset B&W Theme')}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-[10px] opacity-80">Default</span>
|
<span class="text-[10px] opacity-80">Default</span>
|
||||||
</button>
|
</button>
|
||||||
@ -203,18 +225,18 @@ const queryString = Astro.url.search;
|
|||||||
{/* Bottom Legal & Back To Top */}
|
{/* Bottom Legal & Back To Top */}
|
||||||
<div class="flex flex-col sm:flex-row justify-between items-center text-[12px] font-mono text-zinc-500 gap-4">
|
<div class="flex flex-col sm:flex-row justify-between items-center text-[12px] font-mono text-zinc-500 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<span>© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.</span>
|
<span>{t('© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-6">
|
||||||
<a
|
<a
|
||||||
href={href(locale, PATH.about)}
|
href={href(locale, ROUTES.about)}
|
||||||
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
||||||
>
|
>
|
||||||
{t('About')}
|
{t('About')}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={href(locale, PATH.contact)}
|
href={href(locale, ROUTES.contact)}
|
||||||
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
class="nav-btn hover:text-black cursor-pointer uppercase font-bold"
|
||||||
>
|
>
|
||||||
{t('Contact')}
|
{t('Contact')}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
// 全站 URL 路径集中管理
|
// 全站 URL 路由集中管理
|
||||||
// - PATH:逻辑路径(不带 locale 前缀)。静态用字符串,动态用工厂函数。
|
// - ROUTES:逻辑路由(不带 locale 前缀)。静态用字符串,动态用工厂函数。
|
||||||
// - href:服务端(.astro frontmatter)使用,跟随 astro.config 的 prefixDefaultLocale 规则拼前缀。
|
// - href:服务端(.astro frontmatter)使用,跟随 astro.config 的 prefixDefaultLocale 规则拼前缀。
|
||||||
// - clientHref:客户端脚本(Alpine/Vue)使用,astro:i18n 在浏览器不可用,故自行拼接。
|
// - clientHref:客户端脚本(Alpine/Vue)使用,astro:i18n 在浏览器不可用,故自行拼接。
|
||||||
|
|
||||||
import { getRelativeLocaleUrl } from 'astro:i18n';
|
import { getRelativeLocaleUrl } from 'astro:i18n';
|
||||||
|
|
||||||
/** 逻辑路径片段(不含 locale 前缀)。 */
|
/** 逻辑路由片段(不含 locale 前缀)。 */
|
||||||
export const PATH = {
|
export const ROUTES = {
|
||||||
home: '/',
|
home: '/',
|
||||||
shows: '/shows',
|
shows: '/shows',
|
||||||
brands: '/brands',
|
brands: '/brands',
|
||||||
@ -16,7 +16,7 @@ export const PATH = {
|
|||||||
contact: '/contact',
|
contact: '/contact',
|
||||||
login: '/login',
|
login: '/login',
|
||||||
register: '/register',
|
register: '/register',
|
||||||
// 动态路径(工厂函数)
|
// 动态路由(工厂函数)
|
||||||
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}`,
|
||||||
@ -24,8 +24,8 @@ export const PATH = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** 服务端:生成带 locale 前缀的完整 URL(依赖 astro:i18n,遵循 astro.config)。 */
|
/** 服务端:生成带 locale 前缀的完整 URL(依赖 astro:i18n,遵循 astro.config)。 */
|
||||||
export function href(locale: string, path: string): string {
|
export function href(locale: string, route: string): string {
|
||||||
return getRelativeLocaleUrl(locale, path);
|
return getRelativeLocaleUrl(locale, route);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,7 +33,7 @@ export function href(locale: string, path: string): string {
|
|||||||
* 跟随 astro.config.mjs 的 prefixDefaultLocale: true(en 也带 /en 前缀)。
|
* 跟随 astro.config.mjs 的 prefixDefaultLocale: true(en 也带 /en 前缀)。
|
||||||
* 浏览器内无法使用 astro:i18n,故此处自行拼接,规则须与 astro.config 保持一致。
|
* 浏览器内无法使用 astro:i18n,故此处自行拼接,规则须与 astro.config 保持一致。
|
||||||
*/
|
*/
|
||||||
export function clientHref(locale: string, path: string): string {
|
export function clientHref(locale: string, route: string): string {
|
||||||
const base = locale === 'en' ? '/en' : `/${locale}`;
|
const base = locale === 'en' ? '/en' : `/${locale}`;
|
||||||
return path === '/' ? base : `${base}${path}`;
|
return route === '/' ? base : `${base}${route}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import Layout from "@/layouts/Layout.astro"
|
|||||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||||
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
||||||
import { getI18n } from '@/i18n/utils'
|
import { getI18n } from '@/i18n/utils'
|
||||||
import { clientHref, PATH, href } from '@/lib/routes'
|
import { clientHref, ROUTES, href } from '@/lib/routes'
|
||||||
|
|
||||||
const { locale } = getI18n(Astro)
|
const { locale } = getI18n(Astro)
|
||||||
let sidebarBrands: BrandEntry[] = []
|
let sidebarBrands: BrandEntry[] = []
|
||||||
@ -23,20 +23,21 @@ try {
|
|||||||
if (!yearChips.length) yearChips = yearSeed
|
if (!yearChips.length) yearChips = yearSeed
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="All Shows — Portfolio Studio">
|
<Layout title="全部的 runway — Portfolio Studio">
|
||||||
<div class="showsroot w-full" x-data="showsPage()" data-locale={locale}>
|
<div class="showsroot w-full" x-data="showsPage()" data-locale={locale}>
|
||||||
<!-- 顶部:极简眉头 -->
|
<!-- 顶部:极简眉头 -->
|
||||||
<header class="showshead">
|
<header class="showshead">
|
||||||
<div>
|
<div>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
items={[
|
items={[
|
||||||
{ label: 'Home', href: href(locale, PATH.home) },
|
{ label: 'Home', href: href(locale, ROUTES.home) },
|
||||||
{ label: 'Runway Archive', href: href(locale, PATH.shows) },
|
{ label: 'Runway Archive', href: href(locale, ROUTES.shows) },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<span slot="current" x-text="crumbYear">2026 Edition</span>
|
<span slot="current" x-text="crumbYear">2026 Edition</span>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<h1 class="showsh1">All Shows</h1>
|
|
||||||
|
<h1 class="showsh1">全部的 runway</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -492,57 +493,90 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
@keyframes hbxProg { 0% { left: -38%; } 100% { left: 100%; } }
|
@keyframes hbxProg { 0% { left: -38%; } 100% { left: 100%; } }
|
||||||
@keyframes hbxShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
@keyframes hbxShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
||||||
/* ===== 品牌筛选弹窗 ===== */
|
/* ===== 品牌筛选弹窗(语言切换器风格:无 mask、顶部小箭头、干净白底列表) ===== */
|
||||||
.showsmodal {
|
.showsmodal {
|
||||||
@apply fixed inset-0 z-[60] flex items-center justify-center p-[16px];
|
@apply fixed inset-0 z-[60] flex items-start justify-center pt-[14vh] pointer-events-none;
|
||||||
}
|
}
|
||||||
.showsmodal-mask {
|
.showsmodal-mask {
|
||||||
@apply absolute inset-0 bg-black/45;
|
@apply absolute inset-0 pointer-events-auto cursor-default;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
.showsmodal-panel {
|
.showsmodal-panel {
|
||||||
@apply relative w-full max-w-[680px] max-h-[86vh] bg-white flex flex-col border border-black;
|
@apply relative w-full max-w-[460px] max-h-[72vh] bg-white flex flex-col border border-black/10 pointer-events-auto;
|
||||||
|
box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
/* 顶部小三角箭头(语言切换器同款:白底 + 浅边框) */
|
||||||
|
.showsmodal-panel::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: 50%;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: white;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateX(-50%) rotate(45deg);
|
||||||
}
|
}
|
||||||
.showsmodal-head {
|
.showsmodal-head {
|
||||||
@apply flex items-center justify-between px-[16px] py-[13px] border-b border-black shrink-0;
|
@apply flex items-center justify-between px-4 pt-4 pb-2 shrink-0;
|
||||||
}
|
}
|
||||||
.showsmodal-head h3 {
|
.showsmodal-head h3 {
|
||||||
@apply m-0 text-[13px] tracking-[0.2em] uppercase font-bold;
|
@apply m-0 text-[12px] tracking-[0.22em] uppercase font-bold;
|
||||||
}
|
}
|
||||||
.showsmodal-x {
|
.showsmodal-x {
|
||||||
@apply appearance-none bg-white border border-black w-[24px] h-[24px] inline-flex items-center justify-center cursor-pointer text-[12px] leading-none;
|
@apply appearance-none bg-transparent border-0 w-6 h-6 inline-flex items-center justify-center cursor-pointer text-[14px] leading-none text-[#999] transition-colors hover:text-black;
|
||||||
|
}
|
||||||
|
.showsmodal-search {
|
||||||
|
@apply px-4 pt-1 pb-3 shrink-0;
|
||||||
}
|
}
|
||||||
.showsmodal-x:hover { @apply bg-black text-white; }
|
|
||||||
.showsmodal-search { @apply px-[16px] pt-[12px] pb-[8px] shrink-0; }
|
|
||||||
.showsmodal-input {
|
.showsmodal-input {
|
||||||
@apply w-full border border-black px-[10px] py-[8px] text-[13px] outline-none;
|
@apply w-full border border-black/15 px-3 py-2 text-[13px] outline-none bg-[#fafafa] focus:bg-white focus:border-black;
|
||||||
}
|
}
|
||||||
.showsmodal-input:focus { @apply ring-1 ring-black; }
|
|
||||||
.showsmodal-az {
|
.showsmodal-az {
|
||||||
@apply flex flex-wrap gap-[4px] px-[16px] pb-[10px] border-b border-[#e6e6e6] shrink-0;
|
@apply flex flex-wrap gap-1 px-4 pb-3 shrink-0;
|
||||||
}
|
}
|
||||||
.showsmodal-az .az {
|
.showsmodal-az .az {
|
||||||
@apply appearance-none bg-white border border-[#ccc] w-[22px] h-[22px] text-[11px] cursor-pointer leading-none;
|
@apply appearance-none bg-white border border-black/10 w-6 h-6 text-[10px] cursor-pointer leading-none font-mono transition-colors hover:bg-black/5;
|
||||||
|
}
|
||||||
|
.showsmodal-az .az.active {
|
||||||
|
@apply bg-black text-white border-black;
|
||||||
|
}
|
||||||
|
.showsmodal-body {
|
||||||
|
@apply flex-1 min-h-0 overflow-y-auto px-2 py-1;
|
||||||
}
|
}
|
||||||
.showsmodal-az .az.active { @apply bg-black text-white border-black; }
|
|
||||||
.showsmodal-body { @apply flex-1 min-h-0 overflow-y-auto px-[16px] py-[8px]; }
|
|
||||||
.showsmodal-hint {
|
.showsmodal-hint {
|
||||||
@apply text-[11px] tracking-[0.2em] uppercase text-[#888] py-[6px] pb-[8px];
|
@apply text-[10px] tracking-[0.22em] uppercase text-[#999] px-3 py-2;
|
||||||
|
}
|
||||||
|
.showsmodal-loading {
|
||||||
|
@apply flex justify-center py-6 text-[#bdbdbd];
|
||||||
|
}
|
||||||
|
.showsmodal-list {
|
||||||
|
@apply list-none m-0 p-0;
|
||||||
}
|
}
|
||||||
.showsmodal-loading { @apply flex justify-center py-[28px] text-[#bdbdbd]; }
|
|
||||||
.showsmodal-list { @apply list-none m-0 p-0; }
|
|
||||||
.showsmodal-row {
|
.showsmodal-row {
|
||||||
@apply flex items-center gap-[8px] w-full cursor-pointer py-[7px] border-b border-[#f0f0f0] text-[13px];
|
@apply flex items-center gap-2 w-full cursor-pointer px-3 py-2 text-[13px] transition-colors hover:bg-black/5;
|
||||||
|
}
|
||||||
|
.showsmodal-name {
|
||||||
|
@apply flex-1 truncate;
|
||||||
|
}
|
||||||
|
.showsmodal-ct {
|
||||||
|
@apply text-[#999] text-[10px] ml-auto;
|
||||||
|
}
|
||||||
|
.showsmodal-empty {
|
||||||
|
@apply py-6 text-center text-[#999] text-[13px];
|
||||||
}
|
}
|
||||||
.showsmodal-name { @apply flex-1 truncate; }
|
|
||||||
.showsmodal-ct { @apply text-[#999] text-[11px] ml-auto; }
|
|
||||||
.showsmodal-empty { @apply py-[28px] text-center text-[#999] text-[13px]; }
|
|
||||||
.showsmodal-foot {
|
.showsmodal-foot {
|
||||||
@apply flex items-center justify-between px-[16px] py-[12px] border-t border-black shrink-0 text-[12px];
|
@apply flex items-center justify-between px-4 py-3 border-t border-black/5 shrink-0 text-[11px] text-[#888];
|
||||||
|
}
|
||||||
|
.showsmodal-counter {
|
||||||
|
@apply tracking-[0.06em];
|
||||||
}
|
}
|
||||||
.showsmodal-ok {
|
.showsmodal-ok {
|
||||||
@apply appearance-none bg-black text-white border border-black px-[22px] py-[8px] cursor-pointer text-[12px] tracking-[0.1em];
|
@apply appearance-none bg-black text-white border border-black px-4 py-1.5 cursor-pointer text-[12px] tracking-[0.1em] transition-colors hover:bg-black/85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -831,7 +865,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
// 辅助辅助工具函数
|
// 辅助辅助工具函数
|
||||||
toAbsUrl(url: string) { return toAbs(url) },
|
toAbsUrl(url: string) { return toAbs(url) },
|
||||||
articleHref(it: { id: string | number }) { return clientHref(this.locale, PATH.article(it.id)) },
|
articleHref(it: { id: string | number }) { return clientHref(this.locale, ROUTES.article(it.id)) },
|
||||||
isNewAdded(pubAt?: number) { return Date.now() / 1000 - Number(pubAt || 0) < 60 * 60 * 24 * 14 },
|
isNewAdded(pubAt?: number) { return Date.now() / 1000 - Number(pubAt || 0) < 60 * 60 * 24 * 14 },
|
||||||
fmtDate(ts: number) {
|
fmtDate(ts: number) {
|
||||||
if (!ts) return ""
|
if (!ts) return ""
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import Layout from "@/layouts/Layout.astro"
|
|||||||
import Breadcrumb from "@/components/Breadcrumb.astro"
|
import Breadcrumb from "@/components/Breadcrumb.astro"
|
||||||
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
import { getBrands, getYears, type BrandEntry } from "@/lib/api"
|
||||||
import { getI18n } from '@/i18n/utils'
|
import { getI18n } from '@/i18n/utils'
|
||||||
import { clientHref, PATH, href } from '@/lib/routes'
|
import { clientHref, ROUTES, href } from '@/lib/routes'
|
||||||
|
|
||||||
const { locale } = getI18n(Astro)
|
const { locale } = getI18n(Astro)
|
||||||
let sidebarBrands: BrandEntry[] = []
|
let sidebarBrands: BrandEntry[] = []
|
||||||
@ -23,7 +23,7 @@ try {
|
|||||||
if (!yearChips.length) yearChips = yearSeed
|
if (!yearChips.length) yearChips = yearSeed
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="All Shows — Portfolio Studio">
|
<Layout title="全部的 runway — Portfolio Studio">
|
||||||
<!-- 首屏绘制前同步侧边栏折叠状态,杜绝刷新时的闪烁 -->
|
<!-- 首屏绘制前同步侧边栏折叠状态,杜绝刷新时的闪烁 -->
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
try {
|
try {
|
||||||
@ -39,13 +39,14 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
<div>
|
<div>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
items={[
|
items={[
|
||||||
{ label: 'Home', href: href(locale, PATH.home) },
|
{ label: 'Home', href: href(locale, ROUTES.home) },
|
||||||
{ label: 'Runway Archive', href: href(locale, PATH.shows) },
|
{ label: 'Runway Archive', href: href(locale, ROUTES.shows) },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<span slot="current" x-text="crumbYear">2026 Edition</span>
|
<span slot="current" x-text="crumbYear">2026 Edition</span>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<h1 class="showsh1">All Shows</h1>
|
|
||||||
|
<h1 class="showsh1">全部的 runway</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -472,6 +473,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
@keyframes hbxProg { 0% { left: -38%; } 100% { left: 100%; } }
|
@keyframes hbxProg { 0% { left: -38%; } 100% { left: 100%; } }
|
||||||
@keyframes hbxShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
@keyframes hbxShim { 0%, 100% { opacity: .85; } 50% { opacity: .55; } }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -689,7 +691,7 @@ if (!yearChips.length) yearChips = yearSeed
|
|||||||
|
|
||||||
// 辅助辅助工具函数
|
// 辅助辅助工具函数
|
||||||
toAbsUrl(url: string) { return toAbs(url) },
|
toAbsUrl(url: string) { return toAbs(url) },
|
||||||
articleHref(it: { id: string | number }) { return clientHref(this.locale, PATH.article(it.id)) },
|
articleHref(it: { id: string | number }) { return clientHref(this.locale, ROUTES.article(it.id)) },
|
||||||
isNewAdded(pubAt?: number) { return Date.now() / 1000 - Number(pubAt || 0) < 60 * 60 * 24 * 14 },
|
isNewAdded(pubAt?: number) { return Date.now() / 1000 - Number(pubAt || 0) < 60 * 60 * 24 * 14 },
|
||||||
fmtDate(ts: number) {
|
fmtDate(ts: number) {
|
||||||
if (!ts) return ""
|
if (!ts) return ""
|
||||||
|
|||||||
Reference in New Issue
Block a user