import { useEffect, useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { mapApiErrorMessage } from "../api/errorMessages"; import { assignAsset, createAsset, createAssetCategory, fetchAssetAssignees, fetchAssetCategories, fetchAssetDetail, fetchAssetErpTrack, fetchAssets, patchAsset, patchAssetCategory, requeueAssetErpSync, returnAsset, transferAsset, type AssetCategoryItem, type AssetErpStage, type AssetErpTrack, type AssetSensitivityLevel, type AssetStatus, type AssetType, } from "../api/assets"; import { fetchPurchaseRequests } from "../api/purchaseRequests"; import { fetchCostCenters, fetchDocumentCategories, fetchLegalEntities } from "../api/masterData"; import { isApiError } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { hasAnyPermission, hasPermission } from "../auth/permissions"; import { AiSuggestionCard } from "../components/ai/AiSuggestionCard"; import { DateTextInput } from "../components/common/DateTextInput"; import { StatusBadge } from "../components/common/StatusBadge"; import { TableSkeleton } from "../components/common/TableSkeleton"; import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace"; 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"; import { formatDisplayDateTime } from "../lib/displayFormat"; function resolveError(error: unknown) { if (isApiError(error)) { return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; } if (error instanceof Error) { return error.message; } return "请求失败,请稍后重试。"; } function initialCategoryForm() { return { code: "", name: "", documentCategoryId: "", enabled: true, }; } function initialAssetForm() { return { name: "", assetType: "fixed_asset" as AssetType, sensitivityLevel: "internal" as AssetSensitivityLevel, categoryId: "", legalEntityId: "", costCenterId: "", purchaseRequestId: "", serialNo: "", location: "", status: "in_stock" as AssetStatus, assigneeEmployeeId: "", ownerEmployeeId: "", keeperEmployeeId: "", archiveLocation: "", externalRef: "", sourceHash: "", verificationNo: "", confidentialityNote: "", metadataText: "", lastVerifiedAt: "", expiresAt: "", activatedAt: "", retiredAt: "", }; } function assetStatusOptions(): Array<{ value: AssetStatus | ""; label: string }> { return [ { value: "", label: "全部状态" }, { value: "in_stock", label: "库存中" }, { value: "in_use", label: "使用中" }, { value: "under_repair", label: "维修中" }, { value: "retired", label: "已退役" }, { value: "archived", label: "已归档" }, ]; } function assetTypeOptions(): Array<{ value: AssetType | ""; label: string }> { return [ { value: "", label: "全部资产类型" }, { value: "fixed_asset", label: "固定资产 FA" }, { value: "electronic_document", label: "电子资料 EA-DOC" }, { value: "electronic_system", label: "电子系统 EA-SYS" }, { value: "code_repository", label: "代码仓库 EA-REPO" }, { value: "backup_media", label: "备份介质 EA-BKP" }, ]; } function assetTypeLabel(value: AssetType | string | null | undefined) { return assetTypeOptions().find((item) => item.value === value)?.label ?? "未知类型"; } function assetStatusLabel(value: AssetStatus | string | null | undefined) { return assetStatusOptions().find((item) => item.value === value)?.label ?? "未知状态"; } function assetErpChangeTypeLabel(value: string | null | undefined) { const labels: Record = { created: "新建资产卡", updated: "资产信息更新", retired: "资产退役", archived: "资产归档", }; return value ? labels[value] ?? "资产变更" : "-"; } function sensitivityOptions(): Array<{ value: AssetSensitivityLevel | ""; label: string }> { return [ { value: "", label: "全部敏感级别" }, { value: "public_candidate", label: "可公开候选" }, { value: "internal", label: "内部资料" }, { value: "confidential", label: "商业秘密/机密" }, { value: "strictly_confidential", label: "严格保密" }, ]; } function sensitivityLabel(value: AssetSensitivityLevel | string | null | undefined) { return sensitivityOptions().find((item) => item.value === value)?.label ?? "未分级"; } function typeHint(assetType: AssetType) { switch (assetType) { case "electronic_document": return "电子资料只登记摘要、hash、敏感级别和归档位置;S3/S4 原文和完整路径不上传公网 OA。"; case "electronic_system": return "电子系统登记负责人、入口、到期和备份策略;账号密码、token、Secret 不进 OA。"; case "code_repository": return "代码仓库继续由 Git 管理,OA 只登记仓库元数据、负责人和备份策略。"; case "backup_media": return "备份介质记录保管人、加密状态、备份范围和最近恢复验证时间。"; default: return "固定资产支持领用、归还、调拨、盘点和移动扫码。"; } } function unsafeAssetTextReason(value: string) { if (!value.trim()) { return ""; } if (/(?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|private[_-]?key|encodingaeskey)\s*[:=]/i.test(value)) { return "不要填写 token、Secret、密码或密钥"; } if (/\b[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PRIVATE_KEY|ENCODINGAESKEY)[A-Z0-9_]*\b/.test(value)) { return "不要填写密钥类环境变量名或配置片段"; } if (/(?:密码|密钥|私钥|令牌)\s*[::=]/.test(value) || /\bBearer\s+[A-Za-z0-9._-]{10,}/i.test(value)) { return "不要填写密码、密钥、令牌或授权头"; } if (/-----BEGIN [A-Z ]*PRIVATE KEY-----/i.test(value)) { return "不要填写证书私钥或密钥正文"; } if (/(^|[\s"'((,,;;])(?:\/(?:home|root|Users|var|etc|mnt|Volumes|app)\/|[A-Za-z]:[\\/]|\\\\)/i.test(value)) { return "不要填写本机或服务器完整路径"; } if (/(原始\s*(?:payload|xml|报文|正文|表单)|raw\s*(?:payload|xml|body|form))/i.test(value)) { return "不要填写原始 payload、XML、表单或正文"; } return ""; } function formatAssetErpReason(reason: string | null) { switch (reason) { case "erp_sync_disabled": case "env_hard_off": return "ERP 总闸当前关闭"; case "flag_force_off": return "ERP 功能开关当前强制关闭"; case "missing_flag_record": return "ERP 功能开关记录缺失"; default: return reason ?? null; } } function resolveAssetErpTimestamp(stage: AssetErpStage) { return stage.outbox?.updatedAt ?? stage.outbox?.processedAt ?? stage.binding?.lastSyncedAt ?? null; } function resolveAssetErpActionLabel(actionHint: AssetErpTrack["stage"]["actionHint"]) { switch (actionHint) { case "backfill": return "补发"; case "retry": return "重试同步"; case "resync": return "重新同步摘要"; default: return null; } } export function AssetRegistryPage() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const queryClient = useQueryClient(); const { permissions } = useAuth(); const canManage = hasPermission(permissions, "asset.manage"); const canTransfer = hasPermission(permissions, "asset.transfer"); const canIntegrationManage = hasPermission(permissions, "integration.manage"); const canViewAll = hasAnyPermission(permissions, ["asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"]); const canReadProcurementSources = canManage && hasAnyPermission(permissions, ["procurement.view_all", "procurement.manage"]); 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 [selectedAssetId, setSelectedAssetId] = useState(searchParams.get("id") ?? ""); const [isCreatingAsset, setIsCreatingAsset] = useState(false); const [assetForm, setAssetForm] = useState(initialAssetForm); const [assetFormError, setAssetFormError] = useState(""); const [movementEmployeeId, setMovementEmployeeId] = useState(""); const [movementLocation, setMovementLocation] = useState(""); const [movementNote, setMovementNote] = useState(""); const [movementError, setMovementError] = useState(""); const categoriesQuery = useQuery({ queryKey: ["assets", "categories", canManage], queryFn: () => fetchAssetCategories({ includeArchived: canManage, enabled: canManage ? undefined : true }), }); const assetsQuery = useQuery({ queryKey: ["assets", "list", statusFilter, assetTypeFilter, sensitivityFilter, categoryFilter, legalEntityFilter, costCenterFilter, assigneeFilter, assetSearch, mine, canViewAll], queryFn: () => fetchAssets({ status: statusFilter || undefined, assetType: assetTypeFilter || undefined, sensitivityLevel: sensitivityFilter || undefined, categoryId: categoryFilter || undefined, legalEntityId: legalEntityFilter || undefined, costCenterId: costCenterFilter || undefined, assigneeId: assigneeFilter || undefined, q: assetSearch.trim() || undefined, mine: canViewAll ? mine : true, }), }); const assetDetailQuery = useQuery({ enabled: Boolean(selectedAssetId) && !isCreatingAsset, queryKey: ["assets", "detail", selectedAssetId], queryFn: () => fetchAssetDetail(selectedAssetId), }); const assetErpTrackQuery = useQuery({ enabled: canIntegrationManage && Boolean(selectedAssetId) && !isCreatingAsset, queryKey: ["assets", "erp-track", selectedAssetId], queryFn: () => fetchAssetErpTrack(selectedAssetId), }); const legalEntitiesQuery = useQuery({ queryKey: ["assets", "legal-entities"], queryFn: () => fetchLegalEntities({ enabled: true }), }); const costCentersQuery = useQuery({ queryKey: ["assets", "cost-centers"], queryFn: () => fetchCostCenters({ enabled: true }), }); const documentCategoriesQuery = useQuery({ queryKey: ["assets", "document-categories"], queryFn: () => fetchDocumentCategories({ entityType: "asset_card", enabled: true }), }); const assigneesQuery = useQuery({ enabled: canManage, queryKey: ["assets", "assignees"], queryFn: () => fetchAssetAssignees(), }); const approvedPurchasesQuery = useQuery({ enabled: 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(() => { if (isCreatingAsset) { return; } if (selectedAssetId) { if (assetDetailQuery.isLoading) { return; } if (!assetDetailQuery.isError) { return; } } const nextId = assetsQuery.data?.items?.[0]?.id ?? ""; if (nextId !== selectedAssetId) { setSelectedAssetId(nextId); } }, [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 (isCreatingAsset || !assetDetailQuery.data?.asset) { return; } const asset = assetDetailQuery.data.asset; setAssetForm({ name: asset.name, assetType: asset.assetType, sensitivityLevel: asset.sensitivityLevel, categoryId: asset.categoryId, legalEntityId: asset.legalEntityId, costCenterId: asset.costCenterId, purchaseRequestId: asset.purchaseRequestId ?? "", serialNo: asset.serialNo ?? "", location: asset.location ?? "", status: asset.status, assigneeEmployeeId: asset.assigneeEmployeeId ?? "", ownerEmployeeId: asset.ownerEmployeeId ?? "", keeperEmployeeId: asset.keeperEmployeeId ?? "", archiveLocation: asset.archiveLocation ?? "", externalRef: asset.externalRef ?? "", sourceHash: asset.sourceHash ?? "", verificationNo: asset.verificationNo ?? "", confidentialityNote: asset.confidentialityNote ?? "", metadataText: asset.metadata ? JSON.stringify(asset.metadata, null, 2) : "", lastVerifiedAt: asset.lastVerifiedAt ?? "", expiresAt: asset.expiresAt ?? "", activatedAt: asset.activatedAt ?? "", retiredAt: asset.retiredAt ?? "", }); setMovementEmployeeId(""); setMovementLocation(asset.location ?? ""); setMovementNote(""); setMovementError(""); setAssetFormError(""); }, [assetDetailQuery.data, isCreatingAsset]); const filteredAssetCostCenters = useMemo(() => { if (!assetForm.legalEntityId) { return []; } return (costCentersQuery.data?.items ?? []).filter((item) => item.legalEntityId === assetForm.legalEntityId); }, [assetForm.legalEntityId, costCentersQuery.data?.items]); const filteredFilterCostCenters = useMemo(() => { if (!legalEntityFilter) { return costCentersQuery.data?.items ?? []; } return (costCentersQuery.data?.items ?? []).filter((item) => item.legalEntityId === legalEntityFilter); }, [costCentersQuery.data?.items, legalEntityFilter]); const currentAsset = assetDetailQuery.data?.asset ?? null; const assetErpTrack = assetErpTrackQuery.data ?? null; const assetErpStage = assetErpTrack?.stage ?? null; const assetErpActionLabel = assetErpStage ? resolveAssetErpActionLabel(assetErpStage.actionHint) : null; const canEditCurrent = canManage && (isCreatingAsset || currentAsset?.permissions.canEdit); function resetCategoryEditor(category?: AssetCategoryItem) { if (!category) { setEditingCategoryId(""); setCategoryForm(initialCategoryForm()); return; } setEditingCategoryId(category.id); setCategoryForm({ code: category.code, name: category.name, documentCategoryId: category.documentCategoryId ?? "", enabled: category.enabled, }); } function resetAssetEditor() { setIsCreatingAsset(true); setSelectedAssetId(""); setAssetForm(initialAssetForm()); setAssetFormError(""); } function parseAssetMetadata() { const text = assetForm.metadataText.trim(); if (!text) { return null; } const parsed = JSON.parse(text) as unknown; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("扩展元数据必须是 JSON 对象"); } return parsed as Record; } function buildCreateAssetPayload() { return { name: assetForm.name.trim(), assetType: assetForm.assetType, sensitivityLevel: assetForm.sensitivityLevel, categoryId: assetForm.categoryId, legalEntityId: assetForm.legalEntityId, costCenterId: assetForm.costCenterId, purchaseRequestId: assetForm.purchaseRequestId || null, serialNo: assetForm.serialNo.trim() || null, location: assetForm.location.trim() || null, status: assetForm.status, assigneeEmployeeId: assetForm.assigneeEmployeeId || null, ownerEmployeeId: assetForm.ownerEmployeeId || null, keeperEmployeeId: assetForm.keeperEmployeeId || null, archiveLocation: assetForm.archiveLocation.trim() || null, externalRef: assetForm.externalRef.trim() || null, sourceHash: assetForm.sourceHash.trim() || null, verificationNo: assetForm.verificationNo.trim() || null, confidentialityNote: assetForm.confidentialityNote.trim() || null, metadata: parseAssetMetadata(), lastVerifiedAt: assetForm.lastVerifiedAt || null, expiresAt: assetForm.expiresAt || null, activatedAt: assetForm.activatedAt || null, retiredAt: assetForm.retiredAt || null, }; } function buildPatchAssetPayload() { return { name: assetForm.name.trim(), sensitivityLevel: assetForm.sensitivityLevel, categoryId: assetForm.categoryId, legalEntityId: assetForm.legalEntityId, costCenterId: assetForm.costCenterId, serialNo: assetForm.serialNo.trim() || null, location: assetForm.location.trim() || null, status: assetForm.status, ownerEmployeeId: assetForm.ownerEmployeeId || null, keeperEmployeeId: assetForm.keeperEmployeeId || null, archiveLocation: assetForm.archiveLocation.trim() || null, externalRef: assetForm.externalRef.trim() || null, sourceHash: assetForm.sourceHash.trim() || null, verificationNo: assetForm.verificationNo.trim() || null, confidentialityNote: assetForm.confidentialityNote.trim() || null, metadata: parseAssetMetadata(), lastVerifiedAt: assetForm.lastVerifiedAt || null, expiresAt: assetForm.expiresAt || null, activatedAt: assetForm.activatedAt || null, retiredAt: assetForm.retiredAt || null, }; } function validateAssetForm() { if (!assetForm.name.trim()) { throw new Error("请填写资产名称"); } if (!assetForm.categoryId) { throw new Error("请选择资产分类"); } if (!assetForm.legalEntityId) { throw new Error("请选择法体"); } if (!assetForm.costCenterId) { throw new Error("请选择成本中心"); } if (assetForm.verificationNo.trim() && !/^VER-\d{5,}$/.test(assetForm.verificationNo.trim())) { throw new Error("历史验证编号格式应为 VER-NNNNN"); } if (assetForm.sourceHash.trim() && !/^[A-Za-z0-9:_-]+$/.test(assetForm.sourceHash.trim())) { throw new Error("文件 hash / 校验摘要只能包含字母、数字、冒号、下划线和短横线"); } if (["electronic_document", "electronic_system", "code_repository", "backup_media"].includes(assetForm.assetType) && !assetForm.ownerEmployeeId) { throw new Error("电子资产请指定负责人"); } const textFields = [ ["资产名称", assetForm.name], ["归档位置/备份位置", assetForm.archiveLocation], ["外部引用/仓库地址", assetForm.externalRef], ["保密/回收要求", assetForm.confidentialityNote], ["扩展元数据 JSON", assetForm.metadataText], ] as const; for (const [label, value] of textFields) { const reason = unsafeAssetTextReason(value); if (reason) { throw new Error(`${label}:${reason}`); } } } const saveCategoryMutation = useMutation({ mutationFn: async () => { if (!categoryForm.code.trim()) { throw new Error("请填写分类编码"); } if (!categoryForm.name.trim()) { throw new Error("请填写分类名称"); } const payload = { code: categoryForm.code.trim(), name: categoryForm.name.trim(), documentCategoryId: categoryForm.documentCategoryId || null, enabled: categoryForm.enabled, }; return editingCategoryId ? patchAssetCategory(editingCategoryId, payload) : createAssetCategory(payload); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["assets", "categories"] }); toast.success(editingCategoryId ? "资产分类已更新" : "资产分类已创建"); resetCategoryEditor(); }, onError: (error) => { toast.error(resolveError(error)); }, }); const saveAssetMutation = useMutation({ mutationFn: async () => { validateAssetForm(); if (isCreatingAsset) { return createAsset(buildCreateAssetPayload()); } if (!selectedAssetId) { throw new Error("请先选择资产卡片"); } return patchAsset(selectedAssetId, buildPatchAssetPayload()); }, onSuccess: async (data) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["assets", "list"] }), queryClient.invalidateQueries({ queryKey: ["assets", "detail"] }), queryClient.invalidateQueries({ queryKey: ["assets", "erp-track"] }), ]); setAssetFormError(""); setSelectedAssetId(data.asset.id); setIsCreatingAsset(false); toast.success(isCreatingAsset ? "资产卡片已创建" : "资产卡片已更新"); }, onError: (error) => { const message = resolveError(error); setAssetFormError(message); toast.error(message); }, }); const requeueAssetErpMutation = useMutation({ mutationFn: async () => { if (!currentAsset) { throw new Error("请先选择资产卡片"); } return requeueAssetErpSync(currentAsset.id, true); }, onSuccess: async (data) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["assets", "erp-track"] }), queryClient.invalidateQueries({ queryKey: ["assets", "detail"] }), ]); const message = data.duplicated ? "ERP 摘要已是最新状态" : data.requeued ? "资产 ERP 同步已重新入队" : "资产 ERP 同步已入队"; toast.success(message); }, onError: (error) => { toast.error(resolveError(error)); }, }); const assignAssetMutation = useMutation({ mutationFn: async () => { if (!currentAsset) { throw new Error("请先选择资产卡片"); } if (!movementEmployeeId) { throw new Error("请选择领用人"); } return assignAsset(currentAsset.id, { employeeId: movementEmployeeId, note: movementNote.trim() || null, }); }, onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["assets", "list"] }), queryClient.invalidateQueries({ queryKey: ["assets", "detail"] }), ]); setMovementEmployeeId(""); setMovementNote(""); setMovementError(""); toast.success("资产已领用"); }, onError: (error) => { const message = resolveError(error); setMovementError(message); toast.error(message); }, }); const returnAssetMutation = useMutation({ mutationFn: async () => { if (!currentAsset) { throw new Error("请先选择资产卡片"); } return returnAsset(currentAsset.id, { note: movementNote.trim() || null }); }, onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["assets", "list"] }), queryClient.invalidateQueries({ queryKey: ["assets", "detail"] }), ]); setMovementEmployeeId(""); setMovementNote(""); setMovementError(""); toast.success("资产已归还"); }, onError: (error) => { const message = resolveError(error); setMovementError(message); toast.error(message); }, }); const transferAssetMutation = useMutation({ mutationFn: async () => { if (!currentAsset) { throw new Error("请先选择资产卡片"); } const nextLocation = movementLocation.trim() || null; const nextEmployeeId = movementEmployeeId || undefined; if (!nextEmployeeId && nextLocation === (currentAsset.location ?? null)) { throw new Error("请至少调整归属人或位置"); } return transferAsset(currentAsset.id, { toEmployeeId: nextEmployeeId, toLocation: nextLocation, note: movementNote.trim() || null, }); }, onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["assets", "list"] }), queryClient.invalidateQueries({ queryKey: ["assets", "detail"] }), ]); setMovementEmployeeId(""); setMovementNote(""); setMovementError(""); toast.success("资产调拨已完成"); }, onError: (error) => { const message = resolveError(error); setMovementError(message); toast.error(message); }, }); const purchaseOptions = useMemo(() => { const items = approvedPurchasesQuery.data?.items ?? []; const currentPurchase = currentAsset?.purchaseRequest; if (currentPurchase && !items.some((item) => item.id === currentPurchase.id)) { return [ ...items, { id: currentPurchase.id, requestNo: currentPurchase.requestNo, title: currentPurchase.title, status: currentPurchase.status, applicantId: "", submitterId: null, departmentId: "", legalEntityId: null, costCenterId: null, supplierId: null, documentCategoryId: null, budgetPoolId: null, reason: null, currencyCode: "CNY", totalAmount: "0.00", expectedDeliveryDate: null, createdAt: null, updatedAt: null, applicant: null, submitter: null, department: null, legalEntity: null, costCenter: null, supplier: null, documentCategory: null, budgetPool: null, }, ]; } return items; }, [approvedPurchasesQuery.data?.items, currentAsset?.purchaseRequest]); const categoryItems = categoriesQuery.data?.items ?? []; const assetItems = assetsQuery.data?.items ?? []; const selectAsset = (assetId: string) => { setIsCreatingAsset(false); setSelectedAssetId(assetId); }; return ( {canManage ? ( ) : null} } />

资产分类

{canManage ? ( ) : null}
setCategoryForm((current) => ({ ...current, code: event.target.value }))} />
setCategoryForm((current) => ({ ...current, name: event.target.value }))} />
{canManage ? ( ) : null}
{categoriesQuery.isLoading ? : null} {categoriesQuery.isError ? (

{resolveError(categoriesQuery.error)}

) : null} {!categoriesQuery.isLoading && !categoriesQuery.isError ? ( <>
{categoryItems.length ? ( categoryItems.map((item) => ( )) ) : (
暂无资产分类
)}
编码名称状态 {categoryItems.map((item) => ( resetCategoryEditor(item)} className="cursor-pointer"> {item.code}
{item.name}
{item.documentCategory?.name ?? "系统默认"}
{item.enabled ? "启用" : "停用"}
))}
) : null}
setAssetSearch(event.target.value)} /> {canViewAll ? (
) : null}
{assetsQuery.isLoading ? : null} {assetsQuery.isError ? (

{resolveError(assetsQuery.error)}

) : null} {!assetsQuery.isLoading && !assetsQuery.isError ? ( <>
{assetItems.length ? ( assetItems.map((item) => ( )) ) : (
暂无资产卡片
)}
资产类型分类敏感级别状态负责人 {assetItems.length ? ( assetItems.map((item) => ( { selectAsset(item.id); }} className={item.id === selectedAssetId && !isCreatingAsset ? "bg-zinc-50" : "cursor-pointer"}>
{item.assetNo}
{item.name}
{assetTypeLabel(item.assetType)} {item.category?.name ?? "-"} {sensitivityLabel(item.sensitivityLevel)} {item.owner?.name ?? item.assignee?.name ?? "-"}
)) ) : ( 暂无资产卡片 )}
) : null}
{!selectedAssetId && !isCreatingAsset ? (
请先从列表选择资产卡片,或点击“新建资产卡片”。
) : null} {selectedAssetId && !isCreatingAsset && assetDetailQuery.isLoading ? : null} {selectedAssetId && !isCreatingAsset && assetDetailQuery.isError ? (

{resolveError(assetDetailQuery.error)}

) : null} {(isCreatingAsset || currentAsset) && !assetDetailQuery.isLoading ? ( <>

{isCreatingAsset ? "新建资产卡片" : currentAsset?.assetNo ?? "资产卡片"}

{isCreatingAsset ? "创建新的资产台账卡片。" : currentAsset?.name}

{!isCreatingAsset && currentAsset ? : null}
{!isCreatingAsset && currentAsset?.sensitiveFieldsRedacted ? (

当前资产包含敏感字段,你可查看摘要;归档位置、外部引用、hash 和扩展元数据仅管理员或相关责任人可见。

) : null} {assetFormError ? (

{assetFormError}

) : null}

基础信息

维护资产主标识、类型、分类和组织归属。

setAssetForm((current) => ({ ...current, name: event.target.value }))} />

{typeHint(assetForm.assetType)}

责任与归档

明确领用、负责、保管、位置和生命周期日期。

setAssetForm((current) => ({ ...current, serialNo: event.target.value }))} />
setAssetForm((current) => ({ ...current, location: event.target.value }))} />
setAssetForm((current) => ({ ...current, archiveLocation: event.target.value }))} />
setAssetForm((current) => ({ ...current, lastVerifiedAt: event.target.value }))} />
setAssetForm((current) => ({ ...current, expiresAt: event.target.value }))} />
setAssetForm((current) => ({ ...current, activatedAt: event.target.value }))} />
setAssetForm((current) => ({ ...current, retiredAt: event.target.value }))} />

电子资产安全字段

只登记必要摘要,密码、密钥、源码包、原始正文和完整路径不得进入 OA。

setAssetForm((current) => ({ ...current, externalRef: event.target.value }))} />
setAssetForm((current) => ({ ...current, sourceHash: event.target.value }))} />
setAssetForm((current) => ({ ...current, verificationNo: event.target.value }))} />