feat: 完成 F17 报表导出(CSV/XLSX)\n\n- 后端新增 /api/exports/* 导出接口(含权限与审计记录)\n- CSV 采用流式生成,XLSX 使用 exceljs\n- 前端在报销/票据/审计页加入导出入口(下载 Blob)\n- 更新 P5 蓝图 v3 中 F17 状态为 DONE
这个提交包含在:
@@ -1,7 +1,11 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { isApiError } from "../api/types";
|
||||
import { exportAuditLogs } from "../api/exports";
|
||||
import { fetchAuditLogs, type AuditLogListResponse } from "../api/audit";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
|
||||
import { downloadBlob } from "../lib/download";
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
@@ -10,6 +14,7 @@ export function AuditLogsPage() {
|
||||
const [actionFilter, setActionFilter] = useState("");
|
||||
const [targetTypeFilter, setTargetTypeFilter] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [data, setData] = useState<AuditLogListResponse | null>(null);
|
||||
|
||||
@@ -63,7 +68,71 @@ export function AuditLogsPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title="审计工作台" subtitle="只读查看系统关键操作审计记录。" />
|
||||
<PageHeader
|
||||
title="审计工作台"
|
||||
subtitle="只读查看系统关键操作审计记录。"
|
||||
actions={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" type="button" disabled={exporting} data-testid="audit-export">
|
||||
{exporting ? "导出中..." : "导出"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
data-testid="audit-export-csv"
|
||||
onSelect={async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const response = await exportAuditLogs({
|
||||
format: "csv",
|
||||
action: actionFilter || undefined,
|
||||
targetType: targetTypeFilter || undefined,
|
||||
});
|
||||
downloadBlob(response.blob, response.filename ?? "audit-logs.csv");
|
||||
setError("");
|
||||
} catch (err) {
|
||||
if (isApiError(err)) {
|
||||
setError(`${err.code}: ${err.message}`);
|
||||
} else {
|
||||
setError("导出审计日志失败,请稍后重试");
|
||||
}
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
导出 CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
data-testid="audit-export-xlsx"
|
||||
onSelect={async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const response = await exportAuditLogs({
|
||||
format: "xlsx",
|
||||
action: actionFilter || undefined,
|
||||
targetType: targetTypeFilter || undefined,
|
||||
});
|
||||
downloadBlob(response.blob, response.filename ?? "audit-logs.xlsx");
|
||||
setError("");
|
||||
} catch (err) {
|
||||
if (isApiError(err)) {
|
||||
setError(`${err.code}: ${err.message}`);
|
||||
} else {
|
||||
setError("导出审计日志失败,请稍后重试");
|
||||
}
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
导出 Excel
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
/>
|
||||
|
||||
<section className="card-std space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
|
||||
在新工单中引用
屏蔽一个用户