refactor(ui): 提取表单原生组件公共样式并修复面板边距bug
这个提交包含在:
@@ -0,0 +1,144 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户