fix: 修复首次加载重复请求问题 + Stage 3&4 性能优化
修复问题: - MarketOverviewBanner/EventStatsContext: 首次加载时防抖 effect 会导致重复请求 - 添加 isInitialMount ref 标记跳过首次加载 Stage 3 优化 (数据对比/防抖/依赖优化): - EventDailyStats: 数据指纹对比,相同数据不触发重渲染 - MarketOverviewBanner: 日期变化 300ms 防抖 - CombinedCalendar: useRef 优化 handleDateClick 依赖 Stage 4 优化 (跨组件数据共享): - 新增 EventStatsContext 用于 EventDailyStats/MarketOverviewBanner 数据共享(预留) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* EventDailyStats - 事件 TOP 排行面板
|
||||
* 展示当日事件的表现排行
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback, useMemo, memo } from "react";
|
||||
import React, { useState, useEffect, useCallback, useMemo, memo, useRef } from "react";
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
@@ -111,6 +111,9 @@ const EventDailyStats = () => {
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const controls = useAnimationControls();
|
||||
|
||||
// 用于数据对比,避免相同数据触发重渲染
|
||||
const prevDataHashRef = useRef(null);
|
||||
|
||||
const fetchStats = useCallback(async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true);
|
||||
@@ -126,7 +129,18 @@ const EventDailyStats = () => {
|
||||
if (!response.ok) throw new Error("获取数据失败");
|
||||
const data = await response.json();
|
||||
if (data.success || data.code === 200) {
|
||||
// 生成数据指纹用于对比
|
||||
const topPerformers = data.data?.topPerformers || [];
|
||||
const dataHash = topPerformers
|
||||
.slice(0, 10)
|
||||
.map((e) => `${e.id}-${e.avgChg}`)
|
||||
.join("|");
|
||||
|
||||
// 仅当数据真正变化时才更新状态
|
||||
if (dataHash !== prevDataHashRef.current) {
|
||||
prevDataHashRef.current = dataHash;
|
||||
setStats(data.data);
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || "数据格式错误");
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
||||
|
||||
// 月份数据缓存(避免切换月份后再切回时重复请求)
|
||||
const monthCacheRef = useRef({});
|
||||
// 涨停详情缓存 ref(用于 handleDateClick 避免依赖 ztDailyDetails 状态)
|
||||
const ztDailyDetailsRef = useRef({});
|
||||
|
||||
// 加载日历综合数据(带缓存)
|
||||
useEffect(() => {
|
||||
@@ -86,9 +88,8 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
||||
loadCalendarCombinedData();
|
||||
}, [currentMonth]);
|
||||
|
||||
// 处理日期点击 - 打开弹窗
|
||||
const handleDateClick = useCallback(
|
||||
async (date) => {
|
||||
// 处理日期点击 - 打开弹窗(使用 ref 避免依赖 ztDailyDetails 状态)
|
||||
const handleDateClick = useCallback(async (date) => {
|
||||
setSelectedDate(date);
|
||||
setModalOpen(true);
|
||||
setDetailLoading(true);
|
||||
@@ -96,8 +97,8 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
||||
const ztDateStr = formatDateStr(date);
|
||||
const eventDateStr = dayjs(date).format("YYYY-MM-DD");
|
||||
|
||||
// 加载涨停详情
|
||||
const detail = ztDailyDetails[ztDateStr];
|
||||
// 加载涨停详情(使用 ref 访问缓存)
|
||||
const detail = ztDailyDetailsRef.current[ztDateStr];
|
||||
if (detail?.fullData) {
|
||||
setSelectedZtDetail(detail.fullData);
|
||||
} else {
|
||||
@@ -106,6 +107,11 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSelectedZtDetail(data);
|
||||
// 同时更新 ref 和 state
|
||||
ztDailyDetailsRef.current[ztDateStr] = {
|
||||
...ztDailyDetailsRef.current[ztDateStr],
|
||||
fullData: data,
|
||||
};
|
||||
setZtDailyDetails((prev) => ({
|
||||
...prev,
|
||||
[ztDateStr]: { ...prev[ztDateStr], fullData: data },
|
||||
@@ -133,9 +139,7 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
||||
}
|
||||
|
||||
setDetailLoading(false);
|
||||
},
|
||||
[ztDailyDetails]
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 月份变化回调
|
||||
const handleMonthChange = useCallback((year, month) => {
|
||||
|
||||
@@ -67,16 +67,27 @@ const MarketOverviewBanner = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 首次加载标记
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
// 首次加载显示 loading
|
||||
useEffect(() => {
|
||||
fetchStats(selectedDate, true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// 日期变化时静默刷新
|
||||
// 日期变化时静默刷新(带防抖,避免快速切换时多次请求)
|
||||
// 跳过首次加载,避免重复请求
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
return;
|
||||
}
|
||||
if (selectedDate) {
|
||||
const timer = setTimeout(() => {
|
||||
fetchStats(selectedDate, false);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [fetchStats, selectedDate]);
|
||||
|
||||
|
||||
133
src/views/Community/contexts/EventStatsContext.js
Normal file
133
src/views/Community/contexts/EventStatsContext.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* EventStatsContext - 事件统计数据共享 Context
|
||||
* 让 EventDailyStats 和 MarketOverviewBanner 共享同一份 API 数据
|
||||
* 避免重复请求 /api/v1/events/effectiveness-stats
|
||||
*/
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { getApiBase } from "@utils/apiConfig";
|
||||
|
||||
const EventStatsContext = createContext(null);
|
||||
|
||||
/**
|
||||
* EventStatsProvider - 事件统计数据提供者
|
||||
*/
|
||||
export const EventStatsProvider = ({ children }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [stats, setStats] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedDate, setSelectedDate] = useState(
|
||||
new Date().toISOString().split("T")[0]
|
||||
);
|
||||
|
||||
// 数据指纹用于对比,避免相同数据触发重渲染
|
||||
const prevDataHashRef = useRef(null);
|
||||
// 防抖定时器
|
||||
const debounceTimerRef = useRef(null);
|
||||
// 首次加载标记
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
const fetchStats = useCallback(async (dateStr = "", showLoading = false) => {
|
||||
if (showLoading) {
|
||||
setLoading(true);
|
||||
}
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const apiBase = getApiBase();
|
||||
const dateParam = dateStr ? `&date=${dateStr}` : "";
|
||||
const response = await fetch(
|
||||
`${apiBase}/api/v1/events/effectiveness-stats?days=1${dateParam}`
|
||||
);
|
||||
if (!response.ok) throw new Error("获取数据失败");
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success || data.code === 200) {
|
||||
// 生成数据指纹用于对比
|
||||
const topPerformers = data.data?.topPerformers || [];
|
||||
const dataHash = topPerformers
|
||||
.slice(0, 10)
|
||||
.map((e) => `${e.id}-${e.avgChg}`)
|
||||
.join("|");
|
||||
|
||||
// 仅当数据真正变化时才更新状态
|
||||
if (dataHash !== prevDataHashRef.current) {
|
||||
prevDataHashRef.current = dataHash;
|
||||
setStats(data.data);
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message || "数据格式错误");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("获取事件统计失败:", err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 首次加载
|
||||
useEffect(() => {
|
||||
fetchStats(selectedDate, true);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// 日期变化时刷新(带防抖)
|
||||
// 跳过首次加载,避免重复请求
|
||||
useEffect(() => {
|
||||
if (isInitialMount.current) {
|
||||
isInitialMount.current = false;
|
||||
return;
|
||||
}
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
fetchStats(selectedDate, false);
|
||||
}, 300);
|
||||
return () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [selectedDate, fetchStats]);
|
||||
|
||||
// 自动刷新(每60秒)
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => fetchStats(selectedDate, false), 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, [selectedDate, fetchStats]);
|
||||
|
||||
const value = {
|
||||
loading,
|
||||
stats,
|
||||
error,
|
||||
selectedDate,
|
||||
setSelectedDate,
|
||||
refresh: () => fetchStats(selectedDate, false),
|
||||
};
|
||||
|
||||
return (
|
||||
<EventStatsContext.Provider value={value}>
|
||||
{children}
|
||||
</EventStatsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* useEventStats - 使用事件统计数据的 Hook
|
||||
*/
|
||||
export const useEventStats = () => {
|
||||
const context = useContext(EventStatsContext);
|
||||
if (!context) {
|
||||
throw new Error("useEventStats must be used within EventStatsProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default EventStatsContext;
|
||||
Reference in New Issue
Block a user