Files
oneos-truck-date-prototype/data/fleet.js
kkfluous 97a2f54786
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat(integration): 接入 1006 辆真实车辆数据 + 24h 异常派生 + 重点品牌锚定
数据
- data/vehicles-real.js: 从 车辆管理-1167382387645005824.xlsx 抽取 1006 辆,含车牌/VIN/品牌/型号/部门/客户/合同等真实字段
- 接入分布按业务要求: GB32960 在线 700 / JT808 在线 950 / 完全未对接 30
- 业务规则: GB 未对接 226 辆 = 苏龙 197 + 海伯特 29(从帕力安牌中重命名 29 辆)
- DEPARTMENTS 扩展 biz5 / biz6 以匹配 xlsx 部门
- fleet.js 优先使用 RAW_VEHICLES,前 12 辆叠加乍浦港地图坐标

派生状态
- integration.jsx 新增 effectiveStatus(): offline 且 lastSeen ≥ 24h → abnormal (异常)
- ConnPill 增 abnormal 红色态; 重点标记 = 双通道均 abnormal/未对接
- 筛选/KPI 全部基于派生状态实时计算(每 30s 重算)
2026-04-28 15:57:14 +08:00

321 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// data/fleet.js — 羚牛 Hydrogen Fleet · Asset-management dataset
// Switched from driver-centric to asset-centric model per business spec.
// Vehicles relate to: 业务部门 / 业务负责人 / 客户 / 所属公司 / 租赁公司
// Driver field intentionally removed.
(function(){
// ── Companies & departments ──────────────────────────────
const COMPANIES = {
own: ["浙江羚牛氢能科技有限公司", "嘉兴羚牛新能源运营公司"],
lease: ["JXLN-23 浙F氢能", "JXGW-G 浙F氢能", "LNZLHT 嘉兴港区"],
};
const DEPARTMENTS = [
{ id: "biz1", name: "业务一部", lead: "高伟", color: "#1F8B4C" },
{ id: "biz2", name: "业务二部", lead: "陈高伟", color: "#2E8C8C" },
{ id: "biz3", name: "业务三部", lead: "尚建华", color: "#7A8C2E" },
{ id: "biz4", name: "业务四部", lead: "刘念念", color: "#C97A3D" },
{ id: "biz5", name: "业务五部", lead: "周志强", color: "#7A4FB4" },
{ id: "biz6", name: "业务六部", lead: "金可鹏", color: "#B45F8A" },
{ id: "ops", name: "运营部", lead: "张兰", color: "#5C6E7C" },
];
const CUSTOMERS = [
"嘉兴公司自营", "嘉兴氢能业务一部", "欧宝软件",
"汇通运营公司", "嘉兴港区二部", "嘉兴县嘉一部",
"午潮停车场", "—",
];
const PARKINGS = [
"平湖停车场", "平湖停车场异常", "嘉兴公司自营", "欧宝停车场",
"汇通停车场", "嘉兴港区停车场", "午潮码头停车场", "云洋停车场",
];
const CITIES = [
"浙江省·嘉兴市·平湖", "浙江省·嘉兴市", "浙江省·嘉兴市·港区",
"浙江省·嘉兴市·南湖", "广东省·云浮市", "浙江省·杭州市·萧山",
];
// VIN/车架号 prefix patterns from screenshot: LA9HE6.. / LA9G6.. / LJRC1..
const VIN_PREFIXES = ["LA9HE6F2N1A", "LA9G6E7L4N1B", "LJRC14A22NA0", "LA9HE6F8N1C"];
// ── Asset/operation status enums ──────────────────────────
// asset: in_stock(在库) | leasing(租赁中) | abnormal(异常)
// own: self(自有) | lease(外租)
// op: operating(运营中) | suspended(停运) | maintenance(待整备)
// gps: online(在线) | offline(离线)
// grade: A | B | C
// status (legacy): ok | warn | danger | idle (kept for map color coding)
// ── Helper: deterministic pseudo-random ───────────────────
const seed = (n) => { let x = (n*9301+49297)%233280; return () => (x = (x*9301+49297)%233280) / 233280; };
// ── Build vehicles ────────────────────────────────────────
// Start with the 12 mapped vehicles (preserve x/y for the map),
// enrich with business fields. Then add 40 more (no map coords).
const _mapped = [
// Real plate numbers from the data screenshot · 浙F prefix
// Coordinates positioned within Jiaxing Zhapu Port viewport (1240×800, sea below y=620)
{ id: "浙F03980F", x: 320, y: 180, h: 320, status: "ok", speed: 56, soc: 78, src: "B" },
{ id: "浙F03311F", x: 460, y: 280, h: 60, status: "warn", speed: 0, soc: 24, src: "T" },
{ id: "浙F03000F", x: 600, y: 240, h: 110, status: "ok", speed: 78, soc: 31, src: "B" },
{ id: "浙FK800F", x: 760, y: 290, h: 200, status: "ok", speed: 64, soc: 82, src: "T" },
{ id: "浙FK808F", x: 880, y: 410, h: 280, status: "ok", speed: 51, soc: 47, src: "B" },
{ id: "浙F07918F", x: 240, y: 540, h: 30, status: "idle", speed: 0, soc: 96, src: "T" },
{ id: "浙F01505F", x: 600, y: 470, h: 160, status: "ok", speed: 44, soc: 55, src: "B" },
{ id: "浙F30778F", x: 540, y: 380, h: 240, status: "ok", speed: 38, soc: 71, src: "T" },
{ id: "浙F39086F", x: 1000, y: 360, h: 90, status: "danger", speed: 0, soc: 9, src: "B" },
{ id: "浙F02618F", x: 700, y: 540, h: 350, status: "ok", speed: 42, soc: 64, src: "T" },
{ id: "浙F09860F", x: 960, y: 540, h: 70, status: "warn", speed: 0, soc: 18, src: "B" },
{ id: "浙F02399F", x: 820, y: 470, h: 190, status: "ok", speed: 48, soc: 88, src: "J" },
];
// Additional 40 vehicles — list-only, no map coords
const _extra = [
"浙F08991F","浙F05969F","浙F07179F","浙F08278F","浙F02002F","浙F01689F",
"浙F00598F","浙F02608F","浙F08638F","浙F00278F","浙F02289F","浙F06196F",
"浙F00885F","浙F08889F","浙F03127F","浙F04421F","浙F05538F","浙F06693F",
"浙F07412F","浙F08810F","浙F09125F","浙F10232F","浙F11456F","浙F12674F",
"浙F13891F","浙F14037F","浙F15268F","浙F16495F","浙F17712F","浙F18934F",
"浙F19156F","浙F20389F","浙F21516F","浙F22748F","浙F23973F","浙F24196F",
"浙F25425F","浙F26658F","浙F27873F","浙F28095F",
].map(id => ({ id, x: null, y: null, h: 0, status: "ok", speed: 0, soc: 0, src: "T" }));
// ── Enrichment ────────────────────────────────────────────
const _enrich = (v, i) => {
const r = seed(i + 1);
const isLeased = r() < 0.55; // 55% 外租
const dept = DEPARTMENTS[Math.floor(r() * 5)];
const company = isLeased ? COMPANIES.lease[Math.floor(r() * 3)] : COMPANIES.own[Math.floor(r() * 2)];
const ownCompany = COMPANIES.own[Math.floor(r() * 2)];
// Asset status correlates with vehicle status
let asset, op, gps;
if (v.status === "danger") { asset = "abnormal"; op = "suspended"; gps = "offline"; }
else if (v.status === "idle") { asset = "in_stock"; op = "maintenance"; gps = r() < 0.4 ? "online" : "offline"; }
else if (isLeased) { asset = "leasing"; op = "operating"; gps = v.status === "warn" ? "offline" : "online"; }
else { asset = "in_stock"; op = r() < 0.4 ? "maintenance" : "operating"; gps = "online"; }
// Status duration in days
const statusDays = Math.floor(r() * 120) + 1;
// Mileage
const totalKm = Math.floor(r() * 80000) + 12000;
const lastMaintKm = totalKm - Math.floor(r() * 8000) - 1000;
const nextMaintKm = lastMaintKm + 10000;
const kmToMaint = nextMaintKm - totalKm;
// Last maintenance date (days ago)
const lastMaintDays = Math.floor(r() * 90) + 1;
// Grade
const grade = ["A","B","B","C"][Math.floor(r()*4)];
// VIN
const vin = VIN_PREFIXES[Math.floor(r() * VIN_PREFIXES.length)] + String(1000 + Math.floor(r()*9000));
const fleetCode = (i < 12 && r() < 0.3) ? (Math.floor(r()*99)+10) + "Q" : null;
// Operating city
const city = CITIES[Math.floor(r() * CITIES.length)];
// Customer (only if leased)
const customer = asset === "leasing" ? CUSTOMERS[Math.floor(r() * 6)] : (asset === "in_stock" ? "—" : CUSTOMERS[Math.floor(r() * 6)]);
// Parking
const parking = PARKINGS[Math.floor(r() * PARKINGS.length)];
// Contract
const hasContract = asset === "leasing" || (asset === "abnormal" && r() < 0.5);
const contractNo = hasContract ? "JX-" + dept.id.toUpperCase() + "-2024-" + String(2000 + i) : null;
const handoverKm = hasContract ? Math.floor(r() * 3000) + 200 : null;
const returnKm = (asset === "in_stock" && hasContract) ? handoverKm + Math.floor(r()*40000) + 5000 : null;
// Hydrogen pressure (MPa) and motor temp
const h2 = v.status === "danger" ? 0.8 : (v.soc / 100 * 5.6 + 0.2).toFixed(1);
const motorTemp = v.status === "danger" ? 102 : 58 + Math.floor(r()*15);
// ── Brand & model — 真实国内氢能车型样本 ──
const MODELS = [
{ brand: "上汽大通", model: "MIFA-H 氢燃料 MPV" },
{ brand: "上汽大通", model: "EUNIQ 7 氢电版" },
{ brand: "现代", model: "NEXO" },
{ brand: "丰田", model: "MIRAI 第二代" },
{ brand: "格罗夫", model: "格罗夫氢能 SUV" },
{ brand: "海马汽车", model: "7X-H" },
{ brand: "红旗", model: "H5 FCV" },
{ brand: "长安深蓝", model: "SL03 氢燃料版" },
{ brand: "飞驰科技", model: "FCB80 氢燃料客车" },
{ brand: "宇通客车", model: "ZK6105FCEVG3" },
];
const m = MODELS[Math.floor(r() * MODELS.length)];
// ── Integration / 对接情况 ──
// src: T = TBOX(GB/T 32960), J = JT808/1078, B = both
// 状态online (recent) / offline (had data, now stale) / not_connected (从未对接)
// 5% 完全未对接(重点标记) · 12% TBOX 断流 · 8% JT 断流
const integFlag = r();
let gbStatus, jtStatus;
if (i >= 12 && integFlag < 0.05) {
// 5% 全部未对接(重点标记)
gbStatus = "not_connected";
jtStatus = "not_connected";
} else {
if (v.src === "T" || v.src === "B") {
gbStatus = (r() < 0.12) ? "offline" : "online";
} else {
gbStatus = (r() < 0.30) ? "offline" : "not_connected";
}
if (v.src === "J" || v.src === "B") {
jtStatus = (r() < 0.08) ? "offline" : "online";
} else {
jtStatus = (r() < 0.20) ? "offline" : "not_connected";
}
}
// 时间戳生成
const now = Date.now();
const minute = 60 * 1000, hour = 60 * minute, day = 24 * hour;
const tsFor = (st, baseSeed) => {
if (st === "not_connected") return null;
if (st === "online") return now - Math.floor(baseSeed * 5 * minute) - 2000; // 2s ~ 5min
// offline
return now - Math.floor(baseSeed * 7 * day) - 30 * minute; // 30min ~ 7d ago
};
const gbLastSeen = tsFor(gbStatus, r());
const jtLastSeen = tsFor(jtStatus, r());
// 接入时间首次对接日期6 个月内随机)
const onboardDays = Math.floor(r() * 180) + 7;
const onboardAt = now - onboardDays * day;
return {
...v,
plate: v.id,
vin, fleetCode,
city, parking,
asset, own: isLeased ? "lease" : "self", op, gps, grade, statusDays,
dept: dept.id, deptName: dept.name, deptLead: dept.lead, deptColor: dept.color,
customer,
company, ownCompany,
contractNo, handoverKm, returnKm,
totalKm, lastMaintKm, nextMaintKm, kmToMaint, lastMaintDays,
h2, motorTemp,
// Hydrogen-specific
h2Pressure: parseFloat(h2),
range: Math.round(v.soc * 6.2),
// Brand / model
brand: m.brand, model: m.model,
// Integration / 对接情况
gbStatus, gbLastSeen,
jtStatus, jtLastSeen,
onboardAt,
};
};
// ── Build VEHICLES ────────────────────────────────────────
// 优先使用 vehicles-real.js 提供的 1006 辆真实车辆数据;不存在则回退到合成数据。
let VEHICLES;
if (window.RAW_VEHICLES && window.RAW_VEHICLES.length) {
// 从 xlsx 抽取的 1006 辆真实数据
// 前 12 辆叠加乍浦港地图坐标,让总览地图保持原本的演示态
const RAW = window.RAW_VEHICLES;
const MAP_OVERLAY = [
{ x: 320, y: 180, h: 320, status: "ok", speed: 56, soc: 78 },
{ x: 460, y: 280, h: 60, status: "warn", speed: 0, soc: 24 },
{ x: 600, y: 240, h: 110, status: "ok", speed: 78, soc: 31 },
{ x: 760, y: 290, h: 200, status: "ok", speed: 64, soc: 82 },
{ x: 880, y: 410, h: 280, status: "ok", speed: 51, soc: 47 },
{ x: 240, y: 540, h: 30, status: "idle", speed: 0, soc: 96 },
{ x: 600, y: 470, h: 160, status: "ok", speed: 44, soc: 55 },
{ x: 540, y: 380, h: 240, status: "ok", speed: 38, soc: 71 },
{ x: 1000,y: 360, h: 90, status: "danger", speed: 0, soc: 9 },
{ x: 700, y: 540, h: 350, status: "ok", speed: 42, soc: 64 },
{ x: 960, y: 540, h: 70, status: "warn", speed: 0, soc: 18 },
{ x: 820, y: 470, h: 190, status: "ok", speed: 48, soc: 88 },
];
VEHICLES = RAW.map((v, i) => {
const overlay = i < MAP_OVERLAY.length ? MAP_OVERLAY[i] : { x:null, y:null, h:0, status:"ok", speed:0, soc:0 };
const dept = DEPARTMENTS.find(d => d.id === v.dept) || DEPARTMENTS[DEPARTMENTS.length-1];
// 派生氢电指标xlsx 没有这些字段)
const soc = overlay.soc || ((i * 17) % 90 + 10);
const h2 = overlay.status === "danger" ? 0.8 : +(soc / 100 * 5.6 + 0.2).toFixed(1);
const motorTemp = overlay.status === "danger" ? 102 : 58 + (i % 15);
const totalKm = 12000 + ((i * 1331) % 80000);
const lastMaintKm = totalKm - 1000 - ((i * 379) % 8000);
const nextMaintKm = lastMaintKm + 10000;
const kmToMaint = nextMaintKm - totalKm;
const lastMaintDays = 1 + ((i * 41) % 90);
return {
...v,
// overlay map fields
x: overlay.x, y: overlay.y, h: overlay.h,
status: overlay.status, // for map color (ok/warn/danger/idle)
speed: overlay.speed, soc,
// department display fields
deptName: dept.name, deptLead: dept.lead, deptColor: dept.color,
// hydrogen-electric derived
h2, h2Pressure: parseFloat(h2),
range: Math.round(soc * 6.2),
motorTemp,
totalKm, lastMaintKm, nextMaintKm, kmToMaint, lastMaintDays,
// legacy/compat
fleetCode: null,
};
});
} else {
// Fallback: 合成数据
VEHICLES = [..._mapped, ..._extra].map(_enrich);
}
// ── Aggregations for filters ──────────────────────────────
const COUNTS = {
all: VEHICLES.length,
// Asset status
inStock: VEHICLES.filter(v => v.asset === "in_stock").length,
leasing: VEHICLES.filter(v => v.asset === "leasing").length,
abnormal: VEHICLES.filter(v => v.asset === "abnormal").length,
// Ownership
self: VEHICLES.filter(v => v.own === "self").length,
lease: VEHICLES.filter(v => v.own === "lease").length,
// Operation
operating: VEHICLES.filter(v => v.op === "operating").length,
suspended: VEHICLES.filter(v => v.op === "suspended").length,
maintenance: VEHICLES.filter(v => v.op === "maintenance").length,
// GPS
online: VEHICLES.filter(v => v.gps === "online").length,
offline: VEHICLES.filter(v => v.gps === "offline").length,
// Departments
byDept: DEPARTMENTS.reduce((acc, d) => {
acc[d.id] = VEHICLES.filter(v => v.dept === d.id).length;
return acc;
}, {}),
// Integration / 数据接入
gbOnline: VEHICLES.filter(v => v.gbStatus === "online").length,
gbOffline: VEHICLES.filter(v => v.gbStatus === "offline").length,
gbNotConn: VEHICLES.filter(v => v.gbStatus === "not_connected").length,
jtOnline: VEHICLES.filter(v => v.jtStatus === "online").length,
jtOffline: VEHICLES.filter(v => v.jtStatus === "offline").length,
jtNotConn: VEHICLES.filter(v => v.jtStatus === "not_connected").length,
// 完全未对接(重点标记)
bothNone: VEHICLES.filter(v => v.gbStatus === "not_connected" && v.jtStatus === "not_connected").length,
// 任一在线
anyOnline: VEHICLES.filter(v => v.gbStatus === "online" || v.jtStatus === "online").length,
};
// ── User roles for permission demo ────────────────────────
const ROLES = [
{ id: "admin", name: "总管理员", scope: "all", desc: "可见所有车辆 · 所有部门" },
{ id: "biz1_lead",name: "业务一部·负责人", scope: "dept", deptId: "biz1", desc: "仅可见业务一部车辆" },
{ id: "biz2_lead",name: "业务二部·负责人", scope: "dept", deptId: "biz2", desc: "仅可见业务二部车辆" },
{ id: "ops", name: "运营岗", scope: "ops", desc: "可见所有车辆 · 仅运维操作" },
{ id: "finance", name: "财务岗", scope: "finance", desc: "可见合同/资产 · 隐藏实时车况" },
];
// expose
Object.assign(window, {
VEHICLES, DEPARTMENTS, COMPANIES, CUSTOMERS, PARKINGS, CITIES, COUNTS, ROLES,
});
})();