Files
oneos-truck-date-prototype/data/fleet.js
kkfluous b2d0016a0d
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
init: 羚牛车辆数据中心原型 + 部署配置
- React 18 + Babel-in-browser SPA 原型,覆盖 8 个画板:
  实时地图 / 车辆详情 / 历史查询 / 轨迹回放 / 事件规则 / 通知中心 / ESG 碳减排 / 移动端
- 设计系统:IBM Plex Sans + JetBrains Mono,亮/暗双主题,羚牛绿 #007143
- 数据模型:12 + 40 辆车,TBOX (T) / JT808+1078 (JT) / 双源 (B)
- 部署:nginx 静态托管,Dockerfile + woodpecker.yml + docker-compose.yml
- 镜像:harbor.lnh2e.com/lingniu-v1/ln-vdc:<branch>-<VERSION>
- 容器端口 80,宿主映射 8112,含 /healthz 探活
2026-04-28 15:12:32 +08:00

194 lines
9.4 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: "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);
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),
};
};
const 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;
}, {}),
};
// ── 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,
});
})();