perf(DynamicTracking): 优化组件加载体验,子组件懒加载
- 使用 React.lazy() 懒加载所有子面板组件 - 为每个 Tab 添加专属骨架屏 fallback - SubTabContainer 同步渲染,点击立即显示二级导航 - 添加 memo、useCallback、useMemo 性能优化 - 新增 DynamicTrackingSkeleton.tsx 骨架屏组件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* 动态跟踪 Tab 骨架屏组件
|
||||||
|
* 用于懒加载时显示,提供即时反馈
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, VStack, HStack, Skeleton, SkeletonText, Card, CardBody } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新闻动态骨架屏
|
||||||
|
*/
|
||||||
|
export const NewsPanelSkeleton: React.FC = () => (
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
{[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" />
|
||||||
|
<VStack align="start" flex={1} spacing={2}>
|
||||||
|
<Skeleton height="16px" width="80%" />
|
||||||
|
<Skeleton height="12px" width="40%" />
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告列表骨架屏
|
||||||
|
*/
|
||||||
|
export const AnnouncementsSkeleton: React.FC = () => (
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<Card key={i} bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.2)">
|
||||||
|
<CardBody py={3} px={4}>
|
||||||
|
<VStack align="start" spacing={2}>
|
||||||
|
<Skeleton height="16px" width="70%" />
|
||||||
|
<HStack spacing={4}>
|
||||||
|
<Skeleton height="12px" width="80px" />
|
||||||
|
<Skeleton height="12px" width="60px" />
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 财报披露日程骨架屏
|
||||||
|
*/
|
||||||
|
export const DisclosureScheduleSkeleton: React.FC = () => (
|
||||||
|
<Box>
|
||||||
|
<Skeleton height="200px" borderRadius="md" mb={4} />
|
||||||
|
<VStack spacing={2} align="stretch">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<HStack key={i} justify="space-between" p={2}>
|
||||||
|
<Skeleton height="14px" width="100px" />
|
||||||
|
<Skeleton height="14px" width="60px" />
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业绩预告骨架屏
|
||||||
|
*/
|
||||||
|
export const ForecastPanelSkeleton: React.FC = () => (
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
<Card bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.2)">
|
||||||
|
<CardBody>
|
||||||
|
<SkeletonText noOfLines={4} spacing={3} />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
<Card bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.2)">
|
||||||
|
<CardBody>
|
||||||
|
<Skeleton height="120px" borderRadius="md" />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用内容骨架屏(默认 fallback)
|
||||||
|
*/
|
||||||
|
export const ContentSkeleton: React.FC = () => (
|
||||||
|
<Box p={4}>
|
||||||
|
<SkeletonText noOfLines={6} spacing={4} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
NewsPanelSkeleton,
|
||||||
|
AnnouncementsSkeleton,
|
||||||
|
DisclosureScheduleSkeleton,
|
||||||
|
ForecastPanelSkeleton,
|
||||||
|
ContentSkeleton,
|
||||||
|
};
|
||||||
@@ -2,3 +2,12 @@
|
|||||||
|
|
||||||
export { default as NewsPanel } from './NewsPanel';
|
export { default as NewsPanel } from './NewsPanel';
|
||||||
export { default as ForecastPanel } from './ForecastPanel';
|
export { default as ForecastPanel } from './ForecastPanel';
|
||||||
|
|
||||||
|
// 骨架屏组件
|
||||||
|
export {
|
||||||
|
NewsPanelSkeleton,
|
||||||
|
AnnouncementsSkeleton,
|
||||||
|
DisclosureScheduleSkeleton,
|
||||||
|
ForecastPanelSkeleton,
|
||||||
|
ContentSkeleton,
|
||||||
|
} from './DynamicTrackingSkeleton';
|
||||||
|
|||||||
@@ -1,37 +1,73 @@
|
|||||||
// src/views/Company/components/DynamicTracking/index.js
|
// src/views/Company/components/DynamicTracking/index.js
|
||||||
// 动态跟踪 - 独立一级 Tab 组件(包含新闻动态等二级 Tab)
|
// 动态跟踪 - 独立一级 Tab 组件(包含新闻动态等二级 Tab)
|
||||||
|
// 优化:子组件懒加载,骨架屏即时反馈
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo, useCallback, memo, lazy } from 'react';
|
||||||
import { Box } from '@chakra-ui/react';
|
import { Box } 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';
|
||||||
import AnnouncementsPanel from '../CompanyOverview/BasicInfoTab/components/AnnouncementsPanel';
|
import {
|
||||||
import DisclosureSchedulePanel from '../CompanyOverview/BasicInfoTab/components/DisclosureSchedulePanel';
|
NewsPanelSkeleton,
|
||||||
import { NewsPanel, ForecastPanel } from './components';
|
AnnouncementsSkeleton,
|
||||||
|
DisclosureScheduleSkeleton,
|
||||||
|
ForecastPanelSkeleton,
|
||||||
|
} from './components/DynamicTrackingSkeleton';
|
||||||
|
|
||||||
// 二级 Tab 配置
|
// 懒加载子组件
|
||||||
|
const NewsPanel = lazy(() => import('./components/NewsPanel'));
|
||||||
|
const ForecastPanel = lazy(() => import('./components/ForecastPanel'));
|
||||||
|
const AnnouncementsPanel = lazy(() =>
|
||||||
|
import('../CompanyOverview/BasicInfoTab/components/AnnouncementsPanel')
|
||||||
|
);
|
||||||
|
const DisclosureSchedulePanel = lazy(() =>
|
||||||
|
import('../CompanyOverview/BasicInfoTab/components/DisclosureSchedulePanel')
|
||||||
|
);
|
||||||
|
|
||||||
|
// 二级 Tab 配置(带骨架屏 fallback)
|
||||||
const TRACKING_TABS = [
|
const TRACKING_TABS = [
|
||||||
{ key: 'news', name: '新闻动态', icon: FaNewspaper, component: NewsPanel },
|
{
|
||||||
{ key: 'announcements', name: '公司公告', icon: FaBullhorn, component: AnnouncementsPanel },
|
key: 'news',
|
||||||
{ key: 'disclosure', name: '财报披露日程', icon: FaCalendarAlt, component: DisclosureSchedulePanel },
|
name: '新闻动态',
|
||||||
{ key: 'forecast', name: '业绩预告', icon: FaChartBar, component: ForecastPanel },
|
icon: FaNewspaper,
|
||||||
|
component: NewsPanel,
|
||||||
|
fallback: <NewsPanelSkeleton />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'announcements',
|
||||||
|
name: '公司公告',
|
||||||
|
icon: FaBullhorn,
|
||||||
|
component: AnnouncementsPanel,
|
||||||
|
fallback: <AnnouncementsSkeleton />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'disclosure',
|
||||||
|
name: '财报披露日程',
|
||||||
|
icon: FaCalendarAlt,
|
||||||
|
component: DisclosureSchedulePanel,
|
||||||
|
fallback: <DisclosureScheduleSkeleton />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'forecast',
|
||||||
|
name: '业绩预告',
|
||||||
|
icon: FaChartBar,
|
||||||
|
component: ForecastPanel,
|
||||||
|
fallback: <ForecastPanelSkeleton />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态跟踪组件
|
* 动态跟踪组件
|
||||||
*
|
*
|
||||||
* 功能:
|
* 功能:
|
||||||
* - 使用 SubTabContainer 实现二级导航
|
* - 使用 SubTabContainer 实现二级导航(同步渲染,无 loading)
|
||||||
* - Tab1: 新闻动态
|
* - 子组件懒加载,减少初始包体积
|
||||||
* - Tab2: 公司公告
|
* - 每个 Tab 有专属骨架屏,提供即时视觉反馈
|
||||||
* - Tab3: 财报披露日程
|
|
||||||
* - Tab4: 业绩预告
|
|
||||||
*
|
*
|
||||||
* @param {Object} props
|
* @param {Object} props
|
||||||
* @param {string} props.stockCode - 股票代码
|
* @param {string} props.stockCode - 股票代码
|
||||||
*/
|
*/
|
||||||
const DynamicTracking = ({ stockCode: propStockCode }) => {
|
const DynamicTracking = memo(({ stockCode: propStockCode }) => {
|
||||||
const [stockCode, setStockCode] = useState(propStockCode || '000001');
|
const [stockCode, setStockCode] = useState(propStockCode || '000001');
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
@@ -50,6 +86,11 @@ const DynamicTracking = ({ stockCode: propStockCode }) => {
|
|||||||
[stockCode]
|
[stockCode]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tab 切换回调
|
||||||
|
const handleTabChange = useCallback((index) => {
|
||||||
|
setActiveTab(index);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<SubTabContainer
|
<SubTabContainer
|
||||||
@@ -57,12 +98,14 @@ const DynamicTracking = ({ stockCode: propStockCode }) => {
|
|||||||
componentProps={componentProps}
|
componentProps={componentProps}
|
||||||
themePreset="blackGold"
|
themePreset="blackGold"
|
||||||
index={activeTab}
|
index={activeTab}
|
||||||
onTabChange={(index) => setActiveTab(index)}
|
onTabChange={handleTabChange}
|
||||||
isLazy
|
isLazy
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
DynamicTracking.displayName = 'DynamicTracking';
|
||||||
|
|
||||||
export default DynamicTracking;
|
export default DynamicTracking;
|
||||||
|
|||||||
Reference in New Issue
Block a user