{t("member profile")}
+Portfolio Studio
+Archive + 2026 +
+Paris · Milan · Tokyo
+{savedLooksLabel}
+{itemsLabel}
+{noSavedLabel}
+ {browseLabel} +{historyLabel}
+{itemsLabel}
+ +{noHistoryLabel}
+ {browseLabel} +diff --git a/.gitignore b/.gitignore index c99e093..6fc517e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,10 @@ logs/ api-debug.log *.log +# 临时调试脚本 / 错误日志(勿入库) +_tmp* +*.err + # environment variables .env .env.production diff --git a/.workbuddy/memory/2026-09-14.md b/.workbuddy/memory/2026-09-14.md new file mode 100644 index 0000000..6483e7f --- /dev/null +++ b/.workbuddy/memory/2026-09-14.md @@ -0,0 +1,132 @@ +# 2026-09-14 + +## 前端 API 客户端二次拆分:SSG 构建期取数抽到 ssg.ts ✅ +用户嫌 api.ts 杂乱,要求把构建期 SSG 接口单独成文件。 +- 新建 `src/lib/ssg.ts`:`SsgPopularBrand` 类型 + `getSsgIndexRunway`/`getSsgHotBrands`/`getSsgStreetSnapPopular`(原 api.ts 末尾「SSG 构建专用」段)。import `requestSsg` from `./request`、`import type { BrandEntry, StreetSnapItem } from "./api"`(类型仅用,不引入运行期依赖,无环)。 +- `api.ts`:删除上述类型与 3 个函数、`SsgPopularBrand` 类型;import 去掉 `requestSsg`;文件头注释移除 SSG 分组。 +- 消费方改 import:`Index.astro`、`RunwayLooks.astro` 的 `getSsg*` 改从 `@/lib/ssg` 导入。 +- 验证:`astro build` 退出码 0,全部路由预渲染成功(Vite 导入解析 = 全链路校验)。lint 0。 + +## ⚠️ 重要对账:auth.ts 鉴权函数并未真正拆出(纠正 09-13 总结) +09-13 总结称 login/logout/fetchMe 已拆到 `auth.ts`,但 09-14 读磁盘:`auth.ts` 只含会话存储原语(getUser/getAccessToken/saveSession/clearSession 等),`login`/`logout`/`fetchMe`/`enforceIdleLogout` 仍在 `api.ts`(约 387–465 行)。即上次的鉴权拆分未落盘(或已被还原)。已同步修正 MEMORY.md,避免以后再绕回去。 + +## 待用户决定是否继续 +- 是否要把 `login`/`logout`/`fetchMe`/`enforceIdleLogout` 真挪到 `auth.ts`(完成上次未落地的拆分)? +- `getSsrArticle`/`getSsrStreetSnap`(运行期 SSR 按需,非 SSG)是否也要单独成 `ssr.ts`?目前与 `getArticleDetailAuthed`/`getStreetSnapDetailAuthed` 成对留在 api.ts。 + +## 端点路径集中到 endpoints.ts ✅(11:30) +用户要求把 `/api/v1/xxx` 端点字面量用 map/const 集中,且要与页面路由 `ROUTES` 区分。 +- 新建 `src/lib/endpoints.ts`:导出 `endpoints`(分 `public`/`auth`/`me`/`ssg` 四组;动态 id 用函数如 `runwayLook(id)`;`V1="/api/v1"`、`SSG="/api/internal/ssg"` 前缀仅此定义一次)。`as const` 保证字面路径类型。 +- 与 `src/lib/routes.ts` 的 `ROUTES`(前端跳转 URL)严格区分:endpoints=fetch 接口路径;ROUTES=页面路由(带 /en /cn)。命名上选 `endpoints` 而非 `apiRoutes`/`routes`,避免与 Astro 页面路由(及既有 `routes.ts`)混淆。 +- 替换范围:api.ts 21 处、ssg.ts 3 处(含 `?limit=` 模板)、request.ts 1 处(refresh)。共 25 处端点字面量清零;grep 确认仅剩注释与 endpoints.ts 定义常量。 +- 坑:`enforceIdleLogout` 那行 `auth: false }).catch` 有空格,首轮替换因 old_str 缺空格未命中(工具误报成功),已补替换;`getSsr*` 的 `${b}/api/v1/...` 模板改为 `${b}${endpoints.xxx(id)}`。 +- 验证:`astro build` 退出码 0,全部 11 路由预渲染成功(SSG 三端点经新常量正常解析)。 + +## 用户问「api 地址要不要抽象出来」→ 已落地核查(10:5x) +- 结论:地址**已经**集中在 `src/lib/config/`(目录,非单文件):`index.ts` 导出 `config`(`validate(isProd?prod:dev)`)、`types.ts` 定义 `AppConfig`、`env.dev.ts`/`env.prod.ts` 两份值。无 `config.ts` 单文件,import `"./config"` 解析到 `config/index.ts`。 +- 三把址:`config.baseApi`(浏览器运行期,request.ts:30)/ `config.baseApiSsr`(SSR 绝对 URL,request.ts:41,api.ts 经 ssrBase())/ `config.baseApiSsg`(构建期,request.ts:183)。**全站无直接 `import.meta.env.BASE_API` 读取**(memory 早先说的 `.env BASE_API` 已不实,dev/prod 值在 env.*.ts 硬编码)。 +- 仅两处「还散」:① `/api/v1` 版本前缀每个端点内联(api.ts 头注释有意为之,便于全局替换升 v2);② `SSG_TOKEN` 故意读 `import.meta.env.SSG_TOKEN`(密钥,不进客户端 bundle,安全设计勿动)。 +- 用户尚未决定:是否抽 `const API_PREFIX="/api/v1"` 常数。 + +## clientSign 安全性讨论(12:0x)→ 未改代码 +- 实证:`client-sign-secret-change-me` **明文出现在产物** `dist/client/_astro/api.BMQlzPjg.js`(esbuild 只压缩不加密字符串)。`crypto.ts` 是 HMAC 软门槛,非加密。 +- 结论:拒绝 VM 混淆/指纹(对内容站负收益,且救不了静态密钥);真正提难度靠服务端。建议方向=**服务端下发滚动密钥 + TTL/nonce 防重放 + IP 失败限流**(需动 backend_v2 的 `middleware.ClientSign`)。 +- 附带发现(**待修**):`crypto.ts:9` `CLIENT_SIGN_SECRET` 是**硬编码 dev 值、未走 env** → 生产要么功能失效要么形同虚设。 + +## 全站代码审计(12:19 起,只读,未改任何文件)→ 报告已出 +范围:js/css/libs/api/复用/目录/命名/性能。产出分级清单(artifact: code-review-plan.md)。核心发现: +- **P0-1 ⭐ 生产 SSR 仍在跑调试日志**:`lib/http.ts:16` `ENABLED = DEV || import.meta.env.SSR || DEBUG_API`——`import.meta.env.SSR` 在构建出的 Node 端恒 true,故生产每请求都 `res.clone()+await text()` 且 `writeLog()` 往容器 `logs/api-debug.log` **追加写入**(磁盘无限增长)。改法:ENABLED 去掉 SSR 分支。 +- **重复地图**:① `Item.astro`(1161行) vs `StreetSnap.astro`(645行) 灯箱逻辑逐行重复(仅 `#article-gallery`↔`#snap-gallery`),Item 多 density/detailLb → 抽 `createGalleryView()`;② `RunwayLooks` vs `StreetSnaps` 的 pills/goPage/prevPage/nextPage/pageList/markFavs/favState/toggleFav 同构 → 继续下沉 `looks-grid.ts`;③ `full()`(ssrBase 绝对化) 在 Item:17 / StreetSnap:18 逐字相同 → 抽 `ssrAbs()` 到 request.ts;④ `toAbsUrl(){return toAbs(url)}` 是空包装可直接删;⑤ account.astro 的 `favPageList`/`histPageList` 同一段逻辑写两遍。 +- **cn/en 页面**:6 对文件里 8 个**逐字节相同**,`account.astro` 仅差 1 行注释;语言差异 100% 在运行时(`Astro.currentLocale`),页面无任何语言分支 → 可抽共享页面组件。 +- **项目未启用 View Transitions**(Layout 无 ClientRouter,全局搜无):故 `Index.astro:579` 的 `astro:page-load` 监听是死代码(保留无害)。 +- **死配置/过时注释**:`astro.config.mjs` 空 `build{}` 块 + 第 29 行「英文在根路径」注释错误(prefixDefaultLocale:true 实际在 /en);`routes.ts:18-20` 过时注释(称街拍详情走 /street-snaps/[id],实际统一 /item/[id]);`Item.astro:1084` `// TODO 这是干啥的`;`pages/index.astro:3` 「应该跳转到 /en」未实现。 +- **遗留文件**:`_tmp_verify_hist.mjs`/`_tmp_verify2.mjs`/`dev.log`/`server.err`/`footertail.txt`/`README.md`(Astro 模板残留)。`.gitignore` 未覆盖 `_tmp*`/`*.err`/`footertail.txt` → 误入库风险。删除属破坏性操作,**待用户确认**。 +- **配置**:`package.json` `name:"test"`、**无 lint/format/check 脚本**(tsconfig 用 strict 却无处触发校验)。 +- **纠正子代理误判**:dictionary 的 key 大小写差异(`t('Home')` vs dict `home`)**不是死 key**——`translate` 走 `key.toLowerCase()` 查表,都能命中。勿再当死 key 处理。 +- 未深入:`favorites.ts`(8.3KB)、`history.ts`(6.5KB)、`look-grid.css`(8.9KB) 仅做了引用面检查,未逐行审。 +- 用户尚未选择要做哪几项。 + +## i18n 目录迁入 lib 并合并 ✅(17:26,方案 A + 合并 lib/locale) +用户问「i18n 移到 lib 合理吗 + 合成 i18n.ts」。判定:移入 lib 合理(本就是基础设施层,且 `lib/locale.ts` 原本反向 import i18n);合成单文件要当心 dictionary 会无限增长。 +- **新结构**:`src/lib/i18n/index.ts`(配置+工具+API语言透传,原 config.ts+utils.ts+lib/locale.ts 三合一)+ `src/lib/i18n/dictionary.ts`(纯数据,独立保留,随 UI 增长)。 +- **删除**:`src/i18n/{config,utils,dictionary}.ts` + `src/lib/locale.ts` + 空目录 `src/i18n`。 +- 消费方 16 处 import 改写:`@/i18n/utils`→`@/lib/i18n`、`@/i18n/config`→`@/lib/i18n`、`@/lib/locale`→`@/lib/i18n`、`lib/{ssr,request}.ts` 内 `./locale`→`./i18n`。`Layout.astro` 三段合并成一行 `import { getI18n, LOCALE_NAMES, setApiLocale } from '@/lib/i18n'`。 +- **循环依赖注意(良性)**:`index.ts` import `translate` from `./dictionary`,`dictionary.ts` import `DEFAULT_LOCALE/Locale` from `./index`。均「函数体内才用」,模块求值不触发 TDZ,安全。已在 index.ts 头注释标注。 +- 验证:`npm run build` Complete!;`npx astro check` **0 errors / 0 warnings / 5 hints**(hints 全是预存 `is:inline` 提示,非本次引入)。 + +## 清理 5 个 astro check hint ✅(19:18) +用户要求清掉上一轮残留的 5 hint。实际构成(非全 is:inline,前轮口误更正): +- `env.d.ts:8` ts(80003):原 `import type * as Alpine from "alpinejs"` → 改 `import type Alpine from "alpinejs"`(默认导入)。安全依据:tsconfig 继承 astro/strict 且 TS 主动提示 ts(80003) 即证明 esModuleInterop 已开,默认导入被允许;原注释"不能用默认导入"是过时误解,已重写。 +- `LoadingScreen.astro:10` ts(6133)×2:`getI18n(Astro)` 解构出的 `locale`/`getRelativeLocaleUrl` 未用 → 改为 `const { t } = getI18n(Astro)`。 +- `RunwayLooks.astro:263` / `StreetSnaps.astro:219` astro(4000):两个 JSON 数据 ` - - \ No newline at end of file diff --git a/src/components/LoadingScreen.astro b/src/components/LoadingScreen.astro index bfc2cd8..db4eccc 100644 --- a/src/components/LoadingScreen.astro +++ b/src/components/LoadingScreen.astro @@ -3,12 +3,17 @@ // 轻量原则(用户要求"朴实流畅"): // - 只用静态大字 + mono 小标 + 一条 2px 细线进度(transform/opacity 动画,最便宜); // - 去掉昂贵的多层 text-shadow 位移动画与外部字体;最短展示时间压缩到 ~350ms,跟手不拖沓。 + +import { config } from '@/lib/config'; +import { getI18n } from '@/lib/i18n' + +const { t } = getI18n(Astro) ---
@@ -88,15 +93,16 @@ + @@ -279,7 +279,7 @@ const seasonOptions: { value: string; label: string }[] = [ return { locale: '', - hotBrands, + hotBrands: hotBrands as { id: string; name: string }[], SPIN_SVG, // 侧栏分组折叠状态:默认全部收起(不做 localStorage 持久化,每次进页都是关着的)。 collapsedGroups: { diff --git a/src/components/StreetSnap.astro b/src/components/StreetSnap.astro index f38ea2f..a181e27 100644 --- a/src/components/StreetSnap.astro +++ b/src/components/StreetSnap.astro @@ -1,9 +1,10 @@ --- import type { RawStreetSnap } from "@/lib/api" import Breadcrumb from "@/components/Breadcrumb.astro" -import { getI18n } from "@/i18n/utils" +import GalleryLightbox from "@/components/GalleryLightbox.astro" +import { getI18n } from "@/lib/i18n" import { href, ROUTES } from '@/lib/routes' -import { ssrBase } from "@/lib/request" +import { ssrAbs } from "@/lib/request" interface Props { snap: RawStreetSnap | null @@ -15,16 +16,12 @@ interface Props { const { snap, error = null, preview = false, imageTotal = 0 } = Astro.props const { locale, t } = getI18n(Astro) -const full = (u: string): string => { - if (!u) return "" - if (/^https?:\/\//i.test(u)) return u - return ssrBase().replace(/\/$/, "") + u -} - const images = (snap?.images ?? []) .map((im) => ({ id: im.id, - url: full(im.image || ""), + url: ssrAbs(im.image || ""), + // 缩略图:灯箱左栏缩略图条只显示 76~118px,用后端给的 thumb 即可;未返回时回落展示图。 + thumb: ssrAbs(im.thumb || im.image || ""), raw: im.image || "", // 相对路径:收藏快照存相对地址,不把宿主/端口写死进库 name: im.name || "", })) @@ -200,445 +197,34 @@ const displayTitle = (snap?.title || "").replace(/\t/g, " · ") ) } + {/* 大图灯箱:与走秀详情共用 components/GalleryLightbox.astro(标记)+ lib/gallery.ts(交互)。 + 街拍无细节图分组,故不传 details 插槽。 */} {snap && previewImages.length > 0 && ( - +{t("member profile")}
+Portfolio Studio
+Archive + 2026 +
+Paris · Milan · Tokyo
+{itemsLabel}
+{noSavedLabel}
+ {browseLabel} +{itemsLabel}
+ +{noHistoryLabel}
+ {browseLabel} +{membersLabel}
+Portfolio Studio
+Archive + 2026 +
+Paris · Milan · Tokyo
+{promptLabel}
+ + + +{membersLabel}
-Portfolio Studio
-Archive - 2026 -
-Paris · Milan · Tokyo
-{promptLabel}
- - - -{t("member profile")}
-Portfolio Studio
-Archive - 2026 -
-Paris · Milan · Tokyo
-{itemsLabel}
-{noSavedLabel}
- {browseLabel} -{itemsLabel}
- -{noHistoryLabel}
- {browseLabel} -{membersLabel}
-Portfolio Studio
-Archive - 2026 -
-Paris · Milan · Tokyo
-{promptLabel}
- - - -