All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
167 lines
6.8 KiB
JavaScript
167 lines
6.8 KiB
JavaScript
// 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: "通知中心" },
|
|
{ id: "integration", icon: "plug", 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 = ["OneOS数据中台", "实时监控"], 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; |