// Shell: header (with org cascader) + sidebar + router
const { useState, useEffect, useRef, useMemo, useCallback } = React;
const D = window.__CARBON_DATA;
// ─── Org context ────────────────────────────────────────────────
const OrgCtx = React.createContext(null);
function OrgProvider({ children, orgDepthLimit, industryFocus }) {
// Default to group (level 1)
const [orgId, setOrgId] = useState('org-001');
// When industry focus changes, jump to that板块 if found
useEffect(() => {
if (!industryFocus || industryFocus === 'ALL') return;
const target = D.ORG_FLAT.find(o => o.level === 2 && o.industry === industryFocus);
if (target) setOrgId(target.id);
}, [industryFocus]);
const value = useMemo(() => ({
orgId,
setOrgId,
org: D.ORG_FLAT.find(o => o.id === orgId),
path: D.getOrgPath(orgId),
descendants: D.getDescendants(orgId),
orgDepthLimit
}), [orgId, orgDepthLimit]);
return {children};
}
const useOrg = () => React.useContext(OrgCtx);
// Filtered tree by depth limit
function filterTree(node, maxLevel) {
if (node.level > maxLevel) return null;
return {
...node,
children: (node.children || [])
.map(c => filterTree(c, maxLevel))
.filter(Boolean)
};
}
// ─── Org Switcher (cascading dropdown in header) ────────────────
function OrgSwitcher() {
const { org, path, setOrgId, orgDepthLimit } = useOrg();
const [open, setOpen] = useState(false);
const [hover, setHover] = useState([]); // hovered node at each column
const ref = useRef(null);
const limit = orgDepthLimit || 4;
useEffect(() => {
if (!open) return;
const handler = (e) => {
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [open]);
// When opening, initialize columns from current path
useEffect(() => {
if (open) setHover(path.map(p => p.id));
}, [open]);
const tree = filterTree(D.ORG_TREE, limit);
// build columns based on hover
const columns = [];
let cur = tree;
for (let i = 0; cur && cur.children && cur.children.length; i++) {
columns.push(cur.children);
const selectedId = hover[i + 1];
cur = cur.children.find(c => c.id === selectedId);
if (!selectedId) break;
}
// also show root as col 0
const allCols = [[tree], ...columns];
const choose = (node, colIdx) => {
const newHover = hover.slice(0, colIdx + 1);
newHover[colIdx] = node.id;
setHover(newHover);
if (!node.children || node.children.length === 0 || colIdx === limit - 1) {
setOrgId(node.id);
setOpen(false);
}
};
const pathLabel = path.map(p => p.name).join(' / ');
return (
setOpen(v => !v)}>
组织:
{pathLabel}
{open && (
e.stopPropagation()}>
{allCols.map((col, idx) => (
{col.map(n => {
const isLeaf = !n.children || n.children.length === 0 || idx === limit - 1;
const isActive = hover[idx] === n.id;
return (
{
if (!isLeaf) {
const h = hover.slice(0, idx);
h[idx] = n.id;
setHover(h);
}
}}
onClick={() => choose(n, idx)}>
{n.name}
{!isLeaf && }
);
})}
))}
)}
);
}
// ─── Sidebar nav items ──────────────────────────────────────────
const NAV_ITEMS = [
{ group: '碳排放管理', items: [
{ id: 'dashboard', label: '数据看板', icon: 'dashboard' },
{ id: 'data-entry', label: '数据填报', icon: 'edit' },
{ id: 'data-import', label: '批量导入', icon: 'upload' }
]},
{ group: '配置中心', items: [
{ id: 'factor-library', label: '排放因子库', icon: 'book' },
{ id: 'organization', label: '组织架构', icon: 'tree' }
]},
{ group: '报告', items: [
{ id: 'report', label: '报告生成', icon: 'doc' }
]}
];
function Sidebar({ collapsed, route, onNavigate }) {
return (
);
}
// ─── Header ─────────────────────────────────────────────────────
function Header({ collapsed, onToggleSidebar }) {
return (
);
}
// ─── Page Header ────────────────────────────────────────────────
function PageHeader({ title, breadcrumb, extra }) {
const { path } = useOrg();
return (
{(breadcrumb || []).map((b, i) => (
{b}
{i < breadcrumb.length - 1 && /}
))}
{title}
当前:{path[path.length - 1]?.name}
{extra}
);
}
Object.assign(window, {
OrgProvider, useOrg, OrgSwitcher, Sidebar, Header, PageHeader, NAV_ITEMS, filterTree
});