64 lines
7.0 KiB
Markdown
64 lines
7.0 KiB
Markdown
# 项目长期记忆(Project MEMORY.md)
|
||
|
||
> 维护约定:本文件只放**跨会话长期有效**的事实与铁律;当日细节写 `.workbuddy/memory/YYYY-MM-DD.md`。
|
||
> 历次合并去重:2026-09-15(大幅压缩;保留架构/约定/脚本铁律/类型坑/lib分层/验证手段/门禁/账号/i18n;删冗长示例与已过时待办)。
|
||
|
||
## 架构总览
|
||
- 前端 `d:/project/frontend_v2`:Astro(`adapter: node` standalone,SSR 取数)+ Tailwind v4 + Alpine.js。构建 `npm run build`;类型检查 `npm run check`(= `astro check`)。
|
||
- 后端 `d:/project/backend_v2`:Go + Gin + GORM(MySQL)。公开引擎 :8090(`/api/v1/public/*`、`/api/v1/auth/*`);SSG 引擎 :8091(仅 127.0.0.1,nginx 不反代)。
|
||
- 接口 i18n:后端按 `?locale=cn|en` 选列;**语言以 URL 前缀为准**;`prefixDefaultLocale: true`(/en、/cn 都带前缀;根 `/` 由服务端 302 到 /en)。
|
||
- **base 地址全在 `src/lib/config/`**:`config.baseApi`(浏览器) / `config.baseApiSsr`(SSR 绝对) / `config.baseApiSsg`(构建期内部端口)。密钥类(SSG_TOKEN)直读 env。
|
||
- 后端 Windows 编译:`go build -o server-bin.exe`(非 `server-bin`)。前端 dev 不热加载 `.env`,改 base 后 `astro dev stop`+`--background`+浏览器硬刷新。
|
||
- `504 (Outdated Optimize Dep)`:删 `node_modules/.vite` 后重启 dev。
|
||
|
||
## 关键约定
|
||
- 视觉:黑白极简编辑风,纯黑白禁用暖米色/柔和阴影;字体走 Tailwind 体系(系统字体栈,不引外部字体)。
|
||
- 跨组件复用原子样式放 `global.css`(Astro 组件 `<style>` 有作用域哈希,跨组件命中不了)。现含 `.fav-btn`/`.locate-flash`/`@media(hover:none)`/`.no-scrollbar`/`[x-cloak]`。
|
||
- Tailwind v4:`<style>` 顶部 `@reference "tailwindcss";`(编译期指令,不进 HTML);无零值类;能 @apply 就 @apply。
|
||
- Header 下拉 `x-show` 一律带 `x-cloak` 防 FOUC。Layout `<slot/>` 包 `flex-1 flex flex-col`;页内根容器禁用 `m-auto`(用 `w-full`+`max-w-[x] mx-auto`)。
|
||
- 未启用 View Transitions。
|
||
|
||
## Astro `<script>` 铁律(踩坑汇总)
|
||
1. 打包型 `<script>` 拿不到 frontmatter 变量,需脚本顶部重新 import。
|
||
2. **`define:vars` + `import` = 致命**(包成 IIFE 后 import 进函数体 → SyntaxError)。要 import 改用 `data-*` 透传。
|
||
3. 片段页(无 `<html>`)`import '@/styles/global.css'` build 不注入 head → 整页无样式。
|
||
4. **Alpine 表达式只认组件 data / `$magic` / global**,外部函数先存进 data 属性(如 `isAuthed`、`toAbsUrl` 看似空包装但不能删)。
|
||
5. 验证 Alpine 运行时务必真跑浏览器,只看 SSR HTML 会漏掉绑定失效。
|
||
6. 共享组件 `Alpine.data('name', () => ({...}))` + `x-data="name()"`;由工厂展开须显式标注返回类型。
|
||
7. Swiper:`initSwipers()` 先 `destroySwipers()`;`setupSwipers()` rAF 延后 + `DOMContentLoaded`;Tab 用 document 事件委托。
|
||
8. IntersectionObserver 用模块级单例(如 gallery 的 `thumbObserver`),重建前 `?.disconnect()`(别放 Alpine data 属性被代理包一层)。
|
||
|
||
## 类型检查与 Alpine 类型推断(2026-09-14 引入 `npm run check`,已修到 0)
|
||
- 坑 A:`@types/alpinejs` 自引用约束使 data 属性退化 `unknown`/`{}` → 属性显式标注(`user: null as AuthUser|null`、`favIds: [] as string[]`);`Alpine.store` 先 `const s: Store = {...}` 再注册。
|
||
- 坑 B:对象展开让 T 退化 `{}` → 给回调显式标注返回类型 `(): ReturnType<typeof createGalleryView> & X => ({...})`。
|
||
- 排查铁律:「Property does not exist on type '{}'」/「Object of type 'unknown'」先怀疑这两类,别怀疑业务代码。
|
||
- `astro check` 的 stdout 会被吞 → `npx astro check 2>&1 | Out-File -FilePath x.txt -Encoding utf8` 再读;`Get-Content` 须带 `-Encoding utf8`。
|
||
|
||
## lib 模块分层(2026-09-14 核对磁盘)
|
||
- `i18n/index.ts`(配置+工具+API语言透传:原 config/utils/lib/locale 三合一)+ `i18n/dictionary.ts`(纯数据,独立保留)。
|
||
- `auth.ts` 仅会话存储原语;`api.ts` 业务取数+鉴权(`login/logout/fetchMe/enforceIdleLogout` 有意留 api.ts,避免 auth↔request 循环);`ssg.ts` 构建期、`ssr.ts` 运行期、`gallery.ts` 灯箱工厂、`crypto.ts` 签名、`config/` base 址、`endpoints.ts` 后端路径唯一来源、`routes.ts` 前端跳转 URL(带前缀,`href` 用 astro:i18n、`clientHref` 浏览器内自拼)、`http.ts` 调试日志(开关 `DEV||DEBUG_API`)、`favorites.ts`/`history.ts`。
|
||
- 依赖无环:api→{request,crypto,auth,i18n,endpoints};ssr→{request,endpoints,i18n,api(type)};ssg→{request,api(type),endpoints};gallery→{request,auth,favorites};request→{crypto,auth,i18n,config,endpoints}。
|
||
|
||
## 验证手段
|
||
- 改完必跑 `npm run build`(全链路 import + 预渲染)+ `npm run check`。
|
||
- 预渲染页:`dist/client/{en,cn}/<page>/index.html` 归一化对比(去 `astro-cid-*` 与 `_astro/<asset>` 逐字节比)。
|
||
- `prerender=false` 页:临时建 prerender 验证页喂假数据,或 dev :4321 时 curl 真实 SSR(`curl :8090/api/v1/public/runway-looks` 的 `data` 直接是数组)。HTML 单行用 `[regex]::Matches($h,'...').Count` 统计关键标记。
|
||
|
||
## 未登录预览门禁(详情页)
|
||
- 后端硬截断:`/runway-looks/:id`、`/street-snaps/:id` 匿名且图>5 时截断前 5 张 + 回 `preview:true`+`image_total`;已登录(Bearer)返回完整。列表接口全量。
|
||
- 前端:SSR 前 5 张 + 剩余空图遮罩(`x-show="!isAuthed && preview"`+`x-cloak`,"login to view" + "+N photos");登录后取全量追加 `extraImages`。`getUser()` 读 localStorage `fa_user`。
|
||
|
||
## 账号体系
|
||
- 仅预置内部账号(admin/root)。双令牌 JWT(access 7d 无状态 + refresh 30d 仅存 SHA256 落库);登录即吊销该用户全部 refresh。
|
||
- 收藏服务端化 `favorites` 表(按 user_id 隔离,编码串 r=/s=、i=/j=);浏览历史 `histories` 表(唯一键 user_id+target_uid,FIFO 上限 2000)。
|
||
|
||
## i18n 字典约定
|
||
- `dict: Record<string,{cn:string}>`,key 即英文原文(en 返 key),value 只存非默认语言(仅 cn)。`translate(key,locale)` 对 key `toLowerCase()` 查(大小写不敏感,`t('Home')` 命中 `home`);缺翻译 en 回退 + dev 告警。中文绝不可当 key。
|
||
|
||
## 记住语言偏好(2026-09-15 新增)
|
||
- 需求:选过语言后下次进首页直接用上次语言。
|
||
- `i18n/index.ts` 新增 `STORED_LOCALE_KEY='preferred_locale'`、`storeLocale(locale)`、`getStoredLocale()`。
|
||
- Layout `<head>` 顶部 `is:inline`+`define:vars` 脚本:进首页(path==='/' 或 '/'+defaultLocale)且记住非默认语言时 `location.replace('/'+stored)`。
|
||
- 语言切换器 `x-data="langSwitch()"`;`<a>` `@click="open=false; pick('<code>')"`;footer 注册 `langSwitch`(`pick(code){ storeLocale(code) }`)。
|
||
- **铁律:只在显式点击切换器时写 localStorage,绝不按页面加载 URL 写**——否则与首页自动跳转冲突(点了英文又被 URL 的 en 覆盖,首页永远跳不回去)。
|