/* === Shared UI === */
/* global React */
const { useState: useStateUI, useEffect: useEffectUI, useRef: useRefUI } = React;
// Icon helper - simple inline SVGs
const Icons = {
pool: ,
mgr: ,
warn: ,
setting: ,
operation: ,
dashboard: ,
refresh: ,
bell: ,
user: ,
close: ,
download: ,
plus: ,
check: ,
arrow: ,
empty: ,
// Tree icons for Data Dictionary
home: ,
folder: ,
folderOpen: ,
file: ,
// Operation sub-menu icons
batchIcon: ,
ruleIcon: ,
dataIcon: ,
probeIcon: ,
dictIcon: ,
};
// ---- Toast system ----
const ToastContext = React.createContext({ push: () => {} });
function ToastProvider({ children }) {
const [toasts, setToasts] = useStateUI([]);
const push = (msg, type = "success") => {
const id = Date.now() + Math.random();
setToasts((ts) => [...ts, { id, msg, type }]);
setTimeout(() => {
setToasts((ts) => ts.filter((t) => t.id !== id));
}, 3000);
};
return (
{children}
{toasts.map((t) =>
{t.type === "success" ? "✓" : t.type === "warn" ? "!" : t.type === "error" ? "×" : "i"}
{t.msg}
)}
);
}
function useToast() {return React.useContext(ToastContext);}
// ---- Drawer ----
function Drawer({ open, onClose, title, sub, children, footer }) {
useEffectUI(() => {
if (!open) return;
const onKey = (e) => {if (e.key === "Escape") onClose();};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);
if (!open) return null;
return (
{children}
{footer &&
{footer}
}
);
}
// ---- Confirm Dialog ----
function ConfirmModal({ open, onClose, title, message, onConfirm }) {
if (!open) return null;
return (
onClose(false)}>
e.stopPropagation()}>
{title}
);
}
// ---- Sidebar ----
function Sidebar({ route, onNav }) {
const groups = [
{
id: "pool", label: "产品池", icon: Icons.pool, parentRoute: "pool-list",
items: [
{ id: "pool-detail", label: "产品详情" },
{ id: "pool-compare", label: "产品对比" }]
},
{
id: "mgr", label: "管理人分析", icon: Icons.mgr, parentRoute: "mgr-overview",
items: [
{ id: "mgr-strategy", label: "策略配置能力" },
{ id: "mgr-tactical", label: "战术配置能力" },
{ id: "mgr-selection", label: "选基能力" }]
},
{
id: "warn", label: "预警和售后", icon: Icons.warn, parentRoute: "warn-overview",
items: [
{ id: "warn-risk", label: "风险事件预警" },
{ id: "warn-sop", label: "售后 SOP" }]
},
{
id: "operation", label: "运营管理", icon: Icons.operation, parentRoute: "operation-overview",
items: [
{ id: "operation-user", label: "用户权限" },
{ id: "operation-batch", label: "批量任务" },
{ id: "operation-rule", label: "规则策略" },
{ id: "operation-product", label: "产品维护" },
{ id: "operation-data", label: "数据接入" },
{ id: "operation-indicator", label: "指标计算" },
{ id: "operation-probe", label: "数据探查" },
{ id: "operation-dict", label: "数据字典" }]
},
{
id: "dashboard", label: "视图看板", icon: Icons.dashboard, leaf: true,
route: "dashboard"
},
];
const [open, setOpen] = useStateUI({
pool: true, mgr: route.startsWith("mgr"), warn: route.startsWith("warn"), operation: route.startsWith("operation")
});
return (
);
}
// ---- Topbar ----
function Topbar({ title, crumb, onSearchOpen, onNav, drawerOpen, openDrawer }) {
const [q, setQ] = useStateUI("");
const [focused, setFocused] = useStateUI(false);
const wrapRef = useRefUI(null);
useEffectUI(() => {
const onDown = (e) => {
if (wrapRef.current && !wrapRef.current.contains(e.target)) setFocused(false);
};
document.addEventListener("mousedown", onDown);
return () => document.removeEventListener("mousedown", onDown);
}, []);
const results = (() => {
if (!q || q.length < 1) return null;
const ql = q.toLowerCase();
const prods = window.IRDATA.products.filter((p) =>
p.name.toLowerCase().includes(ql) || p.code.toLowerCase().includes(ql) || p.id.toLowerCase().includes(ql)
).slice(0, 8);
const mgrs = window.IRDATA.managers.filter((m) =>
m.name.includes(q) || m.type.includes(q)
);
return { prods, mgrs };
})();
return (
投研看板
setQ(e.target.value)}
onFocus={() => setFocused(true)} />
{focused && results && results.prods.length + results.mgrs.length > 0 &&
{results.prods.length > 0 &&
产品 · {results.prods.length}
{results.prods.map((p) =>
{
openDrawer({ kind: "product", id: p.id });
setQ("");setFocused(false);
}}>
{p.name}
{p.code} · {p.manager}
{(p.annualReturn * 100).toFixed(2)}%
)}
}
{results.mgrs.length > 0 &&
管理人 · {results.mgrs.length}
{results.mgrs.map((m) =>
{
onNav("mgr-strategy", { mgrId: m.id });
setQ("");setFocused(false);
}}>
{m.name}
{m.type} · {m.productCount} 产品
)}
}
}
{focused && q && results && results.prods.length === 0 && results.mgrs.length === 0 &&
}
数据截至
2026-05-20
更新于
2026-05-21 09:30
张
张三
);
}
// ---- Product detail drawer ----
function ProductDetailDrawer({ id, onClose, onNav }) {
if (!id) return null;
const p = window.IRDATA.products.find((x) => x.id === id);
if (!p) return null;
const cat = window.IRDATA.CATEGORIES.find((c) => c.id === p.catId);
return (
}>
核心指标
年化收益
{(p.annualReturn * 100).toFixed(2)}%
夏普比率
{p.sharpe.toFixed(2)}
最大回撤
{(p.maxDrawdown * 100).toFixed(2)}%
同类回撤
{(p.peerMaxDrawdown * 100).toFixed(2)}%
当前回撤
{(p.currentDrawdown * 100).toFixed(2)}%
年化波动
{(p.vol * 100).toFixed(2)}%
卡玛比率
{p.calmar.toFixed(2)}
标签
{cat.name}
{p.labels.map((l, i) => {l})}
);
}
// ---- Generic info drawer (for chart point clicks etc) ----
function InfoDrawer({ payload, onClose }) {
if (!payload) return null;
return (
明细
{payload.kvs && payload.kvs.map((kv, i) =>
)}
{payload.body &&
}
);
}
Object.assign(window, {
Icons, ToastProvider, useToast, Drawer, ConfirmModal, Sidebar, Topbar,
ProductDetailDrawer, InfoDrawer
});