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>
+4
查看文件
@@ -588,6 +588,10 @@
@apply grid gap-5 lg:grid-cols-[minmax(0,1.45fr),minmax(280px,0.72fr)];
}
.detail-workspace-grid--payment {
@apply lg:grid-cols-[minmax(0,1fr),320px];
}
.detail-command-panel {
border-color: rgba(203, 213, 225, 0.92);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.9));
+14 -10
查看文件
@@ -3,6 +3,7 @@ import { resolveErrorReasonMessage, UNKNOWN_VALIDATION_MESSAGE } from "../api/er
import { useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, useSearchParams } from "react-router-dom";
import { Eye } from "lucide-react";
import {
cancelPaymentBatch,
createPaymentBatch,
@@ -827,7 +828,7 @@ export function PaymentBatchesPage() {
<div
className={paymentActionModel.primary || paymentActionModel.more.length ? "pb-[calc(var(--mobile-sticky-action-height,78px)+env(safe-area-inset-bottom,0px)+1.25rem)] lg:pb-0" : ""}
>
<BusinessDetailWorkspace testId="payment-batch-detail-workspace">
<BusinessDetailWorkspace className="detail-workspace-grid--payment" testId="payment-batch-detail-workspace">
<BusinessDetailMain>
<div className="finance-command-panel space-y-3" data-testid="payment-batch-detail-panel">
<div className="flex flex-wrap items-center justify-between gap-3">
@@ -914,7 +915,7 @@ export function PaymentBatchesPage() {
<div className="mt-2 detail-fact-grid text-sm md:grid-cols-4">
<div className="detail-fact-card"><div className="detail-fact-card__label">收款人</div><div className="detail-fact-card__value">{item.payeeName || "-"}</div></div>
<div className="detail-fact-card"><div className="detail-fact-card__label">开户银行</div><div className="detail-fact-card__value">{item.bankName || "-"}</div></div>
<div className="detail-fact-card"><div className="detail-fact-card__label">银行账户</div><div className="detail-fact-card__value">{item.bankAccountMasked || "-"}</div>{canRevealPayeeBank && ["reviewed", "failed"].includes(selectedBatch.status) && selectedBatch.paymentAccount ? <Button type="button" size="sm" variant="secondary" className="mt-2" onClick={() => { setActionReason(""); setRevealedPayeeAccount(""); setReasonAction({ kind: "payeeBank", batchId: selectedBatch.id, batchNo: selectedBatch.batchNo, itemId: item.id, entityCode: item.entityCode }); }}>查看完整账号</Button> : null}</div>
<div className="detail-fact-card"><div className="detail-fact-card__label">银行账户</div><div className="detail-fact-card__value flex items-center gap-1"><span className="min-w-0 break-all">{item.bankAccountMasked || "-"}</span>{canRevealPayeeBank && ["reviewed", "failed"].includes(selectedBatch.status) && selectedBatch.paymentAccount ? <Button type="button" size="sm" variant="ghost" className="h-11 w-11 shrink-0 p-0" aria-label="查看完整账号" title="查看完整账号" onClick={() => { setActionReason(""); setRevealedPayeeAccount(""); setReasonAction({ kind: "payeeBank", batchId: selectedBatch.id, batchNo: selectedBatch.batchNo, itemId: item.id, entityCode: item.entityCode }); }}><Eye aria-hidden="true" className="size-4" /></Button> : null}</div></div>
<div className="detail-fact-card"><div className="detail-fact-card__label">付款渠道</div><div className="detail-fact-card__value">{paymentChannelLabel(item.paymentChannelOverride ?? selectedBatch.defaultPaymentChannel)}</div></div>
</div>
{["reviewed", "failed"].includes(selectedBatch.status) ? (
@@ -1073,18 +1074,21 @@ export function PaymentBatchesPage() {
<BusinessDetailAside>
<BusinessDetailActionPanel
inlineActions
className="hidden lg:block lg:static"
status={<StatusBadge status={selectedBatch.status} label={paymentBatchStatusLabel(selectedBatch.status)} />}
primaryAction={paymentActionModel.primary ? renderPaymentActionButton(paymentActionModel.primary) : undefined}
moreActions={paymentActionModel.more.length ? (
<details className="detail-disclosure">
<summary>更多</summary>
<div className="mt-3 grid gap-2">
{paymentActionModel.more.map((kind) =>
renderPaymentActionButton(kind, kind === "cancel" ? "destructive" : "secondary"),
)}
</div>
</details>
<DropdownMenu>
<DropdownMenuTrigger asChild><Button type="button" variant="secondary">更多</Button></DropdownMenuTrigger>
<DropdownMenuContent align="end">
{paymentActionModel.more.map((kind) => (
<DropdownMenuItem key={kind} data-testid={`payment-batch-action-${kind}`} disabled={paymentActionDisabled(kind)} className={kind === "cancel" ? "min-h-11 text-red-600 focus:text-red-600" : "min-h-11"} onSelect={() => activatePaymentAction(kind)}>
{paymentActionLabel(kind)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : undefined}
summary={(
<>
+4 -13
查看文件
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ArrowLeft } from "lucide-react";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
import { fetchExpenseTypes } from "../api/expenseTypes";
import type { ExpenseStandardCheck, PrecheckResponse } from "../api/feeControl";
import {
@@ -114,7 +115,6 @@ export function ReimbursementDetailPage() {
const [itemError, setItemError] = useState("");
const [confirmDeleteItemId, setConfirmDeleteItemId] = useState("");
const [confirmDeleteReimbursement, setConfirmDeleteReimbursement] = useState(false);
const [showSecondaryDetail, setShowSecondaryDetail] = useState(false);
const [submitConfirmPrecheck, setSubmitConfirmPrecheck] = useState<PrecheckResponse | null>(null);
const [isInvoiceDialogOpen, setIsInvoiceDialogOpen] = useState(false);
const [invoiceSearchInput, setInvoiceSearchInput] = useState("");
@@ -1041,17 +1041,6 @@ export function ReimbursementDetailPage() {
<Link to={`/reimbursements/${detail.id}/print`}>打印 / PDF</Link>
</Button>
) : null,
hasMoreActions ? (
<Button
key="more"
type="button"
variant="secondary"
data-testid="reimbursement-more-toggle"
onClick={() => setShowSecondaryDetail((value) => !value)}
>
{showSecondaryDetail ? "收起更多" : "更多"}
</Button>
) : null,
];
const workspaceEvidence = buildReimbursementApprovalEvidence(detail, {
@@ -1112,8 +1101,10 @@ export function ReimbursementDetailPage() {
</>
);
const workspaceActionPanel = (
<BusinessDetailActionPanel className="lg:static" status={<StatusBadge status={detail.deletedAt ? "deleted" : detail.status} />} primaryAction={primaryAction} secondaryActions={secondaryActions} moreActions={showSecondaryDetail ? <div className="grid gap-2" data-testid="reimbursement-secondary-detail">{detail.status !== "paid" ? <Button asChild variant="secondary"><Link to={"/reimbursements/" + detail.id + "/print"}>打印 / PDF</Link></Button> : null}{canDeleteReimbursement ? <Button data-testid="reimbursement-delete-draft" disabled={deleteReimbursementMutation.isPending} onClick={() => setConfirmDeleteReimbursement(true)} type="button" variant="destructive">删除报销单</Button> : null}</div> : null} summary={<><div>下一步:{nextStepText}</div>{workspaceModel.issueAnchors.map((issue) => <a className="block hover:underline" href={"#" + issue.id} key={issue.label}>{issue.label}</a>)}</>} testId="reimbursement-action-panel">
<BusinessDetailActionPanel inlineActions className="lg:static" status={<StatusBadge status={detail.deletedAt ? "deleted" : detail.status} />} primaryAction={primaryAction} secondaryActions={secondaryActions} moreActions={hasMoreActions ? <DropdownMenu><DropdownMenuTrigger asChild><Button data-testid="reimbursement-more-toggle" type="button" variant="secondary">更多</Button></DropdownMenuTrigger><DropdownMenuContent align="end" data-testid="reimbursement-secondary-detail">{detail.status !== "paid" ? <DropdownMenuItem asChild className="min-h-11"><Link to={"/reimbursements/" + detail.id + "/print"}>打印 / PDF</Link></DropdownMenuItem> : null}{canDeleteReimbursement ? <DropdownMenuItem className="min-h-11 text-red-600 focus:text-red-600" data-testid="reimbursement-delete-draft" disabled={deleteReimbursementMutation.isPending} onSelect={() => setConfirmDeleteReimbursement(true)}>删除报销单</DropdownMenuItem> : null}</DropdownMenuContent></DropdownMenu> : null} summary={<><div>下一步:{nextStepText}</div>{workspaceModel.issueAnchors.map((issue) => <a className="block hover:underline" href={"#" + issue.id} key={issue.label}>{issue.label}</a>)}</>} testId="reimbursement-action-panel">
{(detail.status === "reviewing" && isReimbursementParticipant) || (["reviewing", "approved"].includes(detail.status) && actionError) ? (
<ReimbursementActions status={detail.status} showSubmitAction={false} showWithdrawAction={isReimbursementParticipant} actionError={["reviewing", "approved"].includes(detail.status) ? actionError : ""} submitDisabled={blockStandardChecks.length > 0} submitHint="" submitNotice="" isPending={mutation.isPending} onSubmit={() => void handleSubmitAction()} onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })} />
) : null}
</BusinessDetailActionPanel>
);
@@ -680,7 +680,7 @@ test.describe("报销付款交接一致性", () => {
const link = page.getByRole("link", { name: "查看所在付款批次", exact: true });
await expect(link).toHaveCount(1);
const linkBounds = await link.boundingBox();
expect(linkBounds!.height).toBeGreaterThanOrEqual(44);
expect(Math.round(linkBounds!.height * 100) / 100).toBeGreaterThanOrEqual(44);
if (!mobile) expect(linkBounds!.width).toBeLessThan(180);
await link.click();
await expect(page.getByTestId("payment-batch-processing-panel")).toBeVisible();
@@ -698,7 +698,99 @@ test.describe("报销付款交接一致性", () => {
});
}
test("账号查看图标紧随脱敏值且确认前不请求完整账号", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 960 });
await mockSystemActionApis(page, {
canExportPaymentOrder: true,
meOverride: { roleCodes: ["finance"], permissions: [...me.permissions, "employee.bank.read"] },
});
let reveals = 0;
page.on("request", (request) => { if (new URL(request.url()).pathname.endsWith("/payee-bank/reveal")) reveals += 1; });
await login(page, user);
await page.goto(`/payment-batches?id=${paymentBatchId}`);
const reveal = page.getByRole("button", { name: "查看完整账号", exact: true });
await expect(reveal).toBeVisible();
await expect(reveal).toHaveText("");
await expect(reveal).toHaveAttribute("title", "查看完整账号");
const icon = await reveal.boundingBox();
const account = await reveal.locator("..").locator("span").boundingBox();
expect(icon!.width).toBeGreaterThanOrEqual(44);
expect(icon!.height).toBeGreaterThanOrEqual(44);
expect(icon!.x).toBeGreaterThan(account!.x);
await reveal.focus();
await page.keyboard.press("Enter");
const dialog = page.getByRole("dialog");
await expect(dialog).toContainText("本次查看会记录");
await expect(dialog.getByRole("textbox", { name: "查看收款账号原因" })).toBeVisible();
expect(reveals).toBe(0);
await dialog.getByRole("button", { name: "返回", exact: true }).click();
expect(reveals).toBe(0);
});
for (const width of [390, 1280, 1440]) {
test(`票据紧凑展示${width}多张长名称及预览不丢失`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 960 });
await mockSystemActionApis(page, { reimbursementStatus: "approved" });
const detail = reimbursementDetail("approved");
const sellerName = "合成开票方长名称".repeat(8);
detail.invoices = Array.from({ length: 5 }, (_, index) => ({
...detail.invoices[0],
id: `invoice-layout-${index}`,
invoiceNumber: `SYNTHETIC-LAYOUT-000000000000000000000${index}`,
sellerName,
}));
detail.attachments = [];
await page.route(`**/api/reimbursements/${reimbursementId}`, (route) => fulfillJson(route, detail));
let previews = 0;
await page.route("**/api/invoice-pool/invoice-layout-0/signed-url", async (route) => {
previews += 1;
await fulfillJson(route, { signedUrl: "about:blank", originalName: "合成预览.pdf", fileSize: 1024 });
});
await login(page, user);
await page.goto(`/reimbursements/${reimbursementId}`);
const table = page.getByTestId("reimbursement-invoices-table");
await expect(table).toBeVisible();
await expect(table.getByTestId("approval-invoice-evidence")).toHaveCount(5);
await expect(table.getByText(sellerName, { exact: true })).toHaveCount(5);
if (width >= 1024) await expect(table.getByRole("columnheader", { name: "金额" })).toBeVisible();
else await expect(table.locator("thead")).toBeHidden();
await expect(page.getByText("未上传补充附件", { exact: true })).toBeVisible();
await table.getByRole("button", { name: `查看发票:${detail.invoices[0].invoiceNumber}`, exact: true }).click();
await expect.poll(() => previews).toBe(1);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2)).toBe(true);
await table.screenshot({ path: testInfo.outputPath("invoice-table.png") });
});
}
for (const width of [1280, 1440]) {
test(`桌面${width}付款与报销侧栏等宽且更多紧随主按钮`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 960 });
await mockSystemActionApis(page, { reimbursementStatus: "approved", reimbursementProgress: () => progress("reviewed") });
await login(page, user);
const widths: number[] = [];
for (const [url, panelId] of [
[`/reimbursements/${reimbursementId}`, "reimbursement-action-panel"],
[`/payment-batches?id=${paymentBatchId}`, "payment-batch-action-panel"],
]) {
await page.goto(url);
const panel = page.getByTestId(panelId);
await expect(panel).toBeVisible();
await page.evaluate(() => document.fonts.ready);
widths.push((await panel.locator("../..").boundingBox())!.width);
const row = panel.getByTestId("detail-inline-actions");
const primary = await row.locator("a, button").first().boundingBox();
const more = await row.getByRole("button", { name: "更多", exact: true }).boundingBox();
expect(Math.abs(primary!.y + primary!.height / 2 - more!.y - more!.height / 2)).toBeLessThan(2);
expect(more!.x).toBeGreaterThan(primary!.x + primary!.width);
await row.getByRole("button", { name: "更多", exact: true }).click();
await expect(page.getByRole("menu")).toBeVisible();
await page.keyboard.press("Escape");
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 2)).toBe(true);
await page.screenshot({ path: testInfo.outputPath(`${panelId}.png`), fullPage: true });
}
expect(widths).toEqual([320, 320]);
});
test(`桌面${width}创建批次按钮短文案按内容宽度且只导航不组批`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 960 });
await mockSystemActionApis(page, { reimbursementStatus: "approved", reimbursementProgress: () => progress("none") });
@@ -913,13 +1005,13 @@ test.describe("R7E 全站高风险操作二次确认", () => {
const actionPanel = page.getByTestId("payment-batch-action-panel");
await expect(actionPanel.getByTestId("payment-batch-action-downloadOrder")).toBeVisible();
await actionPanel.getByText("更多", { exact: true }).click();
await expect(actionPanel.getByRole("button", { name: "上传银行回单" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "上传银行回单" })).toBeVisible();
await expect(page.getByTestId("payment-batch-mobile-actions")).toBeHidden();
await expect(page.getByTestId("payment-batch-receipt-panel").getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "登记付款完成" })).toHaveCount(0);
const desktopFileChooser = page.waitForEvent("filechooser");
await actionPanel.getByRole("button", { name: "上传银行回单" }).click();
await page.getByRole("menuitem", { name: "上传银行回单" }).click();
await (await desktopFileChooser).setFiles({
name: "合成银行回单.pdf",
mimeType: "application/pdf",
@@ -1008,7 +1100,7 @@ test.describe("R7E 全站高风险操作二次确认", () => {
await page.screenshot({ path: testInfo.outputPath("after-payment-order.png"), fullPage: true });
await actions.getByText("更多", { exact: true }).click();
if (mobile) await page.getByRole("menuitem", { name: "重新下载制单" }).click();
else await actions.getByTestId("payment-batch-action-redownloadOrder").click();
else await page.getByRole("menuitem", { name: "重新下载制单" }).click();
await expect(dialog).toContainText("请勿重复提交");
await expect(dialog.getByRole("textbox")).toHaveCount(0);
const secondDownload = page.waitForEvent("download");