DOM 操作优化与缓存管理
性能优化 - React.memo、API并行化、useReducer重构
This commit is contained in:
@@ -18,15 +18,10 @@ import { Box, Text, VStack, Tooltip } from "@chakra-ui/react";
|
|||||||
import { keyframes } from "@emotion/react";
|
import { keyframes } from "@emotion/react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
// 动画定义
|
// 动画定义 - 使用 transform 代替 background-position(GPU 加速)
|
||||||
const shimmer = keyframes`
|
const shimmer = keyframes`
|
||||||
0% { background-position: -200% 0; }
|
0% { transform: translateX(-100%); }
|
||||||
100% { background-position: 200% 0; }
|
100% { transform: translateX(100%); }
|
||||||
`;
|
|
||||||
|
|
||||||
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); }
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -408,12 +403,9 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
|
|||||||
const dateStr = dayjs(date).format("YYYYMMDD");
|
const dateStr = dayjs(date).format("YYYYMMDD");
|
||||||
const dateData = dataMapRef.current.get(dateStr);
|
const dateData = dataMapRef.current.get(dateStr);
|
||||||
|
|
||||||
// 找到 day-top 容器并插入自定义内容
|
// 找到 day-top 容器并插入自定义内容(直接替换,无需先清空)
|
||||||
const dayTop = el.querySelector(".fc-daygrid-day-top");
|
const dayTop = el.querySelector(".fc-daygrid-day-top");
|
||||||
if (dayTop) {
|
if (dayTop) {
|
||||||
// 清空默认内容
|
|
||||||
dayTop.innerHTML = "";
|
|
||||||
// 插入自定义内容
|
|
||||||
dayTop.innerHTML = createCellContentHTML(date, dateData, isToday);
|
dayTop.innerHTML = createCellContentHTML(date, dateData, isToday);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -463,17 +455,17 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
|
|||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
position="relative"
|
position="relative"
|
||||||
>
|
>
|
||||||
{/* 闪光效果 */}
|
{/* 闪光效果 - 使用 transform 实现 GPU 加速 */}
|
||||||
<Box
|
<Box
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top="0"
|
top="0"
|
||||||
left="0"
|
left="0"
|
||||||
right="0"
|
w="100%"
|
||||||
bottom="0"
|
h="100%"
|
||||||
bgGradient="linear(to-r, transparent, rgba(255,255,255,0.3), transparent)"
|
bgGradient="linear(to-r, transparent, rgba(255,255,255,0.4), transparent)"
|
||||||
backgroundSize="200% 100%"
|
animation={`${shimmer} 2.5s ease-in-out infinite`}
|
||||||
animation={`${shimmer} 3s linear infinite`}
|
opacity={0.6}
|
||||||
opacity={0.5}
|
pointerEvents="none"
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
@@ -579,7 +571,8 @@ export const FullCalendarPro: React.FC<FullCalendarProProps> = ({
|
|||||||
},
|
},
|
||||||
".fc-daygrid-day.fc-day-today": {
|
".fc-daygrid-day.fc-day-today": {
|
||||||
bg: "rgba(212, 175, 55, 0.15) !important",
|
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": {
|
".fc-daygrid-day-frame": {
|
||||||
minHeight: "50px",
|
minHeight: "50px",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// HeroPanel - 综合日历组件
|
// HeroPanel - 综合日历组件
|
||||||
import React, { useState, useEffect, useCallback, Suspense, lazy, memo } from "react";
|
import React, { useState, useEffect, useCallback, Suspense, lazy, memo, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
HStack,
|
HStack,
|
||||||
@@ -43,12 +43,23 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
|||||||
const [detailLoading, setDetailLoading] = useState(false);
|
const [detailLoading, setDetailLoading] = useState(false);
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
|
||||||
// 加载日历综合数据(一次 API 调用获取所有数据)
|
// 月份数据缓存(避免切换月份后再切回时重复请求)
|
||||||
|
const monthCacheRef = useRef({});
|
||||||
|
|
||||||
|
// 加载日历综合数据(带缓存)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCalendarCombinedData = async () => {
|
const loadCalendarCombinedData = async () => {
|
||||||
try {
|
|
||||||
const year = currentMonth.getFullYear();
|
const year = currentMonth.getFullYear();
|
||||||
const month = currentMonth.getMonth() + 1;
|
const month = currentMonth.getMonth() + 1;
|
||||||
|
const cacheKey = `${year}-${month}`;
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
if (monthCacheRef.current[cacheKey]) {
|
||||||
|
setCalendarData(monthCacheRef.current[cacheKey]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${getApiBase()}/api/v1/calendar/combined-data?year=${year}&month=${month}`
|
`${getApiBase()}/api/v1/calendar/combined-data?year=${year}&month=${month}`
|
||||||
);
|
);
|
||||||
@@ -63,10 +74,8 @@ const CombinedCalendar = memo(({ DetailModal }) => {
|
|||||||
eventCount: item.event_count || 0,
|
eventCount: item.event_count || 0,
|
||||||
indexChange: item.index_change,
|
indexChange: item.index_change,
|
||||||
}));
|
}));
|
||||||
console.log(
|
// 存入缓存
|
||||||
"[HeroPanel] 加载日历综合数据成功,数据条数:",
|
monthCacheRef.current[cacheKey] = formattedData;
|
||||||
formattedData.length
|
|
||||||
);
|
|
||||||
setCalendarData(formattedData);
|
setCalendarData(formattedData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,14 +268,23 @@ const DetailModal = ({
|
|||||||
[dispatch, isStockInWatchlist]
|
[dispatch, isStockInWatchlist]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 加载股票行情(并行加载优化)
|
// 加载股票行情(并行加载 + 缓存去重)
|
||||||
const loadStockQuotes = useCallback(
|
const loadStockQuotes = useCallback(
|
||||||
async (stocks) => {
|
async (stocks) => {
|
||||||
if (!stocks || stocks.length === 0) return;
|
if (!stocks || stocks.length === 0) return;
|
||||||
|
|
||||||
|
// 过滤已缓存的股票,只请求未缓存的
|
||||||
|
const uncachedStocks = stocks.filter(
|
||||||
|
(stock) => !stockQuotes[stock.code]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果全部已缓存,无需请求
|
||||||
|
if (uncachedStocks.length === 0) return;
|
||||||
|
|
||||||
setStockQuotesLoading(true);
|
setStockQuotesLoading(true);
|
||||||
|
|
||||||
// 并行发起所有请求
|
// 并行发起未缓存股票的请求
|
||||||
const promises = stocks.map(async (stock) => {
|
const promises = uncachedStocks.map(async (stock) => {
|
||||||
const code = getSixDigitCode(stock.code);
|
const code = getSixDigitCode(stock.code);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -304,18 +313,18 @@ const DetailModal = ({
|
|||||||
// 等待所有请求完成
|
// 等待所有请求完成
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
// 构建 quotes 对象
|
// 合并新数据到现有缓存
|
||||||
const quotes = {};
|
const newQuotes = { ...stockQuotes };
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
quotes[result.stockCode] = result.quote;
|
newQuotes[result.stockCode] = result.quote;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setStockQuotes(quotes);
|
setStockQuotes(newQuotes);
|
||||||
setStockQuotesLoading(false);
|
setStockQuotesLoading(false);
|
||||||
},
|
},
|
||||||
[setStockQuotes, setStockQuotesLoading]
|
[stockQuotes, setStockQuotes, setStockQuotesLoading]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 显示相关股票
|
// 显示相关股票
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Y轴:板块热度(涨停家数)
|
* Y轴:板块热度(涨停家数)
|
||||||
* 支持时间滑动条查看历史数据
|
* 支持时间滑动条查看历史数据
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
import React, { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Text,
|
Text,
|
||||||
@@ -38,6 +38,9 @@ import {
|
|||||||
/**
|
/**
|
||||||
* ThemeCometChart 主组件
|
* ThemeCometChart 主组件
|
||||||
*/
|
*/
|
||||||
|
// 缓存有效期(5 分钟)
|
||||||
|
const CACHE_DURATION = 5 * 60 * 1000;
|
||||||
|
|
||||||
const ThemeCometChart = ({ onThemeSelect }) => {
|
const ThemeCometChart = ({ onThemeSelect }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [allDatesData, setAllDatesData] = useState({});
|
const [allDatesData, setAllDatesData] = useState({});
|
||||||
@@ -48,8 +51,24 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// 加载所有日期的数据
|
// 数据缓存(避免 tab 切换时重复请求)
|
||||||
|
const dataCacheRef = useRef({ data: null, dates: null, timestamp: null });
|
||||||
|
|
||||||
|
// 加载所有日期的数据(带缓存)
|
||||||
const loadAllData = useCallback(async () => {
|
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);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const apiBase = getApiBase();
|
const apiBase = getApiBase();
|
||||||
@@ -109,6 +128,12 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 存入缓存
|
||||||
|
dataCacheRef.current = {
|
||||||
|
data: dataCache,
|
||||||
|
dates: dates,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
setAllDatesData(dataCache);
|
setAllDatesData(dataCache);
|
||||||
setSliderIndex(0);
|
setSliderIndex(0);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user