审计导出功能--初版
This commit is contained in:
@@ -57,4 +57,14 @@ export const auditDict: Record<
|
||||
actionText: "更新了中心信息",
|
||||
targetLabel: "中心",
|
||||
},
|
||||
AUDIT_EXPORT_SYSTEM: {
|
||||
label: "导出系统审计",
|
||||
actionText: "导出了系统审计日志",
|
||||
targetLabel: "审计日志",
|
||||
},
|
||||
AUDIT_EXPORT_PROJECT: {
|
||||
label: "导出项目审计",
|
||||
actionText: "导出了项目审计日志",
|
||||
targetLabel: "审计日志",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface AuditExportColumn {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const auditExportColumns: AuditExportColumn[] = [
|
||||
{ key: "timestamp", label: "审计时间" },
|
||||
{ key: "actorName", label: "操作人姓名" },
|
||||
{ key: "actorId", label: "操作人账号" },
|
||||
{ key: "actorRoleLabel", label: "操作人角色" },
|
||||
{ key: "eventLabel", label: "操作类型" },
|
||||
{ key: "targetTypeLabel", label: "操作对象类型" },
|
||||
{ key: "targetIdentifier", label: "操作对象标识" },
|
||||
{ key: "description", label: "操作描述" },
|
||||
{ key: "beforeStatus", label: "操作前状态" },
|
||||
{ key: "afterStatus", label: "操作后状态" },
|
||||
{ key: "resultLabel", label: "操作结果" },
|
||||
{ key: "reason", label: "拒绝原因" },
|
||||
{ key: "ip", label: "IP 地址" },
|
||||
{ key: "remark", label: "备注" },
|
||||
];
|
||||
@@ -0,0 +1,55 @@
|
||||
import { getDictLabel, statusDict } from "../../dictionaries";
|
||||
import type { AuditEvent } from "..";
|
||||
import { auditExportColumns } from "./auditExportColumns";
|
||||
|
||||
export interface AuditExportRow {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const extractStatusLabel = (obj?: Record<string, any>) => {
|
||||
if (!obj) return "";
|
||||
if (obj.status) return getDictLabel(statusDict, obj.status) || obj.status;
|
||||
return "";
|
||||
};
|
||||
|
||||
export const formatAuditRow = (event: AuditEvent): AuditExportRow => {
|
||||
const beforeStatus = extractStatusLabel(event.before);
|
||||
const afterStatus = extractStatusLabel(event.after);
|
||||
const descriptionParts: string[] = [];
|
||||
descriptionParts.push(`${event.actorName} ${event.actionText}`);
|
||||
if (event.targetTypeLabel || event.targetName || event.targetId) {
|
||||
descriptionParts.push(
|
||||
`对象:${event.targetTypeLabel || ""}${event.targetName ? `(${event.targetName})` : `(${event.targetId}`})`
|
||||
);
|
||||
}
|
||||
if (event.diffText?.length) {
|
||||
descriptionParts.push(`变更:${event.diffText.join(";")}`);
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp: event.timestamp || "",
|
||||
actorName: event.actorName || "",
|
||||
actorId: event.actorId || "",
|
||||
actorRoleLabel: event.actorRoleLabel || "",
|
||||
eventLabel: event.eventLabel || "",
|
||||
targetTypeLabel: event.targetTypeLabel || "",
|
||||
targetIdentifier: event.targetName || event.targetId || "",
|
||||
description: descriptionParts.join(";"),
|
||||
beforeStatus,
|
||||
afterStatus,
|
||||
resultLabel: event.resultLabel || "",
|
||||
reason: event.reason || "",
|
||||
ip: "",
|
||||
remark: event.diffText?.join(";") || "",
|
||||
};
|
||||
};
|
||||
|
||||
export const formatAuditRows = (events: AuditEvent[]): AuditExportRow[] => {
|
||||
return events.map((ev) => formatAuditRow(ev)).map((row) => {
|
||||
const ordered: AuditExportRow = {};
|
||||
auditExportColumns.forEach((col) => {
|
||||
ordered[col.key] = row[col.key] ?? "";
|
||||
});
|
||||
return ordered;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { auditExportColumns } from "./auditExportColumns";
|
||||
import { formatAuditRows } from "./auditExportFormatter";
|
||||
import type { AuditEvent } from "..";
|
||||
|
||||
const BOM = "\ufeff";
|
||||
|
||||
const escapeCsv = (val: string) => {
|
||||
const safe = (val ?? "").replace(/"/g, '""');
|
||||
return `"${safe}"`;
|
||||
};
|
||||
|
||||
const buildCsv = (rows: Record<string, string>[]) => {
|
||||
const header = auditExportColumns.map((c) => escapeCsv(c.label)).join(",");
|
||||
const body = rows.map((row) => auditExportColumns.map((c) => escapeCsv(row[c.key] || "")).join(",")).join("\n");
|
||||
return `${BOM}${header}\n${body}`;
|
||||
};
|
||||
|
||||
export interface AuditExportOptions {
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export const exportAuditCsv = (events: AuditEvent[], options: AuditExportOptions) => {
|
||||
const rows = formatAuditRows(events);
|
||||
const csv = buildCsv(rows);
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = options.fileName.endsWith(".csv") ? options.fileName : `${options.fileName}.csv`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
};
|
||||
@@ -18,6 +18,7 @@ export interface AuditEvent {
|
||||
before?: Record<string, any>;
|
||||
after?: Record<string, any>;
|
||||
diffText?: string[];
|
||||
reason?: string;
|
||||
result: "SUCCESS" | "FAIL";
|
||||
resultLabel: string;
|
||||
timestamp: string;
|
||||
@@ -71,6 +72,7 @@ export const normalizeAuditEvent = (raw: any, userMap: Record<string, string>):
|
||||
before,
|
||||
after,
|
||||
diffText: Array.isArray(diffText) ? diffText : diffText ? [diffText] : [],
|
||||
reason: detailObj.reason,
|
||||
result: detailObj.result || "SUCCESS",
|
||||
resultLabel: detailObj.result === "FAIL" ? "失败" : "成功",
|
||||
timestamp: raw.created_at,
|
||||
|
||||
Reference in New Issue
Block a user