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

这个提交包含在:
2026-07-18 15:46:14 +08:00
父节点 58c8decef9
当前提交 5d0a5f9a38
共修改 134 个文件,包含 10790 行新增和 1413 行删除
+116 -23
查看文件
@@ -35,8 +35,10 @@ import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { AttachmentGovernanceBadges } from "../components/common/AttachmentGovernanceBadges";
import { AttachmentDeleteConfirmDialog } from "../components/common/AttachmentDeleteConfirmDialog";
import { useAttachmentDownloadReasonDialog } from "../components/common/useAttachmentDownloadReasonDialog";
import { DateTextInput } from "../components/common/DateTextInput";
import { useUnsavedChangesGuard } from "../components/common/UnsavedChangesGuard";
import { Pagination } from "../components/common/Pagination";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
@@ -77,6 +79,21 @@ type DraftItem = {
isAssetCandidate: boolean;
};
function purchaseDraftSignature(value: {
title: string;
reason: string;
currencyCode: string;
legalEntityId: string;
costCenterId: string;
supplierId: string;
documentCategoryId: string;
projectId: string;
expectedDeliveryDate: string;
items: DraftItem[];
}) {
return JSON.stringify(value);
}
function resolveError(error: unknown) {
if (isApiError(error)) {
return mapApiErrorMessage(error.code, error.details, error.message);
@@ -318,9 +335,24 @@ export function PurchaseRequestsPage() {
const [projectId, setProjectId] = useState("");
const [expectedDeliveryDate, setExpectedDeliveryDate] = useState("");
const [items, setItems] = useState<DraftItem[]>([]);
const [savedDraftSignature, setSavedDraftSignature] = useState(() =>
purchaseDraftSignature({
title: "",
reason: "",
currencyCode: "CNY",
legalEntityId: "",
costCenterId: "",
supplierId: "",
documentCategoryId: "",
projectId: "",
expectedDeliveryDate: "",
items: [],
}),
);
const [archivePoNo, setArchivePoNo] = useState("");
const [archiveContractNo, setArchiveContractNo] = useState("");
const [selectedAttachmentName, setSelectedAttachmentName] = useState("");
const [deleteAttachmentTarget, setDeleteAttachmentTarget] = useState<{ id: string; originalName: string } | null>(null);
const [submitPrecheckConfirm, setSubmitPrecheckConfirm] = useState<string | null>(null);
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
@@ -403,17 +435,17 @@ export function PurchaseRequestsPage() {
if (!detailQuery.data || detailQuery.data.id !== selectedId || hydratedDetailId === detailQuery.data.id) {
return;
}
setTitle(detailQuery.data.title || "");
setReason(detailQuery.data.reason || "");
setCurrencyCode(detailQuery.data.currencyCode || "CNY");
setLegalEntityId(detailQuery.data.legalEntityId || "");
setCostCenterId(detailQuery.data.costCenterId || "");
setSupplierId(detailQuery.data.supplierId || "");
setDocumentCategoryId(detailQuery.data.documentCategoryId || "");
setProjectId(detailQuery.data.projectId || "");
setExpectedDeliveryDate(detailQuery.data.expectedDeliveryDate || "");
setItems(
detailQuery.data.items.map((item) => ({
const nextDraft = {
title: detailQuery.data.title || "",
reason: detailQuery.data.reason || "",
currencyCode: detailQuery.data.currencyCode || "CNY",
legalEntityId: detailQuery.data.legalEntityId || "",
costCenterId: detailQuery.data.costCenterId || "",
supplierId: detailQuery.data.supplierId || "",
documentCategoryId: detailQuery.data.documentCategoryId || "",
projectId: detailQuery.data.projectId || "",
expectedDeliveryDate: detailQuery.data.expectedDeliveryDate || "",
items: detailQuery.data.items.map((item) => ({
id: item.id,
description: item.description,
specification: item.specification || "",
@@ -421,7 +453,18 @@ export function PurchaseRequestsPage() {
unitPrice: item.unitPrice,
isAssetCandidate: item.isAssetCandidate,
})),
);
};
setTitle(nextDraft.title);
setReason(nextDraft.reason);
setCurrencyCode(nextDraft.currencyCode);
setLegalEntityId(nextDraft.legalEntityId);
setCostCenterId(nextDraft.costCenterId);
setSupplierId(nextDraft.supplierId);
setDocumentCategoryId(nextDraft.documentCategoryId);
setProjectId(nextDraft.projectId);
setExpectedDeliveryDate(nextDraft.expectedDeliveryDate);
setItems(nextDraft.items);
setSavedDraftSignature(purchaseDraftSignature(nextDraft));
setArchivePoNo(detailQuery.data.archiveOrder?.poNo || "");
setArchiveContractNo(detailQuery.data.archiveOrder?.contractNo || "");
setFormError("");
@@ -576,6 +619,7 @@ export function PurchaseRequestsPage() {
queryClient.invalidateQueries({ queryKey: ["purchase-requests", "detail", selectedId] }),
]);
setFormError("");
setSavedDraftSignature(currentDraftSignature);
toast.success("采购草稿已保存");
},
onError: (error) => {
@@ -716,14 +760,15 @@ export function PurchaseRequestsPage() {
});
const deleteAttachmentMutation = useMutation({
mutationFn: async (attachmentId: string) => {
mutationFn: async ({ attachmentId, reason }: { attachmentId: string; reason: string }) => {
if (!selectedId) {
throw new Error("请先选择采购单");
}
return deletePurchaseAttachment(selectedId, attachmentId);
return deletePurchaseAttachment(selectedId, attachmentId, reason);
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["purchase-requests", "detail", selectedId] });
setDeleteAttachmentTarget(null);
toast.success("附件已删除");
},
onError: (error) => {
@@ -772,6 +817,7 @@ export function PurchaseRequestsPage() {
setFormError("");
validateBeforeSubmit();
await updatePurchaseRequest(selectedId, buildPayload());
setSavedDraftSignature(currentDraftSignature);
const precheck = await precheckPurchaseRequest(selectedId);
const message = buildPrecheckDialogMessage(precheck);
@@ -796,6 +842,37 @@ export function PurchaseRequestsPage() {
const showArchiveSection = Boolean(detail && (detail.status === "approved" || archiveOrder));
const canEditDraft = Boolean(detail?.permissions.canEditDraft);
const currentDraftSignature = useMemo(
() =>
purchaseDraftSignature({
title,
reason,
currencyCode,
legalEntityId,
costCenterId,
supplierId,
documentCategoryId,
projectId,
expectedDeliveryDate,
items,
}),
[
costCenterId,
currencyCode,
documentCategoryId,
expectedDeliveryDate,
items,
legalEntityId,
projectId,
reason,
supplierId,
title,
],
);
const runGuarded = useUnsavedChangesGuard(
Boolean(selectedId) && canEditDraft && currentDraftSignature !== savedDraftSignature,
"采购表单",
);
const canSubmit = Boolean(detail?.permissions.canSubmit);
const canWithdraw = Boolean(detail?.permissions.canWithdraw);
const canArchiveOrder = Boolean(detail?.permissions.canArchiveOrder && canManage);
@@ -813,15 +890,19 @@ export function PurchaseRequestsPage() {
const detailNextStepText = detail ? resolvePurchaseNextStep(detail) : "";
const isPurchaseDetailMode = Boolean(selectedId);
const openPurchaseDetail = (id: string) => {
setSelectedId(id);
setHydratedDetailId("");
runGuarded(() => {
setSelectedId(id);
setHydratedDetailId("");
});
};
const returnToPurchaseList = () => {
setSelectedId("");
setHydratedDetailId("");
const next = new URLSearchParams(searchParams);
next.delete("id");
setSearchParams(next, { replace: true });
runGuarded(() => {
setSelectedId("");
setHydratedDetailId("");
const next = new URLSearchParams(searchParams);
next.delete("id");
setSearchParams(next, { replace: true });
});
};
return (
@@ -1670,7 +1751,7 @@ export function PurchaseRequestsPage() {
size="sm"
variant="secondary"
disabled={!canEditDraft || deleteAttachmentMutation.isPending}
onClick={() => deleteAttachmentMutation.mutate(attachment.id)}
onClick={() => setDeleteAttachmentTarget({ id: attachment.id, originalName: attachment.originalName })}
>
删除
</Button>
@@ -1720,7 +1801,7 @@ export function PurchaseRequestsPage() {
size="sm"
variant="secondary"
disabled={!canEditDraft || deleteAttachmentMutation.isPending}
onClick={() => deleteAttachmentMutation.mutate(attachment.id)}
onClick={() => setDeleteAttachmentTarget({ id: attachment.id, originalName: attachment.originalName })}
>
删除
</Button>
@@ -1877,6 +1958,18 @@ export function PurchaseRequestsPage() {
submitMutation.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 });
}}
/>
</ListWorkspace>
);
}