// 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, }); })();