fix: 统一报销付款详情布局与票据展示(09-04-reimbursement-state-gap-finance-view)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-11 22:28:06 +08:00
父节点 2b4e3faa0e
当前提交 98eabdde7f
共修改 11 个文件,包含 205 行新增和 31 行删除
@@ -87,6 +87,7 @@ export function BusinessDetailActionPanel({
primaryAction,
secondaryActions,
moreActions,
inlineActions = false,
summary,
children,
className,
@@ -98,6 +99,7 @@ export function BusinessDetailActionPanel({
primaryAction?: ReactNode;
secondaryActions?: ReactNode;
moreActions?: ReactNode;
inlineActions?: boolean;
summary?: ReactNode;
children?: ReactNode;
className?: string;
@@ -121,9 +123,10 @@ export function BusinessDetailActionPanel({
<SectionPanel title={title} actions={status} className={cn("detail-action-panel lg:sticky lg:top-24", className)}>
<div className="detail-action-stack" data-testid={testId}>
{hasBeforeActions ? <div>{beforeActions}</div> : null}
{hasPrimaryAction ? <div className="grid gap-2">{primaryAction}</div> : null}
{inlineActions && (hasPrimaryAction || hasMoreActions) ? <div className="flex flex-wrap items-center gap-2" data-testid="detail-inline-actions">{primaryAction}{moreActions}</div> : null}
{!inlineActions && hasPrimaryAction ? <div className="grid gap-2">{primaryAction}</div> : null}
{hasSecondaryActions ? <div className="workflow-action-bar">{secondaryActions}</div> : null}
{hasMoreActions ? <div className="border-t border-zinc-200 pt-3">{moreActions}</div> : null}
{!inlineActions && hasMoreActions ? <div className="border-t border-zinc-200 pt-3">{moreActions}</div> : null}
{hasChildren ? (
<div className={cn(hasActionContent && "border-t border-zinc-200 pt-3")}>{children}</div>
) : null}
@@ -30,7 +30,7 @@ export function ReimbursementDesktopWorkspaceShell({ model, capabilities, slots
)}
/>
{slots.transientFeedback}
<BusinessDetailWorkspace className="mx-auto w-full max-w-7xl">
<BusinessDetailWorkspace className="detail-workspace-grid--payment mx-auto w-full max-w-7xl">
<BusinessDetailMain>
{slots.headerDetails}
{slots.beforeEvidence}
@@ -167,6 +167,43 @@ function InvoiceMaterialRow({ invoice, capabilities, linkTargets }: { invoice: R
);
}
function InvoiceMaterialsTable({ invoices, capabilities }: { invoices: ReimbursementInvoiceEvidence[]; capabilities: ReimbursementEvidenceCapabilities }) {
return (
<div className="overflow-hidden rounded-lg border border-zinc-200" data-testid="reimbursement-invoices-table">
<table aria-label="报销发票" className="block w-full table-fixed text-left text-sm lg:table">
<thead className="hidden bg-zinc-50 text-xs text-zinc-600 lg:table-header-group">
<tr>
<th className="w-[30%] px-3 py-2.5 font-medium" scope="col">发票号码</th>
<th className="px-3 py-2.5 font-medium" scope="col">开票方</th>
<th className="w-24 px-3 py-2.5 font-medium" scope="col">日期</th>
<th className="w-28 px-3 py-2.5 text-right font-medium" scope="col">金额</th>
{capabilities.onUnlinkInvoice ? <th className="w-28 px-3 py-2.5 font-medium" scope="col">操作</th> : null}
</tr>
</thead>
<tbody className="block divide-y divide-zinc-200 bg-white lg:table-row-group">
{invoices.map((invoice) => {
const title = invoice.invoiceNumber || invoice.originalName || "未命名发票";
const statusCopy = evidenceStatusCopy(invoice.amountEvidenceStatus);
const hasBlockingIssue = invoice.amountEvidenceStatus === "ocr_failed";
return (
<tr className="block p-3 lg:table-row lg:p-0" data-testid="approval-invoice-evidence" data-reimbursement-guard-issue={hasBlockingIssue ? "true" : undefined} id={`reimbursement-invoice-${invoice.id}`} key={invoice.id} tabIndex={hasBlockingIssue ? -1 : undefined}>
<td className="block break-all align-top lg:table-cell lg:px-3 lg:py-2">
{capabilities.onOpenInvoice ? <Button aria-label={`查看发票:${title}`} className="h-auto min-h-11 max-w-full justify-start whitespace-normal break-all px-0 text-left" onClick={() => capabilities.onOpenInvoice?.(invoice)} type="button" variant="link">{title}</Button> : <span className="font-medium text-zinc-900">{title}</span>}
{statusCopy ? <p className={hasBlockingIssue ? "text-xs text-red-700" : "text-xs text-zinc-600"}>{statusCopy}</p> : null}
</td>
<td className="block break-words text-zinc-600 [overflow-wrap:anywhere] lg:table-cell lg:px-3 lg:py-3">{invoice.sellerName || invoice.documentSubtype || invoice.documentCategory || "票据"}</td>
<td className="block text-zinc-600 lg:table-cell lg:px-3 lg:py-3">{formatDisplayDate(invoice.invoiceDate)}</td>
<td className="block text-right font-medium tabular-nums lg:table-cell lg:px-3 lg:py-3">{formatMoney(invoice.totalAmount)}</td>
{capabilities.onUnlinkInvoice ? <td className="block lg:table-cell lg:px-3 lg:py-2"><Button className="min-h-11 whitespace-normal" onClick={() => capabilities.onUnlinkInvoice?.(invoice)} size="sm" type="button" variant="secondary">移除对应关系</Button></td> : null}
</tr>
);
})}
</tbody>
</table>
</div>
);
}
function AttachmentMaterialRow({ attachment, capabilities }: { attachment: ReimbursementAttachmentEvidence; capabilities: ReimbursementEvidenceCapabilities }) {
return <div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-zinc-200 px-3 py-2">
<div className="min-w-0 flex-1">
@@ -197,7 +234,7 @@ export function ReimbursementItemEvidenceList({ amountSummary, evidence, capabil
<div className="flex flex-wrap items-center justify-between gap-2"><h3 className="font-medium text-zinc-900">票据与附件</h3>{capabilities.onOpenMaterialBundle ? <Button className="min-h-11" onClick={capabilities.onOpenMaterialBundle} size="sm" type="button" variant="secondary">查看完整材料</Button> : null}</div>
<div className="space-y-2" data-testid="approval-invoices">
<h4 className="text-sm font-medium text-zinc-800">发票({linkedInvoices.length + evidence.unlinkedInvoices.length})</h4>
{linkedInvoices.map((invoice) => <InvoiceMaterialRow capabilities={capabilities} invoice={invoice} key={invoice.id} linkTargets={evidence.items} />)}
{linkedInvoices.length ? (mobile ? linkedInvoices.map((invoice) => <InvoiceMaterialRow capabilities={capabilities} invoice={invoice} key={invoice.id} linkTargets={evidence.items} />) : <InvoiceMaterialsTable capabilities={capabilities} invoices={linkedInvoices} />) : null}
{evidence.unlinkedInvoices.length ? <div className="space-y-2 rounded-lg border border-red-200 bg-red-50 p-3" data-testid="approval-unlinked-invoices" id="reimbursement-workspace-unlinked"><p className="text-sm font-medium text-red-900">尚未对应明细({evidence.unlinkedInvoices.length})</p><p className="text-sm text-red-800">这些票据当前没有明确费用归属,暂时无法提交;请为每张票据选择对应的报销明细。</p>{evidence.unlinkedInvoices.map((invoice) => <InvoiceMaterialRow capabilities={capabilities} invoice={invoice} key={invoice.id} linkTargets={evidence.items} />)}</div> : null}
{!linkedInvoices.length && !evidence.unlinkedInvoices.length ? <p className="text-sm text-zinc-500">暂无发票</p> : null}
</div>