/* === Product Detail page === */ /* global React, LineChart, StackedAreaChart, DrawdownArea, useToast, Icons */ const { useState: useDetailState, useEffect: useDetailEffect } = React; // 安全访问辅助函数 const safeArr = (arr, defaultVal = []) => Array.isArray(arr) ? arr : defaultVal; const safeNum = (n, defaultVal = 0) => typeof n === "number" ? n : defaultVal; // 根据时间范围获取数据的辅助函数 - 按实际日期计算 const getDataByTimeRange = (allData, datesArr, range) => { if (!allData || allData.length === 0 || !datesArr || datesArr.length === 0) return allData; const latestDate = new Date(datesArr[datesArr.length - 1]); let cutoffDate; switch (range) { case "6m": cutoffDate = new Date(latestDate); cutoffDate.setMonth(cutoffDate.getMonth() - 6); break; case "1y": cutoffDate = new Date(latestDate); cutoffDate.setFullYear(cutoffDate.getFullYear() - 1); break; case "all": default: return allData; // 返回全部数据 } // 找到第一个在cutoffDate之后的数据点索引 let startIdx = 0; for (let i = 0; i < datesArr.length; i++) { const d = new Date(datesArr[i]); if (d >= cutoffDate) { startIdx = i; break; } } return allData.slice(startIdx); }; // 获取对应时间范围的日期数组 - 按实际日期计算 const getDatesByTimeRange = (datesArr, range) => { if (!datesArr || datesArr.length === 0) return datesArr; const latestDate = new Date(datesArr[datesArr.length - 1]); let cutoffDate; switch (range) { case "6m": cutoffDate = new Date(latestDate); cutoffDate.setMonth(cutoffDate.getMonth() - 6); break; case "1y": cutoffDate = new Date(latestDate); cutoffDate.setFullYear(cutoffDate.getFullYear() - 1); break; case "all": default: return datesArr; // 返回全部日期 } // 找到第一个在cutoffDate之后的日期索引 let startIdx = 0; for (let i = 0; i < datesArr.length; i++) { const d = new Date(datesArr[i]); if (d >= cutoffDate) { startIdx = i; break; } } return datesArr.slice(startIdx); }; function PageProductDetail({ productId, onNav, openDrawer, setProductId }) { // 安全获取所有需要的数据 const products = safeArr(window.IRDATA?.products); const categories = safeArr(window.IRDATA?.CATEGORIES); const dates = safeArr(window.IRDATA?.DATES); const [timeseriesLoaded, setTimeseriesLoaded] = useDetailState(false); const [timeRange, setTimeRange] = useDetailState("1y"); // 默认显示1年 // 懒加载时序数据 useDetailEffect(() => { if (productId && window.IRDATA?.loadTimeseries) { window.IRDATA.loadTimeseries(productId).then(() => { setTimeseriesLoaded(prev => !prev); }); } }, [productId]); // 如果没有产品数据,显示加载中 if (!products || products.length === 0) { return (
📊

产品数据加载中...

请稍候或刷新页面

); } // 安全查找产品 let p = products.find((x) => x && x.id === productId); if (!p) { p = products[0]; // 使用第一个产品作为备用 } if (!p) { return (
⚠️

未找到产品数据

