统一页面右上角操作区(05-31-unified-page-header-actions)

这个提交包含在:
2026-05-31 16:41:44 +08:00
父节点 b71d23e661
当前提交 dcf34d7ac6
共修改 18 个文件,包含 480 行新增和 183 行删除
@@ -33,6 +33,10 @@ export function WorkspaceHeader({ eyebrow, title, subtitle, meta, actions, class
);
}
export function HeaderActionGroup({ className, children }: WorkspaceChildrenProps) {
return <div className={cn("flex flex-wrap items-center justify-end gap-2", className)}>{children}</div>;
}
export function FilterBar({ className, children }: WorkspaceChildrenProps) {
return <section className={cn("card-std p-3 md:p-3.5", className)}>{children}</section>;
}
+43 -43
查看文件
@@ -593,41 +593,7 @@ export function ApOperationsPage() {
return (
<ListWorkspace>
<WorkspaceHeader
title="应付账款"
actions={
<>
<Button
type="button"
variant="secondary"
onClick={() => refreshWorkbench()}
disabled={summaryQuery.isFetching || paymentBatchesQuery.isFetching || riskFlagsQuery.isFetching || financeLedgerQuery.isFetching}
>
刷新
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="ap-export"
type="button"
variant="secondary"
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? "导出中..." : "导出"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem data-testid="ap-export-csv" onSelect={() => exportMutation.mutate("csv")}>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem data-testid="ap-export-xlsx" onSelect={() => exportMutation.mutate("xlsx")}>
导出 Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
}
/>
<WorkspaceHeader title="应付账款" />
{summaryQuery.isError ? (
<SectionError message={resolveErrorMessage(summaryQuery.error, "应付摘要加载失败")} />
@@ -642,15 +608,49 @@ export function ApOperationsPage() {
<SectionError message={resolveErrorMessage(financeLedgerQuery.error, "财务台账加载失败")} />
) : null}
<div className="finance-command-panel">
<div className="finance-command-panel space-y-3">
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as OperationsTab)}>
<TabsList className="ops-command-bar__tabs !w-full !justify-start">
<TabsTrigger value="pending">待付款 {pendingBatches.length}</TabsTrigger>
<TabsTrigger value="failed">异常付款 {failedItems.length}</TabsTrigger>
<TabsTrigger value="risk">风险标记 {filteredRiskFlags.length}</TabsTrigger>
<TabsTrigger value="reconciliation">对账异常 {partialMatchItems.length}</TabsTrigger>
<TabsTrigger value="ledger">财务台账 {financeLedgerQuery.data?.total ?? 0}</TabsTrigger>
</TabsList>
<div className="ops-command-bar__row">
<TabsList className="ops-command-bar__tabs !justify-start">
<TabsTrigger value="pending">待付款 {pendingBatches.length}</TabsTrigger>
<TabsTrigger value="failed">异常付款 {failedItems.length}</TabsTrigger>
<TabsTrigger value="risk">风险标记 {filteredRiskFlags.length}</TabsTrigger>
<TabsTrigger value="reconciliation">对账异常 {partialMatchItems.length}</TabsTrigger>
<TabsTrigger value="ledger">财务台账 {financeLedgerQuery.data?.total ?? 0}</TabsTrigger>
</TabsList>
<div className="ops-command-bar__actions">
<Button
type="button"
size="sm"
variant="secondary"
onClick={() => refreshWorkbench()}
disabled={summaryQuery.isFetching || paymentBatchesQuery.isFetching || riskFlagsQuery.isFetching || financeLedgerQuery.isFetching}
>
刷新
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="ap-export"
type="button"
size="sm"
variant="secondary"
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? "导出中..." : "导出"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem data-testid="ap-export-csv" onSelect={() => exportMutation.mutate("csv")}>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem data-testid="ap-export-xlsx" onSelect={() => exportMutation.mutate("xlsx")}>
导出 Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<TabsContent value="ledger" className="mt-6 space-y-4">
<div className="finance-command-panel space-y-3 !p-3">
+21 -15
查看文件
@@ -37,13 +37,14 @@ import {
BusinessDetailMain,
BusinessDetailWorkspace,
} from "../components/patterns/BusinessDetailTemplate";
import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { HeaderActionGroup, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import {
BusinessListToolbar,
BusinessMobileList,
BusinessTable,
} from "../components/patterns/BusinessListTemplate";
import { Button } from "../components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
import { Input } from "../components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
import { formatCompactIdentifier, formatDisplayDateTime, formatTextWithCompactLongNumbers, isCompactedIdentifier } from "../lib/displayFormat";
@@ -727,7 +728,7 @@ export function AssetRegistryPage() {
<WorkspaceHeader
title="资产"
actions={
<div className="flex flex-wrap gap-2">
<HeaderActionGroup>
{isAssetDetailMode ? (
<Button type="button" variant="secondary" onClick={returnToAssetList}>
返回列表
@@ -739,21 +740,26 @@ export function AssetRegistryPage() {
新增资产
</Button>
) : null}
<Button type="button" variant="secondary" onClick={() => navigate("/mobile/assets/scan") }>
移动扫码查找
</Button>
{canManage ? (
<Button
type="button"
variant={showCategoryManager ? "default" : "secondary"}
onClick={() => setShowCategoryManager((value) => !value)}
>
资产分类
</Button>
) : null}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" variant={showCategoryManager ? "default" : "secondary"}>
更多
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => navigate("/mobile/assets/scan")}>
移动扫码查找
</DropdownMenuItem>
{canManage ? (
<DropdownMenuItem onSelect={() => setShowCategoryManager((value) => !value)}>
{showCategoryManager ? "收起资产分类" : "资产分类"}
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</HeaderActionGroup>
}
/>
+61 -54
查看文件
@@ -25,7 +25,7 @@ import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { DateTextInput } from "../components/common/DateTextInput";
import { WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { HeaderActionGroup, WorkspaceNotice } from "../components/patterns/ListWorkspace";
import {
BusinessListToolbar,
BusinessMobileList,
@@ -334,12 +334,7 @@ export function ContractsRegistryPage() {
<PageHeader
title="合同"
actions={
<div className="flex flex-wrap gap-2">
{initialProjectId ? (
<Button asChild variant="secondary" type="button">
<Link to="/projects">返回项目</Link>
</Button>
) : null}
<HeaderActionGroup>
<Button
type="button"
onClick={() => {
@@ -349,37 +344,7 @@ export function ContractsRegistryPage() {
>
新增合同
</Button>
<Button
type="button"
variant={showExpiring ? "default" : "secondary"}
disabled={expiringQuery.isLoading}
onClick={() => setShowExpiring((value) => !value)}
>
到期列表 {expiringContractCount}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" variant="secondary" disabled={exportMutation.isPending}>
{exportMutation.isPending ? "导出中..." : "导出"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => setConfirmExportFormat("csv")}>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setConfirmExportFormat("xlsx")}>
导出 Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
type="button"
variant={showAdvancedFilters || filters.expiryDateFrom || filters.expiryDateTo || showTrialData ? "default" : "secondary"}
onClick={() => setShowAdvancedFilters((value) => !value)}
>
筛选
</Button>
</div>
</HeaderActionGroup>
}
/>
@@ -672,27 +637,48 @@ export function ContractsRegistryPage() {
<>
<Button
type="button"
variant="secondary"
onClick={() => {
setFilters({
projectId: initialProjectId,
status: "",
contractType: "",
counterpartyName: "",
expiryDateFrom: "",
expiryDateTo: "",
});
setShowEditor(false);
setEditorId(null);
setEditor(buildEmptyEditor(initialProjectId));
}}
size="sm"
variant={showAdvancedFilters || filters.expiryDateFrom || filters.expiryDateTo || showTrialData ? "default" : "secondary"}
onClick={() => setShowAdvancedFilters((value) => !value)}
>
重置筛选
筛选
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" size="sm" variant={showExpiring ? "default" : "secondary"}>
更多
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{initialProjectId ? (
<DropdownMenuItem asChild>
<Link to="/projects">返回项目</Link>
</DropdownMenuItem>
) : null}
<DropdownMenuItem
disabled={expiringQuery.isLoading}
onSelect={() => setShowExpiring((value) => !value)}
>
{showExpiring ? "退出到期列表" : `到期列表 ${expiringContractCount}`}
</DropdownMenuItem>
<DropdownMenuItem
disabled={exportMutation.isPending}
onSelect={() => setConfirmExportFormat("csv")}
>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem
disabled={exportMutation.isPending}
onSelect={() => setConfirmExportFormat("xlsx")}
>
导出 Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
}
advanced={showAdvancedFilters || filters.expiryDateFrom || filters.expiryDateTo || showTrialData ? (
<div className="grid gap-3 md:grid-cols-[160px_160px_auto] xl:grid-cols-[180px_180px_auto]" data-testid="contracts-advanced-filters">
<div className="grid gap-3 md:grid-cols-[160px_160px_auto_auto] xl:grid-cols-[180px_180px_auto_auto]" data-testid="contracts-advanced-filters">
<div className="space-y-2">
<Label>到期日开始</Label>
<DateTextInput
@@ -714,6 +700,27 @@ export function ContractsRegistryPage() {
{showTrialData ? "隐藏测试数据" : "显示测试数据"}
</Button>
</div>
<div className="flex items-end">
<Button
type="button"
variant="secondary"
onClick={() => {
setFilters({
projectId: initialProjectId,
status: "",
contractType: "",
counterpartyName: "",
expiryDateFrom: "",
expiryDateTo: "",
});
setShowEditor(false);
setEditorId(null);
setEditor(buildEmptyEditor(initialProjectId));
}}
>
重置筛选
</Button>
</div>
</div>
) : null}
>
+37 -32
查看文件
@@ -29,7 +29,7 @@ import { hasPermission } from "../auth/permissions";
import { toast } from "sonner";
import { downloadBlob } from "../lib/download";
import { formatInvoiceOcrErrorMessage } from "../lib/invoiceOcr";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { HeaderActionGroup, ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
import { InvoiceUploadCard } from "./invoice-pool/InvoiceUploadCard";
@@ -475,7 +475,7 @@ export function InvoicePoolPage() {
<WorkspaceHeader
title="票据"
actions={
<div className="flex flex-wrap gap-2">
<HeaderActionGroup>
{isInvoiceDetailMode ? (
<Button type="button" variant="secondary" onClick={returnToInvoiceList}>
返回列表
@@ -484,41 +484,13 @@ export function InvoicePoolPage() {
<>
<Button
type="button"
variant={invoiceActionPanel === "upload" ? "default" : "secondary"}
onClick={() => setInvoiceActionPanel((current) => (current === "upload" ? null : "upload"))}
onClick={() => setInvoiceActionPanel("upload")}
>
上传票据
</Button>
<Button
type="button"
variant={invoiceActionPanel === "bind" ? "default" : "secondary"}
onClick={() => setInvoiceActionPanel((current) => (current === "bind" ? null : "bind"))}
>
绑定
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="invoice-export"
type="button"
variant="secondary"
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? "导出中..." : "导出"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem data-testid="invoice-export-csv" onSelect={() => exportMutation.mutate("csv")}>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem data-testid="invoice-export-xlsx" onSelect={() => exportMutation.mutate("xlsx")}>
导出 Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</HeaderActionGroup>
}
/>
@@ -573,6 +545,39 @@ export function InvoicePoolPage() {
{!isInvoiceDetailMode ? (
<InvoiceListCard
listQuery={listQuery}
actions={
<>
<Button
type="button"
size="sm"
variant={invoiceActionPanel === "bind" ? "default" : "secondary"}
onClick={() => setInvoiceActionPanel((current) => (current === "bind" ? null : "bind"))}
>
绑定
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="invoice-export"
type="button"
size="sm"
variant="secondary"
disabled={exportMutation.isPending}
>
{exportMutation.isPending ? "导出中..." : "导出"}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem data-testid="invoice-export-csv" onSelect={() => exportMutation.mutate("csv")}>
导出 CSV
</DropdownMenuItem>
<DropdownMenuItem data-testid="invoice-export-xlsx" onSelect={() => exportMutation.mutate("xlsx")}>
导出 Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
}
status={status}
setStatus={setStatus}
ownerScope={ownerScope}
+16 -7
查看文件
@@ -21,8 +21,9 @@ import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { HeaderActionGroup, ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import {
BusinessListToolbar,
BusinessMobileList,
BusinessTable,
} from "../components/patterns/BusinessListTemplate";
@@ -346,21 +347,18 @@ export function PaymentBatchesPage() {
<WorkspaceHeader
title="付款"
actions={
<div className="flex flex-wrap gap-2">
<HeaderActionGroup>
{selectedBatchId ? (
<Button type="button" variant="secondary" onClick={returnToList}>
返回列表
</Button>
) : null}
<Button type="button" variant="secondary" onClick={() => void refresh()}>
刷新
</Button>
{!selectedBatchId ? (
<Button type="button" variant={showCreatePanel ? "default" : "secondary"} onClick={() => setShowCreatePanel((current) => !current)}>
<Button type="button" onClick={() => setShowCreatePanel(true)}>
新建批次
</Button>
) : null}
</div>
</HeaderActionGroup>
}
/>
@@ -663,6 +661,17 @@ export function PaymentBatchesPage() {
)
) : (
<>
<BusinessListToolbar
actions={
<Button type="button" size="sm" variant="secondary" onClick={() => void refresh()}>
刷新
</Button>
}
>
<span className="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600">
全部批次 {batches.length}
</span>
</BusinessListToolbar>
<BusinessMobileList testId="payment-batches-mobile-list">
{batches.map((item) => (
<button
+30 -13
查看文件
@@ -14,7 +14,7 @@ import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { DateTextInput } from "../components/common/DateTextInput";
import { StatusBadge } from "../components/common/StatusBadge";
import { ListWorkspace, WorkspaceHeader, WorkspaceNotice } from "../components/patterns/ListWorkspace";
import { HeaderActionGroup, ListWorkspace, WorkspaceHeader, WorkspaceNotice } from "../components/patterns/ListWorkspace";
import {
BusinessListToolbar,
BusinessMobileList,
@@ -22,6 +22,12 @@ import {
} from "../components/patterns/BusinessListTemplate";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../components/ui/dropdown-menu";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import { Textarea } from "../components/ui/textarea";
@@ -179,19 +185,13 @@ export function ProjectsPage() {
<WorkspaceHeader
title="项目"
actions={
<div className="flex gap-2">
<Button asChild variant="secondary" type="button">
<Link to="/contracts-registry">合同</Link>
</Button>
<Button type="button" variant={showTrialData ? "default" : "secondary"} onClick={() => setShowTrialData((value) => !value)}>
{showTrialData ? "隐藏测试数据" : "显示测试数据"}
</Button>
<HeaderActionGroup>
{canManageProjects ? (
<Button type="button" variant={showCreatePanel ? "default" : "secondary"} onClick={() => setShowCreatePanel((value) => !value)}>
<Button type="button" onClick={() => setShowCreatePanel(true)}>
新建项目
</Button>
) : null}
</div>
</HeaderActionGroup>
}
/>
@@ -260,9 +260,26 @@ export function ProjectsPage() {
<div className="space-y-4">
<BusinessListToolbar
actions={
<Button type="button" size="sm" variant={mine ? "default" : "secondary"} onClick={() => updateSearch({ mine: !mine })}>
{mine ? "我相关" : "全范围"}
</Button>
<>
<Button type="button" size="sm" variant={mine ? "default" : "secondary"} onClick={() => updateSearch({ mine: !mine })}>
{mine ? "我相关" : "全范围"}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="button" size="sm" variant={showTrialData ? "default" : "secondary"}>
更多
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link to="/contracts-registry">查看合同</Link>
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setShowTrialData((value) => !value)}>
{showTrialData ? "隐藏测试数据" : "显示测试数据"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
}
>
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr),180px]">
+19 -16
查看文件
@@ -44,7 +44,7 @@ import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
import { Textarea } from "../components/ui/textarea";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { HeaderActionGroup, ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import {
BusinessListToolbar,
BusinessMobileList,
@@ -804,7 +804,7 @@ export function PurchaseRequestsPage() {
<WorkspaceHeader
title="采购"
actions={
<div className="flex flex-wrap gap-2">
<HeaderActionGroup>
{isPurchaseDetailMode ? (
<Button type="button" variant="secondary" onClick={returnToPurchaseList}>
返回列表
@@ -821,19 +821,9 @@ export function PurchaseRequestsPage() {
{createMutation.isPending ? "创建中..." : "新建采购"}
</Button>
) : null}
{canShowAdvancedFilters ? (
<Button
data-testid="purchase-advanced-filters"
type="button"
variant={showAdvancedFilterPanel ? "default" : "secondary"}
onClick={() => setShowAdvancedFilters((current) => !current)}
>
筛选
</Button>
) : null}
</>
)}
</div>
</HeaderActionGroup>
}
/>
@@ -847,9 +837,22 @@ export function PurchaseRequestsPage() {
<>
<BusinessListToolbar
actions={
<span className="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600">
{mine ? "我相关" : "全范围"}
</span>
<>
<span className="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600">
{mine ? "我相关" : "全范围"}
</span>
{canShowAdvancedFilters ? (
<Button
data-testid="purchase-advanced-filters"
type="button"
size="sm"
variant={showAdvancedFilterPanel ? "default" : "secondary"}
onClick={() => setShowAdvancedFilters((current) => !current)}
>
筛选
</Button>
) : null}
</>
}
advanced={showAdvancedFilterPanel ? (
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-4" data-testid="purchase-advanced-filters-panel">
@@ -1,3 +1,4 @@
import type { ReactNode } from "react";
import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query";
import type { InvoicePoolListResponse, InvoicePoolStatus } from "../../api/invoicePool";
import { StatusBadge } from "../../components/common/StatusBadge";
@@ -21,6 +22,7 @@ import { toast } from "sonner";
export type InvoiceListCardProps = {
listQuery: UseQueryResult<InvoicePoolListResponse, Error>;
actions?: ReactNode;
status: InvoicePoolStatus | "";
setStatus: (status: InvoicePoolStatus | "") => void;
ownerScope: "own" | "all";
@@ -84,6 +86,7 @@ function CompactInvoiceNumber({ value }: { value: string }) {
export function InvoiceListCard({
listQuery,
actions,
status,
setStatus,
ownerScope,
@@ -96,7 +99,7 @@ export function InvoiceListCard({
}: InvoiceListCardProps) {
return (
<div className="space-y-4">
<BusinessListToolbar>
<BusinessListToolbar actions={actions}>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<select
className="select-sm"
@@ -1100,8 +1100,10 @@ test("Wave3 PC 详情和通知中心视觉截图", async ({ page }) => {
await page.goto("/contracts-registry?projectId=project-wave3");
await expect(page.getByRole("heading", { name: "合同", exact: true })).toBeVisible();
await expect(page.getByTestId("contracts-registry-table").getByTestId("contracts-registry-row")).toBeVisible();
await expect(page.getByRole("button", { name: "导出" })).toBeVisible();
await page.getByRole("button", { name: "筛选" }).click();
await page.locator(".ops-command-bar__actions").getByRole("button", { name: "更多" }).click();
await expect(page.getByRole("menuitem", { name: "导出 CSV" })).toBeVisible();
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "筛选", exact: true }).click();
await expect(page.getByTestId("contracts-advanced-filters")).toBeVisible();
await page.getByRole("button", { name: "新增合同" }).click();
await expect(page.getByTestId("contracts-editor-panel")).toBeVisible();
@@ -348,6 +348,8 @@ test.describe("R103 cross page UI consistency", () => {
await expect(page.locator("header").filter({ hasText: "业务" }).getByText("采购")).toBeVisible();
await expect(page.getByRole("heading", { name: "采购" })).toBeVisible();
const purchaseHeader = page.locator(".page-header-shell").filter({ hasText: "采购" }).first();
await expect(purchaseHeader.getByRole("button", { name: "筛选" })).toHaveCount(0);
await expect(page.getByTestId("purchase-requests-table")).toBeVisible();
await expect(page.getByTestId("purchase-advanced-filters")).toBeVisible();
await expect(page.getByTestId("purchase-attachment-upload")).toHaveCount(0);
@@ -375,6 +377,9 @@ test.describe("R103 cross page UI consistency", () => {
await page.goto("/invoice-pool");
await expect(page.getByRole("heading", { name: "票据" })).toBeVisible();
const invoiceHeader = page.locator(".page-header-shell").filter({ hasText: "票据" }).first();
await expect(invoiceHeader.getByRole("button", { name: "绑定" })).toHaveCount(0);
await expect(invoiceHeader.getByTestId("invoice-export")).toHaveCount(0);
await expect(page.getByTestId("invoice-detail-panel")).toHaveCount(0);
await page.getByRole("button", { name: "绑定" }).click();
await expect(page.getByTestId("invoice-bind-reimbursement")).toContainText("草稿单 · 2026-05-06");