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,
|
||||
Flex,
|
||||
Text,
|
||||
Spinner,
|
||||
Center,
|
||||
} from '@chakra-ui/react';
|
||||
import { ForecastPanelSkeleton } from './DynamicTrackingSkeleton';
|
||||
import { Tag } from 'antd';
|
||||
import { logger } from '@utils/logger';
|
||||
import axios from '@utils/axiosConfig';
|
||||
@@ -42,9 +42,17 @@ const getForecastTypeStyle = (type) => {
|
||||
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 [loading, setLoading] = useState(false);
|
||||
// 智能初始化 loading:当需要加载数据时,初始值为 true,避免首次渲染闪现空状态
|
||||
const [loading, setLoading] = useState(() => isActive && !!stockCode);
|
||||
|
||||
const loadForecast = useCallback(async () => {
|
||||
if (!stockCode) return;
|
||||
@@ -65,16 +73,15 @@ const ForecastPanel = ({ stockCode }) => {
|
||||
}
|
||||
}, [stockCode]);
|
||||
|
||||
// 加载数据 - activationKey 变化时也会触发重新请求(实现切换刷新)
|
||||
useEffect(() => {
|
||||
loadForecast();
|
||||
}, [loadForecast]);
|
||||
if (isActive) {
|
||||
loadForecast();
|
||||
}
|
||||
}, [isActive, activationKey, loadForecast]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Center py={10}>
|
||||
<Spinner size="lg" color={THEME.gold} />
|
||||
</Center>
|
||||
);
|
||||
return <ForecastPanelSkeleton />;
|
||||
}
|
||||
|
||||
if (!forecast?.forecasts?.length) {
|
||||
|
||||
@@ -6,9 +6,17 @@ import { logger } from '@utils/logger';
|
||||
import axios from '@utils/axiosConfig';
|
||||
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 [loading, setLoading] = useState(false);
|
||||
// 智能初始化 loading:当需要加载数据时,初始值为 true,避免首次渲染闪现空状态
|
||||
const [loading, setLoading] = useState(() => isActive && !!stockCode);
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
@@ -53,12 +61,12 @@ const NewsPanel = ({ stockCode }) => {
|
||||
[stockCode]
|
||||
);
|
||||
|
||||
// 首次加载 - 直接用股票代码搜索
|
||||
// 加载数据 - activationKey 变化时也会触发重新请求(实现切换刷新)
|
||||
useEffect(() => {
|
||||
if (stockCode) {
|
||||
if (isActive && stockCode) {
|
||||
loadNewsEvents(null, 1);
|
||||
}
|
||||
}, [stockCode, loadNewsEvents]);
|
||||
}, [stockCode, isActive, activationKey, loadNewsEvents]);
|
||||
|
||||
// 搜索处理
|
||||
const handleSearchChange = (value) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// 优化:子组件懒加载,骨架屏即时反馈
|
||||
|
||||
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 SubTabContainer from '@components/SubTabContainer';
|
||||
@@ -92,17 +92,19 @@ const DynamicTracking = memo(({ stockCode: propStockCode }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<SubTabContainer
|
||||
tabs={TRACKING_TABS}
|
||||
componentProps={componentProps}
|
||||
themePreset="blackGold"
|
||||
index={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
isLazy
|
||||
size="sm"
|
||||
/>
|
||||
</Box>
|
||||
<Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
|
||||
<CardBody p={0}>
|
||||
<SubTabContainer
|
||||
tabs={TRACKING_TABS}
|
||||
componentProps={componentProps}
|
||||
themePreset="blackGold"
|
||||
index={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
isLazy
|
||||
size="sm"
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user