feat(reimbursement-small-team-form-validation): 完善付款批次核对与回单预览

这个提交包含在:
2026-08-05 01:02:51 +08:00
父节点 9db8bbc338
当前提交 d4400116c5
共修改 24 个文件,包含 979 行新增和 245 行删除
@@ -40,7 +40,7 @@ export function AttachmentGovernanceBadges({
<span className={sensitivityClass(attachment.sensitivityLevel)}>
{SENSITIVITY_LEVEL_LABEL[attachment.sensitivityLevel] ?? attachment.sensitivityLevel}
</span>
{attachmentRequiresDownloadReason(attachment) ? <span className="badge-warning">需填用途</span> : null}
{attachmentRequiresDownloadReason(attachment) ? <span className="badge-warning">下载需说明用途</span> : null}
</div>
);
}
@@ -14,6 +14,7 @@ type Props = {
description: string | null;
editable: boolean;
mobile?: boolean;
showDescription?: boolean;
};
export function ReimbursementHeaderEditor({
@@ -22,14 +23,18 @@ export function ReimbursementHeaderEditor({
description,
editable,
mobile = false,
showDescription = true,
}: Props) {
const queryClient = useQueryClient();
const [editing, setEditing] = useState(false);
const [date, setDate] = useState(reimbursementDate ?? "");
const [reason, setReason] = useState(description ?? "");
const [error, setError] = useState("");
const dirty = editing && (date !== (reimbursementDate ?? "") || reason !== (description ?? ""));
const runGuarded = useUnsavedChangesGuard(dirty, "报销事由和日期");
const dirty = editing && (
date !== (reimbursementDate ?? "") ||
(showDescription && reason !== (description ?? ""))
);
const runGuarded = useUnsavedChangesGuard(dirty, showDescription ? "报销事由和日期" : "报销日期");
useEffect(() => {
if (editing) return;
@@ -40,7 +45,7 @@ export function ReimbursementHeaderEditor({
const mutation = useMutation({
mutationFn: () => updateReimbursement(reimbursementId, {
reimbursementDate: date,
description: reason.trim() || null,
...(showDescription ? { description: reason.trim() || null } : {}),
}),
onSuccess: async () => {
setError("");
@@ -59,16 +64,19 @@ export function ReimbursementHeaderEditor({
});
if (!editing) {
if (!showDescription && !editable) return null;
return (
<div className={mobile ? "mt-3" : "mt-3 rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2"}>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 text-sm leading-6 text-zinc-600">
<span className="font-medium text-zinc-800">报销事由:</span>
{description || "未填写"}
</div>
{showDescription ? (
<div className="min-w-0 text-sm leading-6 text-zinc-600">
<span className="font-medium text-zinc-800">报销事由:</span>
{description || "未填写"}
</div>
) : <div className="min-w-0 text-sm leading-6 text-zinc-600">报销日期可在草稿阶段修改</div>}
{editable ? (
<Button size="sm" type="button" variant="secondary" onClick={() => setEditing(true)}>
编辑
{showDescription ? "编辑" : "编辑报销日期"}
</Button>
) : null}
</div>
@@ -78,15 +86,17 @@ export function ReimbursementHeaderEditor({
return (
<div className="mt-3 space-y-3 rounded-lg border border-primary-100 bg-primary-50/40 p-3" data-testid="reimbursement-header-editor">
<div className={mobile ? "space-y-3" : "grid gap-3 md:grid-cols-[12rem_1fr]"}>
<div className={mobile || !showDescription ? "space-y-3" : "grid gap-3 md:grid-cols-[12rem_1fr]"}>
<label className="space-y-1 text-sm text-zinc-700">
<span>报销日期</span>
<Input type="date" value={date} disabled={mutation.isPending} onChange={(event) => setDate(event.target.value)} />
</label>
<label className="space-y-1 text-sm text-zinc-700">
<span>报销事由</span>
<Textarea rows={2} maxLength={500} value={reason} disabled={mutation.isPending} onChange={(event) => setReason(event.target.value)} />
</label>
{showDescription ? (
<label className="space-y-1 text-sm text-zinc-700">
<span>报销事由</span>
<Textarea rows={2} maxLength={500} value={reason} disabled={mutation.isPending} onChange={(event) => setReason(event.target.value)} />
</label>
) : null}
</div>
{error ? <p className="text-sm text-red-600">{error}</p> : null}
<div className="flex gap-2">