feat(BasicInfoTab): 添加骨架屏并适配延迟加载
- 各 Panel 组件适配新的 hooks 参数格式 - 新增 BasicInfoTabSkeleton 骨架屏组件 - 新增 CompanyOverviewNavSkeleton 导航骨架屏组件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -31,10 +31,14 @@ import LoadingState from "./LoadingState";
|
|||||||
|
|
||||||
interface AnnouncementsPanelProps {
|
interface AnnouncementsPanelProps {
|
||||||
stockCode: string;
|
stockCode: string;
|
||||||
|
/** SubTabContainer 传递的激活状态,控制是否加载数据 */
|
||||||
|
isActive?: boolean;
|
||||||
|
/** 激活次数,变化时触发重新请求 */
|
||||||
|
activationKey?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnnouncementsPanel: React.FC<AnnouncementsPanelProps> = ({ stockCode }) => {
|
const AnnouncementsPanel: React.FC<AnnouncementsPanelProps> = ({ stockCode, isActive = true, activationKey }) => {
|
||||||
const { announcements, loading } = useAnnouncementsData(stockCode);
|
const { announcements, loading } = useAnnouncementsData({ stockCode, enabled: isActive, refreshKey: activationKey });
|
||||||
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const [selectedAnnouncement, setSelectedAnnouncement] = useState<any>(null);
|
const [selectedAnnouncement, setSelectedAnnouncement] = useState<any>(null);
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
/**
|
||||||
|
* BasicInfoTab 骨架屏组件
|
||||||
|
* 用于各个 Tab 面板的加载状态显示
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
SimpleGrid,
|
||||||
|
Skeleton,
|
||||||
|
SkeletonText,
|
||||||
|
SkeletonCircle,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
// 黑金主题骨架屏样式
|
||||||
|
const skeletonStyles = {
|
||||||
|
startColor: 'rgba(212, 175, 55, 0.1)',
|
||||||
|
endColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 卡片骨架屏样式
|
||||||
|
const cardStyle = {
|
||||||
|
bg: 'linear-gradient(145deg, rgba(30, 30, 35, 0.95), rgba(20, 20, 25, 0.98))',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
p: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分支机构骨架屏
|
||||||
|
*/
|
||||||
|
export const BranchesSkeleton: React.FC = () => (
|
||||||
|
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<Box key={i} sx={cardStyle}>
|
||||||
|
{/* 顶部金色装饰线 */}
|
||||||
|
<Box
|
||||||
|
h="2px"
|
||||||
|
bgGradient="linear(to-r, transparent, rgba(212, 175, 55, 0.3), transparent)"
|
||||||
|
mb={4}
|
||||||
|
/>
|
||||||
|
<VStack align="start" spacing={4}>
|
||||||
|
{/* 标题行 */}
|
||||||
|
<HStack justify="space-between" w="full">
|
||||||
|
<HStack spacing={2} flex={1}>
|
||||||
|
<Skeleton
|
||||||
|
{...skeletonStyles}
|
||||||
|
height="28px"
|
||||||
|
width="28px"
|
||||||
|
borderRadius="md"
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
{...skeletonStyles}
|
||||||
|
height="16px"
|
||||||
|
width="60%"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<Skeleton
|
||||||
|
{...skeletonStyles}
|
||||||
|
height="22px"
|
||||||
|
width="60px"
|
||||||
|
borderRadius="full"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 分隔线 */}
|
||||||
|
<Box
|
||||||
|
w="full"
|
||||||
|
h="1px"
|
||||||
|
bgGradient="linear(to-r, rgba(212, 175, 55, 0.2), transparent)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 信息网格 */}
|
||||||
|
<SimpleGrid columns={2} spacing={3} w="full">
|
||||||
|
{[1, 2, 3, 4].map((j) => (
|
||||||
|
<VStack key={j} align="start" spacing={1}>
|
||||||
|
<Skeleton {...skeletonStyles} height="12px" width="50px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" width="80px" />
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工商信息骨架屏
|
||||||
|
*/
|
||||||
|
export const BusinessInfoSkeleton: React.FC = () => (
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
{/* 上半部分:工商信息 + 服务机构 */}
|
||||||
|
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={4}>
|
||||||
|
{/* 工商信息卡片 */}
|
||||||
|
<Box sx={cardStyle}>
|
||||||
|
<HStack spacing={2} mb={4}>
|
||||||
|
<Skeleton {...skeletonStyles} height="16px" width="16px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="16px" width="80px" />
|
||||||
|
</HStack>
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<HStack key={i} spacing={3} p={2}>
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" width="14px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" width="60px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" flex={1} />
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 服务机构卡片 */}
|
||||||
|
<Box sx={cardStyle}>
|
||||||
|
<HStack spacing={2} mb={4}>
|
||||||
|
<Skeleton {...skeletonStyles} height="16px" width="16px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="16px" width="80px" />
|
||||||
|
</HStack>
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<Box key={i} p={4} borderRadius="10px" bg="rgba(255,255,255,0.02)">
|
||||||
|
<HStack spacing={2} mb={2}>
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" width="14px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="12px" width="80px" />
|
||||||
|
</HStack>
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" width="70%" />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{/* 下半部分:主营业务 + 经营范围 */}
|
||||||
|
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={4}>
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<Box key={i} sx={cardStyle}>
|
||||||
|
<HStack spacing={2} mb={4}>
|
||||||
|
<Skeleton {...skeletonStyles} height="16px" width="16px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="16px" width="80px" />
|
||||||
|
</HStack>
|
||||||
|
<SkeletonText
|
||||||
|
{...skeletonStyles}
|
||||||
|
noOfLines={4}
|
||||||
|
spacing={3}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 股权结构骨架屏
|
||||||
|
*/
|
||||||
|
export const ShareholderSkeleton: React.FC = () => (
|
||||||
|
<Box p={4}>
|
||||||
|
<VStack spacing={6} align="stretch">
|
||||||
|
{/* 实际控制人 + 股权集中度 */}
|
||||||
|
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<Box key={i} sx={cardStyle}>
|
||||||
|
<HStack spacing={2} mb={4}>
|
||||||
|
<Skeleton {...skeletonStyles} height="18px" width="18px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="18px" width="100px" />
|
||||||
|
</HStack>
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
{[1, 2, 3].map((j) => (
|
||||||
|
<HStack key={j} justify="space-between">
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" width="80px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" width="60px" />
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{/* 十大股东表格 */}
|
||||||
|
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
||||||
|
{[1, 2].map((i) => (
|
||||||
|
<Box key={i} sx={cardStyle}>
|
||||||
|
<HStack spacing={2} mb={4}>
|
||||||
|
<Skeleton {...skeletonStyles} height="18px" width="18px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="18px" width="100px" />
|
||||||
|
</HStack>
|
||||||
|
<VStack spacing={2} align="stretch">
|
||||||
|
{/* 表头 */}
|
||||||
|
<HStack spacing={4} pb={2} borderBottom="1px solid" borderColor="rgba(212, 175, 55, 0.1)">
|
||||||
|
<Skeleton {...skeletonStyles} height="12px" width="30px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="12px" flex={1} />
|
||||||
|
<Skeleton {...skeletonStyles} height="12px" width="60px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="12px" width="60px" />
|
||||||
|
</HStack>
|
||||||
|
{/* 表格行 */}
|
||||||
|
{[1, 2, 3, 4, 5].map((j) => (
|
||||||
|
<HStack key={j} spacing={4} py={2}>
|
||||||
|
<SkeletonCircle {...skeletonStyles} size="6" />
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" flex={1} />
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" width="60px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="14px" width="60px" />
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理团队骨架屏
|
||||||
|
*/
|
||||||
|
export const ManagementSkeleton: React.FC = () => (
|
||||||
|
<Box p={4}>
|
||||||
|
<VStack spacing={6} align="stretch">
|
||||||
|
{/* 每个分类 */}
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<Box key={i}>
|
||||||
|
{/* 分类标题 */}
|
||||||
|
<HStack spacing={2} mb={4}>
|
||||||
|
<Skeleton {...skeletonStyles} height="20px" width="20px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="18px" width="80px" />
|
||||||
|
<Skeleton
|
||||||
|
{...skeletonStyles}
|
||||||
|
height="20px"
|
||||||
|
width="30px"
|
||||||
|
borderRadius="full"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 人员卡片网格 */}
|
||||||
|
<SimpleGrid columns={{ base: 2, md: 3, lg: 4 }} spacing={4}>
|
||||||
|
{[1, 2, 3, 4].map((j) => (
|
||||||
|
<Box key={j} sx={cardStyle}>
|
||||||
|
<VStack spacing={3}>
|
||||||
|
<SkeletonCircle {...skeletonStyles} size="12" />
|
||||||
|
<Skeleton {...skeletonStyles} height="16px" width="60px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="12px" width="80px" />
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Skeleton {...skeletonStyles} height="10px" width="40px" />
|
||||||
|
<Skeleton {...skeletonStyles} height="10px" width="40px" />
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用内容骨架屏
|
||||||
|
*/
|
||||||
|
export const ContentSkeleton: React.FC = () => (
|
||||||
|
<Box p={4}>
|
||||||
|
<SkeletonText {...skeletonStyles} noOfLines={6} spacing={4} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
BranchesSkeleton,
|
||||||
|
BusinessInfoSkeleton,
|
||||||
|
ShareholderSkeleton,
|
||||||
|
ManagementSkeleton,
|
||||||
|
ContentSkeleton,
|
||||||
|
};
|
||||||
@@ -16,10 +16,12 @@ import { FaSitemap, FaBuilding, FaCheckCircle, FaTimesCircle } from "react-icons
|
|||||||
import { useBranchesData } from "../../hooks/useBranchesData";
|
import { useBranchesData } from "../../hooks/useBranchesData";
|
||||||
import { THEME } from "../config";
|
import { THEME } from "../config";
|
||||||
import { formatDate } from "../utils";
|
import { formatDate } from "../utils";
|
||||||
import LoadingState from "./LoadingState";
|
import { BranchesSkeleton } from "./BasicInfoTabSkeleton";
|
||||||
|
|
||||||
interface BranchesPanelProps {
|
interface BranchesPanelProps {
|
||||||
stockCode: string;
|
stockCode: string;
|
||||||
|
/** SubTabContainer 传递的激活状态,控制是否加载数据 */
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 黑金卡片样式
|
// 黑金卡片样式
|
||||||
@@ -65,11 +67,11 @@ const InfoItem: React.FC<{ label: string; value: string | number }> = ({ label,
|
|||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|
||||||
const BranchesPanel: React.FC<BranchesPanelProps> = ({ stockCode }) => {
|
const BranchesPanel: React.FC<BranchesPanelProps> = ({ stockCode, isActive = true }) => {
|
||||||
const { branches, loading } = useBranchesData(stockCode);
|
const { branches, loading } = useBranchesData({ stockCode, enabled: isActive });
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingState message="加载分支机构数据..." />;
|
return <BranchesSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (branches.length === 0) {
|
if (branches.length === 0) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Center,
|
Center,
|
||||||
Icon,
|
Icon,
|
||||||
Spinner,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import {
|
import {
|
||||||
FaBuilding,
|
FaBuilding,
|
||||||
@@ -27,9 +26,12 @@ import {
|
|||||||
import { COLORS, GLASS, glassCardStyle } from "@views/Company/theme";
|
import { COLORS, GLASS, glassCardStyle } from "@views/Company/theme";
|
||||||
import { THEME } from "../config";
|
import { THEME } from "../config";
|
||||||
import { useBasicInfo } from "../../hooks/useBasicInfo";
|
import { useBasicInfo } from "../../hooks/useBasicInfo";
|
||||||
|
import { BusinessInfoSkeleton } from "./BasicInfoTabSkeleton";
|
||||||
|
|
||||||
interface BusinessInfoPanelProps {
|
interface BusinessInfoPanelProps {
|
||||||
stockCode: string;
|
stockCode: string;
|
||||||
|
/** SubTabContainer 传递的激活状态,控制是否加载数据 */
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 区块标题组件
|
// 区块标题组件
|
||||||
@@ -150,15 +152,11 @@ const TextSection: React.FC<{
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
const BusinessInfoPanel: React.FC<BusinessInfoPanelProps> = ({ stockCode }) => {
|
const BusinessInfoPanel: React.FC<BusinessInfoPanelProps> = ({ stockCode, isActive = true }) => {
|
||||||
const { basicInfo, loading } = useBasicInfo(stockCode);
|
const { basicInfo, loading } = useBasicInfo({ stockCode, enabled: isActive });
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return <BusinessInfoSkeleton />;
|
||||||
<Center h="200px">
|
|
||||||
<Spinner size="lg" color={THEME.gold} />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!basicInfo) {
|
if (!basicInfo) {
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* 公司概览 - 导航骨架屏组件
|
||||||
|
*
|
||||||
|
* 用于懒加载时显示,让二级导航立即可见
|
||||||
|
* 导航使用真实 UI,内容区域显示骨架屏
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Icon,
|
||||||
|
Skeleton,
|
||||||
|
VStack,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
Table,
|
||||||
|
Thead,
|
||||||
|
Tbody,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Td,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
FaShareAlt,
|
||||||
|
FaUserTie,
|
||||||
|
FaSitemap,
|
||||||
|
FaInfoCircle,
|
||||||
|
} 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',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导航配置(与主组件 config.ts 保持同步)
|
||||||
|
const OVERVIEW_TABS = [
|
||||||
|
{ key: 'shareholder', name: '股权结构', icon: FaShareAlt },
|
||||||
|
{ key: 'management', name: '管理团队', icon: FaUserTie },
|
||||||
|
{ key: 'branches', name: '分支机构', icon: FaSitemap },
|
||||||
|
{ key: 'business', name: '工商信息', icon: FaInfoCircle },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 股权结构内容骨架屏
|
||||||
|
*/
|
||||||
|
const ShareholderContentSkeleton: React.FC = () => (
|
||||||
|
<Box p={4}>
|
||||||
|
{/* 表格骨架屏 */}
|
||||||
|
<Card bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.2)">
|
||||||
|
<CardBody p={0}>
|
||||||
|
<Table variant="simple" size="sm">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th borderColor="rgba(212, 175, 55, 0.2)" color="gray.400">
|
||||||
|
<Skeleton height="14px" width="60px" startColor="gray.700" endColor="gray.600" />
|
||||||
|
</Th>
|
||||||
|
<Th borderColor="rgba(212, 175, 55, 0.2)" color="gray.400">
|
||||||
|
<Skeleton height="14px" width="80px" startColor="gray.700" endColor="gray.600" />
|
||||||
|
</Th>
|
||||||
|
<Th borderColor="rgba(212, 175, 55, 0.2)" color="gray.400">
|
||||||
|
<Skeleton height="14px" width="60px" startColor="gray.700" endColor="gray.600" />
|
||||||
|
</Th>
|
||||||
|
<Th borderColor="rgba(212, 175, 55, 0.2)" color="gray.400">
|
||||||
|
<Skeleton height="14px" width="70px" startColor="gray.700" endColor="gray.600" />
|
||||||
|
</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{[1, 2, 3, 4, 5].map((i) => (
|
||||||
|
<Tr key={i}>
|
||||||
|
<Td borderColor="rgba(212, 175, 55, 0.2)">
|
||||||
|
<Skeleton height="14px" width="120px" startColor="gray.700" endColor="gray.600" />
|
||||||
|
</Td>
|
||||||
|
<Td borderColor="rgba(212, 175, 55, 0.2)">
|
||||||
|
<Skeleton height="14px" width="80px" startColor="gray.700" endColor="gray.600" />
|
||||||
|
</Td>
|
||||||
|
<Td borderColor="rgba(212, 175, 55, 0.2)">
|
||||||
|
<Skeleton height="14px" width="60px" startColor="gray.700" endColor="gray.600" />
|
||||||
|
</Td>
|
||||||
|
<Td borderColor="rgba(212, 175, 55, 0.2)">
|
||||||
|
<Skeleton height="14px" width="80px" startColor="gray.700" endColor="gray.600" />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CompanyOverview 导航骨架屏
|
||||||
|
*
|
||||||
|
* 显示真实的导航 Tab(默认选中第一个),内容区域显示骨架屏
|
||||||
|
*/
|
||||||
|
const CompanyOverviewNavSkeleton: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 导航栏容器 - compact 模式(无外边距) */}
|
||||||
|
<Flex
|
||||||
|
bg={DEEP_SPACE.bgGlass}
|
||||||
|
backdropFilter="blur(20px)"
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor={DEEP_SPACE.borderGold}
|
||||||
|
borderRadius={0}
|
||||||
|
mx={0}
|
||||||
|
mb={0}
|
||||||
|
position="relative"
|
||||||
|
boxShadow="none"
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
{OVERVIEW_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>
|
||||||
|
|
||||||
|
{/* 内容区域骨架屏 */}
|
||||||
|
<ShareholderContentSkeleton />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompanyOverviewNavSkeleton;
|
||||||
@@ -19,10 +19,12 @@ import LoadingState from "./LoadingState";
|
|||||||
|
|
||||||
interface DisclosureSchedulePanelProps {
|
interface DisclosureSchedulePanelProps {
|
||||||
stockCode: string;
|
stockCode: string;
|
||||||
|
/** SubTabContainer 传递的激活状态,控制是否加载数据 */
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DisclosureSchedulePanel: React.FC<DisclosureSchedulePanelProps> = ({ stockCode }) => {
|
const DisclosureSchedulePanel: React.FC<DisclosureSchedulePanelProps> = ({ stockCode, isActive = true }) => {
|
||||||
const { disclosureSchedule, loading } = useDisclosureData(stockCode);
|
const { disclosureSchedule, loading } = useDisclosureData({ stockCode, enabled: isActive });
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingState message="加载披露日程..." />;
|
return <LoadingState message="加载披露日程..." />;
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ import {
|
|||||||
ShareholdersTable,
|
ShareholdersTable,
|
||||||
} from "../../components/shareholder";
|
} from "../../components/shareholder";
|
||||||
import TabPanelContainer from "@components/TabPanelContainer";
|
import TabPanelContainer from "@components/TabPanelContainer";
|
||||||
|
import { ShareholderSkeleton } from "./BasicInfoTabSkeleton";
|
||||||
|
|
||||||
interface ShareholderPanelProps {
|
interface ShareholderPanelProps {
|
||||||
stockCode: string;
|
stockCode: string;
|
||||||
|
/** SubTabContainer 传递的激活状态,控制是否加载数据 */
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,17 +26,17 @@ interface ShareholderPanelProps {
|
|||||||
* - ConcentrationCard: 股权集中度卡片
|
* - ConcentrationCard: 股权集中度卡片
|
||||||
* - ShareholdersTable: 股东表格(合并版,支持十大股东和十大流通股东)
|
* - ShareholdersTable: 股东表格(合并版,支持十大股东和十大流通股东)
|
||||||
*/
|
*/
|
||||||
const ShareholderPanel: React.FC<ShareholderPanelProps> = ({ stockCode }) => {
|
const ShareholderPanel: React.FC<ShareholderPanelProps> = ({ stockCode, isActive = true }) => {
|
||||||
const {
|
const {
|
||||||
actualControl,
|
actualControl,
|
||||||
concentration,
|
concentration,
|
||||||
topShareholders,
|
topShareholders,
|
||||||
topCirculationShareholders,
|
topCirculationShareholders,
|
||||||
loading,
|
loading,
|
||||||
} = useShareholderData(stockCode);
|
} = useShareholderData({ stockCode, enabled: isActive });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabPanelContainer loading={loading} loadingMessage="加载股权结构数据...">
|
<TabPanelContainer loading={loading} skeleton={<ShareholderSkeleton />}>
|
||||||
{/* 实际控制人 + 股权集中度 左右分布 */}
|
{/* 实际控制人 + 股权集中度 左右分布 */}
|
||||||
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
|
||||||
<Box>
|
<Box>
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ export { ManagementPanel } from "./management";
|
|||||||
export { default as AnnouncementsPanel } from "./AnnouncementsPanel";
|
export { default as AnnouncementsPanel } from "./AnnouncementsPanel";
|
||||||
export { default as BranchesPanel } from "./BranchesPanel";
|
export { default as BranchesPanel } from "./BranchesPanel";
|
||||||
export { default as BusinessInfoPanel } from "./BusinessInfoPanel";
|
export { default as BusinessInfoPanel } from "./BusinessInfoPanel";
|
||||||
|
|
||||||
|
// 骨架屏组件
|
||||||
|
export * from "./BasicInfoTabSkeleton";
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useManagementData } from "../../../hooks/useManagementData";
|
|||||||
import { THEME } from "../../config";
|
import { THEME } from "../../config";
|
||||||
import TabPanelContainer from "@components/TabPanelContainer";
|
import TabPanelContainer from "@components/TabPanelContainer";
|
||||||
import CategorySection from "./CategorySection";
|
import CategorySection from "./CategorySection";
|
||||||
|
import { ManagementSkeleton } from "../BasicInfoTabSkeleton";
|
||||||
import type {
|
import type {
|
||||||
ManagementPerson,
|
ManagementPerson,
|
||||||
ManagementCategory,
|
ManagementCategory,
|
||||||
@@ -22,6 +23,8 @@ import type {
|
|||||||
|
|
||||||
interface ManagementPanelProps {
|
interface ManagementPanelProps {
|
||||||
stockCode: string;
|
stockCode: string;
|
||||||
|
/** SubTabContainer 传递的激活状态,控制是否加载数据 */
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,8 +71,8 @@ const categorizeManagement = (management: ManagementPerson[]): CategorizedManage
|
|||||||
return categories;
|
return categories;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ManagementPanel: React.FC<ManagementPanelProps> = ({ stockCode }) => {
|
const ManagementPanel: React.FC<ManagementPanelProps> = ({ stockCode, isActive = true }) => {
|
||||||
const { management, loading } = useManagementData(stockCode);
|
const { management, loading } = useManagementData({ stockCode, enabled: isActive });
|
||||||
|
|
||||||
// 使用 useMemo 缓存分类计算结果
|
// 使用 useMemo 缓存分类计算结果
|
||||||
const categorizedManagement = useMemo(
|
const categorizedManagement = useMemo(
|
||||||
@@ -78,7 +81,7 @@ const ManagementPanel: React.FC<ManagementPanelProps> = ({ stockCode }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabPanelContainer loading={loading} loadingMessage="加载管理团队数据...">
|
<TabPanelContainer loading={loading} skeleton={<ManagementSkeleton />}>
|
||||||
{CATEGORY_ORDER.map((category) => {
|
{CATEGORY_ORDER.map((category) => {
|
||||||
const config = CATEGORY_CONFIG[category];
|
const config = CATEGORY_CONFIG[category];
|
||||||
const people = categorizedManagement[category];
|
const people = categorizedManagement[category];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// 基本信息 Tab 组件 - 使用 SubTabContainer 通用组件
|
// 基本信息 Tab 组件 - 使用 SubTabContainer 通用组件
|
||||||
|
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
|
import { Card, CardBody } from "@chakra-ui/react";
|
||||||
import SubTabContainer, { type SubTabConfig } from "@components/SubTabContainer";
|
import SubTabContainer, { type SubTabConfig } from "@components/SubTabContainer";
|
||||||
|
|
||||||
import { THEME, TAB_CONFIG, getEnabledTabs } from "./config";
|
import { THEME, TAB_CONFIG, getEnabledTabs } from "./config";
|
||||||
@@ -65,16 +66,18 @@ const BasicInfoTab: React.FC<BasicInfoTabProps> = ({
|
|||||||
const tabs = useMemo(() => buildTabsConfig(enabledTabs), [enabledTabs]);
|
const tabs = useMemo(() => buildTabsConfig(enabledTabs), [enabledTabs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubTabContainer
|
<Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
|
||||||
tabs={tabs}
|
<CardBody p={0}>
|
||||||
componentProps={{ stockCode }}
|
<SubTabContainer
|
||||||
defaultIndex={defaultTabIndex}
|
tabs={tabs}
|
||||||
onTabChange={onTabChange}
|
componentProps={{ stockCode }}
|
||||||
themePreset="blackGold"
|
defaultIndex={defaultTabIndex}
|
||||||
compact
|
onTabChange={onTabChange}
|
||||||
size="sm"
|
themePreset="blackGold"
|
||||||
contentPadding={0}
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user