init: 羚牛车辆数据中心原型 + 部署配置
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful

- 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 探活
This commit is contained in:
kkfluous
2026-04-28 15:12:32 +08:00
commit b2d0016a0d
59 changed files with 6938 additions and 0 deletions

193
data/fleet.js Normal file
View File

@@ -0,0 +1,193 @@
// 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,
});
})();