From 081d70f551cd061e4018b60c2bab4e433f1542d9 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Fri, 24 Apr 2026 01:36:09 +0800 Subject: [PATCH] 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 --- app/plugins/toast-autoclose-fallback.client.ts | 53 ++++++++ docs/export-feature-recheck-2026-04-24.md | 170 +++++++++++++++++++++++++ packages/drizzle-pkg/db.sqlite | Bin 163840 -> 163840 bytes 3 files changed, 223 insertions(+) create mode 100644 app/plugins/toast-autoclose-fallback.client.ts create mode 100644 docs/export-feature-recheck-2026-04-24.md diff --git a/app/plugins/toast-autoclose-fallback.client.ts b/app/plugins/toast-autoclose-fallback.client.ts new file mode 100644 index 0000000..c285355 --- /dev/null +++ b/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[0] + const timers = new Map>() + + 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[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 +}) diff --git a/docs/export-feature-recheck-2026-04-24.md b/docs/export-feature-recheck-2026-04-24.md new file mode 100644 index 0000000..3280f5a --- /dev/null +++ b/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) diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index acf3d592b3e885589c59a424e1da8363efe5851e..4bc0c412abee2c76ed37d8be8fe073bb8711d57b 100644 GIT binary patch delta 456 zcmZo@;A&{#njpo*ZaY!N2}o{CXzyj&ojF-z^6Or6AY;AQ9Hdg?)-3kx!GS z^|^u=*C*faO9e3+CwugpfEcqUH}va*80#i)>{kIXGAG~cR|YZsnkHkWVJqbkchqrOt<+}8Mt^KZQ@~IU}t7#1X{w)z`(%D`e-8;kY?on!odH9zlXnPJ6{3Q eX?}GUpr91g8O+>3D9u<@nwOWE$BV>H=LP`$ft0-f