From 75fd9924bc17e630e4d532fa607dc39ee57bd03a Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 15 Jan 2026 18:11:48 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=A6=96=E6=AC=A1?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E9=87=8D=E5=A4=8D=E8=AF=B7=E6=B1=82=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20+=20Stage=203&4=20=E6=80=A7=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复问题: - 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 --- .../Community/components/EventDailyStats.js | 18 ++- .../HeroPanel/components/CombinedCalendar.js | 88 ++++++------ .../components/MarketOverviewBanner.js | 15 +- .../Community/contexts/EventStatsContext.js | 133 ++++++++++++++++++ 4 files changed, 208 insertions(+), 46 deletions(-) create mode 100644 src/views/Community/contexts/EventStatsContext.js diff --git a/src/views/Community/components/EventDailyStats.js b/src/views/Community/components/EventDailyStats.js index be6b6f20..d71122c8 100644 --- a/src/views/Community/components/EventDailyStats.js +++ b/src/views/Community/components/EventDailyStats.js @@ -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) { - setStats(data.data); + // 生成数据指纹用于对比 + 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 || "数据格式错误"); } diff --git a/src/views/Community/components/HeroPanel/components/CombinedCalendar.js b/src/views/Community/components/HeroPanel/components/CombinedCalendar.js index 95bf4095..e9409d9d 100644 --- a/src/views/Community/components/HeroPanel/components/CombinedCalendar.js +++ b/src/views/Community/components/HeroPanel/components/CombinedCalendar.js @@ -45,6 +45,8 @@ const CombinedCalendar = memo(({ DetailModal }) => { // 月份数据缓存(避免切换月份后再切回时重复请求) const monthCacheRef = useRef({}); + // 涨停详情缓存 ref(用于 handleDateClick 避免依赖 ztDailyDetails 状态) + const ztDailyDetailsRef = useRef({}); // 加载日历综合数据(带缓存) useEffect(() => { @@ -86,56 +88,58 @@ const CombinedCalendar = memo(({ DetailModal }) => { loadCalendarCombinedData(); }, [currentMonth]); - // 处理日期点击 - 打开弹窗 - const handleDateClick = useCallback( - async (date) => { - setSelectedDate(date); - setModalOpen(true); - setDetailLoading(true); + // 处理日期点击 - 打开弹窗(使用 ref 避免依赖 ztDailyDetails 状态) + const handleDateClick = useCallback(async (date) => { + setSelectedDate(date); + setModalOpen(true); + setDetailLoading(true); - const ztDateStr = formatDateStr(date); - const eventDateStr = dayjs(date).format("YYYY-MM-DD"); + const ztDateStr = formatDateStr(date); + const eventDateStr = dayjs(date).format("YYYY-MM-DD"); - // 加载涨停详情 - const detail = ztDailyDetails[ztDateStr]; - if (detail?.fullData) { - setSelectedZtDetail(detail.fullData); - } else { - try { - const response = await fetch(`/data/zt/daily/${ztDateStr}.json`); - if (response.ok) { - const data = await response.json(); - setSelectedZtDetail(data); - setZtDailyDetails((prev) => ({ - ...prev, - [ztDateStr]: { ...prev[ztDateStr], fullData: data }, - })); - } else { - setSelectedZtDetail(null); - } - } catch { + // 加载涨停详情(使用 ref 访问缓存) + const detail = ztDailyDetailsRef.current[ztDateStr]; + if (detail?.fullData) { + setSelectedZtDetail(detail.fullData); + } else { + try { + const response = await fetch(`/data/zt/daily/${ztDateStr}.json`); + 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 }, + })); + } else { setSelectedZtDetail(null); } - } - - // 加载事件详情 - try { - const response = await eventService.calendar.getEventsForDate( - eventDateStr - ); - if (response.success) { - setSelectedEvents(response.data || []); - } else { - setSelectedEvents([]); - } } catch { + setSelectedZtDetail(null); + } + } + + // 加载事件详情 + try { + const response = await eventService.calendar.getEventsForDate( + eventDateStr + ); + if (response.success) { + setSelectedEvents(response.data || []); + } else { setSelectedEvents([]); } + } catch { + setSelectedEvents([]); + } - setDetailLoading(false); - }, - [ztDailyDetails] - ); + setDetailLoading(false); + }, []); // 月份变化回调 const handleMonthChange = useCallback((year, month) => { diff --git a/src/views/Community/components/MarketOverviewBanner.js b/src/views/Community/components/MarketOverviewBanner.js index 1c03ac1c..a0a8c325 100644 --- a/src/views/Community/components/MarketOverviewBanner.js +++ b/src/views/Community/components/MarketOverviewBanner.js @@ -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) { - fetchStats(selectedDate, false); + const timer = setTimeout(() => { + fetchStats(selectedDate, false); + }, 300); + return () => clearTimeout(timer); } }, [fetchStats, selectedDate]); diff --git a/src/views/Community/contexts/EventStatsContext.js b/src/views/Community/contexts/EventStatsContext.js new file mode 100644 index 00000000..6c495f78 --- /dev/null +++ b/src/views/Community/contexts/EventStatsContext.js @@ -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 ( + + {children} + + ); +}; + +/** + * 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;