统一页面右上角操作区(05-31-unified-page-header-actions)
这个提交包含在:
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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");
|
||||
|
||||
在新工单中引用
屏蔽一个用户