perf(DynamicTracking): 子面板支持延迟加载和骨架屏
- ForecastPanel/NewsPanel 接收 isActive 和 activationKey 控制数据加载 - 使用骨架屏替代 Spinner 加载状态 - Tab 切换时自动刷新数据 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* 动态跟踪 - 导航骨架屏组件
|
||||||
|
*
|
||||||
|
* 用于懒加载时显示,让二级导航立即可见
|
||||||
|
* 导航使用真实 UI,内容区域显示骨架屏
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Icon,
|
||||||
|
Skeleton,
|
||||||
|
VStack,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { FaNewspaper, FaBullhorn, FaCalendarAlt, FaChartBar } from 'react-icons/fa';
|
||||||
|
|
||||||
|
// 深空 FUI 主题配置(与 SubTabContainer 保持一致)
|
||||||
|
const DEEP_SPACE = {
|
||||||
|
bgGlass: 'rgba(12, 14, 28, 0.6)',
|
||||||
|
borderGold: 'rgba(212, 175, 55, 0.2)',
|
||||||
|
borderGoldHover: 'rgba(212, 175, 55, 0.5)',
|
||||||
|
glowGold: '0 0 30px rgba(212, 175, 55, 0.25), 0 4px 20px rgba(0, 0, 0, 0.3)',
|
||||||
|
innerGlow: 'inset 0 1px 0 rgba(255, 255, 255, 0.08)',
|
||||||
|
textWhite: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
textDark: '#0A0A14',
|
||||||
|
selectedBg: 'linear-gradient(135deg, rgba(212, 175, 55, 0.95) 0%, rgba(184, 150, 12, 0.95) 100%)',
|
||||||
|
radius: '12px',
|
||||||
|
radiusLG: '16px',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导航配置(与主组件保持同步)
|
||||||
|
const TRACKING_TABS = [
|
||||||
|
{ key: 'news', name: '新闻动态', icon: FaNewspaper },
|
||||||
|
{ key: 'announcements', name: '公司公告', icon: FaBullhorn },
|
||||||
|
{ key: 'disclosure', name: '财报披露日程', icon: FaCalendarAlt },
|
||||||
|
{ key: 'forecast', name: '业绩预告', icon: FaChartBar },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新闻动态内容骨架屏
|
||||||
|
*/
|
||||||
|
const NewsContentSkeleton: React.FC = () => (
|
||||||
|
<VStack spacing={3} align="stretch" p={4}>
|
||||||
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
|
<Card key={i} bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.2)">
|
||||||
|
<CardBody py={3} px={4}>
|
||||||
|
<HStack spacing={3}>
|
||||||
|
<Skeleton height="40px" width="40px" borderRadius="md" startColor="gray.700" endColor="gray.600" />
|
||||||
|
<VStack align="start" flex={1} spacing={2}>
|
||||||
|
<Skeleton height="16px" width="80%" startColor="gray.700" endColor="gray.600" />
|
||||||
|
<Skeleton height="12px" width="40%" startColor="gray.700" endColor="gray.600" />
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DynamicTracking 导航骨架屏
|
||||||
|
*
|
||||||
|
* 显示真实的导航 Tab(默认选中第一个),内容区域显示骨架屏
|
||||||
|
*/
|
||||||
|
const DynamicTrackingNavSkeleton: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 导航栏容器 */}
|
||||||
|
<Flex
|
||||||
|
bg={DEEP_SPACE.bgGlass}
|
||||||
|
backdropFilter="blur(20px)"
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor={DEEP_SPACE.borderGold}
|
||||||
|
borderRadius={DEEP_SPACE.radiusLG}
|
||||||
|
mx={2}
|
||||||
|
mb={2}
|
||||||
|
position="relative"
|
||||||
|
boxShadow={DEEP_SPACE.innerGlow}
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
{/* 顶部金色光条 */}
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left="50%"
|
||||||
|
transform="translateX(-50%)"
|
||||||
|
width="50%"
|
||||||
|
height="1px"
|
||||||
|
background="linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.4), transparent)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tab 列表 */}
|
||||||
|
<Box
|
||||||
|
flex="1"
|
||||||
|
minW={0}
|
||||||
|
overflowX="auto"
|
||||||
|
css={{
|
||||||
|
'&::-webkit-scrollbar': { display: 'none' },
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack
|
||||||
|
border="none"
|
||||||
|
px={3}
|
||||||
|
py={2}
|
||||||
|
flexWrap="nowrap"
|
||||||
|
gap={1.5}
|
||||||
|
>
|
||||||
|
{TRACKING_TABS.map((tab, idx) => {
|
||||||
|
const isSelected = idx === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={tab.key}
|
||||||
|
color={isSelected ? DEEP_SPACE.textDark : DEEP_SPACE.textWhite}
|
||||||
|
borderRadius={DEEP_SPACE.radius}
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
fontSize="13px"
|
||||||
|
fontWeight={isSelected ? '700' : '500'}
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
flexShrink={0}
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={isSelected ? DEEP_SPACE.borderGoldHover : 'transparent'}
|
||||||
|
position="relative"
|
||||||
|
letterSpacing="0.03em"
|
||||||
|
bg={isSelected ? DEEP_SPACE.selectedBg : 'transparent'}
|
||||||
|
boxShadow={isSelected ? DEEP_SPACE.glowGold : 'none'}
|
||||||
|
transform={isSelected ? 'translateY(-2px)' : 'none'}
|
||||||
|
cursor="default"
|
||||||
|
>
|
||||||
|
<HStack spacing={1.5}>
|
||||||
|
<Icon
|
||||||
|
as={tab.icon}
|
||||||
|
boxSize={3.5}
|
||||||
|
opacity={isSelected ? 1 : 0.7}
|
||||||
|
/>
|
||||||
|
<Text>{tab.name}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{/* 内容区域骨架屏 */}
|
||||||
|
<NewsContentSkeleton />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicTrackingNavSkeleton;
|
||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
Text,
|
Text,
|
||||||
Spinner,
|
|
||||||
Center,
|
Center,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
import { ForecastPanelSkeleton } from './DynamicTrackingSkeleton';
|
||||||
import { Tag } from 'antd';
|
import { Tag } from 'antd';
|
||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import axios from '@utils/axiosConfig';
|
import axios from '@utils/axiosConfig';
|
||||||
@@ -42,9 +42,17 @@ const getForecastTypeStyle = (type) => {
|
|||||||
return styles[type] || { color: THEME.gold, bg: THEME.goldLight, border: THEME.goldBorder };
|
return styles[type] || { color: THEME.gold, bg: THEME.goldLight, border: THEME.goldBorder };
|
||||||
};
|
};
|
||||||
|
|
||||||
const ForecastPanel = ({ stockCode }) => {
|
/**
|
||||||
|
* 业绩预告面板
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {string} props.stockCode - 股票代码
|
||||||
|
* @param {boolean} props.isActive - SubTabContainer 传递的激活状态,控制是否加载数据
|
||||||
|
* @param {number} props.activationKey - 激活次数,变化时触发重新请求
|
||||||
|
*/
|
||||||
|
const ForecastPanel = ({ stockCode, isActive = true, activationKey }) => {
|
||||||
const [forecast, setForecast] = useState(null);
|
const [forecast, setForecast] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
// 智能初始化 loading:当需要加载数据时,初始值为 true,避免首次渲染闪现空状态
|
||||||
|
const [loading, setLoading] = useState(() => isActive && !!stockCode);
|
||||||
|
|
||||||
const loadForecast = useCallback(async () => {
|
const loadForecast = useCallback(async () => {
|
||||||
if (!stockCode) return;
|
if (!stockCode) return;
|
||||||
@@ -65,16 +73,15 @@ const ForecastPanel = ({ stockCode }) => {
|
|||||||
}
|
}
|
||||||
}, [stockCode]);
|
}, [stockCode]);
|
||||||
|
|
||||||
|
// 加载数据 - activationKey 变化时也会触发重新请求(实现切换刷新)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadForecast();
|
if (isActive) {
|
||||||
}, [loadForecast]);
|
loadForecast();
|
||||||
|
}
|
||||||
|
}, [isActive, activationKey, loadForecast]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <ForecastPanelSkeleton />;
|
||||||
<Center py={10}>
|
|
||||||
<Spinner size="lg" color={THEME.gold} />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!forecast?.forecasts?.length) {
|
if (!forecast?.forecasts?.length) {
|
||||||
|
|||||||
@@ -6,9 +6,17 @@ import { logger } from '@utils/logger';
|
|||||||
import axios from '@utils/axiosConfig';
|
import axios from '@utils/axiosConfig';
|
||||||
import NewsEventsTab from '../NewsEventsTab';
|
import NewsEventsTab from '../NewsEventsTab';
|
||||||
|
|
||||||
const NewsPanel = ({ stockCode }) => {
|
/**
|
||||||
|
* 新闻动态面板
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {string} props.stockCode - 股票代码
|
||||||
|
* @param {boolean} props.isActive - SubTabContainer 传递的激活状态,控制是否加载数据
|
||||||
|
* @param {number} props.activationKey - 激活次数,变化时触发重新请求
|
||||||
|
*/
|
||||||
|
const NewsPanel = ({ stockCode, isActive = true, activationKey }) => {
|
||||||
const [newsEvents, setNewsEvents] = useState([]);
|
const [newsEvents, setNewsEvents] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
// 智能初始化 loading:当需要加载数据时,初始值为 true,避免首次渲染闪现空状态
|
||||||
|
const [loading, setLoading] = useState(() => isActive && !!stockCode);
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
page: 1,
|
page: 1,
|
||||||
per_page: 10,
|
per_page: 10,
|
||||||
@@ -53,12 +61,12 @@ const NewsPanel = ({ stockCode }) => {
|
|||||||
[stockCode]
|
[stockCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 首次加载 - 直接用股票代码搜索
|
// 加载数据 - activationKey 变化时也会触发重新请求(实现切换刷新)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (stockCode) {
|
if (isActive && stockCode) {
|
||||||
loadNewsEvents(null, 1);
|
loadNewsEvents(null, 1);
|
||||||
}
|
}
|
||||||
}, [stockCode, loadNewsEvents]);
|
}, [stockCode, isActive, activationKey, loadNewsEvents]);
|
||||||
|
|
||||||
// 搜索处理
|
// 搜索处理
|
||||||
const handleSearchChange = (value) => {
|
const handleSearchChange = (value) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
// 优化:子组件懒加载,骨架屏即时反馈
|
// 优化:子组件懒加载,骨架屏即时反馈
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo, useCallback, memo, lazy } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback, memo, lazy } from 'react';
|
||||||
import { Box } from '@chakra-ui/react';
|
import { Card, CardBody } from '@chakra-ui/react';
|
||||||
import { FaNewspaper, FaBullhorn, FaCalendarAlt, FaChartBar } from 'react-icons/fa';
|
import { FaNewspaper, FaBullhorn, FaCalendarAlt, FaChartBar } from 'react-icons/fa';
|
||||||
|
|
||||||
import SubTabContainer from '@components/SubTabContainer';
|
import SubTabContainer from '@components/SubTabContainer';
|
||||||
@@ -92,17 +92,19 @@ const DynamicTracking = memo(({ stockCode: propStockCode }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
|
||||||
<SubTabContainer
|
<CardBody p={0}>
|
||||||
tabs={TRACKING_TABS}
|
<SubTabContainer
|
||||||
componentProps={componentProps}
|
tabs={TRACKING_TABS}
|
||||||
themePreset="blackGold"
|
componentProps={componentProps}
|
||||||
index={activeTab}
|
themePreset="blackGold"
|
||||||
onTabChange={handleTabChange}
|
index={activeTab}
|
||||||
isLazy
|
onTabChange={handleTabChange}
|
||||||
size="sm"
|
isLazy
|
||||||
/>
|
size="sm"
|
||||||
</Box>
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user