重构采购申请任务化页面(purchase-request-task-layout)
这个提交包含在:
@@ -3,7 +3,7 @@
|
||||
"name": "purchase-request-task-layout",
|
||||
"title": "采购申请任务化页面重构",
|
||||
"description": "按全页面设计规则重构采购申请页:任务卡、状态/证据/动作顺序、跨模块摘要跳转和审批动作归位。",
|
||||
"status": "planning",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": "frontend-page-redesign",
|
||||
"package": null,
|
||||
|
||||
@@ -43,6 +43,22 @@ function purchaseProgressFallback(status: string | undefined) {
|
||||
return "/mobile/approvals?progress=reviewing";
|
||||
}
|
||||
|
||||
function resolveMobilePurchaseEvidence(detail: {
|
||||
supplierId: string | null;
|
||||
legalEntityId: string | null;
|
||||
costCenterId: string | null;
|
||||
documentCategoryId: string | null;
|
||||
budgetPoolId: string | null;
|
||||
projectId: string | null;
|
||||
}) {
|
||||
if (!detail.supplierId) return "待补供应商";
|
||||
if (!detail.legalEntityId || !detail.costCenterId) return "待补法体 / 成本中心";
|
||||
if (!detail.documentCategoryId) return "待补单据分类";
|
||||
if (!detail.budgetPoolId) return "预算池未匹配";
|
||||
if (!detail.projectId) return "未关联项目";
|
||||
return "基础依据已齐,附件在材料区核对";
|
||||
}
|
||||
|
||||
export function MobilePurchaseRequestDetailPage() {
|
||||
const { purchaseRequestId = "" } = useParams();
|
||||
|
||||
@@ -59,6 +75,7 @@ export function MobilePurchaseRequestDetailPage() {
|
||||
? `当前在 ${detail.pendingWorkflow.task.stepName},可从审批详情处理;本页用于核对采购依据。`
|
||||
: resolvePurchaseStatusHint(detail)
|
||||
: "";
|
||||
const assetCandidateCount = detail?.items.filter((item) => item.isAssetCandidate).length ?? 0;
|
||||
|
||||
return (
|
||||
<PageScaffold className="space-y-3 pb-4">
|
||||
@@ -105,6 +122,10 @@ export function MobilePurchaseRequestDetailPage() {
|
||||
<div className="text-xs text-zinc-500">当前节点</div>
|
||||
<div className="mt-1 truncate font-medium text-zinc-900">{detail.pendingWorkflow?.task?.stepName ?? "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">材料摘要</div>
|
||||
<div className="mt-1 truncate font-medium text-zinc-900">{resolveMobilePurchaseEvidence(detail)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-subcard bg-white text-sm">
|
||||
<div className="text-xs font-semibold text-zinc-500">下一步</div>
|
||||
@@ -112,6 +133,28 @@ export function MobilePurchaseRequestDetailPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-purchase-detail-related">
|
||||
<h3 className="text-base font-semibold text-zinc-900">关联与后续</h3>
|
||||
<div className="mobile-field-list mt-3">
|
||||
<div className="mobile-field-row">
|
||||
<div className="mobile-field-label">关联项目</div>
|
||||
<div className="mobile-field-value">
|
||||
{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联项目"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
<div className="mobile-field-label">资产候选</div>
|
||||
<div className="mobile-field-value">
|
||||
{assetCandidateCount > 0 ? `${assetCandidateCount} 项,后续由管理员核对后手动建卡` : "无"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
<div className="mobile-field-label">付款 / 应付</div>
|
||||
<div className="mobile-field-value">仅查看状态,付款状态回到财务页面查看</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-purchase-detail-fields">
|
||||
<h3 className="text-base font-semibold text-zinc-900">业务信息</h3>
|
||||
<div className="mobile-field-list mt-3">
|
||||
|
||||
@@ -38,6 +38,31 @@ function formatDate(value: string | null | undefined) {
|
||||
return value ? value.slice(0, 10) : "-";
|
||||
}
|
||||
|
||||
function resolvePurchaseListNextStep(status: PurchaseRequestStatus) {
|
||||
if (status === "draft") return "继续完善采购材料";
|
||||
if (status === "reviewing") return "查看审批进度";
|
||||
if (status === "rejected") return "按意见补材料";
|
||||
if (status === "approved") return "核对到货 / 归档";
|
||||
return "查看取消记录";
|
||||
}
|
||||
|
||||
function resolvePurchaseListEvidence(item: PurchaseRequestSummary) {
|
||||
if (!item.supplierId) return "待补供应商";
|
||||
if (!item.legalEntityId || !item.costCenterId) return "待补法体 / 成本中心";
|
||||
if (!item.documentCategoryId) return "待补单据分类";
|
||||
if (!item.budgetPoolId) return "预算池未匹配";
|
||||
if (!item.projectId) return "未关联项目";
|
||||
return "基础依据已齐";
|
||||
}
|
||||
|
||||
function resolveMobileEmptyState(status: PurchaseRequestStatus) {
|
||||
if (status === "draft") return "暂无采购草稿";
|
||||
if (status === "reviewing") return "暂无审批中的采购";
|
||||
if (status === "rejected") return "暂无需要补材料的采购";
|
||||
if (status === "approved") return "暂无已通过采购";
|
||||
return "暂无已取消采购";
|
||||
}
|
||||
|
||||
function PurchaseRequestCard({
|
||||
item,
|
||||
currentMobileSource,
|
||||
@@ -74,6 +99,10 @@ function PurchaseRequestCard({
|
||||
item.expectedDeliveryDate ? `到货:${formatDate(item.expectedDeliveryDate)}` : null,
|
||||
])}
|
||||
</div>
|
||||
<div className="mt-3 rounded-lg bg-slate-50 px-3 py-2 text-xs leading-5 text-zinc-600">
|
||||
<div>材料:{resolvePurchaseListEvidence(item)}</div>
|
||||
<div>下一步:{resolvePurchaseListNextStep(item.status)}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -116,9 +145,11 @@ export function MobilePurchaseRequestsPage() {
|
||||
|
||||
return (
|
||||
<PageScaffold className="space-y-4">
|
||||
<PageHeader title="采购" />
|
||||
<PageHeader title="我的采购" />
|
||||
|
||||
<section className="mobile-card">
|
||||
<div className="text-sm font-semibold text-zinc-900">当前采购任务</div>
|
||||
<div className="mt-1 text-xs leading-5 text-zinc-500">手机端只看状态、材料摘要和下一步;台账筛选、全范围和导出留在电脑端。</div>
|
||||
<div className="mobile-info-grid">
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前列表</div>
|
||||
@@ -150,7 +181,7 @@ export function MobilePurchaseRequestsPage() {
|
||||
</div>
|
||||
) : null}
|
||||
{!listQuery.isLoading && !listQuery.isError && !items.length ? (
|
||||
<div className="mobile-empty-state">暂无采购申请</div>
|
||||
<div className="mobile-empty-state">{resolveMobileEmptyState(status)}</div>
|
||||
) : null}
|
||||
{items.map((item) => (
|
||||
<PurchaseRequestCard currentMobileSource={currentMobileSource} item={item} key={item.id} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { Archive, ArrowRight, Clock3, FilePenLine, RotateCcw, Wrench } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
@@ -51,6 +52,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ".
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
|
||||
import type { AttachmentGovernanceFields } from "../lib/attachmentGovernance";
|
||||
import { formatCompactIdentifier, formatDisplayDateTime, formatTextWithCompactLongNumbers, isCompactedIdentifier } from "../lib/displayFormat";
|
||||
import { buildPrecheckDialogMessage } from "../lib/precheck";
|
||||
@@ -115,6 +117,161 @@ function statusOptions(): Array<{ value: PurchaseRequestStatus | ""; label: stri
|
||||
];
|
||||
}
|
||||
|
||||
const PURCHASE_TASK_STATUS_OPTIONS = [
|
||||
{
|
||||
value: "rejected",
|
||||
title: "退回待补材料",
|
||||
idleNote: "暂无被退回的采购",
|
||||
activeNote: "按审批意见补材料后重新提交",
|
||||
icon: RotateCcw,
|
||||
accentClass: "border-red-200 bg-red-50 text-red-700",
|
||||
},
|
||||
{
|
||||
value: "draft",
|
||||
title: "草稿待完善",
|
||||
idleNote: "还没有采购草稿",
|
||||
activeNote: "继续补供应商、明细和附件",
|
||||
icon: FilePenLine,
|
||||
accentClass: "border-amber-200 bg-amber-50 text-amber-700",
|
||||
},
|
||||
{
|
||||
value: "reviewing",
|
||||
title: "审批中进度",
|
||||
idleNote: "暂无审批中的采购",
|
||||
activeNote: "查看当前节点和审批进度",
|
||||
icon: Clock3,
|
||||
accentClass: "border-sky-200 bg-sky-50 text-sky-700",
|
||||
},
|
||||
{
|
||||
value: "approved",
|
||||
title: "待验收 / 归档",
|
||||
idleNote: "暂无已通过待跟进采购",
|
||||
activeNote: "核对到货并登记 PO / 合同摘要",
|
||||
icon: Archive,
|
||||
accentClass: "border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
},
|
||||
] satisfies Array<{
|
||||
value: PurchaseRequestStatus;
|
||||
title: string;
|
||||
idleNote: string;
|
||||
activeNote: string;
|
||||
icon: typeof RotateCcw;
|
||||
accentClass: string;
|
||||
}>;
|
||||
|
||||
function formatPurchaseCurrency(value: string | number | null | undefined, currencyCode = "CNY") {
|
||||
const amount = Number(value ?? 0);
|
||||
if (!Number.isFinite(amount)) {
|
||||
return "-";
|
||||
}
|
||||
return new Intl.NumberFormat("zh-CN", { style: "currency", currency: currencyCode }).format(amount);
|
||||
}
|
||||
|
||||
function formatDate(value: string | null | undefined) {
|
||||
return value ? value.slice(0, 10) : "-";
|
||||
}
|
||||
|
||||
function resolvePurchaseEvidenceHint(item: {
|
||||
supplierId: string | null;
|
||||
legalEntityId: string | null;
|
||||
costCenterId: string | null;
|
||||
documentCategoryId: string | null;
|
||||
budgetPoolId: string | null;
|
||||
projectId: string | null;
|
||||
status: PurchaseRequestStatus;
|
||||
}) {
|
||||
const missingFields = [
|
||||
item.supplierId ? null : "供应商",
|
||||
item.legalEntityId ? null : "法体",
|
||||
item.costCenterId ? null : "成本中心",
|
||||
item.documentCategoryId ? null : "单据分类",
|
||||
].filter(Boolean);
|
||||
|
||||
if (missingFields.length) {
|
||||
return {
|
||||
label: `待补:${missingFields.slice(0, 2).join("、")}${missingFields.length > 2 ? "等" : ""}`,
|
||||
className: "text-amber-700",
|
||||
};
|
||||
}
|
||||
|
||||
if (!item.budgetPoolId) {
|
||||
return { label: "预算池未匹配", className: "text-amber-700" };
|
||||
}
|
||||
|
||||
if (!item.projectId) {
|
||||
return { label: "未关联项目", className: "text-zinc-500" };
|
||||
}
|
||||
|
||||
if (item.status === "approved") {
|
||||
return { label: "可核对验收 / 归档", className: "text-emerald-700" };
|
||||
}
|
||||
|
||||
return { label: "基础依据已齐,附件进详情核对", className: "text-emerald-700" };
|
||||
}
|
||||
|
||||
function resolvePurchaseNextStep(item: { status: PurchaseRequestStatus; pendingWorkflow?: PurchaseRequestDetail["pendingWorkflow"] }) {
|
||||
if (item.status === "draft") {
|
||||
return "继续完善材料并提交";
|
||||
}
|
||||
if (item.status === "reviewing") {
|
||||
return item.pendingWorkflow?.task?.stepName
|
||||
? `看审批进度:${item.pendingWorkflow.task.stepName}`
|
||||
: "查看审批进度";
|
||||
}
|
||||
if (item.status === "rejected") {
|
||||
return "按意见补材料后重交";
|
||||
}
|
||||
if (item.status === "approved") {
|
||||
return "核对到货 / 归档 PO";
|
||||
}
|
||||
return "查看取消记录";
|
||||
}
|
||||
|
||||
function resolvePurchaseRowActionLabel(status: PurchaseRequestStatus) {
|
||||
if (status === "draft") return "继续编辑";
|
||||
if (status === "rejected") return "补材料";
|
||||
if (status === "reviewing") return "看进度";
|
||||
if (status === "approved") return "验收/归档";
|
||||
return "查看记录";
|
||||
}
|
||||
|
||||
function resolvePurchaseEmptyStateCopy(status: PurchaseRequestStatus | "") {
|
||||
if (status === "draft") {
|
||||
return {
|
||||
title: "还没有采购草稿",
|
||||
description: "需要采购时先新建草稿,再补供应商、明细、预算和附件。",
|
||||
};
|
||||
}
|
||||
if (status === "reviewing") {
|
||||
return {
|
||||
title: "暂无审批中的采购",
|
||||
description: "提交后的采购会在这里显示当前审批节点。",
|
||||
};
|
||||
}
|
||||
if (status === "rejected") {
|
||||
return {
|
||||
title: "暂无需要补材料的采购",
|
||||
description: "被退回或驳回的采购会在这里继续修改后重新提交。",
|
||||
};
|
||||
}
|
||||
if (status === "approved") {
|
||||
return {
|
||||
title: "暂无已通过待跟进采购",
|
||||
description: "采购通过后,在这里核对到货、PO/合同归档和付款关联状态。",
|
||||
};
|
||||
}
|
||||
if (status === "cancelled") {
|
||||
return {
|
||||
title: "暂无已取消采购",
|
||||
description: "取消的采购记录会在这里留痕。",
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "暂无采购申请",
|
||||
description: "当前筛选范围内没有采购记录。",
|
||||
};
|
||||
}
|
||||
|
||||
function CompactBusinessCode({ value, copyable = true }: { value: string; copyable?: boolean }) {
|
||||
const displayValue = formatCompactIdentifier(value, { head: 12, tail: 4, minLength: 18 });
|
||||
const compacted = isCompactedIdentifier(value, displayValue);
|
||||
@@ -267,6 +424,7 @@ export function PurchaseRequestsPage() {
|
||||
const [supplierFilter, setSupplierFilter] = useState("");
|
||||
const [legalEntityFilter, setLegalEntityFilter] = useState("");
|
||||
const [costCenterFilter, setCostCenterFilter] = useState("");
|
||||
const [showLedgerTools, setShowLedgerTools] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [selectedId, setSelectedId] = useState(searchParams.get("id") ?? "");
|
||||
const [hydratedDetailId, setHydratedDetailId] = useState("");
|
||||
@@ -328,6 +486,26 @@ export function PurchaseRequestsPage() {
|
||||
}),
|
||||
});
|
||||
|
||||
const draftSummaryQuery = useQuery({
|
||||
queryKey: ["purchase-requests", "task-summary", "mine", "draft"],
|
||||
queryFn: () => fetchPurchaseRequests({ status: "draft", mine: true, page: 1, pageSize: 1 }),
|
||||
});
|
||||
|
||||
const rejectedSummaryQuery = useQuery({
|
||||
queryKey: ["purchase-requests", "task-summary", "mine", "rejected"],
|
||||
queryFn: () => fetchPurchaseRequests({ status: "rejected", mine: true, page: 1, pageSize: 1 }),
|
||||
});
|
||||
|
||||
const reviewingSummaryQuery = useQuery({
|
||||
queryKey: ["purchase-requests", "task-summary", "mine", "reviewing"],
|
||||
queryFn: () => fetchPurchaseRequests({ status: "reviewing", mine: true, page: 1, pageSize: 1 }),
|
||||
});
|
||||
|
||||
const approvedSummaryQuery = useQuery({
|
||||
queryKey: ["purchase-requests", "task-summary", "mine", "approved"],
|
||||
queryFn: () => fetchPurchaseRequests({ status: "approved", mine: true, page: 1, pageSize: 1 }),
|
||||
});
|
||||
|
||||
const detailQuery = useQuery({
|
||||
enabled: Boolean(selectedId),
|
||||
queryKey: ["purchase-requests", "detail", selectedId],
|
||||
@@ -467,10 +645,28 @@ export function PurchaseRequestsPage() {
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
|
||||
const purchaseItems = listQuery.data?.items ?? [];
|
||||
const taskSummaryTotalByStatus = {
|
||||
draft: draftSummaryQuery.data?.total,
|
||||
rejected: rejectedSummaryQuery.data?.total,
|
||||
reviewing: reviewingSummaryQuery.data?.total,
|
||||
approved: approvedSummaryQuery.data?.total,
|
||||
};
|
||||
const hasAdvancedFilters = Boolean(supplierFilter || legalEntityFilter || costCenterFilter || (canViewAll && !mine));
|
||||
const canShowLedgerTools =
|
||||
canViewAll ||
|
||||
Boolean((suppliersQuery.data?.items ?? []).length) ||
|
||||
Boolean((legalEntitiesQuery.data?.items ?? []).length) ||
|
||||
Boolean((costCentersQuery.data?.items ?? []).length);
|
||||
const showAdvancedTools = canShowLedgerTools && (showLedgerTools || hasAdvancedFilters);
|
||||
const emptyState = resolvePurchaseEmptyStateCopy(status);
|
||||
const totalAmountPreview = useMemo(
|
||||
() => items.reduce((sum, item) => sum + Number(moneyPreview(item.quantity, item.unitPrice)), 0).toFixed(2),
|
||||
[items],
|
||||
);
|
||||
const assetCandidateCount = useMemo(
|
||||
() => items.filter((item) => item.isAssetCandidate).length,
|
||||
[items],
|
||||
);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => createPurchaseRequest(),
|
||||
@@ -803,6 +999,16 @@ export function PurchaseRequestsPage() {
|
||||
toast.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
function focusPurchaseStatus(nextStatus: PurchaseRequestStatus | "") {
|
||||
setStatus(nextStatus);
|
||||
setMine(true);
|
||||
setSupplierFilter("");
|
||||
setLegalEntityFilter("");
|
||||
setCostCenterFilter("");
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
const erpTrack = erpTrackQuery.data ?? null;
|
||||
const canRetryErp = Boolean(canIntegrationManage && erpTrack?.permissions.canRetry);
|
||||
const showArchiveSection = Boolean(detail && (detail.status === "approved" || archiveOrder));
|
||||
@@ -820,13 +1026,16 @@ export function PurchaseRequestsPage() {
|
||||
isPurchaseParticipant &&
|
||||
detail?.pendingWorkflow?.task?.assigneeId !== me?.id,
|
||||
);
|
||||
const detailEvidenceHint = detail ? resolvePurchaseEvidenceHint(detail) : null;
|
||||
const detailNextStepText = detail ? resolvePurchaseNextStep(detail) : "";
|
||||
|
||||
return (
|
||||
<ListWorkspace>
|
||||
{downloadReasonDialog}
|
||||
<WorkspaceHeader
|
||||
eyebrow="采购"
|
||||
title="采购申请"
|
||||
subtitle="草稿、附件、审批。"
|
||||
subtitle={mine ? "先看与你相关的采购任务,再进入单据核对材料和进度。" : "当前为全范围台账视角,处理动作仍回到详情或审批任务。"}
|
||||
actions={
|
||||
canCreate ? (
|
||||
<Button
|
||||
@@ -847,23 +1056,91 @@ export function PurchaseRequestsPage() {
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<FilterBar className="space-y-4">
|
||||
<div className="grid gap-3 md:grid-cols-5">
|
||||
<select
|
||||
className="select-std"
|
||||
<section className="grid gap-3 md:grid-cols-2 xl:grid-cols-4" aria-label="我的采购任务">
|
||||
{PURCHASE_TASK_STATUS_OPTIONS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const count = taskSummaryTotalByStatus[item.value];
|
||||
const active = status === item.value && mine && !hasAdvancedFilters;
|
||||
const displayCount = typeof count === "number" ? count : "-";
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`card-std min-h-[124px] p-4 text-left transition hover:border-primary/40 hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-200 ${
|
||||
active ? "border-primary/40 bg-primary/5" : ""
|
||||
}`}
|
||||
data-testid={`purchase-task-card-${item.value}`}
|
||||
key={item.value}
|
||||
onClick={() => focusPurchaseStatus(item.value)}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<span className={`flex size-9 items-center justify-center rounded-lg border ${item.accentClass}`}>
|
||||
<Icon aria-hidden className="size-4" />
|
||||
</span>
|
||||
<ArrowRight aria-hidden className="mt-1 size-4 text-zinc-300" />
|
||||
</div>
|
||||
<div className="mt-4 flex items-end justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold text-zinc-900">{item.title}</div>
|
||||
<div className="mt-1 text-xs leading-5 text-zinc-500">
|
||||
{typeof count === "number" && count > 0 ? item.activeNote : item.idleNote}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-3xl font-semibold tabular-nums text-zinc-950">{displayCount}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
<FilterBar className="ops-command-bar space-y-3 !p-3 md:!p-3.5">
|
||||
<div className="ops-command-bar__row">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<div className="text-xs font-semibold text-zinc-500">采购记录</div>
|
||||
<Tabs
|
||||
data-testid="purchase-status-tabs"
|
||||
value={status}
|
||||
onChange={(event) => {
|
||||
setStatus(event.target.value as PurchaseRequestStatus | "");
|
||||
onValueChange={(value) => {
|
||||
setStatus(value as PurchaseRequestStatus | "");
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<TabsList className="ops-command-bar__tabs !h-auto !justify-start !bg-slate-50">
|
||||
{statusOptions().map((item) => (
|
||||
<option key={item.label} value={item.value}>
|
||||
<TabsTrigger
|
||||
data-testid={`purchase-tab-${item.value || "all"}`}
|
||||
key={item.label}
|
||||
value={item.value}
|
||||
>
|
||||
{item.label}
|
||||
</option>
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</select>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<div className="ops-command-bar__actions">
|
||||
<span className="rounded-full bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600">
|
||||
{mine ? "我相关" : "全范围"}
|
||||
</span>
|
||||
{canShowLedgerTools ? (
|
||||
<Button
|
||||
data-testid="purchase-ledger-tools"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={showAdvancedTools ? "default" : "secondary"}
|
||||
onClick={() => setShowLedgerTools((current) => !current)}
|
||||
>
|
||||
<Wrench aria-hidden className="mr-1 size-4" />
|
||||
台账工具
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAdvancedTools ? (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50/70 p-3" data-testid="purchase-ledger-tools-panel">
|
||||
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-4">
|
||||
<select
|
||||
className="select-std"
|
||||
value={supplierFilter}
|
||||
@@ -919,6 +1196,8 @@ export function PurchaseRequestsPage() {
|
||||
{canViewAll ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
data-testid="purchase-scope-mine"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={mine ? "default" : "secondary"}
|
||||
onClick={() => {
|
||||
@@ -926,9 +1205,11 @@ export function PurchaseRequestsPage() {
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
仅自己
|
||||
我相关
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="purchase-scope-all"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={!mine ? "default" : "secondary"}
|
||||
onClick={() => {
|
||||
@@ -939,8 +1220,14 @@ export function PurchaseRequestsPage() {
|
||||
全范围
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
) : (
|
||||
<div className="rounded-lg bg-white px-3 py-2 text-sm text-zinc-500">
|
||||
当前仅显示与你相关的采购。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</FilterBar>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[420px_minmax(0,1fr)]">
|
||||
@@ -962,7 +1249,9 @@ export function PurchaseRequestsPage() {
|
||||
<>
|
||||
<div className="space-y-3 md:hidden">
|
||||
{purchaseItems.length ? (
|
||||
purchaseItems.map((item) => (
|
||||
purchaseItems.map((item) => {
|
||||
const evidenceHint = resolvePurchaseEvidenceHint(item);
|
||||
return (
|
||||
<button
|
||||
data-testid="purchase-request-mobile-row"
|
||||
key={item.id}
|
||||
@@ -988,42 +1277,55 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 text-xs text-zinc-600">
|
||||
<div>
|
||||
<div className="text-zinc-400">申请人</div>
|
||||
<div className="mt-0.5 truncate text-zinc-700">{item.applicant?.name ?? "-"}</div>
|
||||
<div className="text-zinc-400">金额 / 到货</div>
|
||||
<div className="mt-0.5 font-medium text-zinc-900">
|
||||
{formatPurchaseCurrency(item.totalAmount, item.currencyCode)}
|
||||
</div>
|
||||
<div className="mt-0.5 text-zinc-500">{formatDate(item.expectedDeliveryDate)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-zinc-400">金额</div>
|
||||
<div className="mt-0.5 font-medium text-zinc-900">{item.totalAmount}</div>
|
||||
<div className="text-zinc-400">供应商</div>
|
||||
<div className="mt-0.5 truncate text-zinc-700">{item.supplier?.name ?? "待补供应商"}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="text-zinc-400">关联项目</div>
|
||||
<div className="text-zinc-400">项目 / 证据</div>
|
||||
<div className="mt-0.5 truncate text-zinc-700">
|
||||
{item.project
|
||||
? `${formatTextWithCompactLongNumbers(item.project.projectNo)} · ${item.project.name}`
|
||||
: "未关联项目"}
|
||||
</div>
|
||||
<div className={`mt-0.5 ${evidenceHint.className}`}>{evidenceHint.label}</div>
|
||||
</div>
|
||||
<div className="col-span-2 rounded-md bg-slate-50 px-2 py-1.5 text-zinc-700">
|
||||
下一步:{resolvePurchaseNextStep(item)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="rounded-lg border border-zinc-200 p-4 text-sm text-zinc-500">暂无采购申请</div>
|
||||
<div className="rounded-lg border border-zinc-200 p-4 text-sm text-zinc-500">
|
||||
<div className="font-semibold text-zinc-900">{emptyState.title}</div>
|
||||
<div className="mt-1 leading-6">{emptyState.description}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<PrimaryTable className="hidden md:block">
|
||||
<Table data-testid="purchase-requests-table">
|
||||
<TableHeader>
|
||||
<tr>
|
||||
<TableHead>单据</TableHead>
|
||||
<TableHead>申请人</TableHead>
|
||||
<TableHead>项目</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>金额</TableHead>
|
||||
<TableHead>单据 / 对象</TableHead>
|
||||
<TableHead>金额 / 供应商</TableHead>
|
||||
<TableHead>项目 / 合同</TableHead>
|
||||
<TableHead>状态 / 证据</TableHead>
|
||||
<TableHead>下一步</TableHead>
|
||||
</tr>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{purchaseItems.length ? (
|
||||
purchaseItems.map((item) => (
|
||||
purchaseItems.map((item) => {
|
||||
const evidenceHint = resolvePurchaseEvidenceHint(item);
|
||||
return (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className={item.id === selectedId ? "bg-zinc-50" : undefined}
|
||||
@@ -1039,8 +1341,15 @@ export function PurchaseRequestsPage() {
|
||||
<div className="text-xs text-zinc-500" title={item.title || undefined}>
|
||||
{formatTextWithCompactLongNumbers(item.title || "未命名采购申请")}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">申请人:{item.applicant?.name ?? "-"}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-semibold text-zinc-900">
|
||||
{formatPurchaseCurrency(item.totalAmount, item.currencyCode)}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">{item.supplier?.name ?? "待补供应商"}</div>
|
||||
<div className="text-xs text-zinc-500">到货:{formatDate(item.expectedDeliveryDate)}</div>
|
||||
</TableCell>
|
||||
<TableCell>{item.applicant?.name ?? "-"}</TableCell>
|
||||
<TableCell>
|
||||
{item.project ? (
|
||||
<div>
|
||||
@@ -1052,15 +1361,41 @@ export function PurchaseRequestsPage() {
|
||||
) : (
|
||||
<span className="text-zinc-500">未关联项目</span>
|
||||
)}
|
||||
<div className="mt-1 text-xs text-zinc-500">合同:详情页登记归档摘要</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={item.status} />
|
||||
<div className={`mt-1 text-xs ${evidenceHint.className}`}>{evidenceHint.label}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="max-w-[160px] text-xs leading-5 text-zinc-600" data-testid="purchase-row-next-step">
|
||||
{resolvePurchaseNextStep(item)}
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
data-testid="purchase-row-open"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={["draft", "rejected"].includes(item.status) ? "default" : "secondary"}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedId(item.id);
|
||||
setHydratedDetailId("");
|
||||
}}
|
||||
>
|
||||
{resolvePurchaseRowActionLabel(item.status)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
<TableCell><StatusBadge status={item.status} /></TableCell>
|
||||
<TableCell>{item.totalAmount}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell className="py-4 text-zinc-500" colSpan={5}>
|
||||
暂无采购申请
|
||||
<div className="py-3 text-center">
|
||||
<div className="font-semibold text-zinc-900">{emptyState.title}</div>
|
||||
<div className="mt-1 leading-6 text-zinc-500">{emptyState.description}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -1101,38 +1436,54 @@ export function PurchaseRequestsPage() {
|
||||
<CompactBusinessCode value={resolvePurchaseDisplayCode(detail)} />
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500">申请人:{detail.applicant?.name ?? "-"}</p>
|
||||
<p className="mt-1 text-sm text-zinc-500" data-testid="purchase-status-hint">
|
||||
</div>
|
||||
<StatusBadge data-testid="purchase-detail-status" status={detail.status} />
|
||||
</div>
|
||||
|
||||
<section className="detail-command-panel space-y-4" data-testid="purchase-next-step-panel">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs font-semibold text-zinc-500">当前状态与下一步</div>
|
||||
<p className="mt-1 text-sm leading-6 text-zinc-700" data-testid="purchase-status-hint">
|
||||
{resolvePurchaseStatusHint(detail)}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-medium text-zinc-900">
|
||||
下一步:{detailNextStepText}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<StatusBadge data-testid="purchase-detail-status" status={detail.status} />
|
||||
<div className="flex shrink-0 flex-wrap gap-2">
|
||||
{canEditDraft ? (
|
||||
<Button
|
||||
data-testid="purchase-save"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={!canEditDraft || saveMutation.isPending}
|
||||
disabled={saveMutation.isPending}
|
||||
onClick={() => saveMutation.mutate()}
|
||||
>
|
||||
{saveMutation.isPending ? "保存中..." : "保存草稿"}
|
||||
</Button>
|
||||
) : null}
|
||||
{canSubmit ? (
|
||||
<Button
|
||||
data-testid="purchase-submit"
|
||||
type="button"
|
||||
disabled={!canSubmit || submitMutation.isPending}
|
||||
disabled={submitMutation.isPending}
|
||||
onClick={() => void handleSubmitAction()}
|
||||
>
|
||||
{submitMutation.isPending ? "提交中..." : "提交审批"}
|
||||
</Button>
|
||||
) : null}
|
||||
{canWithdraw ? (
|
||||
<Button
|
||||
data-testid="purchase-withdraw"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={!canWithdraw || withdrawMutation.isPending}
|
||||
disabled={withdrawMutation.isPending}
|
||||
onClick={() => withdrawMutation.mutate()}
|
||||
>
|
||||
{withdrawMutation.isPending ? "撤回中..." : "撤回"}
|
||||
</Button>
|
||||
) : null}
|
||||
{canRemindPendingWorkflow && pendingTaskId ? (
|
||||
<Button
|
||||
data-testid="purchase-remind-workflow"
|
||||
@@ -1147,63 +1498,33 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">采购状态</div>
|
||||
<div className="mt-2"><StatusBadge status={detail.status} /></div>
|
||||
<div className="detail-fact-grid xl:grid-cols-4">
|
||||
<div className="detail-fact-card">
|
||||
<div className="detail-fact-card__label">总金额</div>
|
||||
<div className="detail-fact-card__value">{formatPurchaseCurrency(totalAmountPreview, currencyCode)}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">总金额</div>
|
||||
<div className="mt-2 font-semibold text-zinc-900">{totalAmountPreview}</div>
|
||||
<div className="detail-fact-card">
|
||||
<div className="detail-fact-card__label">当前节点</div>
|
||||
<div className="detail-fact-card__value">{detail.pendingWorkflow?.task?.stepName ?? "-"}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">当前节点</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{detail.pendingWorkflow?.task?.stepName ?? "-"}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
|
||||
<div className="text-zinc-500">附件</div>
|
||||
<div className="mt-2 font-medium text-zinc-900">{detailAttachments.length} 个</div>
|
||||
<div className="detail-fact-card">
|
||||
<div className="detail-fact-card__label">材料摘要</div>
|
||||
<div className={`detail-fact-card__value ${detailEvidenceHint?.className ?? ""}`}>
|
||||
{detailEvidenceHint?.label ?? "-"} · 附件 {detailAttachments.length} 个
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-zinc-200 bg-white p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="font-medium text-zinc-900">关联项目</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联项目"}
|
||||
<div className="detail-fact-card">
|
||||
<div className="detail-fact-card__label">资产候选</div>
|
||||
<div className="detail-fact-card__value">
|
||||
{assetCandidateCount > 0 ? `${assetCandidateCount} 项需后续人工建卡判断` : "无资产候选"}
|
||||
</div>
|
||||
</div>
|
||||
{detail.project ? (
|
||||
<Link to={`/projects/${detail.project.id}`} className="text-sm text-primary hover:underline">
|
||||
查看项目
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<RiskFlagsCard items={detailRiskFlags} title="待处理风险项" />
|
||||
|
||||
<SectionPanel title="审批记录" description="展示最近审批动作、退回或驳回说明,便于申请人按意见修改。">
|
||||
{detailApprovalHistory.length ? (
|
||||
<div className="space-y-2" data-testid="purchase-approval-history">
|
||||
{detailApprovalHistory.map((item) => (
|
||||
<div key={item.id} className="rounded-lg border border-zinc-200 px-3 py-2 text-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="font-medium text-zinc-900">
|
||||
{workflowActionLabel(item.action)} · {item.actorName ?? item.actorId}
|
||||
</span>
|
||||
<span className="text-xs text-zinc-500">{formatApprovalHistoryTime(item.createdAt)}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-zinc-600">{item.comment || "无补充说明"}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-zinc-500">暂无审批记录。</div>
|
||||
)}
|
||||
</SectionPanel>
|
||||
|
||||
<SectionPanel title="业务摘要" description="只保留提交和审批需要核对的采购事实。">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-700">标题</label>
|
||||
@@ -1313,6 +1634,7 @@ export function PurchaseRequestsPage() {
|
||||
{resolvePurchaseBudgetHint(detail)}
|
||||
</div>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -1541,8 +1863,8 @@ export function PurchaseRequestsPage() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-base font-semibold text-zinc-900">附件</h4>
|
||||
<p className="text-sm text-zinc-500">用于订单、报价、验收或情况说明;草稿状态可增删,下载会留痕。</p>
|
||||
<h4 className="text-base font-semibold text-zinc-900">材料证据</h4>
|
||||
<p className="text-sm text-zinc-500">用于报价、订单、验收或情况说明;草稿状态可增删,下载会留痕。</p>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<input
|
||||
@@ -1670,6 +1992,42 @@ export function PurchaseRequestsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SectionPanel title="关联摘要" description="跨模块只展示摘要和入口,采购页不直接执行付款或资产建卡。">
|
||||
<div className="grid gap-3 text-sm md:grid-cols-2">
|
||||
<div className="detail-evidence-card">
|
||||
<div className="font-medium text-zinc-900">关联项目</div>
|
||||
<div className="mt-1 text-zinc-600">
|
||||
{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联项目"}
|
||||
</div>
|
||||
{detail.project ? (
|
||||
<Link to={`/projects/${detail.project.id}`} className="mt-2 inline-flex text-sm text-primary hover:underline">
|
||||
查看项目
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="detail-evidence-card">
|
||||
<div className="font-medium text-zinc-900">资产候选</div>
|
||||
<div className="mt-1 text-zinc-600">
|
||||
{assetCandidateCount > 0
|
||||
? `${assetCandidateCount} 项可能后续建卡;需管理员到货核对后手动创建资产卡片。`
|
||||
: "当前明细未标记资产候选。"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="detail-evidence-card">
|
||||
<div className="font-medium text-zinc-900">付款 / 应付</div>
|
||||
<div className="mt-1 text-zinc-600">本页只展示采购依据;付款状态和回单后续在付款或应付账款页面处理。</div>
|
||||
</div>
|
||||
<div className="detail-evidence-card">
|
||||
<div className="font-medium text-zinc-900">PO / 合同</div>
|
||||
<div className="mt-1 text-zinc-600">
|
||||
{archiveOrder
|
||||
? `已登记 PO ${archiveOrder.poNo}${archiveOrder.contractNo ? ` / 合同 ${archiveOrder.contractNo}` : ""}`
|
||||
: "已通过后由采购/行政登记归档摘要,不在列表下载合同正文。"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionPanel>
|
||||
|
||||
{showArchiveSection ? (
|
||||
<div className="space-y-3 rounded-lg border border-zinc-200 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -1817,6 +2175,26 @@ export function PurchaseRequestsPage() {
|
||||
</SectionPanel>
|
||||
) : null}
|
||||
|
||||
<SectionPanel title="审批记录" description="展示最近审批动作、退回或驳回说明,便于申请人按意见修改。">
|
||||
{detailApprovalHistory.length ? (
|
||||
<div className="space-y-2" data-testid="purchase-approval-history">
|
||||
{detailApprovalHistory.map((item) => (
|
||||
<div key={item.id} className="detail-timeline-item">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="font-medium text-zinc-900">
|
||||
{workflowActionLabel(item.action)} · {item.actorName ?? item.actorId}
|
||||
</span>
|
||||
<span className="text-xs text-zinc-500">{formatApprovalHistoryTime(item.createdAt)}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-zinc-600">{item.comment || "无补充说明"}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-zinc-500">暂无审批记录。</div>
|
||||
)}
|
||||
</SectionPanel>
|
||||
|
||||
<AiSuggestionCard targetType="purchase_request" targetId={detail.id} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -1145,6 +1145,7 @@ test("Wave3 移动关键页视觉截图", async ({ page }) => {
|
||||
|
||||
await page.goto("/mobile/purchase-requests/purchase-wave3");
|
||||
await expect(page.getByTestId("mobile-purchase-detail-summary")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-purchase-detail-related")).toBeVisible();
|
||||
await expect(page.getByTestId("mobile-purchase-detail-items")).toBeVisible();
|
||||
await expect(page.locator("table")).toHaveCount(0);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
|
||||
@@ -287,7 +287,10 @@ async function mockApis(page: Page) {
|
||||
}
|
||||
|
||||
if (pathName === "/api/purchase-requests" && request.method() === "GET") {
|
||||
await fulfillJson(route, { page: 1, pageSize: 20, total: 1, mine: true, items: [purchaseSummary()] });
|
||||
const summary = purchaseSummary();
|
||||
const queryStatus = url.searchParams.get("status");
|
||||
const items = !queryStatus || queryStatus === summary.status ? [summary] : [];
|
||||
await fulfillJson(route, { page: 1, pageSize: 20, total: items.length, mine: true, items });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -345,6 +348,10 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
|
||||
await expect(page.locator("header").filter({ hasText: "业务" }).getByText("采购申请")).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "采购申请" })).toBeVisible();
|
||||
await expect(page.getByTestId("purchase-task-card-draft")).toBeVisible();
|
||||
await expect(page.getByTestId("purchase-ledger-tools")).toBeVisible();
|
||||
await expect(page.getByTestId("purchase-row-next-step")).toContainText("继续完善材料并提交");
|
||||
await expect(page.getByText("付款处理")).toHaveCount(0);
|
||||
await expect(page.getByText("/purchase-requests")).toHaveCount(0);
|
||||
await expect(page.getByTestId("purchase-attachment-upload")).toHaveText("上传附件");
|
||||
await expect(page.getByText("Choose File")).toHaveCount(0);
|
||||
@@ -362,7 +369,7 @@ test.describe("R103 cross page UI consistency", () => {
|
||||
await page.setViewportSize({ width: 1440, height: 960 });
|
||||
await page.goto("/invoice-pool");
|
||||
|
||||
await expect(page.locator("header").filter({ hasText: "财务" }).getByText("票据中心")).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "票据中心" })).toBeVisible();
|
||||
await expect(page.getByTestId("invoice-bind-reimbursement")).toContainText("草稿单 · 2026-05-06");
|
||||
await expect(page.getByTestId("invoice-bind-reimbursement")).not.toContainText(reimbursementId);
|
||||
await expect(page.getByText("/invoice-pool")).toHaveCount(0);
|
||||
|
||||
在新工单中引用
屏蔽一个用户