Web 端本地缓存、首页与会员弹窗体验优化

新增 IndexedDB 与最近文件 L1 缓存、云保存/打开链路;首页支持下载与清空本地缓存;修复清空 storage 后语言初始化报错;优化 design2 背景加载与侧栏标签样式;完善会员激活/支付弹层样式;后端补充文件缩略图字段与 thumb 接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
24kycj
2026-06-11 11:05:44 +08:00
parent 877fd278c2
commit 4ce4486b26
33 changed files with 3190 additions and 1143 deletions
+5 -2
View File
@@ -168,7 +168,8 @@ GET /api/v1/pay/orders?page=1&size=8
| `GET /api/v1/templates` | 首页模板列表(仅 `id/name/type/updated_at`;缩略图按需拉取) |
| `GET /api/v1/templates/{id}` | 模板 JSON 数据(门户标准读取端点,见下) |
| `GET /api/v1/templates/{id}/thumb` | 模板缩略图(从 `.soon``frontDisplayPic` 提取) |
| `GET /api/v1/templates/{id}/file` | **legacy**:裸 `.soon` 流;门户禁止调用,仅供兼容 |
| `GET /api/v1/templates/{id}/file` | `.soon` 流(`Cache-Control: max-age=86400`);门户读缓存首选 |
| `GET /api/v1/files/{id}/thumb` | 云文件缩略图(需登录;从 `.soon``frontDisplayPic` 提取) |
| `GET /api/v1/soon-models` | 兼容别名,同 `GET /api/v1/templates` |
#### `GET /api/v1/templates/{id}`(门户读取模板数据)
@@ -196,10 +197,11 @@ GET /api/v1/pay/orders?page=1&size=8
| 前缀 | 含义 | readJsonFile | 保存 writeFile |
|------|------|--------------|----------------|
| `soondesign_file:{id}:v{ver}` | 云端已登记文件 | `GET /files/{id}` | `PUT /files/{id}` |
| `soondesign_template:{id}` | 云端模板(只读源) | `GET /templates/{id}` | `POST /files`(首次保存新建) |
| `soondesign_template:{id}` | 云端模板(只读源) | `GET /templates/{id}/file` | `POST /files`(首次保存新建) |
| `soondesign_session:...` | 本地/临时会话 | sessionStorage | 已登录 `POST /files`;未登录写 session |
- **保存/打开**走 JSON API**仅**首页「下载」走 `GET /files/{id}/download` 落盘 `.soon`
- 门户**读缓存**与最近文件本地优先策略见 [`WEB-LOCAL-CACHE.md`](WEB-LOCAL-CACHE.md);模板首次加载推荐 `GET /templates/{id}/file`(流式 `.soon`,避免 `GET /templates/{id}` 二次包装)
---
@@ -236,3 +238,4 @@ GET /api/v1/pay/orders?page=1&size=8
|------|------|
| 2026-06-08 | 初版:统一 page/size/totalfiles 兼容 limit/offsetpay/orders 分页 |
| 2026-06-08 | 新增 GET /templates/{id} JSON 契约;Web 虚拟 key 前缀;/file 标 legacy |
| 2026-06-08 | 新增 GET /files/{id}/thumbfiles list 增 `has_thumb`;模板 /file 长缓存 |
+400
View File
@@ -0,0 +1,400 @@
# Web 端本地缓存与最近文件优化(开发计划 · 细化版)
> 状态:P0P7 已实施。与 [API-PAGINATION.md](API-PAGINATION.md)「Web 虚拟 key」交叉引用。
---
## 0. 背景与约束
### 0.1 问题(已核实代码)
| 现象 | 根因 | 代码位置 |
|------|------|----------|
| `GET /templates/{id}` 2030s | PHP 二次 `json_encode` + 大响应 + 客户端双次 parse | `TemplateService::fetchPublicJson``web.js` ~268 行 |
| 重复打开仍打源站 | 无 IndexedDB | 全库无 `indexedDB` |
| 首页最近文件慢 | 每卡 `readJsonFile` 全量 | `index.js` ~381 行 |
| 保存后不显示 | Web `onCloudWriteDone` 未写 history | `design1/output.js` ~1266;仅 fs 分支 `saveHistory` |
| session 易超配额 | 大 JSON 写 session/localStorage | `cloud-files.js` `soonPutSoonSession` |
### 0.2 硬约束
- **不改** `.soon` 格式;**保留** base64。
- **不上 CDN**;靠本机 IDB + 传输路径精简 + L1 元数据。
- 保存仍 `POST/PUT /files` 传完整 `json`**仅读**本地优先。
- 配置:`frontend-web/config/local.js``backend-web/config/local.php`
### 0.3 三层架构
| 层 | 存储 | 内容 | 体积 |
|----|------|------|------|
| L1 | `localStorage` `soondesign_recent` | key/name/type/kind/fileId/thumbRef/time | <50KB |
| L2 | IndexedDB `soondesign_local` | 完整 `.soon` 对象 + thumbs | ≤20 条 blob |
| L3 | HTTP API | miss / 保存 / 可选元数据对账 | 按需 |
### 0.4 脚本加载顺序(三页统一)
`index.web.html` / `design1.web.html` / `design2.web.html`
```
config/local.js → auth → cloud-files.js → soon-local-store.js → soon-recent.js → web.js → 页面逻辑
```
---
## 1. 数据契约
### 1.1 L1`soondesign_recent`
```json
{
"version": 1,
"items": [
{
"key": "soondesign_file:12:v3",
"name": "我的卡片.soon",
"type": 1,
"kind": "cloud",
"fileId": 12,
"thumbRef": "soondesign_file:12:v3",
"time": "2026-06-08 14:30:00"
}
]
}
```
- `thumbRef`:与 L2 `thumbs` 的 cacheKey 相同(非独立字符串前缀)。
- 上限 **20**`soonRecentUpsert` 去重后 unshift,截断尾部。
- **首页以 L1 为展示主数据源**;`GET /files` 仅后台补 `name`/`version`,不默认注入未打开过的云端文件。
### 1.2 L2IndexedDB `soondesign_local` v1
| Store | keyPath | value |
|-------|---------|--------|
| `blobs` | `cacheKey` | `{ json, name, type, source, updatedAt, savedAt, bytes }` |
| `thumbs` | `cacheKey` | `{ dataUrl, savedAt }` |
**cacheKey**
| 来源 | cacheKey | 失效 |
|------|----------|------|
| 云文件 | `soondesign_file:{id}:v{ver}` | PUT 后 version+1 → 新 key;旧 blob 可 `soonLocalRemove` |
| 模板 | `soondesign_template:{id}` | 见 §1.4 |
| 会话 | `soondesign_session:...` | 迁入云 key 后 `soonLocalRenameKey` |
**LRU**`soonLocalEvictLRU(20)` 在每次 `put` 后执行;仅删 blob+thumb**不删 L1**。
### 1.3 L3:读取端点
| 场景 | 端点 | 禁止 |
|------|------|------|
| 模板首次 | `GET /api/v1/templates/{id}/file` | 门户默认不用 `GET /templates/{id}` 包装 |
| 云文件 miss | `GET /api/v1/files/{id}` | — |
| thumb 兜底 | `GET /templates/{id}/thumb`P6 `GET /files/{id}/thumb` | 禁止为 thumb 拉全量 json |
| 保存 | `POST`/`PUT /files` | 不变 |
### 1.4 模板缓存失效策略(细化)
1. 首页点模板前,`index.js` `openTemplateItem` 写入 `window._soonTemplateMeta = { id, name, type, updated_at }`(列表已有 `updated_at`)。
2. IDB `blobs``meta.updatedAt` 存该 `updated_at`
3. `readJsonFile` IDB hit 时:若 `_soonTemplateMeta.updated_at` 存在且与 blob.meta.updatedAt 不一致 → 删 blob`miss` 走网络。
4. 网络:`/templates/{id}/file` + parse`soonLocalPut` 带最新 `updatedAt`
5. 未经过首页的深链打开:无 meta 比较,信任 IDB(可接受;刷新列表后校正)。
### 1.5 共享工具 `soonExtractThumbFromJson(json)`
- 位置:`soon-local-store.js``soon-recent.js`
-`json.frontDisplayPic`;须 `data:image/` 开头。
- 超过 **102400** 字节则返回 `''`(不写 thumbs,首页用 type 默认图)。
- 打开/保存/导入成功后统一调用。
---
## 2. 核心流程
### 2.1 打开(readJsonFile
```mermaid
sequenceDiagram
participant UI as design_page
participant IDB as L2_IDB
participant L1 as L1_recent
participant API as L3_server
UI->>IDB: soonLocalGet(cacheKey)
alt hit_and_valid
IDB-->>UI: json
else miss_or_stale
UI->>API: GET file_or_files
API-->>UI: soon_bytes
UI->>IDB: soonLocalPut + PutThumb
UI->>L1: soonRecentUpsert
end
UI->>UI: openFile_loadFromJSON
```
**补充接入点**P3):`doOpenWithJson` 完成画布加载后 `soonRecentUpsert`(模板/云文件/会话均记录,kind 由 key 推断)。
### 2.2 保存(writeFile + onCloudWriteDone
**模板首次保存**`soondesign_template:*` 或空 key 时弹窗,默认文件名 `design` + `yyMMddHHmmss` + `.soon`(如 `design260610103010.soon`);保存成功后 L1 用 cloud/local 条目替换原 template 项(`soonRecentOnCloudSave` + `prevOpenKey`)。
```mermaid
sequenceDiagram
participant UI as save
participant IDB as L2
participant L1 as L1
participant API as server
UI->>IDB: soonLocalPut当前json
UI->>L1: soonRecentUpsert
alt cloud_key
UI->>API: PUT
API-->>UI: new_version
UI->>IDB: RenameKey_or_new_put
UI->>L1: upsert新key
else new_or_template
UI->>API: POST
end
```
### 2.3 首页最近文件
```mermaid
sequenceDiagram
participant Index as index
participant L1 as L1
participant IDB as thumbs
participant API as metadata_optional
Index->>L1: soonRecentList
loop each_card
Index->>IDB: soonLocalGetThumb(key)
alt no_thumb
Index->>Index: soonAsset默认图或thumb_URL
end
end
opt logged_in
Index->>API: listCloudFiles元数据
Index->>L1: 仅补name/version不增项
end
```
### 2.4 删除卡片
| 操作 | L1 | L2 | 服务器 |
|------|----|----|--------|
| 删本地项 | `soonRecentRemove` | `soonLocalRemove` | — |
| 删云文件 | `soonRecentRemove` | `soonLocalRemove` | `DELETE /files/{id}` |
| 旧 `removeLocalHistoryPath` | 改为调上述 API | 同上 | — |
---
## 3. 分阶段任务(可独立验收)
### Phase 0:契约文档 — 已完成
- [x] 本文档
- [x] `API-PAGINATION.md` 链接与 `/file` 推荐说明
---
### Phase 1IndexedDB 模块(1.5h
**新建** [`frontend-web/js/common/soon-local-store.js`](frontend-web/js/common/soon-local-store.js)
| 任务 ID | 任务 | 细节 |
|---------|------|------|
| P1.1 | `soonLocalOpen` | `indexedDB.open('soondesign_local', 1)``onupgradeneeded``blobs`/`thumbs` |
| P1.2 | `soonLocalGet/Put` | `json` 存对象;`bytes` 估算 `JSON.stringify` 长度 |
| P1.3 | `soonLocalPutThumb/GetThumb` | thumbs store |
| P1.4 | `soonLocalRemove` | 同时删 blobs+thumbs |
| P1.5 | `soonLocalRenameKey` | 读旧→写新→删旧(session→cloud |
| P1.6 | `soonLocalEvictLRU` | 按 `savedAt` 升序删至 ≤20 |
| P1.7 | `soonExtractThumbFromJson` | §1.5 |
| P1.8 | 降级 | `indexedDB` 不可用 → 所有 get 返回 null,put 静默失败 |
| P1.9 | HTML 引入 | 三页在 `web.js` 前引入 |
**验收**
- [ ] DevTools Application → IDB 可见 stores
- [ ] put/get 往返;第 21 条触发 evict
- [ ] 隐私模式不抛未捕获异常
---
### Phase 2web.js 读写穿透(2h
**改** [`frontend-web/js/platform/web.js`](frontend-web/js/platform/web.js)
| 任务 ID | 任务 | 细节 |
|---------|------|------|
| P2.1 | `resolveCacheKey(key)` | 模板/云/ session 统一;云用完整 `soondesign_file:id:vN` |
| P2.2 | read 模板 stale | §1.4miss 走 `GET .../templates/{id}/file` + `text()` + `JSON.parse` |
| P2.3 | read 云文件 | IDB miss → 现有 `authedFetch files/{id}`;成功后 put |
| P2.4 | read session | IDB → sessionStorage 兜底(过渡期) |
| P2.5 | read 后写 thumb | `soonExtractThumbFromJson` + `soonLocalPutThumb` |
| P2.6 | writeFile 成功 | `soonLocalPut`;云 POST/PUT 成功后若 key 变化 `soonLocalRenameKey` |
| P2.7 | write 未登录 | session key + IDB 双写;**停止** `localStorage.setItem(sessionKey, 大JSON)` |
**验收**
- [ ] 模板首次:Network 仅 `/templates/{id}/file`;二次 0 请求
- [ ] 云文件二次打开无 `GET /files/{id}`
- [ ] PUT 后 `openAs.name` 与新 version key 一致且 IDB 可命中
---
### Phase 3L1 最近列表 + 保存/打开写 L1(2h)
**新建** [`frontend-web/js/common/soon-recent.js`](frontend-web/js/common/soon-recent.js)
| 任务 ID | 任务 | 细节 |
|---------|------|------|
| P3.1 | `soonRecentUpsert` | 按 `key` 去重;字段 §1.1`getDate()` 时间格式与现 `saveHistory` 一致 |
| P3.2 | `soonRecentList/Remove` | — |
| P3.3 | `soonRecentKindFromKey` | `cloud`/`template`/`local` |
| P3.4 | `soonRecentMigrateFromHistory` | 读 `soondesign_history``path``key`;无 thumb**仅执行一次**`soondesign_recent_migrated` 标记) |
| P3.5 | `onCloudWriteDone` | design1/2 `output.js`upsert`res.fileKey`,`res.name`, soonType |
| P3.6 | `saveHistory` | Web`soonRecentUpsert`Electron fs 路径保留 |
| P3.7 | `cloud-files` | `soonOpenSoonJsonLocally`、import 成功 upsert |
| P3.8 | `openFile` 加载成功 | design1 `doOpenWithJson` 末尾;design2 `saveInitialState` 末尾 upsert |
| P3.9 | `index` 开模板 | `openTemplateItem` 设置 `_soonTemplateMeta`(§1.4 |
**验收**
- [ ] 云端 Ctrl+S 后回首页立即见卡片
- [ ] 打开模板/云文件后 L1 有对应项
- [ ] 旧 history 用户升级后 L1 有条目(可无 thumb)
---
### Phase 4:首页 loadHistory 改造(1.5h
**改** [`frontend-web/js/index.js`](frontend-web/js/index.js)
| 任务 ID | 任务 | 细节 |
|---------|------|------|
| P4.1 | 数据源 | `soonRecentList()` 为主;启动时 `soonRecentMigrateFromHistory` |
| P4.2 | 删除 merge 云端追加 | 移除 `mergeRecentItems` 中「云端未见过则 push」逻辑;改为 `enrichFromCloud(items, cloudMeta)` 只补 name/version |
| P4.3 | 渲染 thumb | `soonLocalGetThumb(item.key)` → fallback `soonAsset(bg_1/2)`;模板项可用 `/thumb` URL`updated_at``?v=` |
| P4.4 | 删除 readJsonFile 循环 | 移除 ~381 行;**删除** `fileExists` 灰显逻辑(改为始终可点;打开时 miss 再拉+toast |
| P4.5 | 删除 | `removeLocalHistoryPath``soonRecentRemove` + `soonLocalRemove` |
| P4.6 | 刷新 | `pageshow`/`visibilitychange``loadHistory`60s 节流 `_recentLoadedAt` |
| P4.7 | 分页 | 保留 `fileListState` 对 L1 items 分页 |
**验收**
- [ ] 首页 Network0× 全量 `files/{id}` / `templates/{id}`
- [ ] 12 卡仅 thumb 图或 0 额外请求(IDB 命中)
- [ ] 从设计页返回可见新保存项
---
### Phase 5session 迁移与清理(1h
| 任务 ID | 任务 | 细节 |
|---------|------|------|
| P5.1 | `soonPutSoonSession` | 正文 IDBsessionStorage 仅存 `{key}` 标记或短指针 |
| P5.2 | `readJsonFile` session | 优先 IDB |
| P5.3 | `soonTryConsumeCloudImport` | 成功:`soonLocalRenameKey` + `soonRecentUpsert` + 删 session |
| P5.4 | 清理 | 移除 `cloud-files`/`index` 对大 JSON 的 localStorage 写入 |
| P5.5 | `index` 删卡 | 清 sessionStorage 旧 key(保留现有 769 行逻辑并接 IDB) |
**验收**
- [ ] 本地 .soon → 编辑 → 保存 → key 为 `soondesign_file:*`
- [ ] sessionStorage 无 >1MB 字符串
---
### Phase 6:后端轻量增强(可选,1h)
| 任务 ID | 文件 | 细节 |
|---------|------|------|
| P6.1 | `TemplateService::outputFile` | `Cache-Control: public, max-age=86400, must-revalidate` |
| P6.2 | `FileController` + 路由 | `GET /api/v1/files/{id}/thumb` |
| P6.3 | `FileService` | 上传/更新时 `extractThumb` 入 DB(可复用 `TemplateService` 逻辑);`list``has_thumb` |
| P6.4 | `docs/API-PAGINATION.md` | 登记 thumb 端点 |
**验收**
- [ ] `curl -I .../templates/1/file``max-age=86400`
- [ ] 云 thumb 返回 `image/jpeg`
---
### Phase 7:全链路验收(0.5h
| # | 场景 | Network | UI |
|---|------|---------|-----|
| 1 | 首次开模板 | 1× `/templates/{id}/file` | 画布 OK |
| 2 | 再次开同模板 | 0 全量 | <1s |
| 3 | 模板改 updated_at 后再开 | 1× `/file` | 新内容 |
| 4 | 保存新文件(登录) | POST | 回首页有卡 |
| 5 | 再次开云文件 | 0 全量 | PUT 可保存 |
| 6 | 首页 12 最近 | 0 全量 json | thumb OK |
| 7 | 未登录保存 | 无 API | L1+IDB |
| 8 | 删云文件卡 | DELETE | L1+IDB 清除 |
| 9 | IDB 满 20 再开第 21 | 1× 网络 | 最旧 blob 淘汰 |
| 10 | 模板列表 | 1× `/templates` 元数据 | 不变 |
**部署**:前端 P1P5 同批;强刷 `soon-local-store.js`/`soon-recent.js`/`web.js`/`index.js`P6 可稍后。
---
## 4. 改动文件清单
| 阶段 | 文件 |
|------|------|
| P1 | `soon-local-store.js``pages/index|design1|design2.web.html` |
| P2 | `platform/web.js` |
| P3 | `soon-recent.js``design1/2/output.js``cloud-files.js``index.js`meta |
| P4 | `index.js` |
| P5 | `cloud-files.js``web.js` |
| P6 | `TemplateService.php``FileController.php``FileService.php``public/index.php``API-PAGINATION.md` |
**不改动**`.soon` schema`design*-back.js`Electron(后续可选对齐)。
---
## 5. 依赖关系
```mermaid
flowchart TD
P0[P0_done]
P1[P1_IDB]
P2[P2_web_bridge]
P3[P3_recent_L1]
P4[P4_index]
P5[P5_session]
P6[P6_backend_opt]
P7[P7_QA]
P0 --> P1 --> P2 --> P3 --> P4 --> P5 --> P7
P2 --> P6 --> P7
```
**顺序**P1 → P2 → P3 → P4 → P5 → P7P6 与 P3/P4 并行。
---
## 6. 风险与缓解
| 风险 | 缓解 |
|------|------|
| IDB 配额 | LRU 20thumb ≤100KBevict 失败 toast |
| 多设备 | 云 version 为准;409 提示刷新 |
| 模板 stale 漏检 | 首页列表带 `updated_at`;§1.4 |
| PUT 后双 version 缓存 | `onCloudWriteDone` rename + 删旧 key |
| 隐私模式 | 降级 session+网络,与现网一致 |
| 旧用户无 recent | `soonRecentMigrateFromHistory` 一次 |
---
## 7. 不在 scope
- CDN`.soon` 剥离 base64Electron IDBCRDT 离线合并。
---
**确认后**按 P1→P7 实施;每阶段勾选任务 ID 验收后再进入下一阶段。