/* === 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 || ""}
{/* === 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) => (
))}
);
})()}
{/* === 风险控制 (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) => (
))}
{/* === 收益质量 === */}
{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 });