DG7/MRF7 移动信息架构与展示去重治理
这个提交包含在:
@@ -0,0 +1,160 @@
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { fetchPurchaseRequestDetail } from "../api/purchaseRequests";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
formatApprovalHistoryTime,
|
||||
resolvePurchaseDisplayCode,
|
||||
resolvePurchaseStatusHint,
|
||||
workflowActionLabel,
|
||||
} from "../lib/approvalDisplay";
|
||||
import { joinBusinessMeta } from "../lib/businessDisplay";
|
||||
import { resolveErrorMessage } from "../lib/resolveErrorMessage";
|
||||
|
||||
function formatCurrency(value: string | 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 formatBytes(value: number) {
|
||||
if (value < 1024) {
|
||||
return `${value} B`;
|
||||
}
|
||||
if (value < 1024 * 1024) {
|
||||
return `${(value / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
return `${(value / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export function MobilePurchaseRequestDetailPage() {
|
||||
const { purchaseRequestId = "" } = useParams();
|
||||
|
||||
const detailQuery = useQuery({
|
||||
enabled: Boolean(purchaseRequestId),
|
||||
queryKey: ["mobile", "purchase-request", "detail", purchaseRequestId],
|
||||
queryFn: () => fetchPurchaseRequestDetail(purchaseRequestId),
|
||||
});
|
||||
|
||||
const detail = detailQuery.data ?? null;
|
||||
const displayCode = detail ? resolvePurchaseDisplayCode(detail) : "采购详情";
|
||||
|
||||
return (
|
||||
<PageScaffold className="space-y-3 pb-4">
|
||||
<PageHeader
|
||||
title="采购详情"
|
||||
meta={detail ? <StatusBadge status={detail.status} /> : null}
|
||||
actions={
|
||||
<Button asChild size="sm" type="button" variant="secondary">
|
||||
<Link to="/mobile/approvals">返回</Link>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{!purchaseRequestId ? (
|
||||
<div className="mobile-card text-sm text-red-600">缺少采购申请 ID。</div>
|
||||
) : null}
|
||||
|
||||
{detailQuery.isLoading ? <div className="mobile-card text-sm text-zinc-500">加载中...</div> : null}
|
||||
|
||||
{detailQuery.isError ? (
|
||||
<div className="mobile-card text-sm text-red-600">{resolveErrorMessage(detailQuery.error)}</div>
|
||||
) : null}
|
||||
|
||||
{detail ? (
|
||||
<>
|
||||
<section className="mobile-card" data-testid="mobile-purchase-detail-summary">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h2 className="truncate text-base font-semibold text-zinc-900">{displayCode}</h2>
|
||||
<p className="mt-1 text-sm text-zinc-500">{detail.title || "未命名采购申请"}</p>
|
||||
</div>
|
||||
<StatusBadge status={detail.status} />
|
||||
</div>
|
||||
<div className="mobile-info-grid mt-4 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">金额</div>
|
||||
<div className="mt-1 text-lg font-semibold text-zinc-900">
|
||||
{formatCurrency(detail.totalAmount, detail.currencyCode)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">申请人</div>
|
||||
<div className="mt-1 truncate font-medium text-zinc-900">{detail.applicant?.name ?? "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 rounded-lg bg-zinc-50 px-3 py-2 text-xs leading-5 text-zinc-600">
|
||||
{resolvePurchaseStatusHint(detail)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-purchase-detail-fields">
|
||||
<h3 className="text-base font-semibold text-zinc-900">业务信息</h3>
|
||||
<div className="mt-3 space-y-2 text-sm text-zinc-700">
|
||||
<div>{joinBusinessMeta([`供应商:${detail.supplier?.name ?? "-"}`, `期望到货:${formatDate(detail.expectedDeliveryDate)}`])}</div>
|
||||
<div>{joinBusinessMeta([`法体:${detail.legalEntity?.name ?? "-"}`, `成本中心:${detail.costCenter?.name ?? "-"}`])}</div>
|
||||
<div>{joinBusinessMeta([`预算池:${detail.budgetPool?.name ?? "未匹配"}`, `当前节点:${detail.pendingWorkflow?.task?.stepName ?? "-"}`])}</div>
|
||||
{detail.reason ? <p className="leading-6 text-zinc-500">{detail.reason}</p> : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-purchase-detail-items">
|
||||
<h3 className="text-base font-semibold text-zinc-900">采购明细</h3>
|
||||
<div className="mt-3 space-y-2">
|
||||
{detail.items.length ? detail.items.map((item) => (
|
||||
<article className="mobile-list-card text-sm" key={item.id}>
|
||||
<div className="font-medium text-zinc-900">{item.description}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
{joinBusinessMeta([`数量:${item.quantity}`, `单价:${formatCurrency(item.unitPrice, detail.currencyCode)}`, `金额:${formatCurrency(item.lineAmount, detail.currencyCode)}`])}
|
||||
</div>
|
||||
{item.specification ? <div className="mt-1 text-xs text-zinc-500">{item.specification}</div> : null}
|
||||
</article>
|
||||
)) : <div className="mobile-empty-state">暂无采购明细</div>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-purchase-detail-attachments">
|
||||
<h3 className="text-base font-semibold text-zinc-900">附件</h3>
|
||||
<div className="mt-3 space-y-2">
|
||||
{detail.attachments.length ? detail.attachments.map((attachment) => (
|
||||
<article className="mobile-list-card text-sm" key={attachment.id}>
|
||||
<div className="truncate font-medium text-zinc-900">{attachment.originalName}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">
|
||||
{joinBusinessMeta([formatBytes(attachment.fileSize), attachment.uploader?.name, formatDate(attachment.createdAt)])}
|
||||
</div>
|
||||
</article>
|
||||
)) : <div className="mobile-empty-state">暂无附件</div>}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mobile-card" data-testid="mobile-purchase-detail-approval-history">
|
||||
<h3 className="text-base font-semibold text-zinc-900">审批记录</h3>
|
||||
<div className="mt-3 space-y-2">
|
||||
{detail.approvalHistory.length ? detail.approvalHistory.map((item) => (
|
||||
<article className="mobile-list-card text-sm" key={item.id}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="font-medium text-zinc-900">{workflowActionLabel(item.action)}</div>
|
||||
<span className="text-xs text-zinc-500">{formatApprovalHistoryTime(item.createdAt)}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{item.actorName ?? item.actorEmployeeNo ?? "-"}</div>
|
||||
{item.comment ? <div className="mt-1 text-xs text-zinc-500">{item.comment}</div> : null}
|
||||
</article>
|
||||
)) : <div className="mobile-empty-state">暂无审批记录</div>}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : null}
|
||||
</PageScaffold>
|
||||
);
|
||||
}
|
||||
|
||||
export default MobilePurchaseRequestDetailPage;
|
||||
在新工单中引用
屏蔽一个用户