完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)

这个提交包含在:
2026-07-18 15:46:14 +08:00
父节点 58c8decef9
当前提交 5d0a5f9a38
共修改 134 个文件,包含 10790 行新增和 1413 行删除
+40 -10
查看文件
@@ -34,7 +34,9 @@ import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { CardSkeleton } from "../components/common/CardSkeleton";
import { ConfirmDialog } from "../components/common/ConfirmDialog";
import { useUnsavedChangesGuard } from "../components/common/UnsavedChangesGuard";
import { AttachmentGovernanceBadges } from "../components/common/AttachmentGovernanceBadges";
import { AttachmentDeleteConfirmDialog } from "../components/common/AttachmentDeleteConfirmDialog";
import { useAttachmentDownloadReasonDialog } from "../components/common/useAttachmentDownloadReasonDialog";
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
import { StatusBadge } from "../components/common/StatusBadge";
@@ -161,6 +163,7 @@ export function ReimbursementDetailPage() {
const [selectedInvoices, setSelectedInvoices] = useState<SelectedInvoiceMap>({});
const [selectedInvoiceFiles, setSelectedInvoiceFiles] = useState<File[]>([]);
const [selectedAttachmentFiles, setSelectedAttachmentFiles] = useState<File[]>([]);
const [deleteAttachmentTarget, setDeleteAttachmentTarget] = useState<{ id: string; originalName: string } | null>(null);
const [invoiceActionError, setInvoiceActionError] = useState("");
const [pendingOcrInvoiceIds, setPendingOcrInvoiceIds] = useState<string[]>([]);
const [invoiceUploadProgress, setInvoiceUploadProgress] = useState({ completed: 0, total: 0 });
@@ -170,6 +173,13 @@ export function ReimbursementDetailPage() {
amount: "",
description: "",
});
const runGuarded = useUnsavedChangesGuard(
Boolean(itemEditor) ||
selectedInvoiceFiles.length > 0 ||
selectedAttachmentFiles.length > 0 ||
Object.keys(selectedInvoices).length > 0,
"报销表单",
);
const detailQuery = useQuery({
enabled: Boolean(reimbursementId),
@@ -665,9 +675,11 @@ export function ReimbursementDetailPage() {
});
const deleteAttachmentMutation = useMutation({
mutationFn: (attachmentId: string) => deleteReimbursementAttachment(reimbursementId, attachmentId),
mutationFn: ({ attachmentId, reason }: { attachmentId: string; reason: string }) =>
deleteReimbursementAttachment(reimbursementId, attachmentId, reason),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId] });
setDeleteAttachmentTarget(null);
toast.success("附件材料已删除");
},
onError: (error) => {
@@ -796,15 +808,19 @@ export function ReimbursementDetailPage() {
};
const startCreateItem = () => {
setItemEditor({ mode: "create" });
setEditorInitialValues(computeEditorDefaults());
setItemError("");
runGuarded(() => {
setItemEditor({ mode: "create" });
setEditorInitialValues(computeEditorDefaults());
setItemError("");
});
};
const startEditItem = (item: ReimbursementItem) => {
setItemEditor({ mode: "edit", itemId: item.id });
setEditorInitialValues(computeEditorDefaults(item));
setItemError("");
runGuarded(() => {
setItemEditor({ mode: "edit", itemId: item.id });
setEditorInitialValues(computeEditorDefaults(item));
setItemError("");
});
};
const handleItemSave = (data: ItemEditorSaveData) => {
@@ -812,8 +828,10 @@ export function ReimbursementDetailPage() {
};
const handleItemCancel = () => {
setItemEditor(null);
setItemError("");
runGuarded(() => {
setItemEditor(null);
setItemError("");
});
};
const handleSubmitAction = async () => {
@@ -1539,7 +1557,7 @@ export function ReimbursementDetailPage() {
variant="destructive"
size="sm"
disabled={deleteAttachmentMutation.isPending}
onClick={() => deleteAttachmentMutation.mutate(attachment.id)}
onClick={() => setDeleteAttachmentTarget({ id: attachment.id, originalName: attachment.originalName })}
>
删除
</Button>
@@ -1991,6 +2009,18 @@ export function ReimbursementDetailPage() {
confirmLabel="删除草稿"
onConfirm={() => { deleteReimbursementMutation.mutate(); }}
/>
<AttachmentDeleteConfirmDialog
open={Boolean(deleteAttachmentTarget)}
targetName={deleteAttachmentTarget?.originalName}
pending={deleteAttachmentMutation.isPending}
onOpenChange={(open) => {
if (!open) setDeleteAttachmentTarget(null);
}}
onConfirm={(deleteReason) => {
if (!deleteAttachmentTarget) return;
deleteAttachmentMutation.mutate({ attachmentId: deleteAttachmentTarget.id, reason: deleteReason });
}}
/>
</PageScaffold>
);
}