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 排行面板
|
* EventDailyStats - 事件 TOP 排行面板
|
||||||
* 展示当日事件的表现排行
|
* 展示当日事件的表现排行
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useCallback, useMemo, memo } from "react";
|
import React, { useState, useEffect, useCallback, useMemo, memo, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
@@ -111,6 +111,9 @@ const EventDailyStats = () => {
|
|||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
const controls = useAnimationControls();
|
const controls = useAnimationControls();
|
||||||
|
|
||||||
|
// 用于数据对比,避免相同数据触发重渲染
|
||||||
|
const prevDataHashRef = useRef(null);
|
||||||
|
|
||||||
const fetchStats = useCallback(async (isRefresh = false) => {
|
const fetchStats = useCallback(async (isRefresh = false) => {
|
||||||
if (isRefresh) {
|
if (isRefresh) {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
@@ -126,7 +129,18 @@ const EventDailyStats = () => {
|
|||||||
if (!response.ok) throw new Error("获取数据失败");
|
if (!response.ok) throw new Error("获取数据失败");
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success || data.code === 200) {
|
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);
|
setStats(data.data);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.message || "数据格式错误");
|
throw new Error(data.message || "数据格式错误");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
|||||||
|
|
||||||
// 月份数据缓存(避免切换月份后再切回时重复请求)
|
// 月份数据缓存(避免切换月份后再切回时重复请求)
|
||||||
const monthCacheRef = useRef({});
|
const monthCacheRef = useRef({});
|
||||||
|
// 涨停详情缓存 ref(用于 handleDateClick 避免依赖 ztDailyDetails 状态)
|
||||||
|
const ztDailyDetailsRef = useRef({});
|
||||||
|
|
||||||
// 加载日历综合数据(带缓存)
|
// 加载日历综合数据(带缓存)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -86,9 +88,8 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
|||||||
loadCalendarCombinedData();
|
loadCalendarCombinedData();
|
||||||
}, [currentMonth]);
|
}, [currentMonth]);
|
||||||
|
|
||||||
// 处理日期点击 - 打开弹窗
|
// 处理日期点击 - 打开弹窗(使用 ref 避免依赖 ztDailyDetails 状态)
|
||||||
const handleDateClick = useCallback(
|
const handleDateClick = useCallback(async (date) => {
|
||||||
async (date) => {
|
|
||||||
setSelectedDate(date);
|
setSelectedDate(date);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
setDetailLoading(true);
|
setDetailLoading(true);
|
||||||
@@ -96,8 +97,8 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
|||||||
const ztDateStr = formatDateStr(date);
|
const ztDateStr = formatDateStr(date);
|
||||||
const eventDateStr = dayjs(date).format("YYYY-MM-DD");
|
const eventDateStr = dayjs(date).format("YYYY-MM-DD");
|
||||||
|
|
||||||
// 加载涨停详情
|
// 加载涨停详情(使用 ref 访问缓存)
|
||||||
const detail = ztDailyDetails[ztDateStr];
|
const detail = ztDailyDetailsRef.current[ztDateStr];
|
||||||
if (detail?.fullData) {
|
if (detail?.fullData) {
|
||||||
setSelectedZtDetail(detail.fullData);
|
setSelectedZtDetail(detail.fullData);
|
||||||
} else {
|
} else {
|
||||||
@@ -106,6 +107,11 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setSelectedZtDetail(data);
|
setSelectedZtDetail(data);
|
||||||
|
// 同时更新 ref 和 state
|
||||||
|
ztDailyDetailsRef.current[ztDateStr] = {
|
||||||
|
...ztDailyDetailsRef.current[ztDateStr],
|
||||||
|
fullData: data,
|
||||||
|
};
|
||||||
setZtDailyDetails((prev) => ({
|
setZtDailyDetails((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[ztDateStr]: { ...prev[ztDateStr], fullData: data },
|
[ztDateStr]: { ...prev[ztDateStr], fullData: data },
|
||||||
@@ -133,9 +139,7 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDetailLoading(false);
|
setDetailLoading(false);
|
||||||
},
|
}, []);
|
||||||
[ztDailyDetails]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 月份变化回调
|
// 月份变化回调
|
||||||
const handleMonthChange = useCallback((year, month) => {
|
const handleMonthChange = useCallback((year, month) => {
|
||||||
|
|||||||
@@ -67,16 +67,27 @@ const MarketOverviewBanner = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 首次加载标记
|
||||||
|
const isInitialMount = useRef(true);
|
||||||
|
|
||||||
// 首次加载显示 loading
|
// 首次加载显示 loading
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStats(selectedDate, true);
|
fetchStats(selectedDate, true);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 日期变化时静默刷新
|
// 日期变化时静默刷新(带防抖,避免快速切换时多次请求)
|
||||||
|
// 跳过首次加载,避免重复请求
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isInitialMount.current) {
|
||||||
|
isInitialMount.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (selectedDate) {
|
if (selectedDate) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
fetchStats(selectedDate, false);
|
fetchStats(selectedDate, false);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [fetchStats, selectedDate]);
|
}, [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