// artboard-overview.jsx — Asset-management overview // Filter by: 资产状态 / 部门 / 归属 // Card shows: 车牌 + VIN + 城市 + 部门 + 客户 + 资产状态 // Detail shows: 资产档案 + 业务关系 + 实时车况 + 保养预警 (no driver) const AssetStatusChip = ({ status }) => { const map = { in_stock: { label: "在库", bg: "var(--accent-soft)", fg: "var(--accent)", dot: "ok" }, leasing: { label: "租赁" , bg: "rgba(46,140,140,0.15)",fg: "var(--info)", dot: "info" }, abnormal: { label: "异常", bg: "var(--danger-soft)", fg: "var(--danger)", dot: "danger" }, }; const m = map[status] || map.in_stock; return ( {m.label} ); }; const OwnChip = ({ own }) => ( {own === "self" ? "自有" : "外租"} ); const DeptDot = ({ dept }) => { const d = (window.DEPARTMENTS || []).find(x => x.id === dept); if (!d) return null; return ( {d.name} ); }; const ArtboardOverview = () => { const allVehicles = (window.VEHICLES || []); const { role } = (typeof window.useCurrentRole === "function") ? window.useCurrentRole() : { role: null }; // Apply role-based scope before user-facing filters const vehicles = React.useMemo(() => { if (!role || role.scope === "all" || role.scope === "ops" || role.scope === "finance") return allVehicles; if (role.scope === "dept") return allVehicles.filter(v => v.dept === role.deptId); return allVehicles; }, [role, allVehicles]); const counts = (window.COUNTS || {}); const deps = (window.DEPARTMENTS || []); const isDeptScoped = role && role.scope === "dept"; // Scoped counts so KPIs match what the role can actually see const scopedCounts = React.useMemo(() => { const c = { all: vehicles.length, inStock:0, leasing:0, abnormal:0, self:0, lease:0, online:0 }; vehicles.forEach(v => { if (v.asset === "in_stock") c.inStock++; else if (v.asset === "leasing") c.leasing++; else if (v.asset === "abnormal") c.abnormal++; if (v.own === "self") c.self++; else c.lease++; if (v.gps === "online") c.online++; }); return c; }, [vehicles]); // Display-scale: 管理员视角下,KPI 按实际车队规模放大到 1006 辆 / 892 在线 // (部门视角保持真实数字) const FLEET_SIZE = 1006; const FLEET_ONLINE = 892; const scale = !isDeptScoped && scopedCounts.all > 0 ? FLEET_SIZE / scopedCounts.all : 1; const sc = (n) => Math.round(n * scale); const dispTotal = isDeptScoped ? scopedCounts.all : FLEET_SIZE; const dispOnline = isDeptScoped ? scopedCounts.online : FLEET_ONLINE; const dispInStock = isDeptScoped ? scopedCounts.inStock : sc(scopedCounts.inStock); const dispLeasing = isDeptScoped ? scopedCounts.leasing : sc(scopedCounts.leasing); const dispAbnormal = isDeptScoped ? scopedCounts.abnormal : sc(scopedCounts.abnormal); const [selected, setSelected] = React.useState(vehicles[8]?.id || vehicles[0]?.id); React.useEffect(() => { if (vehicles.length && !vehicles.find(x => x.id === selected)) { setSelected(vehicles[0].id); } }, [vehicles, selected]); const [filterAsset, setFilterAsset] = React.useState("all"); // all | in_stock | leasing | abnormal const [filterDept, setFilterDept] = React.useState("all"); const [filterOwn, setFilterOwn] = React.useState("all"); const [search, setSearch] = React.useState(""); const filtered = vehicles.filter(v => { if (filterAsset !== "all" && v.asset !== filterAsset) return false; if (filterDept !== "all" && v.dept !== filterDept) return false; if (filterOwn !== "all" && v.own !== filterOwn) return false; if (search && !v.plate.includes(search) && !v.vin.includes(search)) return false; return true; }); const v = vehicles.find(x => x.id === selected) || vehicles[0]; if (!v) return null; return (
0 ? "+" + dispAbnormal : "0", deltaUp:false }, ]} /> {isDeptScoped && (
数据权限:当前以 {role.name} 身份登录,仅可见本部门 {scopedCounts.all} 辆车 · 全公司共 {FLEET_SIZE} 辆 切换身份请使用右下角 Tweaks · 登录身份
)}
{/* Left: fleet list with asset filters */}
车辆 · {filtered.length}/{scopedCounts.all} 高级
setSearch(e.target.value)}/>
{/* Asset status filter */}
资产状态
{[ {k:"all", l:"全部", c: scopedCounts.all}, {k:"in_stock", l:"在库", c: scopedCounts.inStock}, {k:"leasing", l:"租赁" , c: scopedCounts.leasing}, {k:"abnormal", l:"异常", c: scopedCounts.abnormal}, ].map(o => ( setFilterAsset(o.k)}> {o.l} {o.c} ))}
{/* Ownership */}
归属
{[ {k:"all", l:"全部"}, {k:"self", l:"自有", c: scopedCounts.self}, {k:"lease", l:"外租", c: scopedCounts.lease}, ].map(o => ( setFilterOwn(o.k)}> {o.l}{o.c != null && {o.c}} ))}
{/* Department — hidden when role is dept-scoped (only one dept visible) */} {!isDeptScoped && (
业务部门
setFilterDept("all")}>全部 {deps.map(d => ( setFilterDept(d.id)}> {d.name} {counts.byDept?.[d.id] || 0} ))}
)}
{filtered.map(x => (
setSelected(x.id)} style={{ padding:"10px 14px", borderLeft: "2px solid " + (x.id === selected ? "var(--accent)" : "transparent"), background: x.id === selected ? "var(--accent-soft)" : "transparent", cursor:"pointer", borderBottom:"1px solid var(--border-1)" }}>
{x.plate}
{x.vin}
{x.gps === "offline" && ● GPS离线}
{x.city} {x.customer !== "—" && (<>·{x.customer})}
))} {filtered.length === 0 && (
没有匹配的车辆
)}
{/* Map center */}
setSelected(x.id)} />
{["layers","plus","close","sat","pin"].map((n,i)=>(
))}
{/* Legend by asset */}
在库/正常 租赁 待整备 异常 GPS离线
LIVE | 嘉兴市·平湖 | 14:32:08
{/* Right: vehicle asset detail panel */}
{v.deptName} · {v.deptLead}
{v.plate}
{v.vin}
车辆等级 {v.grade}级 · 状态时长 {v.statusDays}天 {v.fleetCode && <> · 编号 {v.fleetCode} }
{/* 业务关系 */}
业务关系
业务部门 {v.deptName}
业务负责人{v.deptLead}
客户{v.customer}
所属公司{v.ownCompany}
{v.own === "lease" &&
租赁公司{v.company}
} {v.contractNo &&
合同编号{v.contractNo}
}
{/* 实时车况 — 财务身份不可见 */} {role && role.scope === "finance" ? (
实时车况
🔒
实时车况数据已隐藏
财务身份仅可见资产 · 业务关系 · 合同
) : (
实时车况 GPS{v.gps === "online" ? "在线" : "离线"}
氢气压力 {v.h2} MPa
续航 {v.range} km
电机温度 90 ? "var(--danger)" : "var(--fg-0)"}}>{v.motorTemp}°C
停车场 {v.parking}
)} {/* 里程 & 保养 */}
里程与保养
累计里程{v.totalKm.toLocaleString()} km
上次保养{v.lastMaintDays}天前 · {v.lastMaintKm.toLocaleString()}km
下次保养 剩余 {v.kmToMaint.toLocaleString()} km
{v.handoverKm != null && (
交车里程{v.handoverKm.toLocaleString()} km
)} {v.returnKm != null && (
还车里程{v.returnKm.toLocaleString()} km
)}
{v.asset === "abnormal" && (
异常处理
资产状态异常 {v.statusDays}天
停车场标记为异常 · 待业务部门核查
)}
); }; window.ArtboardOverview = ArtboardOverview;