实现企微原生材料预览(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-31 18:57:34 +08:00
父节点 e123d50f86
当前提交 410d8cd71b
共修改 23 个文件,包含 1352 行新增和 69 行删除
@@ -7,6 +7,7 @@ import {
archiveInvoice,
bindInvoice,
fetchBindableInvoices,
fetchInvoiceSignedUrl,
fetchInvoicePoolList,
unbindInvoice,
uploadInvoice,
@@ -19,6 +20,7 @@ import {
deleteReimbursement,
deleteReimbursementItem,
downloadReimbursementAttachment,
fetchReimbursementAttachmentPreviewUrl,
fetchReimbursementDetail,
MAX_REIMBURSEMENT_ATTACHMENT_FILES,
precheckReimbursement,
@@ -65,6 +67,8 @@ import { TEST_IDS } from "../lib/testIds";
import { withUploadTimeout } from "../lib/uploadTimeout";
import { useInvoiceOcrTracker } from "../hooks/useInvoiceOcrTracker";
import { cn } from "../lib/utils";
import { isWecomWebView } from "../lib/runtimeContext";
import { openFileInBrowser, previewFileInWecom } from "../lib/wecomFilePreview";
type ItemFormState = {
mode: "create" | "edit";
@@ -678,10 +682,37 @@ export function MobileReimbursementDetailPage() {
const { requestDownloadReason, downloadReasonDialog } = useAttachmentDownloadReasonDialog();
const openInvoiceMutation = useMutation({
mutationFn: async (invoiceId: string) => {
const result = await fetchInvoiceSignedUrl(invoiceId);
const openedInWecom = await previewFileInWecom({
url: result.signedUrl,
name: result.originalName,
size: result.fileSize,
});
if (!openedInWecom) {
openFileInBrowser(result.signedUrl);
}
},
onError: (error) => {
toast.error(resolveErrorMessage(error));
},
});
const openAttachmentMutation = useMutation({
mutationFn: ({ attachmentId, reason }: { attachmentId: string; reason?: string }) =>
downloadReimbursementAttachment(reimbursementId, attachmentId, reason),
mutationFn: async ({ attachment, reason }: {
attachment: { id: string; originalName: string; fileSize: number } & AttachmentGovernanceFields;
reason?: string;
}) => {
if (isWecomWebView()) {
const result = await fetchReimbursementAttachmentPreviewUrl(reimbursementId, attachment.id, reason);
await previewFileInWecom({ url: result.signedUrl, name: result.originalName, size: result.fileSize });
return null;
}
return downloadReimbursementAttachment(reimbursementId, attachment.id, reason);
},
onSuccess: (result) => {
if (!result) return;
const url = URL.createObjectURL(result.blob);
window.open(url, "_blank", "noopener,noreferrer");
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
@@ -691,12 +722,12 @@ export function MobileReimbursementDetailPage() {
},
});
async function handleOpenAttachment(attachment: { id: string; originalName?: string } & AttachmentGovernanceFields) {
async function handleOpenAttachment(attachment: { id: string; originalName: string; fileSize: number } & AttachmentGovernanceFields) {
const reason = await requestDownloadReason(attachment);
if (reason === null) {
return;
}
openAttachmentMutation.mutate({ attachmentId: attachment.id, reason });
openAttachmentMutation.mutate({ attachment, reason });
}
const handleSubmit = async () => {
@@ -1028,6 +1059,16 @@ export function MobileReimbursementDetailPage() {
移除
</Button>
) : null}
<Button
className="mt-3 w-full"
size="sm"
variant="secondary"
type="button"
disabled={openInvoiceMutation.isPending}
onClick={() => openInvoiceMutation.mutate(invoice.id)}
>
查看原件
</Button>
</article>
);
})}
@@ -1117,7 +1158,7 @@ export function MobileReimbursementDetailPage() {
disabled={openAttachmentMutation.isPending}
onClick={() => void handleOpenAttachment(attachment)}
>
查看
查看原件
</Button>
{canManageAttachments ? (
<Button
+45 -5
查看文件
@@ -12,6 +12,7 @@ import {
deleteReimbursement,
deleteReimbursementItem,
downloadReimbursementAttachment,
fetchReimbursementAttachmentPreviewUrl,
fetchReimbursementDetail,
MAX_REIMBURSEMENT_ATTACHMENT_FILES,
precheckReimbursement,
@@ -26,10 +27,13 @@ import { remindWorkflowTask } from "../api/workflow";
import {
bindInvoice,
fetchBindableInvoices,
fetchInvoiceSignedUrl,
unbindInvoice,
uploadInvoice,
} from "../api/invoicePool";
import { toast } from "sonner";
import { isWecomWebView } from "../lib/runtimeContext";
import { openFileInBrowser, previewFileInWecom } from "../lib/wecomFilePreview";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { CardSkeleton } from "../components/common/CardSkeleton";
@@ -674,10 +678,37 @@ export function ReimbursementDetailPage() {
const { requestDownloadReason, downloadReasonDialog } = useAttachmentDownloadReasonDialog();
const openInvoiceMutation = useMutation({
mutationFn: async (invoiceId: string) => {
const result = await fetchInvoiceSignedUrl(invoiceId);
const openedInWecom = await previewFileInWecom({
url: result.signedUrl,
name: result.originalName,
size: result.fileSize,
});
if (!openedInWecom) {
openFileInBrowser(result.signedUrl);
}
},
onError: (error) => {
toast.error(buildActionErrorMessage(error, "打开发票失败"));
},
});
const openAttachmentMutation = useMutation({
mutationFn: ({ attachmentId, reason }: { attachmentId: string; reason?: string }) =>
downloadReimbursementAttachment(reimbursementId, attachmentId, reason),
mutationFn: async ({ attachment, reason }: {
attachment: { id: string; originalName: string; fileSize: number } & AttachmentGovernanceFields;
reason?: string;
}) => {
if (isWecomWebView()) {
const result = await fetchReimbursementAttachmentPreviewUrl(reimbursementId, attachment.id, reason);
await previewFileInWecom({ url: result.signedUrl, name: result.originalName, size: result.fileSize });
return null;
}
return downloadReimbursementAttachment(reimbursementId, attachment.id, reason);
},
onSuccess: (result) => {
if (!result) return;
const url = URL.createObjectURL(result.blob);
window.open(url, "_blank", "noopener,noreferrer");
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
@@ -687,12 +718,12 @@ export function ReimbursementDetailPage() {
},
});
async function handleOpenAttachment(attachment: { id: string; originalName?: string } & AttachmentGovernanceFields) {
async function handleOpenAttachment(attachment: { id: string; originalName: string; fileSize: number } & AttachmentGovernanceFields) {
const reason = await requestDownloadReason(attachment);
if (reason === null) {
return;
}
openAttachmentMutation.mutate({ attachmentId: attachment.id, reason });
openAttachmentMutation.mutate({ attachment, reason });
}
if (!reimbursementId) {
@@ -1423,6 +1454,15 @@ export function ReimbursementDetailPage() {
<span className="badge-neutral">
{resolveInvoiceStatusLabel(item.status)}
</span>
<Button
type="button"
variant="secondary"
size="sm"
disabled={openInvoiceMutation.isPending}
onClick={() => openInvoiceMutation.mutate(item.id)}
>
查看原件
</Button>
{canUnbindInvoices ? (
<Button
type="button"
@@ -1515,7 +1555,7 @@ export function ReimbursementDetailPage() {
disabled={openAttachmentMutation.isPending}
onClick={() => void handleOpenAttachment(attachment)}
>
查看
查看原件
</Button>
{canEditItems ? (
<Button