feat(P25-2/P25-3/P26-2/P28-1/P24-2/P29-3): 落地内部OA够用版前端入口与运营页面

关联 Issue: P25-2, P25-3, P25-4, P26-2, P26-4, P28-1, P28-2, P28-3, P28-4, P24-2, P29-3
这个提交包含在:
2026-03-21 18:03:48 +08:00
父节点 e39e41287e
当前提交 9905c1c79d
共修改 44 个文件,包含 2029 行新增和 169 行删除
+46 -1
查看文件
@@ -10,6 +10,7 @@ import {
deleteReimbursementItem,
fetchReimbursementDetail,
payReimbursement,
precheckReimbursement,
submitReimbursement,
updateReimbursementItem,
withdrawReimbursement,
@@ -21,6 +22,7 @@ import { hasPermission } from "../auth/permissions";
import { CardSkeleton } from "../components/common/CardSkeleton";
import { ConfirmDialog } from "../components/common/ConfirmDialog";
import { PageHeader } from "../components/common/PageHeader";
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
import { Button } from "../components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
@@ -30,6 +32,7 @@ import {
type ItemEditorInitialValues,
type ItemEditorSaveData,
} from "./reimbursement-detail/ReimbursementItemEditor";
import { buildPrecheckDialogMessage } from "../lib/precheck";
type ItemEditorState = { mode: "create" } | { mode: "edit"; itemId: string } | null;
@@ -319,6 +322,46 @@ export function ReimbursementDetailPage() {
setItemError("");
};
const handleSubmitAction = async () => {
if (!reimbursementId) {
return;
}
try {
setActionError("");
const precheck = await precheckReimbursement(reimbursementId);
const message = buildPrecheckDialogMessage(precheck);
if (precheck.decision === "block") {
setActionError(message);
toast.error("提交前校验未通过");
return;
}
if ((precheck.decision === "warn" || precheck.decision === "manual_review") && !window.confirm(message)) {
return;
}
mutation.mutate({ action: "submit" });
} catch (error) {
if (isApiError(error)) {
const message = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
setActionError(message);
toast.error(message);
return;
}
if (error instanceof Error) {
setActionError(error.message);
toast.error(error.message);
return;
}
setActionError("提交前校验失败,请稍后重试");
toast.error("提交前校验失败,请稍后重试");
}
};
return (
<div className="space-y-6">
<PageHeader
@@ -414,6 +457,8 @@ export function ReimbursementDetailPage() {
</div>
) : null}
<RiskFlagsCard items={detail.riskFlags} title="待处理风险项" />
<AiSuggestionCard targetType="reimbursement" targetId={detail.id} />
<div className="card-std space-y-4 p-6" data-testid="reimbursement-items">
@@ -528,7 +573,7 @@ export function ReimbursementDetailPage() {
canPay={canPay}
actionError={actionError}
isPending={mutation.isPending}
onSubmit={() => mutation.mutate({ action: "submit" })}
onSubmit={() => void handleSubmitAction()}
onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })}
onPay={(payload) => mutation.mutate({ action: "pay", payload })}
/>