Browse Source

feat(toast): implement autoclose fallback for toast notifications

Add a plugin to enhance toast notifications with an autoclose fallback mechanism, ensuring that notifications are removed after a specified duration. This includes handling timers for toast removal and preventing duplicate plugin application.

Made-with: Cursor
main
npmrun 2 weeks ago
parent
commit
081d70f551
  1. 53
      app/plugins/toast-autoclose-fallback.client.ts
  2. 170
      docs/export-feature-recheck-2026-04-24.md
  3. BIN
      packages/drizzle-pkg/db.sqlite

53
app/plugins/toast-autoclose-fallback.client.ts

@ -0,0 +1,53 @@
const TOAST_PATCH_FLAG = '__person_panel_toast_patch_applied__'
const DEFAULT_TOAST_DURATION_MS = 5000
const FALLBACK_EXTRA_MS = 400
export default defineNuxtPlugin(() => {
const g = globalThis as typeof globalThis & { [TOAST_PATCH_FLAG]?: boolean }
if (g[TOAST_PATCH_FLAG]) {
return
}
const toast = useToast()
const originalAdd = toast.add.bind(toast)
const originalRemove = toast.remove.bind(toast)
type ToastId = Parameters<typeof toast.remove>[0]
const timers = new Map<ToastId, ReturnType<typeof setTimeout>>()
const clearFallbackTimer = (id: ToastId) => {
const timer = timers.get(id)
if (!timer) {
return
}
clearTimeout(timer)
timers.delete(id)
}
const scheduleFallbackRemove = (id: ToastId, duration?: number) => {
clearFallbackTimer(id)
const finalDuration = typeof duration === 'number' ? duration : DEFAULT_TOAST_DURATION_MS
if (finalDuration <= 0) {
return
}
const timer = setTimeout(() => {
originalRemove(id)
timers.delete(id)
}, finalDuration + FALLBACK_EXTRA_MS)
timers.set(id, timer)
}
toast.add = ((input: Parameters<typeof toast.add>[0]) => {
const created = originalAdd(input)
scheduleFallbackRemove(created.id, created.duration)
return created
}) as typeof toast.add
toast.remove = ((id: ToastId) => {
clearFallbackTimer(id)
originalRemove(id)
}) as typeof toast.remove
g[TOAST_PATCH_FLAG] = true
})

170
docs/export-feature-recheck-2026-04-24.md

