diff --git a/.workbuddy/memory/2026-09-15.md b/.workbuddy/memory/2026-09-15.md new file mode 100644 index 0000000..4234a55 --- /dev/null +++ b/.workbuddy/memory/2026-09-15.md @@ -0,0 +1,33 @@ +# 2026-09-15 工作日志 + +## 记住语言偏好功能 ✅(11:43–12:00) +用户要求:选过语言后,下次进首页直接用上次的语言。 +- 关键前提:本项目 `prefixDefaultLocale: true`,/en、/cn 都带前缀,根 `/` 由服务端 302 到 `/en`。故「进首页」实际落在 /en,在此做自动跳转。 +- `src/lib/i18n/index.ts` 新增 `STORED_LOCALE_KEY='preferred_locale'`、`storeLocale(locale)`、`getStoredLocale()`(localStorage 读写,单一来源)。 +- `src/layouts/Layout.astro`: + - `` 顶部加 `is:inline` + `define:vars` 脚本,进首页(path==='/' 或 '/'+defaultLocale)且记住了非默认语言时 `location.replace('/'+stored)`。脚本在 head 同步执行,先于首屏绘制,基本无闪白。 + - 语言切换器 `x-data="{ open:false }"` → `langSwitch()`;`` `@click` 改为 `open=false; pick('')`。 + - footer 脚本 import `storeLocale`,注册 `langSwitch` Alpine 组件(含 `pick(code){ storeLocale(code) }`)。 +- **设计铁律**:只在「显式点击切换器」时写 localStorage,绝不按页面加载的 URL 写——否则与首页自动跳转冲突(点了英文又被 URL 上的 en 覆盖,首页永远跳不回去)。 +- 验证:`npm run build` Complete;`astro check` 0/0/0;产物 HTML 确认内联脚本注入 `const defaultLocale="en"; const storedKey="preferred_locale";` 且两个 `pick('cn')`/`pick('en')` 锚点存在。 +- 顺带整理了被截断的超长 MEMORY.md(重写精简版,保留架构/约定/脚本铁律/类型坑/lib分层/验证手段/门禁/账号/i18n/记住语言)。 + +## 语言建议弹窗(未选过语言时按浏览器语言提示)✅(12:11–12:30) +用户追加需求:没选过语言时,按浏览器语言自动判断,并弹小窗提示是否切换。 +- `Layout.astro` footer ` @@ -94,7 +107,7 @@ const headers = { {t('Home')} {t('Portfolio')} - + 2026 @@ -138,7 +151,7 @@ const headers = {
-
+
@@ -255,7 +327,7 @@ const headers = {

{/* Location Badge */} -
+
Paris • Milan @@ -267,13 +339,40 @@ const headers = {
-
+
{t('© 2026 PORTFOLIO STUDIO. ALL RIGHTS RESERVED.')}
{/* 全局登录门禁层(login-required 属性 + 统一登录浮层) */} + + {/* 语言建议弹窗:未选过语言且浏览器语言与当前首页不符时,提示是否切换到该语言 */} + diff --git a/src/lib/api.ts b/src/lib/api.ts index b6fcbe1..f7e1c5a 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -67,15 +67,13 @@ export interface RawArticleImage { image: string // 该接口场景的主用地址(详情=展示样式 !style:high;列表=缩略图 !style:thumb) thumb?: string // 缩略图地址(!style:thumb),仅详情接口返回:供大图灯箱左栏缩略图条 / 右下角细节缩略图等小尺寸场景用,省带宽 name?: string - is_detail?: number // 0=主图 1=细节图(详情接口 images 仅含主图;细节在 details 分组) - look_index?: number // 细节图归属的主图序号(与 images 主图的 look_index 一致) + // 主图 / 细节图不再靠 is_detail 区分:详情接口 images 只含主图,细节图嵌套在所属主图的 detail 下, + // 层级本身已表达类型(前端无任何处读取 is_detail,故不再声明)。 + look_index?: number // 该图归属的主图序号;细节图与所属主图同值 favorited?: boolean // 已登录时该图片是否已收藏(随详情接口返回) -} - -// 某个主图(look)对应的细节图集合(与 RunwayLookDetails 对齐),详情页「细节入口」按需展开。 -export interface RawLookDetails { - look_index: number // 主图序号 - images: RawArticleImage[] // 该主图的细节图列表 + // 仅详情接口填充:该主图(look)下的细节图子集,元素形状与本类型递归同构。 + // 前端「细节入口」直接读它,无需再按 look_index 分组查表。 + detail?: RawArticleImage[] } // 后端文章原始结构(与 PublicArticle / PublicArticleDetail 对齐)。 @@ -87,10 +85,8 @@ export type RawArticle = { brand_name: string brand_id?: string | number // hashid 编码串 image_count?: number // 列表卡片 "N+" 角标用(仅列表接口返回);改造后仅计主图数 - // 详情接口 images 仅含主图(look 图),默认展示;细节图在 details 分组,需经「细节入口」展开。 + // 详情接口 images 仅含主图(look 图),默认展示;每个主图的细节图挂在它自己的 detail 子数组下。 images?: RawArticleImage[] - // 按主图分组的细节图(「细节入口」展开用);无细节时为空数组或不返回。 - details?: RawLookDetails[] favorited?: boolean // 已登录时该图集是否已收藏(随详情接口返回) } diff --git a/src/lib/gallery.ts b/src/lib/gallery.ts index f86b3c4..0892a92 100644 --- a/src/lib/gallery.ts +++ b/src/lib/gallery.ts @@ -23,6 +23,8 @@ export interface GalleryImageRaw { name?: string look_index?: number favorited?: boolean + // 详情接口专有:该主图下的细节图(递归同构)。走秀页据此重建「细节入口」映射。 + detail?: GalleryImageRaw[] } /** 已登录全量取数:返回完整图片集与图集级收藏态。 */ @@ -124,6 +126,10 @@ export function createGalleryView(cfg: GalleryConfig) { if (!full || !Array.isArray(full.images)) return const all = full.images + // 广播完整图集:宿主组件(走秀页)据此重建各自的派生数据(细节图映射)。 + // 未登录只拿得到前 PREVIEW_LIMIT 张主图的细节子树,登录后才有全量,故必须在此补一次广播。 + window.dispatchEvent(new CustomEvent("gallery:full", { detail: { images: all } })) + // 图集级收藏态 → 广播给 fav-fab(去掉单篇 /me/favorites/checks 查询) this.galleryFav = !!full.favorited window.dispatchEvent( diff --git a/src/lib/i18n/dictionary.ts b/src/lib/i18n/dictionary.ts index 9b5d5fe..9df8773 100644 --- a/src/lib/i18n/dictionary.ts +++ b/src/lib/i18n/dictionary.ts @@ -26,7 +26,7 @@ export const dict: Record = { 'login': { cn: '登录' }, 'portfolio': { cn: '作品集' }, 'runway looks': { cn: '秀场造型' }, - 'all runway looks': { cn: '全部的 runway' }, + 'all runway looks': { cn: '秀场' }, // ── Runway 子分类(header 下拉,与列表页 COLLECTION 筛选一致)── 'ready-to-wear': { cn: '女装成衣' }, diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts index bba31eb..e875321 100644 --- a/src/lib/i18n/index.ts +++ b/src/lib/i18n/index.ts @@ -98,3 +98,31 @@ export function withLocale(params?: URLSearchParams, override?: string): URLSear if (loc) p.set('locale', loc); return p; } + +// ── 记住语言偏好 / Remembered locale ── +// 用户在语言切换器里选过语言后,把选择存进 localStorage; +// 下次进首页(默认语言首页 / 或根 /)时自动跳到上次选的语言。 +// 注意:只在「显式点击切换器」时写入,绝不按页面加载时的 URL 写, +// 否则会与首页自动跳转形成冲突(点了英文又被 URL 上的 en 覆盖回去)。 +export const STORED_LOCALE_KEY = 'preferred_locale'; + +/** 读取记住的语言偏好;无 / 非法则返回 null。 */ +export function getStoredLocale(): Locale | null { + if (typeof window === 'undefined') return null; + try { + const v = window.localStorage.getItem(STORED_LOCALE_KEY); + return (LOCALES as readonly string[]).includes(v ?? '') ? (v as Locale) : null; + } catch { + return null; + } +} + +/** 记住语言偏好(语言切换器点击时调用)。 */ +export function storeLocale(locale: Locale): void { + if (typeof window === 'undefined') return; + try { + window.localStorage.setItem(STORED_LOCALE_KEY, locale); + } catch { + /* localStorage 不可用时静默忽略 */ + } +}