feat(assets): migrate asset views and reconcile inventory drilldowns

This commit is contained in:
shishengliang
2026-09-23 14:16:22 +08:00
parent b0cba26fbb
commit 822aabf2ca
77 changed files with 14669 additions and 3856 deletions
@@ -0,0 +1,333 @@
import { inventoryColor } from "../../../shared/assets/inventory";
import { matchesInventorySelection, inventoryModel } from "./inventory-view";
import { ArrowLeft, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useEffect, useRef, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { Skeleton } from "@/components/ui/skeleton";
import {
fetchVehicleList,
fetchWeeklyDetail,
type WeeklyDetailItem,
} from "../api";
import { buildVehicleModalRequest, type VehicleModalSelection } from "../model";
import type { VehicleListItem } from "../types";
import { DataGrid, type Column } from "./DataGrid";
import { ExportButton } from "./export";
import { Failure, FilterFields, PrivateText, unique } from "./shared";
const titles = {
Inventory: "库存车辆",
Operating: "运营车辆",
Abnormal: "异动车辆",
Pending: "待交车车辆",
Delivered: "本周交车",
Returned: "本周还车",
Replaced: "本周替换交车",
};
export function VehicleDetails({
selection,
subject,
close,
returnLabel = "返回统计",
}: {
selection: VehicleModalSelection | null;
subject: string | null;
close: () => void;
returnLabel?: string;
}) {
const returnFocus = useRef<HTMLElement | null>(null);
const [vehicles, setVehicles] = useState<VehicleListItem[]>([]);
const [weekly, setWeekly] = useState<WeeklyDetailItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [retry, setRetry] = useState(0);
const [filters, setFilters] = useState({
model: "",
brandLabel: "",
location: "",
});
useEffect(() => {
let active = true;
setFilters({ model: "", brandLabel: "", location: "" });
setVehicles([]);
setWeekly([]);
setError("");
if (!selection) return;
setLoading(true);
const request = buildVehicleModalRequest(selection, subject);
const run = async () => {
try {
if (request.kind === "weekly") {
const rows = await fetchWeeklyDetail(request.type, {
...request.filters,
subject,
});
if (active) setWeekly(rows);
} else {
const rows = await fetchVehicleList(request.params);
if (active)
setVehicles(
rows.filter((row) => matchesInventorySelection(row, selection)),
);
}
} catch (e) {
if (active) setError(e instanceof Error ? e.message : "加载失败");
} finally {
if (active) setLoading(false);
}
};
void run();
return () => {
active = false;
};
}, [selection, subject, retry]);
const rows = vehicles
.filter((r) =>
Object.entries(filters).every(
([k, v]) => !v || r[k as keyof VehicleListItem] === v,
),
)
.map((r) => ({
...r,
color: inventoryColor(r.color),
displayModel: inventoryModel(r.model, r.brandLabel || "").name,
operator: inventoryModel(r.model, r.brandLabel || "").operator || "—",
pendingContract: r.pendingDelivery?.contractNo || "—",
pendingCustomer: r.pendingDelivery?.customerName || "—",
moveType: r.abnormalMove?.type || "—",
work: r.abnormalMove?.work || "—",
target: r.abnormalMove?.targetCompletionTime || "—",
actual: r.abnormalMove?.actualCompletionTime || "—",
initiating: r.abnormalMove?.initiatingDepartment || "—",
executing: r.abnormalMove?.executingDepartment || "—",
responsible: r.abnormalMove?.responsiblePerson || "—",
driver: r.abnormalMove?.driver || "—",
}));
type Row = (typeof rows)[number];
const columns: Column<Row>[] = [
{ key: "plateNumber", label: "车牌", private: true, width: 140 },
{ key: "brandLabel", label: "品牌" },
{ key: "type", label: "车型" },
{ key: "displayModel", label: "型号", width: 160 },
{ key: "operator", label: "所属公司" },
{ key: "color", label: "颜色" },
{ key: "location", label: "所在地" },
];
if (selection?.category === "Pending")
columns.push(
{
key: "pendingContract",
primary: true,
label: "待交车合同",
width: 180,
},
{
key: "pendingCustomer",
primary: true,
label: "客户(已脱敏)",
width: 180,
private: true,
},
);
if (selection?.category === "Abnormal")
columns.push(
...([
{ key: "moveType", primary: true, label: "异动类型" },
{ key: "target", primary: true, label: "预计结束时间", width: 180 },
{ key: "work", label: "具体工作", width: 220 },
{ key: "initiating", label: "发起部门" },
{ key: "executing", label: "执行部门" },
{ key: "responsible", label: "负责人", private: true },
{ key: "driver", label: "操作司机", private: true },
{ key: "actual", label: "实际完成时间", width: 180 },
] as Column<Row>[]),
);
columns.push(
{
key: "operationStatus",
label: "运营状态",
primary: true,
format: (value) =>
({ "1": "租赁", "2": "自营", "3": "可运营", "4": "待运营" })[
String(value)
] || String(value || "未标注"),
},
{
key: "status",
label: "业务状态",
primary: true,
format: (value) =>
({
Inventory: "库存",
Operating: "运营",
Abnormal: "异动",
Pending: "待交车",
})[String(value)] || String(value),
},
{ key: "departmentName", label: "部门" },
{ key: "customerManager", label: "客户经理", private: true },
{ key: "vin", label: "车架号", private: true, width: 200 },
);
const isWeekly =
selection &&
["Delivered", "Returned", "Replaced"].includes(selection.category || "");
const weeklyColumns: Column<WeeklyDetailItem>[] = [
{ key: "plate_number", label: "车牌", private: true },
{ key: "handover_date", label: "签章日期", width: 180 },
{ key: "contract_type", label: "合同类型" },
{ key: "customer_name", label: "客户", private: true },
];
const title =
selection?.title ||
(selection?.category ? titles[selection.category] : "车辆明细");
const context = [
["城市 / 区域", selection?.location],
["品牌", selection?.brand],
["车型", selection?.type || selection?.vehicleType],
["型号", selection?.model],
["批次", selection?.batch],
["部门", selection?.department],
].filter(([, value]) => value && value !== "All");
return (
<Dialog open={!!selection} onOpenChange={(v) => !v && close()}>
<DialogContent
className="asset-ui asset-detail"
showCloseButton={false}
onOpenAutoFocus={() => {
returnFocus.current =
document.activeElement instanceof HTMLElement
? document.activeElement
: null;
}}
onCloseAutoFocus={(event) => {
if (returnFocus.current?.isConnected) {
event.preventDefault();
returnFocus.current.focus({ preventScroll: true });
}
}}
>
<DialogHeader className="asset-detail-header">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={close}
aria-label={returnLabel}
>
<ArrowLeft />
</Button>
<div className="min-w-0 flex-1">
<p className="mb-1 text-xs text-[var(--label-secondary)]">
资产管理 /{" "}
{selection?.source === "department"
? "部门运营"
: selection?.source === "region"
? "区域分布"
: selection?.source === "customer"
? "客户资产"
: "资产统计"}
</p>
<DialogTitle className="text-lg font-semibold">
{title}
</DialogTitle>
</div>
<Button
variant="ghost"
size="icon"
className="hidden md:inline-flex"
onClick={close}
aria-label="关闭车辆明细"
>
<X />
</Button>
</div>
<DialogDescription>
{subject || "全部公司"} ·{" "}
{loading
? "正在加载车辆…"
: `${isWeekly ? weekly.length : vehicles.length} 条记录`}
</DialogDescription>
{selection?.scopeLabel && (
<p className="text-sm">{selection.scopeLabel}</p>
)}
{context.length > 0 && (
<div className="asset-context" aria-label="当前下钻范围">
{context.map(([label, value]) => (
<span key={label}>
{label}:{value}
</span>
))}
</div>
)}
{selection?.customer && (
<p className="text-xs text-[var(--label-secondary)]">
客户:<PrivateText>{selection.customer}</PrivateText>
</p>
)}
{selection?.manager && (
<p className="text-xs text-[var(--label-secondary)]">
负责人:<PrivateText>{selection.manager}</PrivateText>
</p>
)}
</DialogHeader>
<div className="asset-detail-body" aria-busy={loading}>
{error ? (
<Failure message={error} retry={() => setRetry((n) => n + 1)} />
) : loading ? (
<Skeleton className="h-64" />
) : isWeekly ? (
<>
<ExportButton
rows={weekly}
columns={weeklyColumns}
filename="交还车车辆"
sheetName="交还车车辆"
/>
<DataGrid
rows={weekly}
label="交还车车辆"
columns={weeklyColumns}
/>
</>
) : (
<>
<FilterFields
fields={(["model", "brandLabel", "location"] as const).map(
(key, i) => ({
label: ["型号", "品牌", "所在地"][i],
value: filters[key],
values: unique(vehicles.map((r) => r[key])),
change: (value) =>
setFilters((f) => ({ ...f, [key]: value })),
}),
)}
onReset={() =>
setFilters({ model: "", brandLabel: "", location: "" })
}
/>
<ExportButton
rows={rows}
columns={columns}
filename="车辆明细"
sheetName="车辆"
/>
<DataGrid rows={rows} columns={columns} label="车辆" />
</>
)}
</div>
<footer className="asset-detail-footer">
<span>关闭后返回原来的筛选与浏览位置</span>
<Button variant="outline" onClick={close}>
{returnLabel}
</Button>
</footer>
</DialogContent>
</Dialog>
);
}