---
description: "按时间倒序列出「谁在什么时候对什么做了什么」的追加式历史：每条三段——头像+名字（who）、动作+对象（what）、相对时间（when，hover 看绝对时间）——展开还能看每个字段改前改后的红绿 diff。资源要能追溯、要能出锅时对账，就靠它"
---

# 审计日志（活动追踪 + 字段级 diff）

按时间倒序列出「谁在什么时候对什么做了什么」的追加式历史：每条三段——头像+名字（who）、动作+对象（what）、相对时间（when，hover 看绝对时间）——展开还能看每个字段改前改后的红绿 diff。资源要能追溯、要能出锅时对账，就靠它。

<iframe src="/skills/dashboard/assets/demo-audit-log.html"
        style="width:100%;height:640px;border:1px solid #8884;border-radius:10px"
        loading="lazy" title="形状 demo：审计日志"></iframe>

试这几下：

- 顶部**动作类型**多选（创建/更新/删除/状态变更/登录）—— 任意组合，无选中即全部
- 切换**时间范围**（今天/近 7 天/近 30 天/自定义）—— 自定义会展开两个日期选择器
- 悬浮**相对时间**看到绝对时间戳（例如「2 分钟前」→「2026-07-07 15:32」）
- 点行尾**展开**看字段级 diff：改前红色删除线、改后绿色高亮；只有 `to` 的字段（创建）只出绿色，只有 `from` 的字段（删除）只出红色
- 组合筛选清空时的空态引导放宽条件；「全部展开 / 收起」批量控制

## 规矩

- **追加式历史，不改不删**：审计行只增，`recordAudit()` 与资源的 `create*/update*/delete*` 服务函数写在同一事务里；`requireUser()` 之后拿 actor，diff 服务端算好一起入库。客户端只渲染，不重写、不回填、不重排
- **反向时间序 + 语义化列表**：newest-first，DOM 用 `<ol><li>`，左侧连线和圆点是纯装饰
- **参数一个形状**：`{ resource, resourceId?, types?, from?, to?, page, pageSize }` → `{ rows, total }`。demo 在内存按类型 + 时间范围过滤；真实版把同一份参数交给 `AuditLogRepository.list()`（服务端过滤 + 分页；长历史用**无限滚动**补齐）
- **`tone` 决定色点，`changes` 决定是否可展开**：`tone` 枚举 `created/updated/deleted/status/login` 驱动圆点颜色；`changes` 是 `[{ field, from?, to? }]`——创建可省 `from`、删除可省 `to`，前端只按字符串渲染，不解构原始对象（diff 在服务端算好一起返回）
- **时间双写**：相对时间给一眼扫读，绝对时间挂在 `<time datetime>` 的 `title` 里给追溯；服务端存 ISO，前端本地化
- **权限**：审计日志通常需要管理员角色 —— 真实版在路由和 server fn 都要 `requireRole("admin")`，普通用户拿不到这个页面；demo 略去这一层

> 蓝本：`add-audit-log.md`[^add-audit-log]（open-dashboard @ `aa9815f`，MIT，Invariants 已消化进上面「规矩」）

??? abstract "demo 源码：`assets/demo-audit-log.html`（自包含、未压缩）"

    ```html
    --8<-- "skills/dashboard/assets/demo-audit-log.html"
    ```

[^add-audit-log]: [`add-audit-log.md`](https://github.com/ahpxex/open-dashboard/blob/aa9815f426e9520841c4aa15a341bfd9f866091f/.claude/skills/add-component/references/add-audit-log.md)，2026-07-28 读。
