fix: 股票详情页K线 React 310 崩溃(useMemo置于early return前) + 时间范围切换

- 修复 React error #310:displayData 的 useMemo 原置于 if(loading) early return 之后,
  导致 loading 切换时 hook 数量变化触发崩溃。移到所有 early return 之前。
- K线一次性拉取 365 天,前端按 3月/6月/1年 切片展示,默认近3月。
This commit is contained in:
Sakurasan
2026-08-28 03:41:18 +08:00
parent d5516b72d7
commit 24c8b31301
2 changed files with 67 additions and 33 deletions
+45 -17
View File
@@ -86,6 +86,7 @@ function StockDetail() {
const [fundFlowLoading, setFundFlowLoading] = useState(false);
const [fundFlowPeriod, setFundFlowPeriod] = useState(21);
const [dailyTableDays, setDailyTableDays] = useState<number>(7);
const [chartRange, setChartRange] = useState(90); // 默认近3月
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [inCollection, setInCollection] = useState(false);
@@ -175,9 +176,8 @@ function StockDetail() {
// 阶段2:并行加载历史K线 + 资金流向(后置加载,不阻塞首屏)
const addedDate = new Date(addedAt);
const now = new Date();
const daysSinceAdded = Math.floor((now.getTime() - addedDate.getTime()) / (1000 * 60 * 60 * 24));
const chartDays = Math.min(Math.max(90, daysSinceAdded), 120);
// 一次性拉取最多 1 年(365天)K线,展示范围由 chartRange 前端切片
const chartDays = 365;
setChartLoading(true);
setFundFlowLoading(true);
@@ -288,6 +288,14 @@ function StockDetail() {
const roe = latest.indicators["加权净资产收益率"];
return typeof roe === "number" ? roe : null;
}, [financialData]);
// 展示数据 = 按 chartRange 切片最近 N 天(chartData 存了全年,这里只取窗口)
// 必须在所有 early return 之前调用,保证 hook 顺序一致
const displayData = useMemo(() => {
if (chartData.length <= chartRange) return chartData;
return chartData.slice(chartData.length - chartRange);
}, [chartData, chartRange]);
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
@@ -334,19 +342,19 @@ function StockDetail() {
// 找到添加日期在图表中的位置
const addedDateObj = stockInfo ? new Date(stockInfo.addedAt) : null;
const addedDateStr = addedDateObj ? addedDateObj.toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit" }) : "";
const addedDataPoint = inCollection ? chartData.find(d => d.isAddedDate) : undefined;
const addedDataPoint = inCollection ? displayData.find(d => d.isAddedDate) : undefined;
// 计算X轴刻度:保证添加日期始终有标签
const tickDates = (() => {
if (chartData.length === 0) return [];
const first = chartData[0].date;
const last = chartData[chartData.length - 1].date;
if (displayData.length === 0) return [];
const first = displayData[0].date;
const last = displayData[displayData.length - 1].date;
const ticks = [first];
const added = addedDataPoint?.date;
const targetCount = Math.min(7, chartData.length);
const step = Math.max(1, Math.floor((chartData.length - 1) / (targetCount - 2)));
for (let i = step; i < chartData.length - 1; i += step) {
ticks.push(chartData[i].date);
const targetCount = Math.min(7, displayData.length);
const step = Math.max(1, Math.floor((displayData.length - 1) / (targetCount - 2)));
for (let i = step; i < displayData.length - 1; i += step) {
ticks.push(displayData[i].date);
}
if (added && !ticks.includes(added)) {
ticks.push(added);
@@ -516,12 +524,32 @@ function StockDetail() {
<CardTitle className="text-base md:text-lg">
K线图
<span className="text-xs md:text-sm font-normal text-muted-foreground ml-2">
{chartLoading ? "(加载中...)" : `(${chartData.length}个交易日)`}
{chartLoading ? "(加载中...)" : `(${displayData.length}个交易日)`}
</span>
</CardTitle>
<span className="text-xs text-muted-foreground hidden sm:inline">
双指缩放 · 拖动选区查看
</span>
<div className="flex items-center gap-1.5 flex-wrap">
{/* 时间范围:3月 / 6月 / 1年 */}
<div className="flex gap-0.5 text-xs border rounded-md overflow-hidden">
{[
{ key: 90, label: "3月" },
{ key: 180, label: "6月" },
{ key: 365, label: "1年" },
].map((r) => (
<button
key={r.key}
onClick={() => setChartRange(r.key)}
className={`px-2.5 py-1 transition-colors ${
chartRange === r.key ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"
}`}
>
{r.label}
</button>
))}
</div>
<span className="text-xs text-muted-foreground hidden sm:inline">
双指缩放 · 拖动选区查看
</span>
</div>
</div>
</CardHeader>
<CardContent className="px-2 md:px-6 pb-2 md:pb-6">
@@ -540,7 +568,7 @@ function StockDetail() {
>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={chartData}
data={displayData}
margin={{ top: 10, right: 8, left: 0, bottom: 0 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" opacity={0.5} />
@@ -554,7 +582,7 @@ function StockDetail() {
height={56}
tick={(props: any) => {
const { x, y, payload } = props;
const dataPoint = chartData.find(d => d.date === payload.value);
const dataPoint = displayData.find(d => d.date === payload.value);
const isAddedDate = inCollection && dataPoint?.isAddedDate;
return (