重构资产盘点与移动扫码页面(05-30-asset-inventory-mobile-redesign)

这个提交包含在:
2026-05-30 13:23:47 +08:00
父节点 aa8f65ce2a
当前提交 ab0d57bd25
共修改 7 个文件,包含 321 行新增和 84 行删除
+58 -5
查看文件
@@ -14,6 +14,7 @@ import {
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { PageHeader } from "../components/common/PageHeader";
import { BarcodeScannerPanel } from "../components/mobile/BarcodeScannerPanel";
import { PageScaffold } from "../components/patterns/PageScaffold";
@@ -82,6 +83,30 @@ function inventoryResultLabel(result: InventoryCheckResult) {
}
}
function assetTypeLabel(assetType: AssetItem["assetType"]) {
switch (assetType) {
case "fixed_asset":
return "固定资产 FA";
case "electronic_document":
return "电子资料 EA-DOC";
case "electronic_system":
return "电子系统 EA-SYS";
case "code_repository":
return "代码仓库 EA-REPO";
case "backup_media":
return "备份介质 EA-BKP";
default:
return "资产";
}
}
function inventoryScopeLabel(inventoryCheck: InventoryCheck) {
if (inventoryCheck.scopeType === "all") return "全量盘点";
if (inventoryCheck.scopeType === "legal_entity") return "法体盘点";
if (inventoryCheck.scopeType === "cost_center") return "成本中心盘点";
return "位置盘点";
}
function formatDateTime(value: string) {
try {
return new Date(value).toLocaleString("zh-CN");
@@ -123,6 +148,7 @@ export function MobileAssetScanPage() {
const [error, setError] = useState("");
const [recordResult, setRecordResult] = useState<InventoryCheckResult>("matched");
const [recordNote, setRecordNote] = useState("");
const [confirmRecordScan, setConfirmRecordScan] = useState(false);
const inventoryChecksQuery = useQuery({
enabled: isInventoryMode && canRecordInventory,
@@ -388,20 +414,35 @@ export function MobileAssetScanPage() {
</Button>
) : (
<Button type="button" variant="secondary" onClick={() => navigate("/mobile")}>
返回资产台账
返回移动工作台
</Button>
)}
</div>
);
return (
<PageScaffold className="space-y-4">
<PageScaffold className="mobile-page-stack">
<PageHeader
title={isInventoryMode ? "移动扫码盘点" : "移动资产扫码"}
subtitle={isInventoryMode ? "在盘点任务上下文中扫码定位资产并快捷录入结果。" : "扫码或手工输入资产编号,快速查看资产卡片。"}
actions={actions}
/>
<section className="mobile-card mobile-focus-card mobile-page-stack" data-testid="mobile-asset-scan-focus">
<div className="mobile-section-header mobile-section-header--top">
<div>
<h3 className="mobile-section-title">{isInventoryMode ? "先扫码,再确认盘点" : "扫码查资产"}</h3>
<p className="mt-1 text-sm text-zinc-600">二维码不可用时,可输入资产编号、序列号或名称兜底。</p>
</div>
<span className={`mobile-section-meta ${isOnline ? "text-emerald-600" : "text-orange-600"}`}>{isOnline ? "在线" : "离线"}</span>
</div>
<div className="mobile-info-grid text-sm">
<div className="mobile-subcard">模式:{isInventoryMode ? "盘点录入" : "资产查看"}</div>
<div className="mobile-subcard">确认:先看摘要,再提交</div>
<div className="mobile-subcard">范围:{selectedInventoryCheck ? inventoryScopeLabel(selectedInventoryCheck) : "按当前权限"}</div>
</div>
</section>
{error ? (
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>
) : null}
@@ -440,7 +481,7 @@ export function MobileAssetScanPage() {
<div className="mobile-list-card flex flex-wrap items-center justify-between gap-3 text-sm">
<div>
<div className="font-medium text-zinc-900">{selectedInventoryCheck.checkNo}</div>
<div className="text-zinc-500">状态:{inventoryStatusLabel(selectedInventoryCheck.status)} · 范围:{selectedInventoryCheck.scopeType} / {selectedInventoryCheck.scopeRefId ?? "全量"}</div>
<div className="text-zinc-500">状态:{inventoryStatusLabel(selectedInventoryCheck.status)} · 范围:{inventoryScopeLabel(selectedInventoryCheck)} / {selectedInventoryCheck.scopeRefId ?? "全量"}</div>
</div>
{canRecordInventory && selectedInventoryCheck.status === "draft" ? (
<Button type="button" onClick={() => startCheckMutation.mutate()} disabled={startCheckMutation.isPending}>
@@ -504,7 +545,7 @@ export function MobileAssetScanPage() {
) : null}
{selectedAsset ? (
<div className="mobile-card space-y-4" data-testid="mobile-asset-scan-detail">
<div className="mobile-card mobile-focus-card space-y-4" data-testid="mobile-asset-scan-detail">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 className="text-lg font-semibold text-zinc-900">{selectedAsset.assetNo}</h3>
@@ -516,12 +557,14 @@ export function MobileAssetScanPage() {
</div>
<div className="mobile-info-grid">
<div className="mobile-subcard text-sm">类型:{assetTypeLabel(selectedAsset.assetType)}</div>
<div className="mobile-subcard text-sm">状态:{statusLabel(selectedAsset.status)}</div>
<div className="mobile-subcard text-sm">法体:{selectedAsset.legalEntity?.name ?? "-"}</div>
<div className="mobile-subcard text-sm">成本中心:{selectedAsset.costCenter?.name ?? "-"}</div>
<div className="mobile-subcard text-sm">位置:{selectedAsset.location ?? "-"}</div>
<div className="mobile-subcard text-sm">归属人:{selectedAsset.assignee?.name ?? "未分配"}</div>
<div className="mobile-subcard text-sm">序列号:{selectedAsset.serialNo ?? "-"}</div>
<div className="mobile-subcard text-sm">责任人:{selectedAsset.owner?.name ?? "-"}</div>
</div>
{isInventoryMode && canRecordInventory && selectedInventoryCheck ? (
@@ -548,7 +591,7 @@ export function MobileAssetScanPage() {
<div className="flex flex-wrap gap-2">
<Button
type="button"
onClick={() => void handleRecord()}
onClick={() => setConfirmRecordScan(true)}
disabled={recordMutation.isPending || !selectedInventoryCheck || selectedInventoryCheck.status === "completed"}
>
{isOnline ? (recordMutation.isPending ? "提交中..." : "提交盘点结果") : "离线加入队列"}
@@ -598,6 +641,16 @@ export function MobileAssetScanPage() {
))}
</div>
) : null}
<ActionConfirmDialog
open={confirmRecordScan}
onOpenChange={setConfirmRecordScan}
kind="submit"
targetName={selectedAsset?.assetNo}
description={`将按“${inventoryResultLabel(recordResult)}”记录当前资产盘点结果${isOnline ? "" : ",当前离线时会先进入待同步队列"}。请确认资产名称、位置和责任人摘要无误。`}
confirmLabel={isOnline ? "确认提交结果" : "确认加入队列"}
onConfirm={() => handleRecord()}
/>
</PageScaffold>
);
}