145 行
6.4 KiB
TypeScript
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>
|
|
);
|
|
}
|