/* === Product Compare page === */ /* global React, LineChart, useToast, Icons */ const { useState: useCmpState, useMemo: useCmpMemo } = React; const CMP_COLORS = ["#0f5cff", "#ef3f3f", "#00a99d", "#ff7a1a", "#6b4ce6", "#14a065", "#f5a623"]; const fmtPct = (v) => v == null ? '-' : (v * 100).toFixed(2) + '%'; const fmtNum = (v, decimals = 2) => v == null ? '-' : v.toFixed(decimals); function PageProductCompare({ compareIds, setCompareIds, openDrawer, onNav }) { const { products, CATEGORIES, DATES } = window.IRDATA; const toast = useToast(); const [pickerOpen, setPickerOpen] = useCmpState(false); const [catFilter, setCatFilter] = useCmpState("all"); const cmpProducts = compareIds.map((id) => products.find((p) => p.id === id)).filter(Boolean); const remove = (id) => { setCompareIds(compareIds.filter((x) => x !== id)); toast.push("已移出对比", "info"); }; const add = (id) => { if (compareIds.includes(id)) return; if (compareIds.length >= 6) {toast.push("对比数量已达上限 6 只", "warn");return;} setCompareIds([...compareIds, id]); toast.push(`已加入对比:${products.find((p) => p.id === id).name}`); }; const exportCSV = () => { if (cmpProducts.length === 0) {toast.push("请先加入产品", "warn");return;} const head = ["产品", "代码", "管理人", "分类", "年化收益", "最大回撤", "年化波动", "夏普", "卡玛"]; const rows = cmpProducts.map((p) => [ p.name, p.code, p.manager, CATEGORIES.find((c) => c.id === p.catId)?.name || '未分类', fmtPct(p.annualReturn), fmtPct(p.maxDrawdown), fmtPct(p.vol), fmtNum(p.sharpe), fmtNum(p.calmar)] ); const csv = [head, ...rows].map((r) => r.join(",")).join("\n"); const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url;a.download = "产品对比.csv";a.click(); URL.revokeObjectURL(url); toast.push("已导出 CSV 文件"); }; const available = products.filter((p) => !compareIds.includes(p.id) && (catFilter === "all" || p.catId === catFilter) ); return ( {/* Selection bar */}
对比清单 · {cmpProducts.length} 只
最多支持 6 只产品同框对比
{cmpProducts.map((p, i) =>
{p.name}
)}
{pickerOpen &&
分类筛选: setCatFilter("all")}>全部 {CATEGORIES.map((c) => setCatFilter(c.id)}>{c.name} )}
{available.map((p) => { const cat = CATEGORIES.find((c) => c.id === p.catId) || { name: "未分类", color: "#999", soft: "#eee" }; return (
add(p.id)} style={{ padding: "8px 10px", background: "#fff", border: "1px solid var(--border)", borderRadius: 6, cursor: "pointer", display: "flex", justifyContent: "space-between", alignItems: "center", gap: 8 }} onMouseEnter={(e) => e.currentTarget.style.borderColor = "var(--primary)"} onMouseLeave={(e) => e.currentTarget.style.borderColor = "var(--border)"}>
{p.name}
{p.code}
{cat.name}
); })}
}
{/* Nav comparison chart */}
产品净值对比图
区间:{DATES[0]} ~ {DATES[DATES.length - 1]}
{cmpProducts.length === 0 ?
{Icons.empty}
请添加产品后查看对比
:
{cmpProducts.map((p, i) =>
{p.name}
)}
({ name: p.name, color: CMP_COLORS[i % CMP_COLORS.length], data: p.navSeries }))} dates={DATES} height={320} format="nav" />
}
{/* Comparison table */}
指标对比
{cmpProducts.length === 0 ?
{Icons.empty}
暂无对比数据
:
{cmpProducts.map((p, i) => { const cat = CATEGORIES.find((c) => c.id === p.catId) || { name: "未分类", color: "#999", soft: "#eee" }; return ( ); })}
产品 管理人 分类 年化收益 最大回撤 波动率 夏普 卡玛 操作
{p.name} {p.code}
{p.manager} {cat.name} {fmtPct(p.annualReturn)} {fmtPct(p.maxDrawdown)} {fmtPct(p.vol)} {fmtNum(p.sharpe)} {fmtNum(p.calmar)}
}
); } Object.assign(window, { PageProductCompare });