init: 羚牛车辆数据中心原型 + 部署配置
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
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:
166
components/chrome.jsx
Normal file
166
components/chrome.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
// chrome.jsx — Sidebar, Topbar shared chrome (route-aware)
|
||||
|
||||
const SIDEBAR_ITEMS = [
|
||||
{ id: "overview", icon: "map", label: "实时地图" },
|
||||
{ id: "detail", icon: "car", label: "车辆详情" },
|
||||
{ id: "history", icon: "history", label: "历史查询" },
|
||||
{ id: "playback", icon: "route", label: "轨迹回放" },
|
||||
{ id: "alarm", icon: "bell", label: "事件规则" },
|
||||
{ id: "inbox", icon: "inbox", label: "通知中心" },
|
||||
];
|
||||
const SIDEBAR_SUB = [
|
||||
{ id: "esg", icon: "chart", label: "ESG·碳减排" },
|
||||
{ id: "canvas", icon: "settings", label: "设计画板" },
|
||||
];
|
||||
|
||||
const Sidebar = ({ active }) => {
|
||||
// resolve current route from context if present, fall back to prop
|
||||
const ctx = (typeof window.useRoute === "function") ? window.useRoute() : null;
|
||||
const cur = (ctx && ctx.route) || active || "overview";
|
||||
const nav = (ctx && ctx.navigate) || ((p) => { window.location.hash = "#/" + p; });
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<div className="logo" title="羚牛 · Lingniu" onClick={() => nav("overview")} style={{
|
||||
cursor:"pointer",
|
||||
background:"#FFFFFF",
|
||||
border:"1px solid var(--border-1)",
|
||||
boxShadow:"0 1px 2px rgba(47,40,40,.06)",
|
||||
overflow:"hidden",
|
||||
padding:0,
|
||||
}}>
|
||||
<img src="assets/logo_light.svg" alt="羚牛"
|
||||
style={{height:24, width:"auto", maxWidth:"none", marginLeft:-3, display:"block", pointerEvents:"none"}}/>
|
||||
</div>
|
||||
{SIDEBAR_ITEMS.map(i => (
|
||||
<div key={i.id}
|
||||
className={"nav-item" + (i.id === cur ? " active" : "")}
|
||||
title={i.label}
|
||||
onClick={() => nav(i.id)}
|
||||
style={{cursor:"pointer"}}>
|
||||
<Icon name={i.icon} size={18}/>
|
||||
</div>
|
||||
))}
|
||||
<div className="sidebar-divider"/>
|
||||
{SIDEBAR_SUB.map(i => (
|
||||
<div key={i.id}
|
||||
className={"nav-item" + (i.id === cur ? " active" : "")}
|
||||
title={i.label}
|
||||
onClick={() => nav(i.id)}
|
||||
style={{cursor:"pointer"}}>
|
||||
<Icon name={i.icon} size={18}/>
|
||||
</div>
|
||||
))}
|
||||
<div style={{flex:1}}/>
|
||||
<div className="avatar" title="张工">ZG</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Topbar = ({ crumbs = ["羚牛车辆数据中心", "实时监控"], kpis = [], showSearch = true }) => {
|
||||
const ctx = (typeof window.useRoute === "function") ? window.useRoute() : null;
|
||||
const isMobile = ctx && ctx.isMobile;
|
||||
return (
|
||||
<div className="topbar">
|
||||
{isMobile && (
|
||||
<button onClick={ctx.openDrawer} aria-label="菜单" style={{
|
||||
width:36, height:36, display:"grid", placeItems:"center", borderRadius:8,
|
||||
background:"transparent", border:"none", color:"var(--fg-1)", cursor:"pointer",
|
||||
marginLeft:-4,
|
||||
}}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<div className="crumbs">
|
||||
{crumbs.map((c, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span className="sep">/</span>}
|
||||
<span className={i === crumbs.length - 1 ? "now" : "muted"}>{c}</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="kpi-row">
|
||||
{kpis.map((k, i) => (
|
||||
<div key={i} className="kpi-mini">
|
||||
<span className="lbl">{k.lbl}</span>
|
||||
<span className="val tnum">{k.val}</span>
|
||||
{k.delta && <span className={"delta " + (k.deltaUp ? "up" : "down")}>{k.deltaUp ? "▲" : "▼"} {k.delta}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showSearch && (
|
||||
<div className="search" style={{marginLeft: "auto"}}>
|
||||
<Icon name="search" size={13}/>
|
||||
<input placeholder="搜索车牌 / VIN / 部门 / 客户…"/>
|
||||
<kbd>⌘K</kbd>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="right">
|
||||
<div className="icon-btn"><Icon name="refresh" size={15}/></div>
|
||||
<div className="icon-btn">
|
||||
<Icon name="bell" size={15}/>
|
||||
<span className="dot"/>
|
||||
</div>
|
||||
<ThemeToggle/>
|
||||
<div className="icon-btn"><Icon name="settings" size={15}/></div>
|
||||
<RoleBadge/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Role badge — shows current logged-in user, opens tweaks panel on click
|
||||
const RoleBadge = () => {
|
||||
const { role } = (typeof window.useCurrentRole === "function") ? window.useCurrentRole() : { role: null };
|
||||
if (!role) return <div className="avatar" style={{marginLeft: 4}}>张</div>;
|
||||
const initial = role.name.replace(/.*·/,"").charAt(0) || role.name.charAt(0);
|
||||
const isAdmin = role.scope === "all";
|
||||
const tip = role.scope === "dept" ? `仅可见 ${role.deptId === "biz1" ? "业务一部" : "业务二部"} 车辆` : role.desc;
|
||||
return (
|
||||
<div title={tip} style={{
|
||||
display:"flex", alignItems:"center", gap:8, padding:"4px 4px 4px 10px",
|
||||
borderRadius: 16, border:"1px solid var(--border-1)",
|
||||
background: isAdmin ? "transparent" : "var(--accent-soft)",
|
||||
cursor:"pointer", marginLeft: 4,
|
||||
}}>
|
||||
<div className="col" style={{lineHeight:1.1, alignItems:"flex-end"}}>
|
||||
<span style={{fontSize:11, color:"var(--fg-1)", fontWeight:500}}>{role.name}</span>
|
||||
<span style={{fontSize:9, color: isAdmin ? "var(--fg-3)" : "var(--accent)", textTransform:"uppercase", letterSpacing:0.5}}>
|
||||
{role.scope === "all" ? "FULL ACCESS" : role.scope === "dept" ? "DEPT SCOPE" : role.scope === "ops" ? "OPS" : "FINANCE"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="avatar" style={{margin:0}}>{initial}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Theme toggle — switches between light & dark themes
|
||||
const ThemeToggle = () => {
|
||||
const ctx = (typeof window.useTheme === "function") ? window.useTheme() : null;
|
||||
if (!ctx) return null;
|
||||
const { theme, setTheme } = ctx;
|
||||
const isDark = theme === "dark";
|
||||
return (
|
||||
<button className="icon-btn" onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
title={isDark ? "切换到亮色" : "切换到暗色"}
|
||||
style={{background:"transparent", border:"none", cursor:"pointer", padding:0}}>
|
||||
{isDark ? (
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
window.Sidebar = Sidebar;
|
||||
window.Topbar = Topbar;
|
||||
Reference in New Issue
Block a user