请返回产品池选择产品

); } // 安全获取分类 let safeCat = { color: "#0f5cff", name: "未分类" }; if (p.catId) { const cat = categories.find(c => c && c.id === p.catId); if (cat) { safeCat = cat; } } const toast = useToast(); const [legendOff, setLegendOff] = useDetailState(new Set()); const isOff = (k) => legendOff.has(k); const toggleLegend = (k) => { const s = new Set(legendOff); s.has(k) ? s.delete(k) : s.add(k); setLegendOff(s); }; // 获取当前时间范围过滤后的日期 const filteredDates = getDatesByTimeRange(dates, timeRange); // 获取当前时间范围过滤后的净值数据 const filteredNavSeries = getDataByTimeRange(safeArr(p.navSeries), dates, timeRange); const filteredBenchSeries = getDataByTimeRange(safeArr(p.benchmarkSeries), dates, timeRange); const filteredPeerSeries = getDataByTimeRange(safeArr(p.peerSeries), dates, timeRange); const lineSeries = [ { key: "self", name: p.name || "产品", color: safeCat.color, data: filteredNavSeries }, { key: "bench", name: "模拟基准", color: "#6c7a93", data: filteredBenchSeries }, { key: "peer", name: "模拟同类中位数", color: "#00a99d", data: filteredPeerSeries }, ].filter((s) => !isOff(s.key)); // 获取当前时间范围过滤后的回撤数据 const filteredDrawdown = getDataByTimeRange(safeArr(p.drawdown), dates, timeRange); // strategy metrics const sm = p.strategyMetrics || {}; const allocColors = ["#0f5cff", "#00a99d", "#ff7a1a", "#6b4ce6"]; const allocWithSeries = safeArr(p.allocation); const allocDates = safeArr(p.allocationDates); // 获取当前时间范围过滤后的策略数据 - 使用策略自己的日期 const filteredAllocWithSeries = allocWithSeries.map(s => ({ ...s, series: getDataByTimeRange(safeArr(s.series), allocDates, timeRange) })); // 获取过滤后的策略日期 const filteredAllocDates = getDatesByTimeRange(allocDates, timeRange); return ( {/* Product picker bar */}
查看产品:
{/* Profile */}
{p.rating || "N/A"}
{p.name || "产品名称"}
{p.code || ""} 管理人:{p.manager || ""} 类型:{p.type || ""} 分类:{safeCat.name}
最新净值
{typeof p.nav === "number" ? p.nav.toFixed(4) : "N/A"}
年化收益
{(safeNum(p.annualReturn) * 100).toFixed(2)}%
夏普
{safeNum(p.sharpe).toFixed(2)}
系统结论
{p.conclusion || ""}
主要风险
{p.risk || ""}
{/* === 4 modules in 2x2 grid === */}
{/* === 业绩表现 === */}
业绩表现
净值走势 产品 vs 基准 vs 同类中位数
{[ { key: "self", name: p.name || "产品", color: safeCat.color }, { key: "bench", name: "模拟基准", color: "#6c7a93" }, { key: "peer", name: "模拟同类中位数", color: "#00a99d" }, ].map((it) => (
toggleLegend(it.key)}> {it.name}
))}
{ openDrawer({ kind: "info", payload: { title: `净值点 · ${filteredDates[idx] || ""}`, sub: p.name || "产品", kvs: [ { k: p.name || "产品", v: {filteredNavSeries[idx]?.toFixed(4) || "N/A"} }, { k: "模拟基准", v: {filteredBenchSeries[idx]?.toFixed(4) || "N/A"} }, { k: "同类中位数", v: {filteredPeerSeries[idx]?.toFixed(4) || "N/A"} }, { k: "超额", v: {filteredNavSeries[idx] && filteredBenchSeries[idx] ? ((filteredNavSeries[idx] / filteredBenchSeries[idx] - 1) * 100).toFixed(2) + "%" : "N/A"} }, ], body: "点击净值曲线任意点,可查看当日产品净值、基准、同类对比及超额收益。", } }); }} />
收益指标 累计与年化口径
{(() => { const navArr = safeArr(p.navSeries); const last = navArr[navArr.length - 1]; const startVal = navArr[0]; const weekRef = navArr[Math.max(0, navArr.length - 6)]; const weekChg = typeof last === "number" && typeof weekRef === "number" ? last / weekRef - 1 : 0; const sinceChg = typeof last === "number" && typeof startVal === "number" ? last / startVal - 1 : 0; const items = [ { label: "累计净值", val: typeof last === "number" ? last.toFixed(4) : "N/A", color: "var(--fg)" }, { label: "本周净值涨跌", val: (weekChg * 100).toFixed(2) + "%", color: weekChg >= 0 ? "var(--red)" : "var(--green)" }, { label: "成立以来净值涨跌", val: (sinceChg * 100).toFixed(2) + "%", color: sinceChg >= 0 ? "var(--red)" : "var(--green)" }, { label: "成立以来年化收益", val: (safeNum(p.annualReturn) * 100).toFixed(2) + "%", color: safeNum(p.annualReturn) >= 0 ? "var(--red)" : "var(--green)" }, ]; return (
{items.map((m, i) => (
{m.label}
{m.val}
))}
); })()}
{/* === 风险控制 (merged: drawdown chart + 4 metrics) === */}
风险控制
回撤曲线 历史净值相对前高的跌幅
风险指标 关键风险口径汇总
{[ { label: "当前回撤", val: (safeNum(p.currentDrawdown) * 100).toFixed(2) + "%", color: "var(--orange)" }, { label: "最大回撤", val: (safeNum(p.maxDrawdown) * 100).toFixed(2) + "%", color: "var(--red)" }, { label: "夏普比率", val: safeNum(p.sharpe).toFixed(2), color: "var(--primary)" }, { label: "卡玛比率", val: safeNum(p.calmar).toFixed(2), color: "var(--purple)" }, ].map((m, i) => (
{m.label}
{m.val}
))}
{/* === 收益质量 === */}
收益质量
{Icons.empty}
收益质量指标待接入
归因模块预计 2026 Q3 上线
{/* === 策略行为 (merged: allocation + strategy metrics) === */}
策略行为
配置变化 四类策略权重的逐月推移
{filteredAllocWithSeries.map((s, i) => (
{s.name}
))}
{filteredAllocWithSeries.length > 0 ? ( ({ name: s.name, color: s.color || allocColors[i], data: safeArr(s.series), }))} labels={filteredAllocDates.length > 0 ? filteredAllocDates : Array.from({ length: safeArr(filteredAllocWithSeries[0]?.series).length }, (_, i) => `M${i + 1}`)} height={160} /> ) : (
暂无配置数据
)}
策略指标 调仓与集中度
策略配置能力
{safeNum(sm.strategic, 0.7).toFixed(2)}
战术择时能力
{safeNum(sm.tactical, 0.65).toFixed(2)}
基金选择能力
{safeNum(sm.selection, 0.8).toFixed(2)}
综合得分
{safeNum(p.score, 0.75).toFixed(2)}
); } Object.assign(window, { PageProductDetail });