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, fetchAssets, patchAsset, patchAssetCategory, returnAsset, transferAsset, type AssetCategoryItem, type AssetItem, 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 { DateTextInput } from "../components/common/DateTextInput"; import { useUnsavedChangesGuard } from "../components/common/UnsavedChangesGuard"; import { StatusBadge } from "../components/common/StatusBadge"; import { TableSkeleton } from "../components/common/TableSkeleton"; import { BusinessDetailActionPanel, BusinessDetailAside, BusinessDetailFact, BusinessDetailFactGrid, BusinessDetailMain, BusinessDetailWorkspace, } from "../components/patterns/BusinessDetailTemplate"; import { HeaderActionGroup, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { BusinessListToolbar, BusinessMobileList, BusinessTable, } from "../components/patterns/BusinessListTemplate"; import { Button } from "../components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu"; 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"; 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 sensitivityOptions(): Array<{ value: AssetSensitivityLevel | ""; label: string }> { return [ { value: "", label: "全部敏感级别" }, { value: "public_candidate", label: "可公开候选" }, { value: "internal", label: "内部资料" }, { value: "confidential", label: "商业秘密/机密" }, { value: "strictly_confidential", label: "严格保密" }, ]; } function isElectronicAssetType(assetType: AssetType) { return assetType !== "fixed_asset"; } function formatEmployeeSummary(employee: { name: string; employeeNo: string } | null | undefined) { return employee ? `${employee.name} · ${employee.employeeNo}` : "-"; } function CompactAssetNo({ value, copyable = true }: { value: string; copyable?: boolean }) { const displayValue = formatCompactIdentifier(value, { head: 14, tail: 6, minLength: 24 }); const compacted = isCompactedIdentifier(value, displayValue); async function copyAssetNo() { if (!compacted) return; try { await navigator.clipboard.writeText(value); toast.success("已复制完整资产编号"); } catch { toast.error("复制失败,可在资产详情中查看完整编号"); } } return ( {displayValue} {compacted && copyable ? ( ) : null} ); } 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 ""; } 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 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 [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(""); 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 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(() => { 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; const nextAssetForm = { 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 ?? "", }; setAssetForm(nextAssetForm); setSavedAssetFormSignature(JSON.stringify(nextAssetForm)); 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 canEditCurrent = canManage && (isCreatingAsset || currentAsset?.permissions.canEdit); const runGuarded = useUnsavedChangesGuard( Boolean(isCreatingAsset || selectedAssetId) && Boolean(canEditCurrent) && JSON.stringify(assetForm) !== savedAssetFormSignature, "资产表单", ); const hasAdvancedAssetFilters = Boolean( sensitivityFilter || categoryFilter || legalEntityFilter || costCenterFilter || assigneeFilter, ); const showAssetAdvancedFilters = showAdvancedFilters || hasAdvancedAssetFilters; 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() { runGuarded(() => { const nextAssetForm = initialAssetForm(); setIsCreatingAsset(true); setSelectedAssetId(""); setAssetForm(nextAssetForm); setSavedAssetFormSignature(JSON.stringify(nextAssetForm)); 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"] }), ]); setAssetFormError(""); setSavedAssetFormSignature(JSON.stringify(assetForm)); setSelectedAssetId(data.asset.id); setIsCreatingAsset(false); toast.success(isCreatingAsset ? "资产卡片已创建" : "资产卡片已更新"); }, onError: (error) => { const message = resolveError(error); setAssetFormError(message); toast.error(message); }, }); 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 orderedAssetItems = useMemo(() => { return [...assetItems].sort((a, b) => { const score = (item: AssetItem) => { let value = 0; if (item.status === "under_repair") value += 50; if (item.assetType === "fixed_asset" && !item.assignee) value += 35; if (!item.lastVerifiedAt) value += 25; if (item.status === "in_use") value += 10; return value; }; const scoreDelta = score(b) - score(a); if (scoreDelta !== 0) return scoreDelta; return (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""); }); }, [assetItems]); const selectAsset = (assetId: string) => { runGuarded(() => { setIsCreatingAsset(false); setSelectedAssetId(assetId); }); }; const returnToAssetList = () => { runGuarded(() => { setIsCreatingAsset(false); setSelectedAssetId(""); const next = new URLSearchParams(searchParams); next.delete("id"); setSearchParams(next, { replace: true }); }); }; const isAssetDetailMode = isCreatingAsset || Boolean(selectedAssetId); return ( {isAssetDetailMode ? ( ) : ( <> {canManage ? ( ) : null} navigate("/mobile/assets/scan")}> 移动扫码查找 {canManage ? ( setShowCategoryManager((value) => !value)}> {showCategoryManager ? "收起资产分类" : "资产分类"} ) : null} )} } /> {!isAssetDetailMode ? (
{showCategoryManager && canManage ? (

资产分类

{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"> {formatTextWithCompactLongNumbers(item.code)}
{formatTextWithCompactLongNumbers(item.name)}
{item.documentCategory?.name ?? "系统默认"}
{item.enabled ? "启用" : "停用"}
))}
) : null}
) : null}
{canViewAll ? ( <> ) : null} )} advanced={showAssetAdvancedFilters ? (
) : null} >
setAssetSearch(event.target.value)} />
{assetsQuery.isLoading ? : null} {assetsQuery.isError ? (

{resolveError(assetsQuery.error)}

) : null} {!assetsQuery.isLoading && !assetsQuery.isError ? ( <> {orderedAssetItems.length ? ( orderedAssetItems.map((item) => ( )) ) : (
暂无资产卡片
)}
资产 状态 责任人 保管/位置 盘点 操作 {orderedAssetItems.length ? ( orderedAssetItems.map((item) => (
{formatTextWithCompactLongNumbers(item.name)}
{assetTypeLabel(item.assetType)}
{formatEmployeeSummary(item.owner ?? item.assignee)}
归属 {formatEmployeeSummary(item.assignee)}
{formatEmployeeSummary(item.keeper)}
{formatTextWithCompactLongNumbers(item.location ?? "-")}
{item.lastVerifiedAt ? formatDisplayDateTime(item.lastVerifiedAt) : "未登记"} )) ) : ( 暂无资产卡片 )}
) : null}
) : (
{selectedAssetId && !isCreatingAsset && assetDetailQuery.isLoading ? : null} {selectedAssetId && !isCreatingAsset && assetDetailQuery.isError ? (

{resolveError(assetDetailQuery.error)}

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

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

{!isCreatingAsset ? (

{formatTextWithCompactLongNumbers(currentAsset?.name)}

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

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

) : null} {assetFormError ? (

{assetFormError}

) : null} {!isCreatingAsset && currentAsset ? ( {formatEmployeeSummary(currentAsset.owner ?? currentAsset.assignee)} {currentAsset.keeper ? formatEmployeeSummary(currentAsset.keeper) : currentAsset.location ?? "待确认"} {currentAsset.lastVerifiedAt ? formatDisplayDateTime(currentAsset.lastVerifiedAt) : "未登记"} ) : null}

基础信息

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

责任与归档

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 }))} />
{isElectronicAssetType(assetForm.assetType) ? (

电子资产安全字段

不填写密码、密钥、源码包、原始正文或完整路径。

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