@ -0,0 +1,170 @@
# 导出功能复核与开发指引(2026-04-24)
## 1. 文档目的
本文件用于后续迭代开发时快速理解“用户数据导出”功能的当前实现状态、行为边界与已知风险,避免重复摸索。
---
## 2. 复核范围
本次复核覆盖:
- 后端 API:创建 / 列表 / 下载 / 删除
- 服务层:任务状态机、任务删除与目录清理、导出执行流程
- 导出产物:`data/`、`files/`、`manifest.json`
- 前端导出中心交互:创建、刷新、下载、删除、重新导出
- 关键测试可执行性
---
## 3. 关键代码位置
- API
- `server/api/me/export/request.post.ts`
- `server/api/me/export/tasks.get.ts`
- `server/api/me/export/tasks/[id]/download.get.ts`
- `server/api/me/export/tasks/[id].delete.ts`
- 服务层
- `server/service/export/jobs.ts`
- `server/service/export/run.ts`
- `server/service/export/build-data.ts`
- `server/service/export/build-files.ts`
- `server/service/export/build-manifest.ts`
- 前端
- `app/pages/me/export/index.vue`
- `app/types/export.ts`
- 测试
- `server/service/export/jobs.test.ts`
- `server/service/export/build-manifest.test.ts`
- `server/utils/me-export-request-body.test.ts`
---
## 4. 当前功能状态(复核结论)
### 4.1 已实现并可用
- **任务生命周期**
- 支持 `queued -> running -> succeeded/failed/expired`
- 同一用户存在 `queued/running` 任务时,禁止再次创建(409)
- **导出产物构建**
- 导出目录结构:
- `manifest.json`
- `data/*.json`
- `files/*`
- `manifest` 包含:
- `schemaVersion`
- `exportedAt/exportCutoffAt`
- `maskPolicy`
- `stats`
- `checksums.data/files`
- **下载完整包**
- 下载接口返回完整压缩包(`tar.gz`)
- 通过 `tar -czf -` 流式返回,不落地额外压缩文件
- **任务列表自愈**
- 刷新任务列表时会检查已完成任务的导出文件可用性
- 缺失/过期会自动标记为 `expired` 并写入错误信息
- **任务删除**
- `DELETE /api/me/export/tasks/:id`
- 非 `running` 任务可删
- 删除时尝试清理对应 `.tmp/exports` 目录,并做路径边界保护
- **前端交互**
- 支持创建、刷新、下载、删除
- `expired` 任务显示“重新导出”按钮
---
## 5. 实测结果(本地复核)
执行命令:
```bash
bun test server/service/export/jobs.test.ts
bun test server/service/export/build-manifest.test.ts
bun test server/utils/me-export-request-body.test.ts
```
结果:
- `server/service/export/jobs.test.ts`:通过(12 pass)
- `server/service/export/build-manifest.test.ts`:通过(1 pass)
- `server/utils/me-export-request-body.test.ts`:失败(`Cannot find package 'h3'`)
---
## 6. 已知问题与风险
### R1(中):`me-export-request-body` 单测依赖问题
- 现象:测试环境无法解析 `h3`,导致 `server/utils/me-export-request-body.test.ts` 失败。
- 影响:该 util 的单测在当前环境不稳定,CI/新开发机可能复现。
- 建议:
- 方案 A:`me-export-request-body.ts` 改为抛项目通用错误对象(`{ statusCode, statusMessage }`),不直接依赖 `h3`
- 方案 B:明确把 `h3` 作为可解析依赖并固定版本,确保测试环境一致。
### R2(中):下载包格式依赖系统 `tar`
- 当前下载使用子进程 `tar` 打包,依赖运行环境存在 `tar` 命令。
- 建议:
- 在部署文档里明确依赖项;
- 或改为 Node 库打包,避免系统命令依赖。
### R3(中):下载失败时状态修正为 `expired` 的语义边界
- 当前将“文件缺失/打包失败/过期”统一落到 `expired`
- 建议:
- 若需更细粒度运维排障,可新增状态或错误码区分(如 `EXPORT_MISSING`、`EXPORT_PACKAGE_FAILED`)。
---
## 7. 对外接口行为(当前版本)
### 7.1 创建任务
- `POST /api/me/export/request`
- body:`{ "maskPolicy": "masked" | "raw" }`(可省略,默认 `masked`
- 返回:`taskId`, `status`
### 7.2 获取任务列表
- `GET /api/me/export/tasks`
- 返回当前用户任务
- 返回前会自动做“已完成任务文件存在性检查”,必要时改为 `expired`
### 7.3 下载任务产物
- `GET /api/me/export/tasks/:id/download`
- 仅允许下载本人且 `succeeded` 的任务
- 返回完整 `tar.gz`
- 常见错误:
- `409`:任务未完成
- `410`:任务过期或文件丢失(并同步标记为 `expired`
### 7.4 删除任务
- `DELETE /api/me/export/tasks/:id`
- 仅允许删除本人任务
- `running` 任务返回 `409`
---
## 8. 后续开发建议(按优先级)
1. **修复 R1**:先解决 `h3` 解析导致的单测不稳定。
2. **补 API 级测试**:增加 `request/tasks/download/delete` 的集成测试。
3. **下载格式可配置**:支持 `zip`(前端兼容性更好)或可选 `tar.gz/zip`
4. **导出清理任务**:增加后台任务,定期清理已过期目录与旧任务数据。
5. **更细状态与错误码**:提高运维可观测性。
---
## 9. 最终结论
- **功能可用性**:通过(可用于后续开发迭代)
- **工程稳定性**:有条件通过(需优先修复 R1)

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.
Loading…
Cancel
Save