From cb03f2b63df046f2a183e272740ddb38ca36204b Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 30 May 2026 03:29:07 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=8A=A5=E9=94=80=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E8=AF=81=E6=8D=AE=E4=B8=8E=E5=8A=A8=E4=BD=9C=E5=8C=BA?= =?UTF-8?q?=EF=BC=88reimbursement-detail-evidence-actions=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../task.json | 2 +- client/src/pages/ReimbursementDetailPage.tsx | 629 +++++++++--------- .../ReimbursementActions.tsx | 82 +-- client/tests/e2e/helpers/featureFlags.ts | 40 ++ client/tests/e2e/main-flow.spec.ts | 79 ++- .../e2e/pc-detail-mobile-visual-wave3.spec.ts | 1 + .../e2e/r102-reimbursement-main-flow.spec.ts | 5 + .../r111-invoice-reimbursement-guard.spec.ts | 2 + .../reimbursement-payment-batch-flow.spec.ts | 40 +- 9 files changed, 434 insertions(+), 446 deletions(-) create mode 100644 client/tests/e2e/helpers/featureFlags.ts diff --git a/.trellis/tasks/05-30-reimbursement-detail-evidence-actions/task.json b/.trellis/tasks/05-30-reimbursement-detail-evidence-actions/task.json index 37b67a43..f22427ef 100644 --- a/.trellis/tasks/05-30-reimbursement-detail-evidence-actions/task.json +++ b/.trellis/tasks/05-30-reimbursement-detail-evidence-actions/task.json @@ -3,7 +3,7 @@ "name": "reimbursement-detail-evidence-actions", "title": "报销详情证据与动作区重构", "description": "按全页面设计规则重构报销详情:状态与下一步先行,证据材料居中,付款动作集中,右侧动作区降噪。", - "status": "planning", + "status": "in_progress", "dev_type": null, "scope": "frontend-page-redesign", "package": null, diff --git a/client/src/pages/ReimbursementDetailPage.tsx b/client/src/pages/ReimbursementDetailPage.tsx index 45220922..fe0daeb9 100644 --- a/client/src/pages/ReimbursementDetailPage.tsx +++ b/client/src/pages/ReimbursementDetailPage.tsx @@ -14,7 +14,6 @@ import { deleteReimbursementItem, downloadReimbursementAttachment, fetchReimbursementDetail, - payReimbursement, precheckReimbursement, submitReimbursement, updateReimbursementItem, @@ -53,8 +52,6 @@ import { Input } from "../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { DetailSideMeta } from "../components/patterns/DetailSideMeta"; import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero"; -import { StatStrip } from "../components/patterns/ListWorkspace"; -import { MetricCard } from "../components/patterns/MetricCard"; import { PageScaffold } from "../components/patterns/PageScaffold"; import { SectionPanel } from "../components/patterns/SectionPanel"; import { @@ -69,7 +66,7 @@ import type { AttachmentGovernanceFields } from "../lib/attachmentGovernance"; import { formatDisplayDate } from "../lib/displayFormat"; import { TEST_IDS } from "../lib/testIds"; import { PDF_FILE_ACCEPT, rejectedPdfMessage, splitPdfFiles } from "../lib/pdfFiles"; -import { ReimbursementActions, type PayPayload } from "./reimbursement-detail/ReimbursementActions"; +import { ReimbursementActions } from "./reimbursement-detail/ReimbursementActions"; import { ReimbursementItemEditor, type ItemEditorInitialValues, @@ -162,6 +159,15 @@ function materialCompletenessLabel(count: number, noun: string) { return count > 0 ? `${count} ${noun}` : `缺${noun}`; } +function paymentChannelLabel(value: string | null | undefined) { + if (value === "bank") return "银行转账"; + if (value === "alipay") return "支付宝"; + if (value === "wechat") return "微信"; + if (value === "cash") return "现金"; + if (value === "other") return "其他"; + return value || "-"; +} + export function ReimbursementDetailPage() { const { reimbursementId = "" } = useParams(); const navigate = useNavigate(); @@ -265,7 +271,7 @@ export function ReimbursementDetailPage() { }, [bindableInvoicesQuery.data?.items]); const mutation = useMutation({ - mutationFn: async (params: { action: "submit" } | { action: "withdraw"; comment: string } | { action: "pay"; payload: PayPayload }) => { + mutationFn: async (params: { action: "submit" } | { action: "withdraw"; comment: string }) => { if (!reimbursementId) { throw new Error("missing reimbursementId"); } @@ -277,13 +283,6 @@ export function ReimbursementDetailPage() { if (params.action === "withdraw") { return withdrawReimbursement(reimbursementId, params.comment); } - - return payReimbursement(reimbursementId, { - paymentVoucherNo: params.payload.paymentVoucherNo || undefined, - paymentDate: params.payload.paymentDate || undefined, - paymentChannel: params.payload.paymentChannel, - paymentNote: params.payload.paymentNote || undefined, - }); }, onSuccess: async (_data, params) => { await Promise.all([ @@ -295,7 +294,7 @@ export function ReimbursementDetailPage() { if (params.action === "submit") { setSubmitConfirmPrecheck(null); } - const actionMessages: Record = { submit: "已提交", withdraw: "已撤回", pay: "付款成功" }; + const actionMessages: Record = { submit: "已提交", withdraw: "已撤回" }; toast.success(actionMessages[params.action] ?? "操作成功"); }, onError: (error) => { @@ -991,7 +990,6 @@ export function ReimbursementDetailPage() { const itemCount = detail.items.length; const invoiceCount = detail.invoices.length; - const attachmentCount = (detail.attachments ?? []).length; const hasBlockingPrecheck = blockStandardChecks.length > 0; const currentHandlerText = (() => { if (detail.status === "reviewing") { @@ -1027,7 +1025,7 @@ export function ReimbursementDetailPage() { : "查看审批进度"; } if (detail.status === "approved") { - return canPay ? "执行付款" : "等待财务付款"; + return "等待财务处理"; } if (detail.status === "paid") { return "查看付款结果"; @@ -1083,9 +1081,7 @@ export function ReimbursementDetailPage() { if (detail.status === "approved") { return ( ); } @@ -1237,10 +1233,10 @@ export function ReimbursementDetailPage() {
付款凭证号
{payment.paymentVoucherNo ?? "-"}
-
-
付款渠道
-
{payment.paymentChannel ?? "-"}
-
+
+
付款渠道
+
{paymentChannelLabel(payment.paymentChannel)}
+
操作人 / 时间
@@ -1251,54 +1247,44 @@ export function ReimbursementDetailPage() {
) : null} - {showSecondaryDetail ? ( -
-
-
-

更多入口

-
- {detail.status !== "paid" ? ( - - ) : null} - - {canDeleteReimbursement ? ( - - ) : null} + {showSecondaryDetail ? ( +
+
+
+
+

更多入口

+

低频操作集中在这里,材料和关联事项已放在主内容区。

+
+
+ {detail.status !== "paid" ? ( + + ) : null} + + {canDeleteReimbursement ? ( + + ) : null} +
+
-
+ ) : null} + - - - {detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"} - - } - /> - - - {isPaidReimbursement ? "已付款" : "未付款"}} - /> - - -
-
+ +
+
报销编号
{displayCode}
@@ -1312,195 +1298,17 @@ export function ReimbursementDetailPage() {
{detail.submitter?.name ?? "-"} / {detail.owner?.name ?? "-"}
-
说明
-
{detail.description || "-"}
-
-
-
- -
-
-
-

付款状态

-

凭证、日期和操作人用于财务台账核对。

-
- - {detail.status === "paid" ? "已付款" : "未付款"} - -
-
-
-
付款日期
-
{payment.paymentDate ?? "-"}
-
-
-
付款凭证号
-
{payment.paymentVoucherNo ?? "-"}
-
-
-
付款渠道
-
{payment.paymentChannel ?? "-"}
-
-
-
操作人 / 时间
+
关联项目
- {payment.paidBy?.name ?? "-"} · {formatDateTime(payment.paidAt)} + {detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}
-
- - {detail.linkedTravelRequest ? ( -
-
-
-

- 关联差旅申请:{detail.linkedTravelRequest.requestNo ?? detail.linkedTravelRequest.id} -

-

{detail.linkedTravelRequest.purpose || "未填写出差目的"}

-
- - 查看申请 - -
-
-
-
日期范围
-
- {detail.linkedTravelRequest.startDate ?? "-"} 至 {detail.linkedTravelRequest.endDate ?? "-"} -
-
-
-
预估金额
-
¥{detail.linkedTravelRequest.estimatedAmount}
-
-
-
申请编号
-
- {detail.linkedTravelRequest.requestNo ?? detail.linkedTravelRequest.id} -
-
-
+
+ {detail.description || "未填写报销说明"}
- ) : null} - -
-
-
-

关联项目

-

用于项目执行面板汇总。

-
- {detail.project ? ( - - 查看项目 - - ) : null} -
-
- -
-
-
-

补充附件

-

合同、订单、支付截图或情况说明;敏感附件下载会要求填写用途。

-
- {canEditItems ? ( -
- - setSelectedAttachmentFiles(Array.from(event.target.files ?? []))} - /> - -
- ) : null} -
- - {(detail.attachments ?? []).length ? ( -
- {(detail.attachments ?? []).map((attachment) => ( -
-
-
{attachment.originalName}
-
- {Math.ceil(attachment.fileSize / 1024)} KB · {formatDateTime(attachment.createdAt)} -
- -
-
- - {canEditItems ? ( - - ) : null} -
-
- ))} -
- ) : ( -

暂无附件材料

- )} -
-
- ) : null} -
+
@@ -1736,10 +1544,169 @@ export function ReimbursementDetailPage() { )}
- + - -
+ +
+
+
+

补充附件

+

合同、订单、支付截图或情况说明;敏感附件下载会要求填写用途。

+
+ {canEditItems ? ( +
+ + setSelectedAttachmentFiles(Array.from(event.target.files ?? []))} + /> + +
+ ) : null} +
+ + {(detail.attachments ?? []).length ? ( +
+ {(detail.attachments ?? []).map((attachment) => ( +
+
+
{attachment.originalName}
+
+ {Math.ceil(attachment.fileSize / 1024)} KB · {formatDateTime(attachment.createdAt)} +
+ +
+
+ + {canEditItems ? ( + + ) : null} +
+
+ ))} +
+ ) : ( +

暂无附件材料

+ )} +
+
+ + +
+
+
+
+

关联项目

+

用于项目执行面板汇总,不在报销详情复制项目流程。

+
+ {detail.project ? ( + + 查看项目 + + ) : null} +
+ {canEditItems ? ( + + ) : ( +
+ {detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联项目"} +
+ )} +
+ + {detail.linkedTravelRequest ? ( +
+
+
+

+ 关联差旅申请:{detail.linkedTravelRequest.requestNo ?? "未生成申请编号"} +

+

{detail.linkedTravelRequest.purpose || "未填写出差目的"}

+
+ + 查看申请 + +
+
+
+
日期范围
+
+ {detail.linkedTravelRequest.startDate ?? "-"} 至 {detail.linkedTravelRequest.endDate ?? "-"} +
+
+
+
预估金额
+
¥{detail.linkedTravelRequest.estimatedAmount}
+
+
+
申请编号
+
+ {detail.linkedTravelRequest.requestNo ?? "未生成申请编号"} +
+
+
+
+ ) : ( +
+ 未关联差旅申请。 +
+ )} +
+
+ + +
{budgetHint ? (

预算提示

@@ -1831,85 +1798,100 @@ export function ReimbursementDetailPage() { ) : null} ) : null} -
- -
+
+
-
- {showSecondaryDetail ? ( - -
报销编号:{displayCode}
-
报销人:{detail.owner?.name ?? "-"}
-
提交人:{detail.submitter?.name ?? "-"}
-
当前审批:{detail.pendingWorkflow?.task?.step_name ?? "-"}
-
明细数:{detail.items.length}
-
票据数:{detail.invoices.length}
-
附件数:{attachmentCount}
-
关联项目:{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}
-
- ) : null} - - {detail.status === "reviewing" && detail.workflowProgress ? ( -
- -
- {detail.workflowProgress.steps.map((step, index) => ( -
-
- - 第 {index + 1} 步 · {step.name} - - - {step.status === "completed" ? "已完成" : step.status === "current" ? "当前节点" : "待处理"} - -
+ +
+ {detail.workflowProgress ? ( +
+
+ 当前位于第 {detail.workflowProgress.currentStepIndex + 1} / {detail.workflowProgress.totalSteps} 步。
- ))} -
- -
- ) : null} - - {showSecondaryDetail ? ( - - {detail.approvalHistory.length ? ( -
- {detail.approvalHistory.map((item) => ( -
-
- {workflowActionLabel(item.action)} - {formatDateTime(item.createdAt)} -
-
- {item.stepName ?? "-"} · {item.actorName || "系统"} -
- {item.comment ?
{item.comment}
: null} +
+ {detail.workflowProgress.steps.map((step, index) => ( +
+
+ + 第 {index + 1} 步 · {step.name} + + + {step.status === "completed" ? "已完成" : step.status === "current" ? "当前节点" : "待处理"} + +
+
+ ))}
- ))} +
+ ) : ( +
+ 当前没有进行中的审批节点。 +
+ )} + +
+

审批记录

+ {detail.approvalHistory.length ? ( +
+ {detail.approvalHistory.map((item) => ( +
+
+ {workflowActionLabel(item.action)} + {formatDateTime(item.createdAt)} +
+
+ {item.stepName ?? "-"} · {item.actorName || "系统"} +
+ {item.comment ?
{item.comment}
: null} +
+ ))} +
+ ) : ( +
暂无审批记录。
+ )}
- ) : ( -
暂无审批记录。
- )} +
+
+
+ +
+ + {["draft", "rejected"].includes(detail.status) ? ( +
补齐明细和 PDF 发票后提交审批;附件作为补充材料放在主内容区。
+ ) : null} + {detail.status === "reviewing" ? ( +
审批动作只在“待我处理”或任务详情执行;如需修改,可撤回后补正。
+ ) : null} + {detail.status === "approved" ? ( +
+
本页只显示付款状态;付款登记、批量付款和回单处理请进入付款上下文。
+ {canPay ? ( + + ) : null} +
+ ) : null} + {detail.status === "paid" ? ( +
已付款报销用于查证据、看回单状态和打印归档,不再编辑或重复付款。
+ ) : null} + {detail.status === "cancelled" ? ( +
该报销已取消,仅保留查看和归档记录。
+ ) : null}
- ) : null} 0} submitHint="" @@ -1917,7 +1899,6 @@ export function ReimbursementDetailPage() { isPending={mutation.isPending} onSubmit={() => void handleSubmitAction()} onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })} - onPay={(payload) => mutation.mutate({ action: "pay", payload })} />
diff --git a/client/src/pages/reimbursement-detail/ReimbursementActions.tsx b/client/src/pages/reimbursement-detail/ReimbursementActions.tsx index 9e9a0ac3..e006c6c9 100644 --- a/client/src/pages/reimbursement-detail/ReimbursementActions.tsx +++ b/client/src/pages/reimbursement-detail/ReimbursementActions.tsx @@ -1,63 +1,38 @@ import { useState } from "react"; import type { ReimbursementStatus } from "../../api/reimbursements"; import { Button } from "../../components/ui/button"; -import { Input } from "../../components/ui/input"; import { Textarea } from "../../components/ui/textarea"; -type PaymentChannel = "bank" | "alipay" | "wechat" | "cash" | "other"; - -function todayDateString() { - return new Date().toISOString().slice(0, 10); -} - -export type PayPayload = { - paymentVoucherNo: string; - paymentDate: string; - paymentChannel: PaymentChannel; - paymentNote: string; -}; - export interface ReimbursementActionsProps { status: ReimbursementStatus; - canPay: boolean; actionError: string; submitDisabled?: boolean; submitHint?: string; submitNotice?: string; showSubmitAction?: boolean; showWithdrawAction?: boolean; - showPayAction?: boolean; isPending: boolean; onSubmit: () => void; onWithdraw: (comment: string) => void; - onPay: (payload: PayPayload) => void; } export function ReimbursementActions({ status, - canPay, actionError, submitDisabled = false, submitHint = "", submitNotice = "", showSubmitAction = true, showWithdrawAction = true, - showPayAction = true, isPending, onSubmit, onWithdraw, - onPay, }: ReimbursementActionsProps) { const [withdrawComment, setWithdrawComment] = useState("申请撤回并补充说明后重提"); - const [paymentVoucherNo, setPaymentVoucherNo] = useState(""); - const [paymentDate, setPaymentDate] = useState(todayDateString); - const [paymentChannel, setPaymentChannel] = useState("bank"); - const [paymentNote, setPaymentNote] = useState(""); const canShowSubmit = showSubmitAction && ["draft", "rejected"].includes(status); const canShowWithdraw = showWithdrawAction && status === "reviewing"; - const canShowPay = showPayAction && canPay && status === "approved"; - if (!canShowSubmit && !canShowWithdraw && !canShowPay && !actionError && !submitHint && !submitNotice) { + if (!canShowSubmit && !canShowWithdraw && !actionError && !submitHint && !submitNotice) { return null; } @@ -113,61 +88,6 @@ export function ReimbursementActions({
) : null} - - {canShowPay ? ( -
-

财务支付

- setPaymentVoucherNo(event.target.value)} - placeholder="付款凭证号" - value={paymentVoucherNo} - /> - setPaymentDate(event.target.value)} - type="date" - value={paymentDate} - /> - -