diff --git a/src/components/Calendar/FullCalendarPro.tsx b/src/components/Calendar/FullCalendarPro.tsx index 106cdace..10bf5264 100644 --- a/src/components/Calendar/FullCalendarPro.tsx +++ b/src/components/Calendar/FullCalendarPro.tsx @@ -18,15 +18,10 @@ import { Box, Text, VStack, Tooltip } from "@chakra-ui/react"; import { keyframes } from "@emotion/react"; import dayjs from "dayjs"; -// 动画定义 +// 动画定义 - 使用 transform 代替 background-position(GPU 加速) const shimmer = keyframes` - 0% { background-position: -200% 0; } - 100% { background-position: 200% 0; } -`; - -const glow = keyframes` - 0%, 100% { box-shadow: 0 0 5px rgba(212, 175, 55, 0.3); } - 50% { box-shadow: 0 0 20px rgba(212, 175, 55, 0.6); } + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } `; /** @@ -408,12 +403,9 @@ export const FullCalendarPro: React.FC = ({ const dateStr = dayjs(date).format("YYYYMMDD"); const dateData = dataMapRef.current.get(dateStr); - // 找到 day-top 容器并插入自定义内容 + // 找到 day-top 容器并插入自定义内容(直接替换,无需先清空) const dayTop = el.querySelector(".fc-daygrid-day-top"); if (dayTop) { - // 清空默认内容 - dayTop.innerHTML = ""; - // 插入自定义内容 dayTop.innerHTML = createCellContentHTML(date, dateData, isToday); } }, []); @@ -463,17 +455,17 @@ export const FullCalendarPro: React.FC = ({ overflow="hidden" position="relative" > - {/* 闪光效果 */} + {/* 闪光效果 - 使用 transform 实现 GPU 加速 */} = ({ }, ".fc-daygrid-day.fc-day-today": { bg: "rgba(212, 175, 55, 0.15) !important", - animation: `${glow} 2s ease-in-out infinite`, + boxShadow: "0 0 15px rgba(212, 175, 55, 0.5)", + // 移除呼吸动画,使用固定 boxShadow 高亮"今天",避免内容闪烁 }, ".fc-daygrid-day-frame": { minHeight: "50px", diff --git a/src/views/Community/components/HeroPanel/components/CombinedCalendar.js b/src/views/Community/components/HeroPanel/components/CombinedCalendar.js index 993081f5..95bf4095 100644 --- a/src/views/Community/components/HeroPanel/components/CombinedCalendar.js +++ b/src/views/Community/components/HeroPanel/components/CombinedCalendar.js @@ -1,5 +1,5 @@ // HeroPanel - 综合日历组件 -import React, { useState, useEffect, useCallback, Suspense, lazy, memo } from "react"; +import React, { useState, useEffect, useCallback, Suspense, lazy, memo, useRef } from "react"; import { Box, HStack, @@ -43,12 +43,23 @@ const CombinedCalendar = memo(({ DetailModal }) => { const [detailLoading, setDetailLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); - // 加载日历综合数据(一次 API 调用获取所有数据) + // 月份数据缓存(避免切换月份后再切回时重复请求) + const monthCacheRef = useRef({}); + + // 加载日历综合数据(带缓存) useEffect(() => { const loadCalendarCombinedData = async () => { + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth() + 1; + const cacheKey = `${year}-${month}`; + + // 检查缓存 + if (monthCacheRef.current[cacheKey]) { + setCalendarData(monthCacheRef.current[cacheKey]); + return; + } + try { - const year = currentMonth.getFullYear(); - const month = currentMonth.getMonth() + 1; const response = await fetch( `${getApiBase()}/api/v1/calendar/combined-data?year=${year}&month=${month}` ); @@ -63,10 +74,8 @@ const CombinedCalendar = memo(({ DetailModal }) => { eventCount: item.event_count || 0, indexChange: item.index_change, })); - console.log( - "[HeroPanel] 加载日历综合数据成功,数据条数:", - formattedData.length - ); + // 存入缓存 + monthCacheRef.current[cacheKey] = formattedData; setCalendarData(formattedData); } } diff --git a/src/views/Community/components/HeroPanel/components/DetailModal/DetailModal.js b/src/views/Community/components/HeroPanel/components/DetailModal/DetailModal.js index 63b37d47..021a2e70 100644 --- a/src/views/Community/components/HeroPanel/components/DetailModal/DetailModal.js +++ b/src/views/Community/components/HeroPanel/components/DetailModal/DetailModal.js @@ -268,14 +268,23 @@ const DetailModal = ({ [dispatch, isStockInWatchlist] ); - // 加载股票行情(并行加载优化) + // 加载股票行情(并行加载 + 缓存去重) const loadStockQuotes = useCallback( async (stocks) => { if (!stocks || stocks.length === 0) return; + + // 过滤已缓存的股票,只请求未缓存的 + const uncachedStocks = stocks.filter( + (stock) => !stockQuotes[stock.code] + ); + + // 如果全部已缓存,无需请求 + if (uncachedStocks.length === 0) return; + setStockQuotesLoading(true); - // 并行发起所有请求 - const promises = stocks.map(async (stock) => { + // 并行发起未缓存股票的请求 + const promises = uncachedStocks.map(async (stock) => { const code = getSixDigitCode(stock.code); try { const response = await fetch( @@ -304,18 +313,18 @@ const DetailModal = ({ // 等待所有请求完成 const results = await Promise.all(promises); - // 构建 quotes 对象 - const quotes = {}; + // 合并新数据到现有缓存 + const newQuotes = { ...stockQuotes }; results.forEach((result) => { if (result) { - quotes[result.stockCode] = result.quote; + newQuotes[result.stockCode] = result.quote; } }); - setStockQuotes(quotes); + setStockQuotes(newQuotes); setStockQuotesLoading(false); }, - [setStockQuotes, setStockQuotesLoading] + [stockQuotes, setStockQuotes, setStockQuotesLoading] ); // 显示相关股票 diff --git a/src/views/Community/components/ThemeCometChart.js b/src/views/Community/components/ThemeCometChart.js index 2082c702..048ec95a 100644 --- a/src/views/Community/components/ThemeCometChart.js +++ b/src/views/Community/components/ThemeCometChart.js @@ -4,7 +4,7 @@ * Y轴:板块热度(涨停家数) * 支持时间滑动条查看历史数据 */ -import React, { useState, useEffect, useMemo, useCallback } from "react"; +import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { Box, Text, @@ -38,6 +38,9 @@ import { /** * ThemeCometChart 主组件 */ +// 缓存有效期(5 分钟) +const CACHE_DURATION = 5 * 60 * 1000; + const ThemeCometChart = ({ onThemeSelect }) => { const [loading, setLoading] = useState(true); const [allDatesData, setAllDatesData] = useState({}); @@ -48,8 +51,24 @@ const ThemeCometChart = ({ onThemeSelect }) => { const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); - // 加载所有日期的数据 + // 数据缓存(避免 tab 切换时重复请求) + const dataCacheRef = useRef({ data: null, dates: null, timestamp: null }); + + // 加载所有日期的数据(带缓存) const loadAllData = useCallback(async () => { + // 检查缓存是否有效(5分钟内) + const now = Date.now(); + if ( + dataCacheRef.current.timestamp && + now - dataCacheRef.current.timestamp < CACHE_DURATION && + dataCacheRef.current.data + ) { + setAllDatesData(dataCacheRef.current.data); + setAvailableDates(dataCacheRef.current.dates); + setLoading(false); + return; + } + setLoading(true); try { const apiBase = getApiBase(); @@ -109,6 +128,12 @@ const ThemeCometChart = ({ onThemeSelect }) => { } }); + // 存入缓存 + dataCacheRef.current = { + data: dataCache, + dates: dates, + timestamp: Date.now(), + }; setAllDatesData(dataCache); setSliderIndex(0); } else {