All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
数据 - 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 重算)
321 lines
15 KiB
JavaScript
321 lines
15 KiB
JavaScript
// 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,
|
||
});
|
||
})();
|