Files
kkfluous a47faf66f0
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
chore: 面包屑首项改为 OneOS数据中台
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 20:51:50 +08:00

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;