Files
frontend_v2/.workbuddy/memory/MEMORY.md
toom1996 3f79cef587 update
2026-09-15 19:59:14 +08:00

7.0 KiB
Raw Blame History

项目长期记忆(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 覆盖,首页永远跳不回去)。