R7E-16E/R7E-16F 高风险操作确认统一

这个提交包含在:
2026-05-15 22:55:00 +08:00
父节点 5c38d810c1
当前提交 bf791d7a67
共修改 18 个文件,包含 979 行新增和 68 行删除
@@ -0,0 +1,52 @@
import type { ReactNode } from "react";
import { getActionConfirmCopy, type ActionConfirmKind } from "../../lib/actionConfirm";
import { ConfirmDialog } from "./ConfirmDialog";
type ActionConfirmDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
kind: ActionConfirmKind;
targetName?: string;
count?: number;
title?: string;
description?: ReactNode;
confirmLabel?: string;
variant?: "default" | "destructive";
testId?: string;
confirmTestId?: string;
cancelTestId?: string;
onConfirm: () => void | Promise<void>;
};
export function ActionConfirmDialog({
open,
onOpenChange,
kind,
targetName,
count,
title,
description,
confirmLabel,
variant,
testId,
confirmTestId,
cancelTestId,
onConfirm,
}: ActionConfirmDialogProps) {
const copy = getActionConfirmCopy(kind, { targetName, count, detail: description });
return (
<ConfirmDialog
open={open}
onOpenChange={onOpenChange}
title={title ?? copy.title}
description={description ?? copy.description}
confirmLabel={confirmLabel ?? copy.confirmLabel}
variant={variant ?? copy.variant}
testId={testId}
confirmTestId={confirmTestId}
cancelTestId={cancelTestId}
onConfirm={onConfirm}
/>
);
}
@@ -31,9 +31,9 @@ export function ConfirmDialog({
confirmLabel = "确认",
cancelLabel = "取消",
variant = "default",
testId,
confirmTestId,
cancelTestId,
testId = "action-confirm-dialog",
confirmTestId = "action-confirm-submit",
cancelTestId = "action-confirm-cancel",
onConfirm,
}: ConfirmDialogProps) {
const [pending, setPending] = useState(false);
@@ -25,8 +25,10 @@ import { EmptyStateCard } from '../common/EmptyStateCard';
import { Pagination } from '../common/Pagination';
import { StatusBadge } from '../common/StatusBadge';
import { Button } from '../ui/button';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea';
const PAGE_SIZE = 10;
@@ -68,6 +70,8 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
const [costCenterFilter, setCostCenterFilter] = useState('');
const [periodStartFilter, setPeriodStartFilter] = useState('');
const [periodEndFilter, setPeriodEndFilter] = useState('');
const [confirmItemTarget, setConfirmItemTarget] = useState<TravelReconciliationItem | null>(null);
const [confirmItemNote, setConfirmItemNote] = useState('');
const [page, setPage] = useState(1);
const [actionItemId, setActionItemId] = useState<string | null>(null);
@@ -248,11 +252,8 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
});
function handleConfirm(item: TravelReconciliationItem) {
const note = window.prompt('确认备注(可选)', item.confirmNote ?? '');
if (note === null) {
return;
}
confirmMutation.mutate({ item, note: note.trim() || null });
setConfirmItemTarget(item);
setConfirmItemNote(item.confirmNote ?? '');
}
const detail = detailQuery.data ?? null;
@@ -260,6 +261,7 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
return (
<>
<div className="grid gap-6 xl:grid-cols-[360px_minmax(0,1fr)]">
<section className="space-y-6">
{canCreateBatch ? (
@@ -520,5 +522,64 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
) : null}
</section>
</div>
<Dialog
open={Boolean(confirmItemTarget)}
onOpenChange={(open) => {
if (!open) {
setConfirmItemTarget(null);
setConfirmItemNote('');
}
}}
>
<DialogContent className="max-w-md" data-testid="action-confirm-dialog">
<DialogHeader>
<DialogTitle>确认对账异常项</DialogTitle>
<DialogDescription>
确认后该异常项将进入已确认状态,并写入差旅对账处理记录。备注可空。
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<label className="text-sm font-medium text-zinc-900" htmlFor="travel-reconciliation-confirm-note">
确认备注
</label>
<Textarea
id="travel-reconciliation-confirm-note"
data-testid="travel-reconciliation-confirm-note"
value={confirmItemNote}
onChange={(event) => setConfirmItemNote(event.target.value)}
placeholder="可补充异常确认说明"
rows={3}
/>
</div>
<DialogFooter>
<Button
type="button"
variant="secondary"
data-testid="action-confirm-cancel"
disabled={confirmMutation.isPending}
onClick={() => {
setConfirmItemTarget(null);
setConfirmItemNote('');
}}
>
取消
</Button>
<Button
type="button"
data-testid="action-confirm-submit"
disabled={!confirmItemTarget || confirmMutation.isPending}
onClick={() => {
if (!confirmItemTarget) return;
confirmMutation.mutate({ item: confirmItemTarget, note: confirmItemNote.trim() || null });
setConfirmItemTarget(null);
setConfirmItemNote('');
}}
>
{confirmMutation.isPending ? '处理中...' : '确认异常'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}