feat: 完成 F17 报表导出(CSV/XLSX)\n\n- 后端新增 /api/exports/* 导出接口(含权限与审计记录)\n- CSV 采用流式生成,XLSX 使用 exceljs\n- 前端在报销/票据/审计页加入导出入口(下载 Blob)\n- 更新 P5 蓝图 v3 中 F17 状态为 DONE

这个提交包含在:
2026-02-09 19:35:51 +08:00
父节点 2c58061ec7
当前提交 f233a0d5bc
共修改 11 个文件,包含 2062 行新增和 2 行删除
+45
查看文件
@@ -3,6 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import { fetchMyDelegations } from "../api/delegations";
import { exportInvoicePool } from "../api/exports";
import {
archiveInvoice,
bindInvoice,
@@ -19,9 +20,11 @@ import {
import { fetchReimbursements } from "../api/reimbursements";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { downloadBlob } from "../lib/download";
import { PageHeader } from "../components/common/PageHeader";
import { Button } from "../components/ui/button";
import { Card } from "../components/ui/card";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
const PAGE_SIZE = 20;
@@ -267,6 +270,26 @@ export function InvoicePoolPage() {
},
});
const exportMutation = useMutation({
mutationFn: (format: "csv" | "xlsx") =>
exportInvoicePool({
format,
status: status || undefined,
ownerScope,
}),
onSuccess: (response, format) => {
downloadBlob(response.blob, response.filename ?? `invoice-pool.${format}`);
setError("");
},
onError: (err) => {
if (isApiError(err)) {
setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`);
} else {
setError("导出失败,请稍后重试");
}
},
});
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
const detail = detailQuery.data;
const canEditDetail = Boolean(
@@ -281,6 +304,28 @@ export function InvoicePoolPage() {
<PageHeader
title="票据中心"
subtitle="支持上传、OCR 状态轮询、绑定报销与签名预览。"
actions={
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="invoice-export"
variant="secondary"
type="button"
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? "导出中..." : "导出"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem data-testid="invoice-export-csv" onSelect={() => exportMutation.mutate("csv")}>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem data-testid="invoice-export-xlsx" onSelect={() => exportMutation.mutate("xlsx")}>
导出 Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
}
/>
{error ? (