重构采购申请任务化页面(purchase-request-task-layout)

这个提交包含在:
2026-05-30 10:29:49 +08:00
父节点 e0aa2ead30
当前提交 b1121c14fe
共修改 6 个文件,包含 727 行新增和 267 行删除
@@ -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} />
文件差异内容过多而无法显示 加载差异
@@ -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);