实现报销材料合并预览与差旅概要(07-27-yang-jun-reimbursement-readiness)
这个提交包含在:
@@ -35,7 +35,8 @@ import {
|
||||
} from "../api/invoicePool";
|
||||
import { toast } from "sonner";
|
||||
import { isWecomWebView } from "../lib/runtimeContext";
|
||||
import { openFileInBrowser, previewFileInWecom, previewImagesInWecom } from "../lib/wecomFilePreview";
|
||||
import { openFileInBrowser, previewFileInWecom } from "../lib/wecomFilePreview";
|
||||
import { hasCompleteTravelSummary } from "../lib/reimbursementTravel";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { CardSkeleton } from "../components/common/CardSkeleton";
|
||||
@@ -45,6 +46,7 @@ import { AttachmentGovernanceBadges } from "../components/common/AttachmentGover
|
||||
import { AttachmentDeleteConfirmDialog } from "../components/common/AttachmentDeleteConfirmDialog";
|
||||
import { useAttachmentDownloadReasonDialog } from "../components/common/useAttachmentDownloadReasonDialog";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { ReimbursementTravelSummary } from "../components/reimbursements/ReimbursementTravelSummary";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
@@ -736,15 +738,13 @@ export function ReimbursementDetailPage() {
|
||||
setMaterialPreviewError("");
|
||||
if (isWecomWebView()) {
|
||||
try {
|
||||
await previewImagesInWecom(bundle.pages.map((page) => page.url));
|
||||
await previewFileInWecom({ url: bundle.signedUrl, name: bundle.originalName, size: bundle.fileSize });
|
||||
return;
|
||||
} catch {
|
||||
toast.error("企业微信滑动预览未打开,已切换到页面预览");
|
||||
toast.error("企业微信材料预览未打开,已切换到浏览器预览");
|
||||
}
|
||||
}
|
||||
navigate(`/reimbursements/${reimbursementId}/materials`, {
|
||||
state: { bundle, from: `/reimbursements/${reimbursementId}` },
|
||||
});
|
||||
openFileInBrowser(bundle.signedUrl);
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = buildActionErrorMessage(error, "连续预览生成失败,可逐份打开原件");
|
||||
@@ -891,6 +891,12 @@ export function ReimbursementDetailPage() {
|
||||
|
||||
try {
|
||||
setActionError("");
|
||||
if (!hasCompleteTravelSummary(detail)) {
|
||||
const message = "请先补全出差事由、地点和起止日期";
|
||||
setActionError(message);
|
||||
toast.error(message);
|
||||
return;
|
||||
}
|
||||
if (detail.invoices.length < 1) {
|
||||
setActionError(missingInvoiceSubmitMessage);
|
||||
toast.error(missingInvoiceSubmitMessage);
|
||||
@@ -1285,6 +1291,12 @@ export function ReimbursementDetailPage() {
|
||||
) : null}
|
||||
</SectionPanel>
|
||||
|
||||
<ReimbursementTravelSummary
|
||||
detail={detail}
|
||||
reimbursementId={reimbursementId}
|
||||
editable={Boolean(canEditItems && isReimbursementParticipant)}
|
||||
/>
|
||||
|
||||
<SectionPanel title="报销明细">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4" data-testid="reimbursement-items">
|
||||
@@ -1436,6 +1448,7 @@ export function ReimbursementDetailPage() {
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<p className="text-sm text-zinc-600">
|
||||
发票 {detail.invoices.length} · 补充附件 {(detail.attachments ?? []).length}
|
||||
<span className="mt-1 block text-xs text-zinc-500">打开后上下滚动,连续查看全部材料。合并预览仅供核对,原始文件另行保留。</span>
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{detail.invoices.length || (detail.attachments ?? []).length ? (
|
||||
@@ -1445,7 +1458,7 @@ export function ReimbursementDetailPage() {
|
||||
disabled={materialPreviewMutation.isPending}
|
||||
onClick={() => void handleOpenAllMaterials()}
|
||||
>
|
||||
{materialPreviewMutation.isPending ? "生成预览中..." : "查看全部材料"}
|
||||
{materialPreviewMutation.isPending ? "正在准备材料预览…" : "查看材料合并预览"}
|
||||
</Button>
|
||||
) : null}
|
||||
{canOpenInvoiceBindDialog ? (
|
||||
|
||||
在新工单中引用
屏蔽一个用户