文件
hyb-oa/client/src/pages/invoice-pool/InvoiceListCard.tsx
T

145 行
6.4 KiB
TypeScript

import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query";
import type { InvoicePoolListResponse, InvoicePoolStatus } from "../../api/invoicePool";
import { StatusBadge } from "../../components/common/StatusBadge";
import { TableSkeleton } from "../../components/common/TableSkeleton";
import { Pagination } from "../../components/common/Pagination";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../components/ui/dropdown-menu";
export type InvoiceListCardProps = {
listQuery: UseQueryResult<InvoicePoolListResponse, Error>;
status: InvoicePoolStatus | "";
setStatus: (status: InvoicePoolStatus | "") => void;
ownerScope: "own" | "all";
setOwnerScope: (scope: "own" | "all") => void;
page: number;
setPage: (page: number) => void;
totalPages: number;
detailInvoiceId: string;
setDetailInvoiceId: (id: string) => void;
signedUrlMutation: UseMutationResult<{ signedUrl: string; expiresAt: string }, Error, string>;
};
export function InvoiceListCard({
listQuery,
status,
setStatus,
ownerScope,
setOwnerScope,
page,
setPage,
totalPages,
setDetailInvoiceId,
signedUrlMutation,
}: InvoiceListCardProps) {
return (
<div className="card-std space-y-4">
<div className="flex items-center justify-between">
<h3 className="card-title">筛选与列表</h3>
<div className="flex items-center gap-2">
<select
className="select-sm"
data-testid="invoice-filter-status"
onChange={(event) => {
setStatus(event.target.value as InvoicePoolStatus | "");
setPage(1);
}}
value={status}
>
<option value="">全部状态</option>
<option value="pending_ocr">识别中</option>
<option value="available">可用</option>
<option value="ocr_failed">识别失败</option>
<option value="bound">已绑定</option>
<option value="archived">已归档</option>
</select>
<select
className="select-sm"
data-testid="invoice-filter-owner-scope"
onChange={(event) => {
setOwnerScope(event.target.value as "own" | "all");
setPage(1);
}}
value={ownerScope}
>
<option value="own">仅看自己</option>
<option value="all">查看全部</option>
</select>
</div>
</div>
{listQuery.isLoading ? <TableSkeleton columns={5} rows={5} /> : null}
{!listQuery.isLoading ? (
<>
<div className="table-container">
<table className="w-full text-left text-sm text-zinc-600" data-testid="invoice-pool-table">
<thead className="bg-zinc-50/50">
<tr>
<th className="table-th w-[280px]">票据流水号</th>
<th className="table-th w-[120px]">状态</th>
<th className="table-th w-[200px]">发票号码</th>
<th className="table-th">识别情况 (OCR)</th>
<th className="table-th w-[80px] text-right">操作</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100">
{(listQuery.data?.items ?? []).length ? (
listQuery.data?.items.map((item) => (
<tr className="table-row" data-testid="invoice-row" key={item.id}>
<td className="table-td">
<div className="font-mono text-xs text-zinc-500">{item.id}</div>
</td>
<td className="table-td"><StatusBadge status={item.status} /></td>
<td className="table-td font-medium text-zinc-900">
{item.invoiceCode || "-"}-{item.invoiceNumber || "-"}
</td>
<td className="table-td">
{item.status === "pending_ocr" ? (
<span className="flex items-center gap-1.5 text-amber-600 font-medium">
<span className="size-1.5 rounded-full bg-amber-500 animate-pulse" />
识别中 <span className="text-zinc-400 font-normal text-xs">(重试: {item.ocrAttempts})</span>
</span>
) : item.status === "ocr_failed" ? (
<span className="flex items-center gap-1.5 text-red-600">
<span className="size-1.5 rounded-full bg-red-500" />
识别失败 <span className="text-zinc-400 text-xs">({item.ocrLastError || "未知错误"})</span>
</span>
) : (
<span className="text-zinc-400 text-xs">解析成功</span>
)}
</td>
<td className="table-td text-right">
<DropdownMenu>
<DropdownMenuTrigger className="rounded-md p-1 hover:bg-zinc-100 focus:outline-none transition-colors">
<span className="material-symbols-outlined text-[20px] text-zinc-400">more_horiz</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
<DropdownMenuItem data-testid="invoice-detail-button" onSelect={() => setDetailInvoiceId(item.id)}>
查看详情
</DropdownMenuItem>
<DropdownMenuItem data-testid="invoice-preview-button" onSelect={() => signedUrlMutation.mutate(item.id)}>
下载 / 预览
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))
) : (
<tr>
<td className="table-td text-center text-zinc-500 py-8" colSpan={5}>
暂无票据数据
</td>
</tr>
)}
</tbody>
</table>
</div>
<Pagination page={page} totalPages={totalPages} onPageChange={setPage} testIdPrefix="invoice" />
</>
) : null}
</div>
);
}