修复报销详情时区并按 KISS 收敛页面(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-31 16:44:31 +08:00
父节点 35b44749a5
当前提交 39c3e808a3
共修改 10 个文件,包含 223 行新增和 227 行删除
@@ -59,6 +59,7 @@ import {
} from "../lib/invoiceStatus";
import { resolveErrorMessage } from "../lib/resolveErrorMessage";
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
import { formatDisplayDateTime } from "../lib/displayFormat";
import type { AttachmentGovernanceFields } from "../lib/attachmentGovernance";
import { TEST_IDS } from "../lib/testIds";
import { withUploadTimeout } from "../lib/uploadTimeout";
@@ -122,13 +123,6 @@ function formatDate(value: string | null | undefined) {
return value || "未填写";
}
function formatDateTime(value: string | null | undefined) {
if (!value) {
return "-";
}
return value.slice(0, 19).replace("T", " ");
}
function workflowActionLabel(action: string) {
const labels: Record<string, string> = {
start: "提交",
@@ -171,34 +165,6 @@ function itemLabel(item: ReimbursementItem) {
return desc ? `${typeName} · ${desc}` : typeName;
}
function resolveMobileReimbursementNextStep(input: {
status: ReimbursementStatus;
canEdit: boolean;
itemCount: number;
invoiceCount: number;
attachmentCount: number;
}) {
if (input.canEdit && input.itemCount === 0) {
return "先添加报销明细,再绑定发票或补充附件。";
}
if (input.canEdit && input.invoiceCount === 0) {
return "建议先添加或上传发票,提交前会做完整性检查。";
}
if (input.status === "draft" || input.status === "rejected") {
return "核对金额、发票和附件后,使用底部按钮提交审批。";
}
if (input.status === "reviewing") {
return "正在审批中,可查看节点和历史;需要补材料时按审批意见处理。";
}
if (input.status === "approved") {
return "已通过审批,等待财务付款和归档。";
}
if (input.status === "paid") {
return "已完成付款,本页用于查看发票、附件和审批留痕。";
}
return input.attachmentCount > 0 ? "查看附件、发票和审批记录。" : "查看当前报销状态和审批记录。";
}
export function MobileReimbursementDetailPage() {
const { reimbursementId = "" } = useParams();
const navigate = useNavigate();
@@ -848,14 +814,7 @@ export function MobileReimbursementDetailPage() {
const displayCode = resolveReimbursementDisplayCode(detail);
const showSubmitAction = Boolean(canEdit && canSubmitExpense);
const showStickyActions = Boolean(showSubmitAction || canOpenInvoiceDialog);
const showSecondaryActions = Boolean(actionError || canDeleteDraft || !canEdit);
const detailNextStep = resolveMobileReimbursementNextStep({
status: detail.status,
canEdit,
itemCount: detail.items.length,
invoiceCount: detail.invoices.length,
attachmentCount: detail.attachments?.length ?? 0,
});
const showSecondaryActions = Boolean(actionError || canDeleteDraft);
return (
<PageScaffold
@@ -883,31 +842,23 @@ export function MobileReimbursementDetailPage() {
<section className="mobile-card mobile-focus-card mobile-page-stack" data-testid="mobile-reimbursement-detail-focus">
<div className="mobile-section-header mobile-section-header--top">
<div className="min-w-0">
<h2 className="mobile-section-title">当前办理摘要</h2>
<p className="mobile-list-card__subtitle mt-1">{detailNextStep}</p>
<div>
<h2 className="mobile-section-title">报销概况</h2>
</div>
<span className={cn("rounded-full px-2.5 py-1 text-xs font-medium", STATUS_TONE[detail.status])}>
{STATUS_LABEL[detail.status]}
</span>
</div>
<div>
<div data-testid="mobile-reimbursement-summary">
<div className="text-xs text-zinc-500">合计金额</div>
<div className="mt-1 text-3xl font-semibold text-zinc-900">{formatCurrency(detail.totalAmount)}</div>
</div>
<div className="mobile-metric-grid mt-4 text-sm">
<div className="mt-4 grid grid-cols-2 gap-3 text-sm">
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">明细</div>
<div className="mt-1 font-semibold text-zinc-900">{detail.items.length}</div>
</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">发票</div>
<div className="mt-1 font-semibold text-zinc-900">{detail.invoices.length}</div>
</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">日期</div>
<div className="text-xs text-zinc-500">报销日期</div>
<div className="mt-1 truncate font-semibold text-zinc-900">{formatDate(detail.reimbursementDate)}</div>
</div>
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">报销人</div>
<div className="mt-1 truncate font-semibold text-zinc-900">{detail.owner?.name ?? "-"}</div>
</div>
</div>
{detail.description ? <p className="mt-3 text-sm leading-6 text-zinc-600">{detail.description}</p> : null}
</section>
@@ -1092,7 +1043,7 @@ export function MobileReimbursementDetailPage() {
<div className="flex items-center justify-between gap-3">
<div>
<h2 className="text-base font-semibold text-zinc-900">附件材料</h2>
<p className="mt-1 text-xs text-zinc-500">PDF/JPG/PNG/WebP,单次最多 5 个</p>
{canManageAttachments ? <p className="mt-1 text-xs text-zinc-500">PDF/JPG/PNG/WebP,单次最多 5 个</p> : null}
</div>
{canManageAttachments ? (
<Button
@@ -1154,7 +1105,7 @@ export function MobileReimbursementDetailPage() {
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">{attachment.originalName}</div>
<div className="mt-1 text-xs text-zinc-500">
{Math.ceil(attachment.fileSize / 1024)} KB · {formatDateTime(attachment.createdAt)}
{Math.ceil(attachment.fileSize / 1024)} KB · {formatDisplayDateTime(attachment.createdAt)}
</div>
<AttachmentGovernanceBadges attachment={attachment} className="mt-2" />
</div>
@@ -1218,7 +1169,7 @@ export function MobileReimbursementDetailPage() {
<article key={item.id} className="mobile-list-card">
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-semibold text-zinc-900">{workflowActionLabel(item.action)}</div>
<div className="shrink-0 text-xs text-zinc-500">{formatDateTime(item.createdAt)}</div>
<div className="shrink-0 text-xs text-zinc-500">{formatDisplayDateTime(item.createdAt)}</div>
</div>
<div className="mt-1 text-xs text-zinc-500">
{item.stepName ?? "-"} · {item.actorName || "系统"}
@@ -1253,7 +1204,6 @@ export function MobileReimbursementDetailPage() {
删除草稿
</Button>
) : null}
{!canEdit ? <div className="text-sm text-zinc-500">当前状态不可编辑。</div> : null}
</div>
</section>
) : null}