R53 A1-A6:ERP 集成运维可视化与业务同步状态接入

- ErpIntegrationPage Tab 化:主数据同步 / Outbox 队列管理 / ID 映射查看器
- Outbox 分页查询、详情、重试与统计端点
- IntegrationBinding 分页查询、routePath 推导与采购/资产深链
- 报销/采购/资产详情区接入 ERP 同步状态卡片
- 单测覆盖 Outbox 与 Binding 端点
这个提交包含在:
2026-04-07 10:33:58 +08:00
父节点 64d9ded024
当前提交 7cc6dc3397
共修改 9 个文件,包含 1989 行新增和 146 行删除
+46 -13
查看文件
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { mapApiErrorMessage } from "../api/errorMessages";
@@ -31,6 +31,7 @@ import { PageHeader } from "../components/common/PageHeader";
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
@@ -114,6 +115,7 @@ function resolveAssetErpActionLabel(actionHint: AssetErpTrack["stage"]["actionHi
export function AssetRegistryPage() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const queryClient = useQueryClient();
const { permissions } = useAuth();
@@ -133,7 +135,7 @@ export function AssetRegistryPage() {
const [assigneeFilter, setAssigneeFilter] = useState("");
const [mine, setMine] = useState(!canViewAll);
const [selectedAssetId, setSelectedAssetId] = useState("");
const [selectedAssetId, setSelectedAssetId] = useState(searchParams.get("id") ?? "");
const [isCreatingAsset, setIsCreatingAsset] = useState(false);
const [assetForm, setAssetForm] = useState(initialAssetForm);
const [assetFormError, setAssetFormError] = useState("");
@@ -167,7 +169,7 @@ export function AssetRegistryPage() {
});
const assetErpTrackQuery = useQuery({
enabled: Boolean(selectedAssetId) && !isCreatingAsset,
enabled: canIntegrationManage && Boolean(selectedAssetId) && !isCreatingAsset,
queryKey: ["assets", "erp-track", selectedAssetId],
queryFn: () => fetchAssetErpTrack(selectedAssetId),
});
@@ -199,6 +201,13 @@ export function AssetRegistryPage() {
queryFn: () => fetchPurchaseRequests({ status: "approved", mine: false, page: 1, pageSize: 100 }),
});
useEffect(() => {
const searchSelectedId = searchParams.get("id") ?? "";
if (searchSelectedId && searchSelectedId !== selectedAssetId) {
setSelectedAssetId(searchSelectedId);
}
}, [searchParams, selectedAssetId]);
useEffect(() => {
if (isCreatingAsset) {
return;
@@ -217,6 +226,32 @@ export function AssetRegistryPage() {
}
}, [assetDetailQuery.isError, assetDetailQuery.isLoading, assetsQuery.data?.items, isCreatingAsset, selectedAssetId]);
useEffect(() => {
const currentId = searchParams.get("id") ?? "";
if (isCreatingAsset) {
if (!currentId) {
return;
}
const next = new URLSearchParams(searchParams);
next.delete("id");
setSearchParams(next, { replace: true });
return;
}
if (!selectedAssetId && !currentId) {
return;
}
if (selectedAssetId === currentId) {
return;
}
const next = new URLSearchParams(searchParams);
if (selectedAssetId) {
next.set("id", selectedAssetId);
} else {
next.delete("id");
}
setSearchParams(next, { replace: true });
}, [isCreatingAsset, searchParams, selectedAssetId, setSearchParams]);
useEffect(() => {
if (!assetDetailQuery.data?.asset) {
return;
@@ -937,15 +972,13 @@ export function AssetRegistryPage() {
</div>
) : null}
{selectedAssetId && !isCreatingAsset ? (
<div className="space-y-3 rounded-xl border border-zinc-200 p-4">
<div>
<h4 className="text-sm font-semibold text-zinc-900">资产 ERP 同步轨道</h4>
<p className="text-xs text-zinc-500">仅同步资产法体、成本中心与退役/归档等财务台账摘要,不接管实物流转。</p>
</div>
{selectedAssetId && !isCreatingAsset && canIntegrationManage ? (
<SectionPanel
title="ERP 同步状态"
description="仅同步资产法体、成本中心与退役/归档等财务台账摘要,不接管实物流转。"
>
{assetErpTrackQuery.isLoading ? (
<p className="rounded-lg bg-zinc-50 px-3 py-2 text-sm text-zinc-500">ERP 轨道加载中...</p>
<p className="rounded-lg bg-zinc-50 px-3 py-2 text-sm text-zinc-500">ERP 同步轨道加载中...</p>
) : assetErpTrackQuery.isError ? (
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{resolveError(assetErpTrackQuery.error)}</p>
) : assetErpStage ? (
@@ -984,7 +1017,7 @@ export function AssetRegistryPage() {
</div>
) : null}
{canIntegrationManage && assetErpTrack?.permissions.canRetry && assetErpActionLabel ? (
{assetErpTrack?.permissions.canRetry && assetErpActionLabel ? (
<div className="flex justify-end">
<Button
type="button"
@@ -1001,7 +1034,7 @@ export function AssetRegistryPage() {
) : (
<p className="rounded-lg bg-zinc-50 px-3 py-2 text-sm text-zinc-500">暂无 ERP 轨道信息</p>
)}
</div>
</SectionPanel>
) : null}
{selectedAssetId && !isCreatingAsset && currentAsset ? (