From 1118bfadd747431de91f6e4f0f0931e3a2efbf77 Mon Sep 17 00:00:00 2001 From: shunli Date: Fri, 7 Aug 2026 11:00:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(08-05-business-detail-routing):=20?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E9=87=87=E8=B4=AD=E4=B8=8E=E8=B5=84=E4=BA=A7?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/GlobalSearchDialog.tsx | 13 +- client/src/features/navigation/schema.ts | 3 + client/src/lib/businessDetailRoutes.ts | 200 +++++++++++++++++ client/src/lib/notificationLinks.ts | 6 +- client/src/lib/pcDetailNavigation.ts | 22 +- client/src/lib/runtimeContext.ts | 2 +- client/src/pages/AssetRegistryPage.tsx | 211 ++++++++++-------- client/src/pages/ProjectDetailPage.tsx | 13 +- client/src/pages/PurchaseRequestsPage.tsx | 178 +++++++++------ client/src/router/index.tsx | 32 +++ client/src/router/lazy/AssetCreatePage.ts | 1 + client/src/router/lazy/AssetDetailPage.ts | 1 + .../router/lazy/PurchaseRequestDetailPage.ts | 1 + client/src/router/lazy/index.ts | 3 + client/tests/e2e/assets-flow.spec.ts | 9 +- .../tests/e2e/business-detail-routing.spec.ts | 94 ++++++++ client/tests/e2e/global-search.spec.ts | 166 +++++++++++++- .../e2e/manual/asset1d-public-trial.spec.ts | 4 +- .../r7e-browser-business-trialrun.spec.ts | 4 +- .../e2e/pc-detail-mobile-visual-wave3.spec.ts | 2 +- .../e2e/permission-direct-access.spec.ts | 5 + client/tests/e2e/procurement-flow.spec.ts | 4 +- .../r103-cross-page-ui-consistency.spec.ts | 110 +++++++-- scripts/qa/route-manifest.json | 3 + 24 files changed, 891 insertions(+), 196 deletions(-) create mode 100644 client/src/lib/businessDetailRoutes.ts create mode 100644 client/src/router/lazy/AssetCreatePage.ts create mode 100644 client/src/router/lazy/AssetDetailPage.ts create mode 100644 client/src/router/lazy/PurchaseRequestDetailPage.ts create mode 100644 client/tests/e2e/business-detail-routing.spec.ts diff --git a/client/src/components/GlobalSearchDialog.tsx b/client/src/components/GlobalSearchDialog.tsx index 1f18bcd9..16e4a957 100644 --- a/client/src/components/GlobalSearchDialog.tsx +++ b/client/src/components/GlobalSearchDialog.tsx @@ -4,6 +4,7 @@ import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { searchGlobal, type GlobalSearchHit } from "../api/search"; import { isApiError } from "../api/types"; +import { buildAssetDetailPath, buildPurchaseRequestDetailPath } from "../lib/businessDetailRoutes"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog"; @@ -88,6 +89,16 @@ function resolveBrowseHref(groupKey: SearchGroupKey, items: GlobalSearchHit[]) { return null; } +function resolveSearchHitHref(item: GlobalSearchHit) { + if (item.entityType === "purchase_request") { + return buildPurchaseRequestDetailPath(item.entityId); + } + if (item.entityType === "asset") { + return buildAssetDetailPath(item.entityId); + } + return item.href; +} + export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { const navigate = useNavigate(); const [keyword, setKeyword] = useState(""); @@ -158,7 +169,7 @@ export function GlobalSearchDialog({ open, onOpenChange }: { open: boolean; onOp key={`${item.entityType}-${item.entityId}`} type="button" className="w-full rounded-lg border border-zinc-200 px-3 py-3 text-left hover:bg-zinc-50" - onClick={() => closeAndNavigate(item.href)} + onClick={() => closeAndNavigate(resolveSearchHitHref(item))} >
{item.title}
diff --git a/client/src/features/navigation/schema.ts b/client/src/features/navigation/schema.ts index d9cc7b5a..f48ccfa8 100644 --- a/client/src/features/navigation/schema.ts +++ b/client/src/features/navigation/schema.ts @@ -15,6 +15,7 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [ { path: "/wecom-approval-archive", title: "企微审批历史", group: "业务" }, { path: "/projects", title: "项目", group: "业务" }, { path: "/contracts-registry", title: "合同", group: "业务" }, + { path: "/purchase-requests/", title: "采购详情", group: "业务" }, { path: "/purchase-requests", title: "采购", group: "业务" }, { path: "/travel-requests", title: "差旅申请", group: "更多" }, { path: "/budgets", title: "预算管理", group: "财务" }, @@ -27,6 +28,8 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [ { path: "/reimbursements/", title: "报销详情", group: "工作" }, { path: "/reimbursements", title: "报销", group: "工作" }, { path: "/leave-requests", title: "请假记录", group: "业务" }, + { path: "/assets/new", title: "新增资产", group: "业务" }, + { path: "/assets/", title: "资产详情", group: "业务" }, { path: "/assets", title: "资产", group: "业务" }, { path: "/workflow/tasks/", title: "审批详情", group: "工作" }, { path: "/workflow/tasks", title: "待我审批", group: "工作" }, diff --git a/client/src/lib/businessDetailRoutes.ts b/client/src/lib/businessDetailRoutes.ts new file mode 100644 index 00000000..56c0ec3f --- /dev/null +++ b/client/src/lib/businessDetailRoutes.ts @@ -0,0 +1,200 @@ +import type { PurchaseRequestStatus } from "../api/purchaseRequests"; +import type { AssetSensitivityLevel, AssetStatus, AssetType } from "../api/assets"; + +const PURCHASE_STATUS_VALUES = new Set([ + "draft", + "reviewing", + "approved", + "rejected", + "cancelled", +]); +const ASSET_STATUS_VALUES = new Set([ + "in_stock", + "in_use", + "under_repair", + "retired", + "archived", +]); +const ASSET_TYPE_VALUES = new Set([ + "fixed_asset", + "electronic_document", + "electronic_system", + "code_repository", + "backup_media", +]); +const ASSET_SENSITIVITY_VALUES = new Set([ + "public_candidate", + "internal", + "confidential", + "strictly_confidential", +]); + +export const ASSET_CREATE_PATH = "/assets/new"; + +function normalizedEntityId(value: string) { + return value.trim(); +} + +function optionalSearchValue(searchParams: URLSearchParams, key: string) { + return (searchParams.get(key) ?? "").trim(); +} + +function positivePage(value: string | null) { + if (!value || !/^[1-9]\d*$/.test(value)) { + return 1; + } + const parsed = Number(value); + return Number.isSafeInteger(parsed) ? parsed : 1; +} + +function appendOptional(target: URLSearchParams, key: string, value: string) { + if (value) { + target.set(key, value); + } +} + +export function buildPurchaseRequestDetailPath(purchaseRequestId: string) { + const normalized = normalizedEntityId(purchaseRequestId); + return normalized ? `/purchase-requests/${encodeURIComponent(normalized)}` : "/purchase-requests"; +} + +export function buildAssetDetailPath(assetId: string) { + const normalized = normalizedEntityId(assetId); + return normalized ? `/assets/${encodeURIComponent(normalized)}` : "/assets"; +} + +export function readPurchaseListFilters( + searchParams: URLSearchParams, + canViewAll: boolean, +): { + status: PurchaseRequestStatus | ""; + mine: boolean; + supplierId: string; + legalEntityId: string; + costCenterId: string; + page: number; +} { + const rawStatus = searchParams.get("status"); + const status = rawStatus === "all" + ? "" + : rawStatus && PURCHASE_STATUS_VALUES.has(rawStatus as PurchaseRequestStatus) + ? rawStatus as PurchaseRequestStatus + : "draft"; + const scope = searchParams.get("scope"); + + return { + status, + mine: !(canViewAll && scope === "all"), + supplierId: optionalSearchValue(searchParams, "supplierId"), + legalEntityId: optionalSearchValue(searchParams, "legalEntityId"), + costCenterId: optionalSearchValue(searchParams, "costCenterId"), + page: positivePage(searchParams.get("page")), + }; +} + +export function normalizePurchaseListSearchParams(searchParams: URLSearchParams) { + const normalized = new URLSearchParams(); + const rawStatus = searchParams.get("status"); + if (rawStatus === "all" || (rawStatus && PURCHASE_STATUS_VALUES.has(rawStatus as PurchaseRequestStatus))) { + normalized.set("status", rawStatus); + } + const rawScope = searchParams.get("scope"); + if (rawScope === "own" || rawScope === "all") { + normalized.set("scope", rawScope); + } + appendOptional(normalized, "supplierId", optionalSearchValue(searchParams, "supplierId")); + appendOptional(normalized, "legalEntityId", optionalSearchValue(searchParams, "legalEntityId")); + appendOptional(normalized, "costCenterId", optionalSearchValue(searchParams, "costCenterId")); + const page = positivePage(searchParams.get("page")); + if (page > 1) { + normalized.set("page", String(page)); + } + return normalized; +} + +export function readAssetListFilters( + searchParams: URLSearchParams, + canViewAll: boolean, +): { + q: string; + status: AssetStatus | ""; + assetType: AssetType | ""; + sensitivity: AssetSensitivityLevel | ""; + categoryId: string; + legalEntityId: string; + costCenterId: string; + assigneeId: string; + mine: boolean; +} { + const rawStatus = searchParams.get("status"); + const rawAssetType = searchParams.get("assetType"); + const rawSensitivity = searchParams.get("sensitivity"); + const scope = searchParams.get("scope"); + + return { + q: searchParams.get("q") ?? "", + status: rawStatus && ASSET_STATUS_VALUES.has(rawStatus as AssetStatus) ? rawStatus as AssetStatus : "", + assetType: rawAssetType && ASSET_TYPE_VALUES.has(rawAssetType as AssetType) ? rawAssetType as AssetType : "", + sensitivity: rawSensitivity && ASSET_SENSITIVITY_VALUES.has(rawSensitivity as AssetSensitivityLevel) + ? rawSensitivity as AssetSensitivityLevel + : "", + categoryId: optionalSearchValue(searchParams, "categoryId"), + legalEntityId: optionalSearchValue(searchParams, "legalEntityId"), + costCenterId: optionalSearchValue(searchParams, "costCenterId"), + assigneeId: optionalSearchValue(searchParams, "assigneeId"), + mine: scope === "own" || !(canViewAll && (scope === "all" || scope == null)), + }; +} + +export function normalizeAssetListSearchParams(searchParams: URLSearchParams) { + const normalized = new URLSearchParams(); + appendOptional(normalized, "q", optionalSearchValue(searchParams, "q")); + const rawStatus = searchParams.get("status"); + if (rawStatus && ASSET_STATUS_VALUES.has(rawStatus as AssetStatus)) { + normalized.set("status", rawStatus); + } + const rawAssetType = searchParams.get("assetType"); + if (rawAssetType && ASSET_TYPE_VALUES.has(rawAssetType as AssetType)) { + normalized.set("assetType", rawAssetType); + } + const rawSensitivity = searchParams.get("sensitivity"); + if (rawSensitivity && ASSET_SENSITIVITY_VALUES.has(rawSensitivity as AssetSensitivityLevel)) { + normalized.set("sensitivity", rawSensitivity); + } + appendOptional(normalized, "categoryId", optionalSearchValue(searchParams, "categoryId")); + appendOptional(normalized, "legalEntityId", optionalSearchValue(searchParams, "legalEntityId")); + appendOptional(normalized, "costCenterId", optionalSearchValue(searchParams, "costCenterId")); + appendOptional(normalized, "assigneeId", optionalSearchValue(searchParams, "assigneeId")); + const rawScope = searchParams.get("scope"); + if (rawScope === "own" || rawScope === "all") { + normalized.set("scope", rawScope); + } + return normalized; +} + +function listSource(pathname: string, searchParams: URLSearchParams) { + const search = searchParams.toString(); + return search ? `${pathname}?${search}` : pathname; +} + +export function resolvePurchaseRequestLegacyRedirect(searchParams: URLSearchParams) { + const purchaseRequestId = normalizedEntityId(searchParams.get("id") ?? ""); + if (!purchaseRequestId) { + return null; + } + return { + to: buildPurchaseRequestDetailPath(purchaseRequestId), + from: listSource("/purchase-requests", normalizePurchaseListSearchParams(searchParams)), + }; +} + +export function resolveAssetLegacyRedirect(searchParams: URLSearchParams) { + const assetId = normalizedEntityId(searchParams.get("id") ?? ""); + if (!assetId) { + return null; + } + return { + to: buildAssetDetailPath(assetId), + from: listSource("/assets", normalizeAssetListSearchParams(searchParams)), + }; +} diff --git a/client/src/lib/notificationLinks.ts b/client/src/lib/notificationLinks.ts index 34108fd7..64a582e4 100644 --- a/client/src/lib/notificationLinks.ts +++ b/client/src/lib/notificationLinks.ts @@ -1,3 +1,5 @@ +import { buildAssetDetailPath, buildPurchaseRequestDetailPath } from "./businessDetailRoutes"; + type NotificationTarget = { targetType: string | null; targetId: string | null; @@ -31,7 +33,7 @@ export function resolveNotificationTargetLink(item: NotificationTarget) { } if (item.targetType === "purchase_request") { - return `/purchase-requests?highlight=${item.targetId}`; + return buildPurchaseRequestDetailPath(item.targetId); } if (item.targetType === "travel_request") { @@ -39,7 +41,7 @@ export function resolveNotificationTargetLink(item: NotificationTarget) { } if (item.targetType === "asset" || item.targetType === "asset_card") { - return `/assets?highlight=${item.targetId}`; + return buildAssetDetailPath(item.targetId); } if (item.targetType === "inventory_check") { diff --git a/client/src/lib/pcDetailNavigation.ts b/client/src/lib/pcDetailNavigation.ts index c11b1c0d..ad3958e5 100644 --- a/client/src/lib/pcDetailNavigation.ts +++ b/client/src/lib/pcDetailNavigation.ts @@ -2,22 +2,34 @@ export type PcDetailNavigationState = { from?: string; }; +export type PcDetailSourcePolicy = "reimbursement" | "purchase-request" | "asset"; + export function buildPcDetailSource(pathname: string, search: string) { return `${pathname}${search}`; } -export function resolvePcDetailSource(value: unknown, currentPath: string) { +export function resolvePcDetailSource( + value: unknown, + currentPath: string, + policy: PcDetailSourcePolicy = "reimbursement", +) { if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || value.includes("#")) { return null; } - const url = new URL(value, "https://hyb-oa.invalid"); - const isReimbursementList = url.pathname === "/reimbursements"; - const isWorkflowTask = /^\/workflow\/tasks\/[^/]+$/.test(url.pathname); - if (!isReimbursementList && !isWorkflowTask) { + const baseUrl = new URL("https://hyb-oa.invalid"); + const url = new URL(value, baseUrl); + if (url.origin !== baseUrl.origin) { return null; } + const allowed = policy === "purchase-request" + ? url.pathname === "/purchase-requests" || /^\/projects\/[^/]+$/.test(url.pathname) + : policy === "asset" + ? url.pathname === "/assets" + : url.pathname === "/reimbursements" || /^\/workflow\/tasks\/[^/]+$/.test(url.pathname); + if (!allowed) return null; + const source = `${url.pathname}${url.search}`; return source === currentPath ? null : source; } diff --git a/client/src/lib/runtimeContext.ts b/client/src/lib/runtimeContext.ts index c73bd853..92443b40 100644 --- a/client/src/lib/runtimeContext.ts +++ b/client/src/lib/runtimeContext.ts @@ -100,7 +100,7 @@ export function resolveMobileEquivalentPath(pathname: string, search = "") { return "/mobile/approvals"; } - if (normalizedPath === "/assets" || normalizedPath === "/inventory-checks") { + if (normalizedPath === "/assets" || normalizedPath.startsWith("/assets/") || normalizedPath === "/inventory-checks") { return "/mobile/assets/scan"; } diff --git a/client/src/pages/AssetRegistryPage.tsx b/client/src/pages/AssetRegistryPage.tsx index d3451ec8..8e6901cc 100644 --- a/client/src/pages/AssetRegistryPage.tsx +++ b/client/src/pages/AssetRegistryPage.tsx @@ -1,8 +1,9 @@ import { resolveErrorMessage as resolveUserErrorMessage } from "../lib/resolveErrorMessage"; -import { useEffect, useMemo, useState } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; +import { useDeferredValue, useEffect, useMemo, useState } from "react"; +import { Navigate, useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; +import { isApiError } from "../api/types"; import { assignAsset, createAsset, @@ -48,6 +49,17 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { Input } from "../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { formatCompactIdentifier, formatDisplayDateTime, formatTextWithCompactLongNumbers, isCompactedIdentifier } from "../lib/displayFormat"; +import { + ASSET_CREATE_PATH, + buildAssetDetailPath, + readAssetListFilters, + resolveAssetLegacyRedirect, +} from "../lib/businessDetailRoutes"; +import { + buildPcDetailSource, + resolvePcDetailSource, + type PcDetailNavigationState, +} from "../lib/pcDetailNavigation"; function resolveError(error: unknown) { return resolveUserErrorMessage(error, "请求失败,请稍后重试。"); @@ -199,8 +211,33 @@ function unsafeAssetTextReason(value: string) { } export function AssetRegistryPage() { + const [searchParams] = useSearchParams(); + const legacyRedirect = resolveAssetLegacyRedirect(searchParams); + if (legacyRedirect) { + return ; + } + return ; +} + +export function AssetCreatePage() { + return ; +} + +export function AssetDetailPage() { + const { assetId = "" } = useParams(); + return ; +} + +function AssetRegistryWorkspace({ + mode, + assetId = "", +}: { + mode: "list" | "create" | "detail"; + assetId?: string; +}) { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); + const location = useLocation(); const queryClient = useQueryClient(); const { permissions } = useAuth(); @@ -208,24 +245,43 @@ export function AssetRegistryPage() { const canTransfer = hasPermission(permissions, "asset.transfer"); const canViewAll = hasAnyPermission(permissions, ["asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]); const canReadProcurementSources = canManage && hasAnyPermission(permissions, ["procurement.view_all", "procurement.manage"]); + const isCreatingAsset = mode === "create"; + const isAssetListMode = mode === "list"; + const selectedAssetId = mode === "detail" ? assetId.trim() : ""; + const listFilters = useMemo( + () => readAssetListFilters(searchParams, canViewAll), + [canViewAll, searchParams], + ); + const { + q: assetSearch, + status: statusFilter, + assetType: assetTypeFilter, + sensitivity: sensitivityFilter, + categoryId: categoryFilter, + legalEntityId: legalEntityFilter, + costCenterId: costCenterFilter, + assigneeId: assigneeFilter, + mine, + } = listFilters; + const deferredAssetSearch = useDeferredValue(assetSearch.trim()); + const currentSource = buildPcDetailSource(location.pathname, location.search); + const navigationState = location.state as PcDetailNavigationState | null; + const backSource = resolvePcDetailSource(navigationState?.from, currentSource, "asset") ?? "/assets"; + const listSource = buildPcDetailSource(location.pathname, location.search); + + function updateListSearch(updates: Record) { + const next = new URLSearchParams(searchParams); + for (const [key, value] of Object.entries(updates)) { + if (value) next.set(key, value); + else next.delete(key); + } + setSearchParams(next, { replace: true }); + } const [categoryForm, setCategoryForm] = useState(initialCategoryForm); const [editingCategoryId, setEditingCategoryId] = useState(""); - - const [statusFilter, setStatusFilter] = useState(""); - const [assetTypeFilter, setAssetTypeFilter] = useState(""); - const [sensitivityFilter, setSensitivityFilter] = useState(""); - const [categoryFilter, setCategoryFilter] = useState(""); - const [legalEntityFilter, setLegalEntityFilter] = useState(""); - const [costCenterFilter, setCostCenterFilter] = useState(""); - const [assigneeFilter, setAssigneeFilter] = useState(""); - const [assetSearch, setAssetSearch] = useState(""); - const [mine, setMine] = useState(!canViewAll); const [showCategoryManager, setShowCategoryManager] = useState(false); const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); - - const [selectedAssetId, setSelectedAssetId] = useState(searchParams.get("id") ?? ""); - const [isCreatingAsset, setIsCreatingAsset] = useState(false); const [assetForm, setAssetForm] = useState(initialAssetForm); const [savedAssetFormSignature, setSavedAssetFormSignature] = useState(() => JSON.stringify(initialAssetForm())); const [assetFormError, setAssetFormError] = useState(""); @@ -240,7 +296,8 @@ export function AssetRegistryPage() { }); const assetsQuery = useQuery({ - queryKey: ["assets", "list", statusFilter, assetTypeFilter, sensitivityFilter, categoryFilter, legalEntityFilter, costCenterFilter, assigneeFilter, assetSearch, mine, canViewAll], + enabled: isAssetListMode, + queryKey: ["assets", "list", statusFilter, assetTypeFilter, sensitivityFilter, categoryFilter, legalEntityFilter, costCenterFilter, assigneeFilter, deferredAssetSearch, mine, canViewAll], queryFn: () => fetchAssets({ status: statusFilter || undefined, @@ -250,13 +307,13 @@ export function AssetRegistryPage() { legalEntityId: legalEntityFilter || undefined, costCenterId: costCenterFilter || undefined, assigneeId: assigneeFilter || undefined, - q: assetSearch.trim() || undefined, + q: deferredAssetSearch || undefined, mine: canViewAll ? mine : true, }), }); const assetDetailQuery = useQuery({ - enabled: Boolean(selectedAssetId) && !isCreatingAsset, + enabled: mode === "detail" && Boolean(selectedAssetId), queryKey: ["assets", "detail", selectedAssetId], queryFn: () => fetchAssetDetail(selectedAssetId), }); @@ -272,6 +329,7 @@ export function AssetRegistryPage() { }); const documentCategoriesQuery = useQuery({ + enabled: isAssetListMode && canManage, queryKey: ["assets", "document-categories"], queryFn: () => fetchDocumentCategories({ entityType: "asset_card", enabled: true }), }); @@ -283,44 +341,11 @@ export function AssetRegistryPage() { }); const approvedPurchasesQuery = useQuery({ - enabled: canReadProcurementSources, + enabled: isCreatingAsset && canReadProcurementSources, queryKey: ["assets", "approved-purchases"], 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(() => { - 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 (isCreatingAsset || !assetDetailQuery.data?.asset) { return; @@ -406,12 +431,7 @@ export function AssetRegistryPage() { function resetAssetEditor() { runGuarded(() => { - const nextAssetForm = initialAssetForm(); - setIsCreatingAsset(true); - setSelectedAssetId(""); - setAssetForm(nextAssetForm); - setSavedAssetFormSignature(JSON.stringify(nextAssetForm)); - setAssetFormError(""); + navigate(ASSET_CREATE_PATH, { state: { from: listSource } }); }); } @@ -561,9 +581,13 @@ export function AssetRegistryPage() { ]); setAssetFormError(""); setSavedAssetFormSignature(JSON.stringify(assetForm)); - setSelectedAssetId(data.asset.id); - setIsCreatingAsset(false); toast.success(isCreatingAsset ? "资产卡片已创建" : "资产卡片已更新"); + if (isCreatingAsset) { + navigate(buildAssetDetailPath(data.asset.id), { + replace: true, + state: { from: backSource }, + }); + } }, onError: (error) => { const message = resolveError(error); @@ -718,20 +742,16 @@ export function AssetRegistryPage() { }, [assetItems]); const selectAsset = (assetId: string) => { runGuarded(() => { - setIsCreatingAsset(false); - setSelectedAssetId(assetId); + navigate(buildAssetDetailPath(assetId), { state: { from: listSource } }); }); }; const returnToAssetList = () => { runGuarded(() => { - setIsCreatingAsset(false); - setSelectedAssetId(""); - const next = new URLSearchParams(searchParams); - next.delete("id"); - setSearchParams(next, { replace: true }); + navigate(backSource, { replace: true }); }); }; - const isAssetDetailMode = isCreatingAsset || Boolean(selectedAssetId); + const isAssetDetailMode = !isAssetListMode; + const detailAccessDenied = assetDetailQuery.isError && isApiError(assetDetailQuery.error) && [403, 404].includes(assetDetailQuery.error.status); return ( @@ -741,7 +761,7 @@ export function AssetRegistryPage() { {isAssetDetailMode ? ( ) : ( <> @@ -888,10 +908,10 @@ export function AssetRegistryPage() { <> {canViewAll ? ( <> - - @@ -909,14 +929,14 @@ export function AssetRegistryPage() { )} advanced={showAssetAdvancedFilters ? (
- updateListSearch({ sensitivity: event.target.value || null })}> {sensitivityOptions().map((item) => ( ))} - updateListSearch({ categoryId: event.target.value || null })}> {(categoriesQuery.data?.items ?? []).map((item) => ( {filteredFilterCostCenters.map((item) => ( ))} - updateListSearch({ assigneeId: event.target.value || null })} disabled={!canViewAll}> {(assigneesQuery.data?.items ?? []).map((item) => ( ))} - updateListSearch({ assetType: event.target.value || null })}> {assetTypeOptions().map((item) => (