fix: 优化报销制单下载与付款进度(09-08-reimbursement-payment-closeout)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-10 16:39:01 +08:00
父节点 73fcd5a65a
当前提交 fe025e65cf
共修改 15 个文件,包含 364 行新增和 42 行删除
+9
查看文件
@@ -165,6 +165,15 @@ function parseFilenameFromContentDisposition(headerValue: string | null): string
}
const parts = headerValue.split(";").map((part) => part.trim());
const encodedPart = parts.find((part) => part.toLowerCase().startsWith("filename*="));
const encodedValue = encodedPart?.slice("filename*=".length).trim().replace(/^"|"$/g, "");
const utf8Value = encodedValue?.match(/^UTF-8'[^']*'(.*)$/i);
if (utf8Value) {
try {
const decoded = decodeURIComponent(utf8Value[1]);
if (decoded) return decoded;
} catch {}
}
const filenamePart = parts.find((part) => part.toLowerCase().startsWith("filename="));
if (!filenamePart) {
return null;
+2 -2
查看文件
@@ -215,11 +215,11 @@ export async function cancelPaymentBatch(batchId: string, reason: string) {
});
}
export async function downloadPaymentOrder(batchId: string, reason: string): Promise<ApiBlobResponse> {
export async function downloadPaymentOrder(batchId: string): Promise<ApiBlobResponse> {
return apiRequestBlob(`/api/payment-batches/${batchId}/payment-order`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ reason }),
body: JSON.stringify({}),
});
}
+20 -20
查看文件
@@ -561,10 +561,10 @@ export function PaymentBatchesPage() {
});
const paymentOrderMutation = useMutation({
mutationFn: ({ batchId, reason }: { batchId: string; reason: string }) =>
downloadPaymentOrder(batchId, reason),
onSuccess: async (response) => {
downloadBlob(response.blob, response.filename ?? "payment-order.xls");
mutationFn: ({ batchId }: { batchId: string; batchNo: string }) =>
downloadPaymentOrder(batchId),
onSuccess: async (response, variables) => {
downloadBlob(response.blob, response.filename ?? `报销制单_${variables.batchNo.replace(/[^A-Za-z0-9-]/g, "_")}.xls`);
setReasonAction(null);
setActionReason("");
await refresh();
@@ -864,15 +864,6 @@ export function PaymentBatchesPage() {
) : null}
</div>
<div className="finance-command-panel space-y-3" data-testid="payment-batch-processing-panel">
<h3 className="text-base font-semibold text-zinc-900">处理进度</h3>
<ProcessingProgress
progress={selectedBatch.processingProgress}
ariaLabel="付款批次处理进度"
testId="payment-batch-processing-progress"
/>
</div>
<div className="finance-command-panel space-y-4" data-testid="payment-batch-items-panel">
<h3 className="text-base font-semibold text-zinc-900">
{selectedBatch.entityType === "reimbursement" ? "本批次报销单" : "本批次差旅订单"}
@@ -1080,8 +1071,9 @@ export function PaymentBatchesPage() {
</div>
</BusinessDetailMain>
<BusinessDetailAside className="hidden lg:block">
<BusinessDetailAside>
<BusinessDetailActionPanel
className="lg:static"
status={<StatusBadge status={selectedBatch.status} label={paymentBatchStatusLabel(selectedBatch.status)} />}
primaryAction={paymentActionModel.primary ? renderPaymentActionButton(paymentActionModel.primary) : undefined}
moreActions={paymentActionModel.more.length ? (
@@ -1102,6 +1094,14 @@ export function PaymentBatchesPage() {
)}
testId="payment-batch-action-panel"
/>
<div className="finance-command-panel space-y-3" data-testid="payment-batch-processing-panel">
<h3 className="text-base font-semibold text-zinc-900">处理进度</h3>
<ProcessingProgress
progress={selectedBatch.processingProgress}
ariaLabel="付款批次处理进度"
testId="payment-batch-processing-progress"
/>
</div>
</BusinessDetailAside>
</BusinessDetailWorkspace>
@@ -1316,20 +1316,20 @@ export function PaymentBatchesPage() {
</DialogHeader>
{reasonAction?.kind === "payeeBank" && revealedPayeeAccount ? (
<div className="mt-4 rounded-lg border border-amber-200 bg-amber-50 p-3 font-mono text-sm text-amber-900 break-all">{revealedPayeeAccount}</div>
) : <div className="mt-4 space-y-2">
) : reasonAction?.kind !== "paymentOrder" ? <div className="mt-4 space-y-2">
<Textarea
value={actionReason}
onChange={(event) => setActionReason(event.target.value)}
aria-label={reasonAction?.kind === "voidReceipt" ? "回单作废原因" : reasonAction?.kind === "paymentOrder" ? "制单下载原因" : reasonAction?.kind === "payeeBank" ? "查看收款账号原因" : "批次取消原因"}
aria-label={reasonAction?.kind === "voidReceipt" ? "回单作废原因" : reasonAction?.kind === "payeeBank" ? "查看收款账号原因" : "批次取消原因"}
rows={4}
maxLength={500}
placeholder={reasonAction?.kind === "voidReceipt" ? "说明回单为何作废" : reasonAction?.kind === "paymentOrder" ? "说明本次制单用途" : reasonAction?.kind === "payeeBank" ? "说明查看完整账号的业务用途" : "说明批次为何不再继续"}
placeholder={reasonAction?.kind === "voidReceipt" ? "说明回单为何作废" : reasonAction?.kind === "payeeBank" ? "说明查看完整账号的业务用途" : "说明批次为何不再继续"}
/>
<div className="flex items-center justify-between text-xs text-zinc-500">
<span>原因需填写 2–500 个字符,并写入审计记录。</span>
<span>{actionReason.trim().length}/500</span>
</div>
</div>}
</div> : null}
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => {
setReasonAction(null);
@@ -1341,7 +1341,7 @@ export function PaymentBatchesPage() {
type="button"
variant={reasonAction?.kind === "paymentOrder" ? "default" : "destructive"}
disabled={
actionReason.trim().length < 2 ||
(reasonAction?.kind !== "paymentOrder" && actionReason.trim().length < 2) ||
cancelMutation.isPending ||
voidReceiptMutation.isPending ||
paymentOrderMutation.isPending ||
@@ -1353,7 +1353,7 @@ export function PaymentBatchesPage() {
if (reasonAction.kind === "cancel") {
cancelMutation.mutate({ batchId: reasonAction.batchId, reason });
} else if (reasonAction.kind === "paymentOrder") {
paymentOrderMutation.mutate({ batchId: reasonAction.batchId, reason });
paymentOrderMutation.mutate({ batchId: reasonAction.batchId, batchNo: reasonAction.batchNo });
} else if (reasonAction.kind === "payeeBank") {
revealPayeeBankMutation.mutate({ batchId: reasonAction.batchId, itemId: reasonAction.itemId, reason });
} else {