重构资产盘点与移动扫码页面(05-30-asset-inventory-mobile-redesign)
这个提交包含在:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户