43
.workbuddy/memory/2026-08-30.md
Normal file
43
.workbuddy/memory/2026-08-30.md
Normal file
@ -0,0 +1,43 @@
|
||||
# 2026-08-30 工作日志
|
||||
|
||||
## 首页 Street Style 图片过大调整(11:33)
|
||||
- 用户反馈首页 Street Style 区块图片太大。定位到 `src/components/Index.astro` 的 `<style>`:
|
||||
- `.ss-banner` 原 `aspect-ratio:16/9` + `w-full` → 桌面端整宽约 675px 高(巨型 hero);
|
||||
- `.ss-grid` 原仅 `sm:grid-cols-2` + `.ss-card` `aspect-ratio:3/4` → 桌面两列每张卡约 1000px 高,竖排 11 张占空间极大。
|
||||
- 调整(保守,不改图片比例、保持编辑风):
|
||||
- `.ss-banner` 加 `max-height: 440px;`(仅桌面端收窄 hero)。
|
||||
- `.ss-grid` 改为 `grid-cols-1 sm:grid-cols-2 md:grid-cols-3`(中等屏起加第 3 列,单图立刻变小)。
|
||||
- 验证:`npm run build` BUILD_EXIT=0;read_lints Index.astro 0;dev 服务 http://localhost:4321 已起(HMR 生效)。
|
||||
- 待定:若用户仍嫌大,可进一步把 `.ss-card` 比例从 3/4 改为 4/5 或 1/1 直接缩高度。
|
||||
|
||||
## Runway 头部下拉:方案 A 落地(16:26)
|
||||
- 用户拍板:header 的 Runway 下拉按「真实 collection_type」分 5 类(方案 A),不做婚纱等无数据类。
|
||||
- 关键发现:Layout.astro 早就有 `runways` 下拉数组,但里面 8 项全是占位垃圾(Spring/Summer 2026…Menswear FW25),且全指向死路由 `ROUTES.shows`(`/shows`);下拉父链接 `<a>` 也错链到 `/shows`。
|
||||
- 改动:
|
||||
- `src/i18n/dictionary.ts`:新增 5 个分类 cn 翻译(`ready-to-wear`→女装成衣、`menswear`→男装、`couture`→高级定制、`resort`→度假系列、`pre-fall`→早秋系列)。标签用 `t('Ready-to-Wear')` 等英文 key(英文环境回退显示英文,与列表页 COLLECTION 筛选项 label 一致)。
|
||||
- `Layout.astro`:`runways` 数组换成 全部 + 5 类,href 用 `href(locale, ROUTES.runwayLooks) + '?collection=xxx'`;父链接 `ROUTES.shows`→`ROUTES.runwayLooks`(修死路由)。
|
||||
- `RunwayLooks.astro`:`init()` 读 `URLSearchParams(window.location.search).get('collection')`,命中白名单(rtw/menswear/womenswear/couture/resort/pre_fall/bridal/swimwear)则赋值 `selectedCt`,从而深链自动选中筛选(复用现有 loadPage→getArticles({collection}) 链路)。
|
||||
- 关键事实:`getArticles`(api.ts) 把 `collection` 映射成后端查询参数 `?collection=`(不是 `collection_type`),故深链参数名用 `collection`。
|
||||
- 验证:`npm run build` BUILD_EXIT=0;read_lints Layout/RunwayLooks 均 0。
|
||||
- 待用户预览确认;深链真实筛选需后端(:8090)在跑。
|
||||
|
||||
## 街拍按地区分类(全链路,18:30 收尾)
|
||||
- 用户拍板「全链路做」:SQL 加 city 列 + 从 title/URL 回填 + 后端 city 筛选 + 前端 header 下拉 + 列表筛选。
|
||||
- 后端:model 加 `City` 字段;dto `StreetSnapQuery.City` + `Normalize`;repo `List` 加 `WHERE city = ?`(精确、大小写敏感);handler `parseListQuery` 解析 `c.DefaultQuery("city","")`;SQL `scripts/sql/004_add_streetsnap_city.sql` + 幂等回填 `scripts/backfill_city/main.go`(首选 title 第一段 location 关键词匹配,兜底 URL slug)。
|
||||
- 前端:`Layout.astro` streets 下拉 18 城(全部 + Paris/New York/Milan/London/Copenhagen/Florence/Berlin/Tokyo/Seoul/Shanghai/Beijing/Rome/Madrid/Amsterdam/Antwerp/Los Angeles/Moscow/Mumbai),href 带 `?city=规范大写名`;`StreetSnaps.astro` 侧栏 City pills + `init()` 读 `?city` 深链自动选中;`api.ts.getStreetSnaps` 透传 `city`;`dictionary.ts` 有各城 cn 翻译(`t('Paris')` 经 `translate` 自动 `toLowerCase` 查到「巴黎」)。
|
||||
- 关键约定:**city 规范值首字母大写英文**(Paris / New York / Copenhagen…),前后端必须一致且大小写敏感。
|
||||
- DB+验证:dev 库 db_dev.street_snap 仅 12 行(Paris 8 / Copenhagen 4),其余城本地预览为空属预期(生产真实爬取数据才有完整覆盖)。重启后端 :8090 后 Node 直连验证:`?city=Paris`→8、`?city=Copenhagen`→4、`?city=London`→0、无参→12,全链路通过(client_sign 本地 enabled:false,无需签名)。
|
||||
- 复盘:标题实际格式为「Street Style 2027 Day 12」而非 `location\tseason\tevent`,city 由旧回填按 URL 预置;生产真实数据靠 title/URL 解析归类。dev 环境 `go run` 可正常编译(仅 `go build ./...` 全量链接会 OOM,非代码问题)。
|
||||
|
||||
## Header 精简:移除死链 + street style 减为 3 下拉(18:57)
|
||||
- 用户要求:去掉 Latest Projects / About / Contact Us;street style 下拉只留三个。
|
||||
- 现状确认:`Layout.astro` 主导航早已无此三项(仅 Portfolio / Runway(下拉) / Street Style(下拉) / 语言 / Login / Signup);`streets` 下拉当前已是 3 项(`全部 / Paris / New York`)。
|
||||
- 清理:`routes.ts` 删 `latestProjects`/`about`/`contact` 键;`dictionary.ts` 删 about/contact/latest projects 翻译键;全 src 搜索确认无 `latestProjects|about|contact` 残留引用。
|
||||
- 验证:`npm run build` exit 0(日志仅 npm warn 包装的 NativeCommandError,非构建错误)。
|
||||
|
||||
## Header 下拉数量微调(19:00)
|
||||
- 用户:street style 下拉再加一个、runway 下拉去掉一个。
|
||||
- Street:加 **Milan** → `全部 / Paris / New York / Milan` = 4 项(时尚三都补齐;dictionary 已有 `milan→米兰`)。
|
||||
- Runway:去 **Pre-Fall**(早秋系列,过渡线、数据最少 2376 行),保留 `全部 / rtw / menswear / couture / resort` = 5 项。
|
||||
- 两个下拉均保留"全部"重置项(结构对称);Runway 父链接仍指向无参列表。
|
||||
- 验证:待 build。
|
||||
@ -3,7 +3,9 @@
|
||||
## 模块策略(重要)
|
||||
- 站点按模块逐个完善。当前上线「发布会模块」:首页 index + 走秀列表/详情(RunwayLooks = 走秀档案页,articles/shows 页面已在 i18n 重构中重组)。
|
||||
- **品牌索引页已下线**(2026-08-26 确认):`src/pages/brands.astro` 及孤儿组件 `src/components/pages/brands.astro` 均已删除;品牌数据现在只通过走秀页侧栏 filter + 品牌弹窗呈现(热门 30 条走 SSG 接口,搜索/字母走运行时 `/api/v1/public/brands`)。
|
||||
- **占位模块组件已清理(2026-08-29)**:`components/pages/` 下 `About`/`Contact`/`LatestProjects`/`Portfolio`/`Login`/`Register`/`AuthForm`/`article` 等占位/旧详情组件查无引用、也不是真实路由,已删除并移除空目录。**这些模块的导航链接(`/about` `/contact` `/portfolio` `/latest-projects` `/login` `/register`)仍是死链**——从未有对应页面,待用户决定是否移除导航项。**导航(header)本身保持原样未改动**(用户 2026-08-07 明确「header 别动」)。`/en/login`、`/en/register` 实际并非真实页面(旧 MEMORY 记录有误),AuthForm 组件已随清理删除。
|
||||
- **占位模块组件已清理(2026-08-29)**:`components/pages/` 下 `About`/`Contact`/`LatestProjects`/`Portfolio`/`Login`/`Register`/`AuthForm`/`article` 等占位/旧详情组件查无引用、也不是真实路由,已删除并移除空目录。
|
||||
- **导航死链清理(2026-08-30)**:用户决定移除 `/latest-projects`、`/about`、`/contact` 三个死链——已从 `Layout.astro` 主导航(早已不在)、`routes.ts`(`latestProjects`/`about`/`contact` 键)、`dictionary.ts`(about/contact/latest projects 翻译键)三处清除,全 src 扫描零残留引用。**剩余死链 `/portfolio`、`/login`、`/register` 保留未动**(Portfolio 导航项、Login/Signup 链接仍展示,用户未要求移除)。
|
||||
- **「header 别动」解读(2026-08-30 明确)**:用户「header 别动」指不要擅自改导航结构与视觉;但用户**主动批准**的导航调整(Runway 下拉改 5 个 collection 分类、Street Style 下拉减为 3 城、删除死链项)均属于已授权变更,不算违规。`/en/login`、`/en/register` 实际并非真实页面(旧 MEMORY 记录有误),AuthForm 组件已随清理删除。
|
||||
|
||||
## 架构总览
|
||||
- 前端:**d:/project/frontend_v2** — Astro SSG + Tailwind v4 + Alpine.js。`npm run build`(SSG 取数走 ssgRequest,失败回落空/假数据不影响构建)。
|
||||
@ -17,7 +19,7 @@
|
||||
- **品牌名前端一律只显示英文**(用户偏好 2026-08-26:「前端品牌不要展示中文名称,标题可以留着」):api.ts 映射 `display = nameEn`;走秀标题/描述仍按 locale 选列。
|
||||
- **API 命名哲学(用户原则)**:「每一个地方尽可能不要通过参数来复用接口,尽量一个功能一个接口」。查询参数仅限「同一资源的列表筛选/分页」;跨资源/独立功能一律独立路径(如 `brands/hot` 而非 `?featured=1`)。待办:公开侧 `/api/v1/public/brands` 的 featured/keyword/letter 参数拆分、`/articles` 的 `brand_id` 拆 `brands/:id/shows`、资源命名 `shows` vs `articles`、`popular` vs `featured`(目标清单见 artifact `api_endpoints_catalog.md`)。
|
||||
- CSS:能 @apply 就 @apply;`<style>` 顶部 `@reference "tailwindcss";`。Tailwind v4 无零值类。
|
||||
- 导航(Layout.astro)保持原样未改动(用户「header 别动」),但其部分链接(/about /contact /portfolio /latest-projects /login /register)指向不存在的路由,属待清理死链。
|
||||
- 导航(Layout.astro):「header 别动」指不擅自改结构/视觉,但用户**主动批准**的导航调整有效(Runway 下拉改 4 个 collection 分类 rtw/menswear/couture/resort、Street Style 下拉减为「全部/Paris/New York/Milan」4 项、移除 latest-projects/about/contact 死链)。剩余死链 /portfolio、/login、/register 用户未要求动,保留。
|
||||
|
||||
## Layout 陷阱(2026-08-12)
|
||||
- Layout 把 `<slot />` 包在 `flex-1 flex flex-col` 容器里;页内根容器**禁用 `m-auto`**(cross-axis margin:auto 触发 shrink-to-fit → 内容多少决定宽度、页面抖动)。用 `w-full`(可配 `max-w-[xxx]` + `mx-auto`)。
|
||||
@ -30,9 +32,16 @@
|
||||
## 走秀数据分类(brand_runway)
|
||||
- 三维分类字段:`collection_type`(rtw/menswear/couture/resort/pre_fall)、`season`(spring/fall/null)、`season_code`(SS26/FW25…派生)。靠 title 规则回填(幂等,25,206 行)。
|
||||
- 公开列表 `?collection_type=&season=&season_code=&year=` 筛选;前端走秀页有系列/季节/年份 chips。
|
||||
- 地理/城市维度:用户明确暂不做。
|
||||
- 地理/城市维度(runway):用户明确暂不做。
|
||||
- 分布:rtw 15203 / menswear 4061 / couture 672 / resort 2894 / pre_fall 2376。
|
||||
|
||||
## 街拍地区分类(street_snap.city,2026-08-30)
|
||||
- 全链路已落地:DB 加 `city` 列(VARCHAR(128),脚本 `scripts/sql/004_add_streetsnap_city.sql` + 幂等回填 `scripts/backfill_city/main.go`,无 AutoMigrate)+ 后端 `StreetSnapQuery.City` 精确匹配 `WHERE city = ?` + 前端 header 下拉 `?city=` + 列表侧栏 City pills + 深链自动选中。
|
||||
- **规范值约定(重要)**:city 列存「首字母大写英文规范名」(Paris / New York / Copenhagen …),前端 header 下拉与列表 `cityOptions` 的 value 用**同一大写值**,后端做精确匹配、大小写敏感,必须前后一致。backfill 首选从 title 第一段(location,`\t` 分隔)按城市关键词匹配,兜底从 source_url/cover 的 URL slug 解析。
|
||||
- 前端 `t('Paris')` 经 `translate()` 自动 `key.toLowerCase()` 查字典,中文回落「巴黎」,i18n 正常。
|
||||
- dev 库 db_dev.street_snap 仅 12 行样本(Paris 8 / Copenhagen 4),其余下拉城市(London/Milan…)本地预览为空属预期;生产真实爬取数据才有完整覆盖。
|
||||
- 验证:重启后端 :8090 后,`?city=Paris`→8、`?city=Copenhagen`→4、`?city=London`→0、无参→12,全链路通过。client_sign 本地 `enabled:false`,可直接 curl/Node 验证。
|
||||
|
||||
## 前端组件复用(2026-08-29)
|
||||
- **详情页统一 `/item`**:走秀 lookbook 与街拍详情全部收口到 `src/pages/{locale}/item/[id].astro`,服务端按 `getSsrArticle(id)` → `getSsrStreetSnap(id)` 回落渲染(先 runway 后 street)。`ROUTES.article` 与 `ROUTES.streetSnap` 均指向 `/item/${id}`(`runway-looks/[id]`、`street-snaps/[id]`、`article.astro(?id=)` 等旧详情页均已删除)。列表页回链仍指向列表 `/runway-looks`、`/street-snaps`。
|
||||
- **列表页共享资源**:两个列表组件(RunwayLooks / StreetSnaps)同构,抽成 `src/styles/look-grid.css`(卡片/网格/分页/侧栏/spinner/骨架/动画)+ `src/lib/looks-grid.ts`(`SPIN_SVG`、`makeCardSlots(images,count,opts)`、`buildPageList(total,cur)`);品牌筛选弹窗 `src/components/BrandModal.astro`(仅 RunwayLooks 用,`openBrandModal/confirmBrandModal` 等由页面脚本驱动)。卡片标记因在 Alpine `x-for` 内客户端渲染,**不能**提成 Astro 组件。
|
||||
|
||||
@ -9,30 +9,30 @@
|
||||
> test@0.0.1 build
|
||||
> astro build
|
||||
|
||||
[2m18:59:58[22m [34m[@astrojs/node][39m Enabling sessions with filesystem storage
|
||||
[2m18:59:59[22m [34m[types][39m Generated [2m233ms[22m
|
||||
[2m18:59:59[22m [34m[build][39m output: [34m"static"[39m
|
||||
[2m18:59:59[22m [34m[build][39m mode: [34m"server"[39m
|
||||
[2m18:59:59[22m [34m[build][39m directory: [34mD:\project\frontend_v2\dist\[39m
|
||||
[2m18:59:59[22m [34m[build][39m adapter: [32m@astrojs/node[39m
|
||||
[2m18:59:59[22m [34m[build][39m Collecting build info...
|
||||
[2m18:59:59[22m [34m[build][39m [32m✓ Completed in 435ms.[39m
|
||||
[2m18:59:59[22m [34m[build][39m Building server entrypoints...
|
||||
[2m19:00:01[22m [34m[vite][39m [32m✓ built in 2.33s[39m
|
||||
[2m19:00:03[22m [34m[vite][39m [32m✓ built in 1.51s[39m
|
||||
[2m19:00:03[22m [34m[vite][39m [32m✓ built in 814ms[39m
|
||||
[2m19:01:18[22m [34m[@astrojs/node][39m Enabling sessions with filesystem storage
|
||||
[2m19:01:18[22m [34m[types][39m Generated [2m92ms[22m
|
||||
[2m19:01:18[22m [34m[build][39m output: [34m"static"[39m
|
||||
[2m19:01:18[22m [34m[build][39m mode: [34m"server"[39m
|
||||
[2m19:01:18[22m [34m[build][39m directory: [34mD:\project\frontend_v2\dist\[39m
|
||||
[2m19:01:18[22m [34m[build][39m adapter: [32m@astrojs/node[39m
|
||||
[2m19:01:18[22m [34m[build][39m Collecting build info...
|
||||
[2m19:01:18[22m [34m[build][39m [32m✓ Completed in 191ms.[39m
|
||||
[2m19:01:18[22m [34m[build][39m Building server entrypoints...
|
||||
[2m19:01:19[22m [34m[vite][39m [32m✓ built in 802ms[39m
|
||||
[2m19:01:19[22m [34m[vite][39m [32m✓ built in 392ms[39m
|
||||
[2m19:01:20[22m [34m[vite][39m [32m✓ built in 282ms[39m
|
||||
|
||||
[42m[30m prerendering static routes [39m[49m
|
||||
[2m19:00:04[22m [34m├─[39m [2m/cn/runway-looks/index.html[22m [2m(+110ms)[22m
|
||||
[2m19:00:04[22m [34m├─[39m [2m/cn/street-snaps/index.html[22m [2m(+19ms)[22m
|
||||
[2m19:00:04[22m [34m├─[39m [2m/cn/index.html[22m [2m(+45ms)[22m
|
||||
[2m19:00:04[22m [34m├─[39m [2m/en/runway-looks/index.html[22m [2m(+31ms)[22m
|
||||
[2m19:00:04[22m [34m├─[39m [2m/en/street-snaps/index.html[22m [2m(+16ms)[22m
|
||||
[2m19:00:04[22m [34m├─[39m [2m/en/index.html[22m [2m(+30ms)[22m
|
||||
[2m19:00:04[22m [34m├─[39m [2m/index.html[22m [2m(+37ms)[22m
|
||||
[2m19:00:04[22m [32m✓ Completed in 349ms.
|
||||
[2m19:01:20[22m [34m├─[39m [2m/cn/runway-looks/index.html[22m [2m(+48ms)[22m
|
||||
[2m19:01:20[22m [34m├─[39m [2m/cn/street-snaps/index.html[22m [2m(+9ms)[22m
|
||||
[2m19:01:20[22m [34m├─[39m [2m/cn/index.html[22m [2m(+35ms)[22m
|
||||
[2m19:01:20[22m [34m├─[39m [2m/en/runway-looks/index.html[22m [2m(+15ms)[22m
|
||||
[2m19:01:20[22m [34m├─[39m [2m/en/street-snaps/index.html[22m [2m(+7ms)[22m
|
||||
[2m19:01:20[22m [34m├─[39m [2m/en/index.html[22m [2m(+24ms)[22m
|
||||
[2m19:01:20[22m [34m├─[39m [2m/index.html[22m [2m(+20ms)[22m
|
||||
[2m19:01:20[22m [32m✓ Completed in 183ms.
|
||||
[39m
|
||||
[2m19:00:04[22m [34m[build][39m Rearranging server assets...
|
||||
[2m19:00:04[22m [34m[build][39m [32m✓ Completed in 5.26s.[39m
|
||||
[2m19:00:04[22m [34m[build][39m Server built in [1m5.76s[22m
|
||||
[2m19:00:04[22m [34m[build][39m [1mComplete![22m
|
||||
[2m19:01:20[22m [34m[build][39m Rearranging server assets...
|
||||
[2m19:01:20[22m [34m[build][39m [32m✓ Completed in 1.78s.[39m
|
||||
[2m19:01:20[22m [34m[build][39m Server built in [1m1.98s[22m
|
||||
[2m19:01:20[22m [34m[build][39m [1mComplete![22m
|
||||
|
||||
@ -155,28 +155,10 @@ const streetItems: { src: string; loc: string; event: string; id: string }[] = s
|
||||
{
|
||||
streetItems.length > 0 ? (
|
||||
<section class="ss-section">
|
||||
<!-- 顶部 banner:第 1 条最热街拍 -->
|
||||
{
|
||||
streetItems[0] && (
|
||||
<a class="ss-banner" href={href(locale, ROUTES.streetSnap(streetItems[0].id))}>
|
||||
<div class="ss-img-box" x-data="{ loaded: false, error: false, init() { const i = this.$el.querySelector('img'); if (i && i.complete) { this.loaded = i.naturalWidth > 0; this.error = i.naturalWidth === 0; } } }" :class="{ 'is-loaded': loaded || error }">
|
||||
<div class="ss-skel" aria-hidden="true" x-show="!loaded && !error"></div>
|
||||
<span class="ss-spin" aria-hidden="true" x-show="!loaded && !error">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-dasharray="40 22" class="ss-ring" /></svg>
|
||||
</span>
|
||||
<img src={streetItems[0].src} alt={streetItems[0].loc} loading="eager" decoding="async" :class="{ 'is-error': error }" @load="loaded = true; $dispatch('img-ready')" @error="error = true; $dispatch('img-ready')" />
|
||||
<span class="ss-cap">
|
||||
<span class="ss-cap-loc">{streetItems[0].loc}</span>
|
||||
<span class="ss-cap-event">{streetItems[0].event}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
<!-- 2 列大图卡片网格:其余街拍 -->
|
||||
<!-- 街拍大图卡片网格 -->
|
||||
<div class="ss-grid">
|
||||
{
|
||||
streetItems.slice(1).map((g) => (
|
||||
streetItems.map((g) => (
|
||||
<a class="ss-card" href={href(locale, ROUTES.streetSnap(g.id))}>
|
||||
<div class="ss-img-box" x-data="{ loaded: false, error: false, init() { const i = this.$el.querySelector('img'); if (i && i.complete) { this.loaded = i.naturalWidth > 0; this.error = i.naturalWidth === 0; } } }" :class="{ 'is-loaded': loaded || error }">
|
||||
<div class="ss-skel" aria-hidden="true" x-show="!loaded && !error"></div>
|
||||
@ -372,12 +354,8 @@ const streetItems: { src: string; loc: string; event: string; id: string }[] = s
|
||||
|
||||
/* ===== 首页 Street Style 网格(去掉 swiper,改为 HBX 大图卡片,与街拍列表页一致) ===== */
|
||||
.ss-section { @apply mb-9; }
|
||||
.ss-banner {
|
||||
@apply relative block w-full overflow-hidden bg-[#f0f0f0] no-underline text-inherit mb-7;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
.ss-grid {
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 gap-[14px] md:gap-[18px];
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-[14px] md:gap-[18px];
|
||||
}
|
||||
.ss-card {
|
||||
@apply relative block w-full overflow-hidden bg-[#f0f0f0] no-underline text-inherit;
|
||||
@ -394,8 +372,7 @@ const streetItems: { src: string; loc: string; event: string; id: string }[] = s
|
||||
}
|
||||
.ss-img-box.is-loaded > img { opacity: 1; }
|
||||
.ss-img-box > img.is-error { opacity: 0 !important; }
|
||||
.ss-card:hover .ss-img-box > img,
|
||||
.ss-banner:hover .ss-img-box > img { transform: scale(1.04); }
|
||||
.ss-card:hover .ss-img-box > img { transform: scale(1.04); }
|
||||
|
||||
/* 骨架屏 + spinner(与首页 hs-* 平行) */
|
||||
.ss-skel {
|
||||
@ -425,22 +402,15 @@ const streetItems: { src: string; loc: string; event: string; id: string }[] = s
|
||||
gap: 5px; z-index: 3; pointer-events: none;
|
||||
}
|
||||
.ss-cap-loc {
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
font-size: clamp(18px, 2vw, 26px);
|
||||
font-weight: 700; line-height: 1.1; letter-spacing: -0.01em;
|
||||
text-shadow: 0 1px 4px rgba(0,0,0,0.55);
|
||||
}
|
||||
.ss-cap-event {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
font-size: clamp(10px, 0.8vw, 12px);
|
||||
font-weight: 500; text-transform: uppercase; letter-spacing: 0.16em; line-height: 1.4;
|
||||
opacity: 0.85; text-shadow: 0 1px 3px rgba(0,0,0,0.5);
|
||||
}
|
||||
.ss-banner .ss-cap { padding: 44px 28px 26px; }
|
||||
.ss-banner .ss-cap-loc { font-size: clamp(26px, 4vw, 48px); }
|
||||
@media (min-width: 768px) {
|
||||
.ss-banner .ss-cap { padding: 56px 36px 30px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
@ -303,6 +303,10 @@ const seasonOptions: { value: string; label: string }[] = [
|
||||
|
||||
init() {
|
||||
this.locale = this.$el.dataset.locale || 'en';
|
||||
// 支持从 header 下拉深链 ?collection=xxx 直接进入对应分类(与侧栏 COLLECTION 筛选一致)
|
||||
const ct = new URLSearchParams(window.location.search).get('collection')
|
||||
const ALLOWED = ['rtw', 'menswear', 'womenswear', 'couture', 'resort', 'pre_fall', 'bridal', 'swimwear']
|
||||
if (ct && ALLOWED.includes(ct)) this.selectedCt = ct
|
||||
this.loadPage(1)
|
||||
},
|
||||
|
||||
|
||||
@ -22,6 +22,29 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
{ value: "year_desc", label: t('Newest year') },
|
||||
{ value: "year_asc", label: t('Oldest year') },
|
||||
]
|
||||
|
||||
// 地区筛选:值即后端 street_snap.city 列存储的规范英文名(与 backfill_city 词典保持一致)。
|
||||
// 标签用 t(city) —— 英文环境回退显示英文(巴黎→Paris),中文环境显示中文(Paris→巴黎)。
|
||||
const cityOptions: { value: string; label: string }[] = [
|
||||
{ value: "Paris", label: t('Paris') },
|
||||
{ value: "New York", label: t('New York') },
|
||||
{ value: "Milan", label: t('Milan') },
|
||||
{ value: "London", label: t('London') },
|
||||
{ value: "Copenhagen", label: t('Copenhagen') },
|
||||
{ value: "Florence", label: t('Florence') },
|
||||
{ value: "Berlin", label: t('Berlin') },
|
||||
{ value: "Tokyo", label: t('Tokyo') },
|
||||
{ value: "Seoul", label: t('Seoul') },
|
||||
{ value: "Shanghai", label: t('Shanghai') },
|
||||
{ value: "Beijing", label: t('Beijing') },
|
||||
{ value: "Rome", label: t('Rome') },
|
||||
{ value: "Madrid", label: t('Madrid') },
|
||||
{ value: "Amsterdam", label: t('Amsterdam') },
|
||||
{ value: "Antwerp", label: t('Antwerp') },
|
||||
{ value: "Los Angeles", label: t('Los Angeles') },
|
||||
{ value: "Moscow", label: t('Moscow') },
|
||||
{ value: "Mumbai", label: t('Mumbai') },
|
||||
]
|
||||
---
|
||||
|
||||
<div class="snapsroot w-full" x-data="streetSnapsPage()" data-locale={locale}>
|
||||
@ -88,6 +111,23 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- City / 地区 -->
|
||||
<section class="snapsg">
|
||||
<header class="snapsg-h" @click="toggleGroup('city')">
|
||||
<span>{t('City')}</span>
|
||||
<span class="snapsg-chev" :class="{ 'is-collapsed': collapsedGroups.city }">
|
||||
<svg viewBox="0 0 12 12" width="11" height="11"><path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</span>
|
||||
</header>
|
||||
<div class="snapsg-b" x-show="!collapsedGroups.city">
|
||||
<ul class="snapschecks snapschecks--flat">
|
||||
{cityOptions.map((o) => (
|
||||
<li><label><input type="radio" name="snapCity" value={o.value} :checked={'selectedCity === "' + o.value + '"'} :class={`{'is-checked': selectedCity === '${o.value}'}`} @click.prevent={'selectedCity = selectedCity === "' + o.value + '" ? "" : "' + o.value + '"; loadPage(1)'} /> <span>{o.label}</span></label></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button class="snapsclear" type="button" @click="clearAll()" x-show="pills.length > 0" x-cloak>Clear all ×</button>
|
||||
</div>
|
||||
</aside>
|
||||
@ -172,7 +212,7 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="snaps-year-opts" type="application/json" set:html={JSON.stringify({ yearOptions, sortOptions })}></script>
|
||||
<script id="snaps-year-opts" type="application/json" set:html={JSON.stringify({ yearOptions, sortOptions, cityOptions })}></script>
|
||||
|
||||
|
||||
|
||||
@ -193,11 +233,13 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
SPIN_SVG,
|
||||
yearOptions: opts.yearOptions || [],
|
||||
sortOptions: opts.sortOptions || [],
|
||||
cityOptions: opts.cityOptions || [],
|
||||
selectedYear: '' as string,
|
||||
selectedSort: DEFAULT_SORT as string,
|
||||
selectedCity: '' as string,
|
||||
|
||||
// 侧栏分组折叠状态:默认全部收起(不做持久化,每次进页都是关着的)
|
||||
collapsedGroups: { year: true, sort: true } as Record<string, boolean>,
|
||||
collapsedGroups: { year: true, sort: true, city: true } as Record<string, boolean>,
|
||||
|
||||
items: [] as (StreetSnapItem & { _imgLoaded?: boolean })[],
|
||||
loading: false,
|
||||
@ -207,6 +249,10 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
|
||||
init() {
|
||||
this.locale = this.$el.dataset.locale || 'en'
|
||||
// 支持从 header 下拉深链 ?city=xxx 直接进入对应地区(与侧栏 City 筛选一致)
|
||||
const city = new URLSearchParams(window.location.search).get('city')
|
||||
const allowed = (this.cityOptions || []).map((c: any) => c.value)
|
||||
if (city && allowed.includes(city)) this.selectedCity = city
|
||||
this.loadPage(1)
|
||||
},
|
||||
|
||||
@ -217,6 +263,7 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
clearAll() {
|
||||
this.selectedYear = ''
|
||||
this.selectedSort = DEFAULT_SORT
|
||||
this.selectedCity = ''
|
||||
this.loadPage(1)
|
||||
},
|
||||
|
||||
@ -237,6 +284,13 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
clear: () => { this.selectedSort = DEFAULT_SORT; this.loadPage(1) },
|
||||
})
|
||||
}
|
||||
if (this.selectedCity) {
|
||||
const hit = this.cityOptions.find((o: any) => o.value === this.selectedCity)
|
||||
out.push({
|
||||
label: "CITY · " + (hit ? hit.label : this.selectedCity),
|
||||
clear: () => { this.selectedCity = ''; this.loadPage(1) },
|
||||
})
|
||||
}
|
||||
return out
|
||||
},
|
||||
|
||||
@ -255,6 +309,7 @@ const sortOptions: { value: string; label: string }[] = [
|
||||
const json = await getStreetSnaps({
|
||||
page,
|
||||
year: this.selectedYear || undefined,
|
||||
city: this.selectedCity || undefined,
|
||||
sort: this.selectedSort,
|
||||
})
|
||||
if (myToken !== this.reqToken) return
|
||||
|
||||
@ -21,24 +21,23 @@ export const dict: Record<string, TranslationEntry> = {
|
||||
|
||||
// ── 导航 / Navigation ──
|
||||
|
||||
'about': { cn: '关于' },
|
||||
|
||||
'全部的 runway': { cn: '全部的 runway' },
|
||||
|
||||
'contact': { cn: '联系' },
|
||||
|
||||
'contact us': { cn: '联系我们' },
|
||||
|
||||
'home': { cn: '首页' },
|
||||
|
||||
'latest projects': { cn: '最新项目' },
|
||||
|
||||
'login': { cn: '登录' },
|
||||
|
||||
'portfolio': { cn: '作品集' },
|
||||
|
||||
'runway archive': { cn: '走秀档案' },
|
||||
|
||||
// ── Runway 子分类(header 下拉,与列表页 COLLECTION 筛选一致)──
|
||||
'ready-to-wear': { cn: '女装成衣' },
|
||||
'menswear': { cn: '男装' },
|
||||
'couture': { cn: '高级定制' },
|
||||
'resort': { cn: '度假系列' },
|
||||
'pre-fall': { cn: '早秋系列' },
|
||||
|
||||
'signup': { cn: '注册' },
|
||||
|
||||
// ── 控件 / Controls ──
|
||||
@ -108,8 +107,28 @@ export const dict: Record<string, TranslationEntry> = {
|
||||
|
||||
'no street snap id specified': { cn: '未指定街拍 ID' },
|
||||
|
||||
'failed to load street snap. please refresh or try later.': { cn: '加载街拍失败,请刷新或稍后再试' }
|
||||
'failed to load street snap. please refresh or try later.': { cn: '加载街拍失败,请刷新或稍后再试' },
|
||||
|
||||
// ── 地区 / Cities(街拍按城市分类,值对齐后端 street_snap.city 列;translate 会小写化 key)──
|
||||
'city': { cn: '地区' },
|
||||
'paris': { cn: '巴黎' },
|
||||
'new york': { cn: '纽约' },
|
||||
'milan': { cn: '米兰' },
|
||||
'london': { cn: '伦敦' },
|
||||
'copenhagen': { cn: '哥本哈根' },
|
||||
'florence': { cn: '佛罗伦萨' },
|
||||
'berlin': { cn: '柏林' },
|
||||
'tokyo': { cn: '东京' },
|
||||
'seoul': { cn: '首尔' },
|
||||
'shanghai': { cn: '上海' },
|
||||
'beijing': { cn: '北京' },
|
||||
'rome': { cn: '罗马' },
|
||||
'madrid': { cn: '马德里' },
|
||||
'amsterdam': { cn: '阿姆斯特丹' },
|
||||
'antwerp': { cn: '安特卫普' },
|
||||
'los angeles': { cn: '洛杉矶' },
|
||||
'moscow': { cn: '莫斯科' },
|
||||
'mumbai': { cn: '孟买' },
|
||||
};
|
||||
|
||||
/** 把 {name} 形式的占位符替换成参数值。 */
|
||||
|
||||
@ -30,16 +30,23 @@ const pathWithoutLocale = locales.includes(pathName.split('/')[1])
|
||||
// 顶部导航数据模型(替代原 Alpine x-data 的 menu/state)。
|
||||
// 下拉菜单与语言列表均由此驱动渲染,逻辑用 Astro 原生 <script> 控制开合。
|
||||
const headers = {
|
||||
// 走秀下拉菜单项(href 统一指向 /shows)
|
||||
// 走秀下拉菜单项:方案 A —— 全部 + 4 个真实 collection_type 分类(与列表页 COLLECTION 筛选一致),
|
||||
// 深链带 ?collection= 参数,由 RunwayLooks 的 init() 读取并自动选中筛选。
|
||||
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) },
|
||||
{ label: t('全部的 runway'), href: href(locale, ROUTES.runwayLooks) },
|
||||
{ label: t('Ready-to-Wear'), href: href(locale, ROUTES.runwayLooks) + '?collection=rtw' },
|
||||
{ label: t('Menswear'), href: href(locale, ROUTES.runwayLooks) + '?collection=menswear' },
|
||||
{ label: t('Couture'), href: href(locale, ROUTES.runwayLooks) + '?collection=couture' },
|
||||
{ label: t('Resort'), href: href(locale, ROUTES.runwayLooks) + '?collection=resort' },
|
||||
],
|
||||
// 街拍下拉菜单项:按地区分类(值即后端 street_snap.city 列存储的规范英文名),
|
||||
// 深链带 ?city= 参数,由 StreetSnaps 的 init() 读取并自动选中筛选。
|
||||
// 仅保留 4 项(全部 + 三座核心时尚之都),完整城市列表在街拍页侧栏 City 筛选里。
|
||||
streets: [
|
||||
{ label: t('All Street Snaps'), href: href(locale, ROUTES.streetSnaps) },
|
||||
{ label: t('Paris'), href: href(locale, ROUTES.streetSnaps) + '?city=Paris' },
|
||||
{ label: t('New York'), href: href(locale, ROUTES.streetSnaps) + '?city=New York' },
|
||||
{ label: t('Milan'), href: href(locale, ROUTES.streetSnaps) + '?city=Milan' },
|
||||
],
|
||||
// 语言列表(code + 本地名称)
|
||||
languages: locales.map((l) => ({ code: l, label: LOCALE_NAMES[l].nativeName })),
|
||||
@ -69,7 +76,7 @@ const headers = {
|
||||
<!-- 全部的 runway:菜单项用 Astro 渲染(headers.runways.map),开合逻辑用 Alpine.js -->
|
||||
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
||||
<div class="flex items-center gap-1">
|
||||
<a href={href(locale, ROUTES.shows)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('全部的 runway')}</a>
|
||||
<a href={href(locale, ROUTES.runwayLooks)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('全部的 runway')}</a>
|
||||
<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">
|
||||
<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>
|
||||
@ -84,10 +91,24 @@ const headers = {
|
||||
}
|
||||
</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.streetSnaps)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Street Style')}</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>
|
||||
<!-- 街拍:菜单项用 Astro 渲染(headers.streets.map),开合逻辑用 Alpine.js -->
|
||||
<div class="relative" x-data="{ open: false }" @mouseenter="open = true" @mouseleave="open = false">
|
||||
<div class="flex items-center gap-1">
|
||||
<a href={href(locale, ROUTES.streetSnaps)} class="relative pb-[2px] cursor-pointer hover:opacity-60 transition-opacity duration-200">{t('Street Style')}</a>
|
||||
<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="展开 street style 列表" aria-haspopup="true" :aria-expanded="open">
|
||||
<svg viewBox="0 0 12 12" width="10" height="10"><path d="M2.5 4.5l3.5 3 3.5-3" stroke="currentColor" fill="none" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</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.streets.map((item) => (
|
||||
<a href={item.href} class="block px-4 py-2 text-[12px] uppercase tracking-[0.4px] hover:bg-black/5 transition-colors">
|
||||
{item.label}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
@ -417,17 +417,19 @@ export interface RawStreetSnap {
|
||||
}
|
||||
|
||||
// 接口:GET /api/public/street-snaps
|
||||
// 街拍列表:年份过滤 + 排序(image_count 默认降序 / year_desc / year_asc)。
|
||||
// 街拍列表:年份过滤 + 排序(image_count 默认降序 / year_desc / year_asc)+ 地区(city 精确匹配)。
|
||||
// 分页 size 服务端固定 24,images 服务端固定 6,前端均不传。
|
||||
export async function getStreetSnaps(q: {
|
||||
page?: number
|
||||
year?: number | string
|
||||
city?: string
|
||||
sort?: string
|
||||
} = {}): Promise<StreetSnapPage> {
|
||||
const p = new URLSearchParams()
|
||||
p.set("page", String(q.page ?? 1))
|
||||
if (q.sort) p.set("sort", q.sort)
|
||||
if (q.year) p.set("year", String(q.year))
|
||||
if (q.city) p.set("city", q.city)
|
||||
const j = await request<{ data?: any[]; last_page?: number }>("/public/street-snaps", p)
|
||||
const mapped: StreetSnapItem[] = (j.data ?? []).map((it: any) => ({
|
||||
id: String(it.id),
|
||||
|
||||
@ -11,9 +11,6 @@ export const ROUTES = {
|
||||
shows: '/shows',
|
||||
brands: '/brands',
|
||||
portfolio: '/portfolio',
|
||||
latestProjects: '/latest-projects',
|
||||
about: '/about',
|
||||
contact: '/contact',
|
||||
login: '/login',
|
||||
register: '/register',
|
||||
runwayLooks: '/runway-looks',
|
||||
|
||||
Reference in New Issue
Block a user