- 将 useCompanyOverviewData(9个API)拆分为独立 Hooks: - useBasicInfo: 基本信息(首屏唯一加载) - useShareholderData: 股东信息(4个API) - useManagementData: 管理层信息 - useAnnouncementsData: 公告数据 - useBranchesData: 分支机构 - useDisclosureData: 披露日程 - BasicInfoTab 使用子组件实现真正的懒加载: - ShareholderTabPanel、ManagementTabPanel 等 - 配合 Chakra UI isLazy,切换 Tab 时才加载数据 - 首屏 API 请求从 9 个减少到 1 个 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
995 lines
31 KiB
JavaScript
995 lines
31 KiB
JavaScript
// src/views/Company/components/CompanyOverview/BasicInfoTab.js
|
||
// 基本信息 Tab - 股权结构、管理团队、公司公告、分支机构、工商信息
|
||
// 懒加载优化:使用 isLazy + 独立 Hooks,点击 Tab 时才加载对应数据
|
||
|
||
import React from "react";
|
||
import {
|
||
Box,
|
||
VStack,
|
||
HStack,
|
||
Text,
|
||
Heading,
|
||
Badge,
|
||
Icon,
|
||
Card,
|
||
CardBody,
|
||
CardHeader,
|
||
SimpleGrid,
|
||
Avatar,
|
||
Table,
|
||
Thead,
|
||
Tbody,
|
||
Tr,
|
||
Th,
|
||
Td,
|
||
TableContainer,
|
||
Tag,
|
||
Tooltip,
|
||
Divider,
|
||
Center,
|
||
Code,
|
||
Tabs,
|
||
TabList,
|
||
TabPanels,
|
||
Tab,
|
||
TabPanel,
|
||
Stat,
|
||
StatLabel,
|
||
StatNumber,
|
||
StatHelpText,
|
||
IconButton,
|
||
Button,
|
||
Modal,
|
||
ModalOverlay,
|
||
ModalContent,
|
||
ModalHeader,
|
||
ModalCloseButton,
|
||
ModalBody,
|
||
ModalFooter,
|
||
useDisclosure,
|
||
Spinner,
|
||
} from "@chakra-ui/react";
|
||
|
||
// 懒加载 Hooks
|
||
import { useShareholderData } from "./hooks/useShareholderData";
|
||
import { useManagementData } from "./hooks/useManagementData";
|
||
import { useAnnouncementsData } from "./hooks/useAnnouncementsData";
|
||
import { useBranchesData } from "./hooks/useBranchesData";
|
||
import { useDisclosureData } from "./hooks/useDisclosureData";
|
||
import { ExternalLinkIcon } from "@chakra-ui/icons";
|
||
import {
|
||
FaShareAlt,
|
||
FaUserTie,
|
||
FaBullhorn,
|
||
FaSitemap,
|
||
FaInfoCircle,
|
||
FaCrown,
|
||
FaChartPie,
|
||
FaUsers,
|
||
FaChartLine,
|
||
FaArrowUp,
|
||
FaArrowDown,
|
||
FaChartBar,
|
||
FaBuilding,
|
||
FaGlobe,
|
||
FaShieldAlt,
|
||
FaBriefcase,
|
||
FaCircle,
|
||
FaEye,
|
||
FaVenusMars,
|
||
FaGraduationCap,
|
||
FaPassport,
|
||
FaCalendarAlt,
|
||
} from "react-icons/fa";
|
||
|
||
// 格式化工具函数
|
||
const formatUtils = {
|
||
formatPercentage: (value) => {
|
||
if (value === null || value === undefined) return "-";
|
||
return `${(value * 100).toFixed(2)}%`;
|
||
},
|
||
formatNumber: (value) => {
|
||
if (value === null || value === undefined) return "-";
|
||
if (value >= 100000000) {
|
||
return `${(value / 100000000).toFixed(2)}亿`;
|
||
} else if (value >= 10000) {
|
||
return `${(value / 10000).toFixed(2)}万`;
|
||
}
|
||
return value.toLocaleString();
|
||
},
|
||
formatShares: (value) => {
|
||
if (value === null || value === undefined) return "-";
|
||
if (value >= 100000000) {
|
||
return `${(value / 100000000).toFixed(2)}亿股`;
|
||
} else if (value >= 10000) {
|
||
return `${(value / 10000).toFixed(2)}万股`;
|
||
}
|
||
return `${value.toLocaleString()}股`;
|
||
},
|
||
formatDate: (dateStr) => {
|
||
if (!dateStr) return "-";
|
||
return dateStr.split("T")[0];
|
||
},
|
||
};
|
||
|
||
// 股东类型标签组件
|
||
const ShareholderTypeBadge = ({ type }) => {
|
||
const typeConfig = {
|
||
基金: { color: "blue", icon: FaChartBar },
|
||
个人: { color: "green", icon: FaUserTie },
|
||
法人: { color: "purple", icon: FaBuilding },
|
||
QFII: { color: "orange", icon: FaGlobe },
|
||
社保: { color: "red", icon: FaShieldAlt },
|
||
保险: { color: "teal", icon: FaShieldAlt },
|
||
信托: { color: "cyan", icon: FaBriefcase },
|
||
券商: { color: "pink", icon: FaChartLine },
|
||
};
|
||
|
||
const config = Object.entries(typeConfig).find(([key]) =>
|
||
type?.includes(key)
|
||
)?.[1] || { color: "gray", icon: FaCircle };
|
||
|
||
return (
|
||
<Badge colorScheme={config.color} size="sm">
|
||
<Icon as={config.icon} mr={1} boxSize={3} />
|
||
{type}
|
||
</Badge>
|
||
);
|
||
};
|
||
|
||
// ============================================
|
||
// 懒加载 TabPanel 子组件
|
||
// 每个子组件独立调用 Hook,配合 isLazy 实现真正的懒加载
|
||
// ============================================
|
||
|
||
/**
|
||
* 股权结构 Tab Panel - 懒加载子组件
|
||
*/
|
||
const ShareholderTabPanel = ({ stockCode }) => {
|
||
const {
|
||
actualControl,
|
||
concentration,
|
||
topShareholders,
|
||
topCirculationShareholders,
|
||
loading,
|
||
} = useShareholderData(stockCode);
|
||
|
||
// 计算股权集中度变化
|
||
const getConcentrationTrend = () => {
|
||
const grouped = {};
|
||
concentration.forEach((item) => {
|
||
if (!grouped[item.end_date]) {
|
||
grouped[item.end_date] = {};
|
||
}
|
||
grouped[item.end_date][item.stat_item] = item;
|
||
});
|
||
return Object.entries(grouped)
|
||
.sort((a, b) => b[0].localeCompare(a[0]))
|
||
.slice(0, 5);
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<Center h="200px">
|
||
<VStack>
|
||
<Spinner size="lg" color="blue.500" />
|
||
<Text fontSize="sm" color="gray.500">
|
||
加载股权结构数据...
|
||
</Text>
|
||
</VStack>
|
||
</Center>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<VStack spacing={6} align="stretch">
|
||
{actualControl.length > 0 && (
|
||
<Box>
|
||
<HStack mb={4}>
|
||
<Icon as={FaCrown} color="gold" boxSize={5} />
|
||
<Heading size="sm">实际控制人</Heading>
|
||
</HStack>
|
||
<Card variant="outline">
|
||
<CardBody>
|
||
<HStack justify="space-between">
|
||
<VStack align="start">
|
||
<Text fontWeight="bold" fontSize="lg">
|
||
{actualControl[0].actual_controller_name}
|
||
</Text>
|
||
<HStack>
|
||
<Badge colorScheme="purple">
|
||
{actualControl[0].control_type}
|
||
</Badge>
|
||
<Text fontSize="sm" color="gray.500">
|
||
截至 {formatUtils.formatDate(actualControl[0].end_date)}
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
<Stat textAlign="right">
|
||
<StatLabel>控制比例</StatLabel>
|
||
<StatNumber color="purple.500">
|
||
{formatUtils.formatPercentage(actualControl[0].holding_ratio)}
|
||
</StatNumber>
|
||
<StatHelpText>
|
||
{formatUtils.formatShares(actualControl[0].holding_shares)}
|
||
</StatHelpText>
|
||
</Stat>
|
||
</HStack>
|
||
</CardBody>
|
||
</Card>
|
||
</Box>
|
||
)}
|
||
|
||
{concentration.length > 0 && (
|
||
<Box>
|
||
<HStack mb={4}>
|
||
<Icon as={FaChartPie} color="blue.500" boxSize={5} />
|
||
<Heading size="sm">股权集中度</Heading>
|
||
</HStack>
|
||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||
{getConcentrationTrend()
|
||
.slice(0, 1)
|
||
.map(([date, items]) => (
|
||
<Card key={date} variant="outline">
|
||
<CardHeader pb={2}>
|
||
<Text fontSize="sm" color="gray.500">
|
||
{formatUtils.formatDate(date)}
|
||
</Text>
|
||
</CardHeader>
|
||
<CardBody pt={2}>
|
||
<VStack spacing={3} align="stretch">
|
||
{Object.entries(items).map(([key, item]) => (
|
||
<HStack key={key} justify="space-between">
|
||
<Text fontSize="sm">{item.stat_item}</Text>
|
||
<HStack>
|
||
<Text fontWeight="bold" color="blue.500">
|
||
{formatUtils.formatPercentage(item.holding_ratio)}
|
||
</Text>
|
||
{item.ratio_change && (
|
||
<Badge
|
||
colorScheme={
|
||
item.ratio_change > 0 ? "red" : "green"
|
||
}
|
||
>
|
||
<Icon
|
||
as={
|
||
item.ratio_change > 0 ? FaArrowUp : FaArrowDown
|
||
}
|
||
mr={1}
|
||
boxSize={3}
|
||
/>
|
||
{Math.abs(item.ratio_change).toFixed(2)}%
|
||
</Badge>
|
||
)}
|
||
</HStack>
|
||
</HStack>
|
||
))}
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
))}
|
||
</SimpleGrid>
|
||
</Box>
|
||
)}
|
||
|
||
{topShareholders.length > 0 && (
|
||
<Box>
|
||
<HStack mb={4}>
|
||
<Icon as={FaUsers} color="green.500" boxSize={5} />
|
||
<Heading size="sm">十大股东</Heading>
|
||
<Badge>
|
||
{formatUtils.formatDate(topShareholders[0].end_date)}
|
||
</Badge>
|
||
</HStack>
|
||
<TableContainer>
|
||
<Table size="sm" variant="striped">
|
||
<Thead>
|
||
<Tr>
|
||
<Th>排名</Th>
|
||
<Th>股东名称</Th>
|
||
<Th>股东类型</Th>
|
||
<Th isNumeric>持股数量</Th>
|
||
<Th isNumeric>持股比例</Th>
|
||
<Th>股份性质</Th>
|
||
</Tr>
|
||
</Thead>
|
||
<Tbody>
|
||
{topShareholders.slice(0, 10).map((shareholder, idx) => (
|
||
<Tr key={idx}>
|
||
<Td>
|
||
<Badge colorScheme={idx < 3 ? "red" : "gray"}>
|
||
{shareholder.shareholder_rank}
|
||
</Badge>
|
||
</Td>
|
||
<Td>
|
||
<Tooltip label={shareholder.shareholder_name}>
|
||
<Text noOfLines={1} maxW="200px">
|
||
{shareholder.shareholder_name}
|
||
</Text>
|
||
</Tooltip>
|
||
</Td>
|
||
<Td>
|
||
<ShareholderTypeBadge type={shareholder.shareholder_type} />
|
||
</Td>
|
||
<Td isNumeric fontWeight="medium">
|
||
{formatUtils.formatShares(shareholder.holding_shares)}
|
||
</Td>
|
||
<Td isNumeric>
|
||
<Text color="blue.500" fontWeight="bold">
|
||
{formatUtils.formatPercentage(
|
||
shareholder.total_share_ratio
|
||
)}
|
||
</Text>
|
||
</Td>
|
||
<Td>
|
||
<Badge size="sm" variant="outline">
|
||
{shareholder.share_nature || "流通股"}
|
||
</Badge>
|
||
</Td>
|
||
</Tr>
|
||
))}
|
||
</Tbody>
|
||
</Table>
|
||
</TableContainer>
|
||
</Box>
|
||
)}
|
||
|
||
{topCirculationShareholders.length > 0 && (
|
||
<Box>
|
||
<HStack mb={4}>
|
||
<Icon as={FaChartLine} color="purple.500" boxSize={5} />
|
||
<Heading size="sm">十大流通股东</Heading>
|
||
<Badge>
|
||
{formatUtils.formatDate(topCirculationShareholders[0].end_date)}
|
||
</Badge>
|
||
</HStack>
|
||
<TableContainer>
|
||
<Table size="sm" variant="striped">
|
||
<Thead>
|
||
<Tr>
|
||
<Th>排名</Th>
|
||
<Th>股东名称</Th>
|
||
<Th>股东类型</Th>
|
||
<Th isNumeric>持股数量</Th>
|
||
<Th isNumeric>流通股比例</Th>
|
||
</Tr>
|
||
</Thead>
|
||
<Tbody>
|
||
{topCirculationShareholders.slice(0, 10).map((shareholder, idx) => (
|
||
<Tr key={idx}>
|
||
<Td>
|
||
<Badge colorScheme={idx < 3 ? "orange" : "gray"}>
|
||
{shareholder.shareholder_rank}
|
||
</Badge>
|
||
</Td>
|
||
<Td>
|
||
<Tooltip label={shareholder.shareholder_name}>
|
||
<Text noOfLines={1} maxW="250px">
|
||
{shareholder.shareholder_name}
|
||
</Text>
|
||
</Tooltip>
|
||
</Td>
|
||
<Td>
|
||
<ShareholderTypeBadge type={shareholder.shareholder_type} />
|
||
</Td>
|
||
<Td isNumeric fontWeight="medium">
|
||
{formatUtils.formatShares(shareholder.holding_shares)}
|
||
</Td>
|
||
<Td isNumeric>
|
||
<Text color="purple.500" fontWeight="bold">
|
||
{formatUtils.formatPercentage(
|
||
shareholder.circulation_share_ratio
|
||
)}
|
||
</Text>
|
||
</Td>
|
||
</Tr>
|
||
))}
|
||
</Tbody>
|
||
</Table>
|
||
</TableContainer>
|
||
</Box>
|
||
)}
|
||
</VStack>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 管理团队 Tab Panel - 懒加载子组件
|
||
*/
|
||
const ManagementTabPanel = ({ stockCode }) => {
|
||
const { management, loading } = useManagementData(stockCode);
|
||
|
||
// 管理层职位分类
|
||
const getManagementByCategory = () => {
|
||
const categories = {
|
||
高管: [],
|
||
董事: [],
|
||
监事: [],
|
||
其他: [],
|
||
};
|
||
|
||
management.forEach((person) => {
|
||
if (
|
||
person.position_category === "高管" ||
|
||
person.position_name?.includes("总")
|
||
) {
|
||
categories["高管"].push(person);
|
||
} else if (
|
||
person.position_category === "董事" ||
|
||
person.position_name?.includes("董事")
|
||
) {
|
||
categories["董事"].push(person);
|
||
} else if (
|
||
person.position_category === "监事" ||
|
||
person.position_name?.includes("监事")
|
||
) {
|
||
categories["监事"].push(person);
|
||
} else {
|
||
categories["其他"].push(person);
|
||
}
|
||
});
|
||
|
||
return categories;
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<Center h="200px">
|
||
<VStack>
|
||
<Spinner size="lg" color="blue.500" />
|
||
<Text fontSize="sm" color="gray.500">
|
||
加载管理团队数据...
|
||
</Text>
|
||
</VStack>
|
||
</Center>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<VStack spacing={6} align="stretch">
|
||
{Object.entries(getManagementByCategory()).map(
|
||
([category, people]) =>
|
||
people.length > 0 && (
|
||
<Box key={category}>
|
||
<HStack mb={4}>
|
||
<Icon
|
||
as={
|
||
category === "高管"
|
||
? FaUserTie
|
||
: category === "董事"
|
||
? FaCrown
|
||
: category === "监事"
|
||
? FaEye
|
||
: FaUsers
|
||
}
|
||
color={
|
||
category === "高管"
|
||
? "blue.500"
|
||
: category === "董事"
|
||
? "purple.500"
|
||
: category === "监事"
|
||
? "green.500"
|
||
: "gray.500"
|
||
}
|
||
boxSize={5}
|
||
/>
|
||
<Heading size="sm">{category}</Heading>
|
||
<Badge>{people.length}人</Badge>
|
||
</HStack>
|
||
|
||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
||
{people.map((person, idx) => (
|
||
<Card key={idx} variant="outline" size="sm">
|
||
<CardBody>
|
||
<HStack spacing={3} align="start">
|
||
<Avatar
|
||
name={person.name}
|
||
size="md"
|
||
bg={
|
||
category === "高管"
|
||
? "blue.500"
|
||
: category === "董事"
|
||
? "purple.500"
|
||
: category === "监事"
|
||
? "green.500"
|
||
: "gray.500"
|
||
}
|
||
/>
|
||
<VStack align="start" spacing={1} flex={1}>
|
||
<HStack>
|
||
<Text fontWeight="bold">{person.name}</Text>
|
||
{person.gender && (
|
||
<Icon
|
||
as={FaVenusMars}
|
||
color={
|
||
person.gender === "男"
|
||
? "blue.400"
|
||
: "pink.400"
|
||
}
|
||
boxSize={3}
|
||
/>
|
||
)}
|
||
</HStack>
|
||
<Text fontSize="sm" color="blue.600">
|
||
{person.position_name}
|
||
</Text>
|
||
<HStack spacing={2} flexWrap="wrap">
|
||
{person.education && (
|
||
<Tag size="sm" variant="subtle">
|
||
<Icon as={FaGraduationCap} mr={1} boxSize={3} />
|
||
{person.education}
|
||
</Tag>
|
||
)}
|
||
{person.birth_year && (
|
||
<Tag size="sm" variant="subtle">
|
||
{new Date().getFullYear() -
|
||
parseInt(person.birth_year)}
|
||
岁
|
||
</Tag>
|
||
)}
|
||
{person.nationality &&
|
||
person.nationality !== "中国" && (
|
||
<Tag size="sm" colorScheme="orange">
|
||
<Icon as={FaPassport} mr={1} boxSize={3} />
|
||
{person.nationality}
|
||
</Tag>
|
||
)}
|
||
</HStack>
|
||
<Text fontSize="xs" color="gray.500">
|
||
任职日期:{formatUtils.formatDate(person.start_date)}
|
||
</Text>
|
||
</VStack>
|
||
</HStack>
|
||
</CardBody>
|
||
</Card>
|
||
))}
|
||
</SimpleGrid>
|
||
</Box>
|
||
)
|
||
)}
|
||
</VStack>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 公司公告 Tab Panel - 懒加载子组件
|
||
*/
|
||
const AnnouncementsTabPanel = ({ stockCode }) => {
|
||
const { announcements, loading: announcementsLoading } =
|
||
useAnnouncementsData(stockCode);
|
||
const { disclosureSchedule, loading: disclosureLoading } =
|
||
useDisclosureData(stockCode);
|
||
|
||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||
const [selectedAnnouncement, setSelectedAnnouncement] = React.useState(null);
|
||
|
||
const handleAnnouncementClick = (announcement) => {
|
||
setSelectedAnnouncement(announcement);
|
||
onOpen();
|
||
};
|
||
|
||
const loading = announcementsLoading || disclosureLoading;
|
||
|
||
if (loading) {
|
||
return (
|
||
<Center h="200px">
|
||
<VStack>
|
||
<Spinner size="lg" color="blue.500" />
|
||
<Text fontSize="sm" color="gray.500">
|
||
加载公告数据...
|
||
</Text>
|
||
</VStack>
|
||
</Center>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<VStack spacing={4} align="stretch">
|
||
{disclosureSchedule.length > 0 && (
|
||
<Box>
|
||
<HStack mb={3}>
|
||
<Icon as={FaCalendarAlt} color="orange.500" />
|
||
<Text fontWeight="bold">财报披露日程</Text>
|
||
</HStack>
|
||
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={3}>
|
||
{disclosureSchedule.slice(0, 4).map((schedule, idx) => (
|
||
<Card
|
||
key={idx}
|
||
variant="outline"
|
||
size="sm"
|
||
bg={schedule.is_disclosed ? "green.50" : "orange.50"}
|
||
>
|
||
<CardBody p={3}>
|
||
<VStack spacing={1}>
|
||
<Badge
|
||
colorScheme={schedule.is_disclosed ? "green" : "orange"}
|
||
>
|
||
{schedule.report_name}
|
||
</Badge>
|
||
<Text fontSize="sm" fontWeight="bold">
|
||
{schedule.is_disclosed ? "已披露" : "预计"}
|
||
</Text>
|
||
<Text fontSize="xs">
|
||
{formatUtils.formatDate(
|
||
schedule.is_disclosed
|
||
? schedule.actual_date
|
||
: schedule.latest_scheduled_date
|
||
)}
|
||
</Text>
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
))}
|
||
</SimpleGrid>
|
||
</Box>
|
||
)}
|
||
|
||
<Divider />
|
||
|
||
<Box>
|
||
<HStack mb={3}>
|
||
<Icon as={FaBullhorn} color="blue.500" />
|
||
<Text fontWeight="bold">最新公告</Text>
|
||
</HStack>
|
||
<VStack spacing={2} align="stretch">
|
||
{announcements.map((announcement, idx) => (
|
||
<Card
|
||
key={idx}
|
||
variant="outline"
|
||
size="sm"
|
||
cursor="pointer"
|
||
onClick={() => handleAnnouncementClick(announcement)}
|
||
_hover={{ bg: "gray.50" }}
|
||
>
|
||
<CardBody p={3}>
|
||
<HStack justify="space-between">
|
||
<VStack align="start" spacing={1} flex={1}>
|
||
<HStack>
|
||
<Badge size="sm" colorScheme="blue">
|
||
{announcement.info_type || "公告"}
|
||
</Badge>
|
||
<Text fontSize="xs" color="gray.500">
|
||
{formatUtils.formatDate(announcement.announce_date)}
|
||
</Text>
|
||
</HStack>
|
||
<Text fontSize="sm" fontWeight="medium" noOfLines={1}>
|
||
{announcement.title}
|
||
</Text>
|
||
</VStack>
|
||
<HStack>
|
||
{announcement.format && (
|
||
<Tag size="sm" variant="subtle">
|
||
{announcement.format}
|
||
</Tag>
|
||
)}
|
||
<IconButton
|
||
size="sm"
|
||
icon={<ExternalLinkIcon />}
|
||
variant="ghost"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
window.open(announcement.url, "_blank");
|
||
}}
|
||
/>
|
||
</HStack>
|
||
</HStack>
|
||
</CardBody>
|
||
</Card>
|
||
))}
|
||
</VStack>
|
||
</Box>
|
||
</VStack>
|
||
|
||
{/* 公告详情模态框 */}
|
||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||
<ModalOverlay />
|
||
<ModalContent>
|
||
<ModalHeader>
|
||
<VStack align="start" spacing={1}>
|
||
<Text>{selectedAnnouncement?.title}</Text>
|
||
<HStack>
|
||
<Badge colorScheme="blue">
|
||
{selectedAnnouncement?.info_type || "公告"}
|
||
</Badge>
|
||
<Text fontSize="sm" color="gray.500">
|
||
{formatUtils.formatDate(selectedAnnouncement?.announce_date)}
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
</ModalHeader>
|
||
<ModalCloseButton />
|
||
<ModalBody>
|
||
<VStack align="start" spacing={3}>
|
||
<Text fontSize="sm">
|
||
文件格式:{selectedAnnouncement?.format || "-"}
|
||
</Text>
|
||
<Text fontSize="sm">
|
||
文件大小:{selectedAnnouncement?.file_size || "-"} KB
|
||
</Text>
|
||
</VStack>
|
||
</ModalBody>
|
||
<ModalFooter>
|
||
<Button
|
||
colorScheme="blue"
|
||
mr={3}
|
||
onClick={() => window.open(selectedAnnouncement?.url, "_blank")}
|
||
>
|
||
查看原文
|
||
</Button>
|
||
<Button variant="ghost" onClick={onClose}>
|
||
关闭
|
||
</Button>
|
||
</ModalFooter>
|
||
</ModalContent>
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 分支机构 Tab Panel - 懒加载子组件
|
||
*/
|
||
const BranchesTabPanel = ({ stockCode }) => {
|
||
const { branches, loading } = useBranchesData(stockCode);
|
||
|
||
if (loading) {
|
||
return (
|
||
<Center h="200px">
|
||
<VStack>
|
||
<Spinner size="lg" color="blue.500" />
|
||
<Text fontSize="sm" color="gray.500">
|
||
加载分支机构数据...
|
||
</Text>
|
||
</VStack>
|
||
</Center>
|
||
);
|
||
}
|
||
|
||
if (branches.length === 0) {
|
||
return (
|
||
<Center h="200px">
|
||
<VStack>
|
||
<Icon as={FaSitemap} boxSize={12} color="gray.300" />
|
||
<Text color="gray.500">暂无分支机构信息</Text>
|
||
</VStack>
|
||
</Center>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||
{branches.map((branch, idx) => (
|
||
<Card key={idx} variant="outline">
|
||
<CardBody>
|
||
<VStack align="start" spacing={3}>
|
||
<HStack justify="space-between" w="full">
|
||
<Text fontWeight="bold">{branch.branch_name}</Text>
|
||
<Badge
|
||
colorScheme={
|
||
branch.business_status === "存续" ? "green" : "red"
|
||
}
|
||
>
|
||
{branch.business_status}
|
||
</Badge>
|
||
</HStack>
|
||
|
||
<SimpleGrid columns={2} spacing={2} w="full">
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontSize="xs" color="gray.500">
|
||
注册资本
|
||
</Text>
|
||
<Text fontSize="sm" fontWeight="medium">
|
||
{branch.register_capital || "-"}
|
||
</Text>
|
||
</VStack>
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontSize="xs" color="gray.500">
|
||
法人代表
|
||
</Text>
|
||
<Text fontSize="sm" fontWeight="medium">
|
||
{branch.legal_person || "-"}
|
||
</Text>
|
||
</VStack>
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontSize="xs" color="gray.500">
|
||
成立日期
|
||
</Text>
|
||
<Text fontSize="sm" fontWeight="medium">
|
||
{formatUtils.formatDate(branch.register_date)}
|
||
</Text>
|
||
</VStack>
|
||
<VStack align="start" spacing={1}>
|
||
<Text fontSize="xs" color="gray.500">
|
||
关联企业
|
||
</Text>
|
||
<Text fontSize="sm" fontWeight="medium">
|
||
{branch.related_company_count || 0} 家
|
||
</Text>
|
||
</VStack>
|
||
</SimpleGrid>
|
||
</VStack>
|
||
</CardBody>
|
||
</Card>
|
||
))}
|
||
</SimpleGrid>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 工商信息 Tab Panel - 使用父组件传入的 basicInfo
|
||
*/
|
||
const BusinessInfoTabPanel = ({ basicInfo }) => {
|
||
if (!basicInfo) {
|
||
return (
|
||
<Center h="200px">
|
||
<Text color="gray.500">暂无工商信息</Text>
|
||
</Center>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<VStack spacing={4} align="stretch">
|
||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={6}>
|
||
<Box>
|
||
<Heading size="sm" mb={3}>
|
||
工商信息
|
||
</Heading>
|
||
<VStack align="start" spacing={2}>
|
||
<HStack w="full">
|
||
<Text fontSize="sm" color="gray.600" minW="80px">
|
||
统一信用代码
|
||
</Text>
|
||
<Code fontSize="xs">{basicInfo.credit_code}</Code>
|
||
</HStack>
|
||
<HStack w="full">
|
||
<Text fontSize="sm" color="gray.600" minW="80px">
|
||
公司规模
|
||
</Text>
|
||
<Text fontSize="sm">{basicInfo.company_size}</Text>
|
||
</HStack>
|
||
<HStack w="full">
|
||
<Text fontSize="sm" color="gray.600" minW="80px">
|
||
注册地址
|
||
</Text>
|
||
<Text fontSize="sm" noOfLines={2}>
|
||
{basicInfo.reg_address}
|
||
</Text>
|
||
</HStack>
|
||
<HStack w="full">
|
||
<Text fontSize="sm" color="gray.600" minW="80px">
|
||
办公地址
|
||
</Text>
|
||
<Text fontSize="sm" noOfLines={2}>
|
||
{basicInfo.office_address}
|
||
</Text>
|
||
</HStack>
|
||
</VStack>
|
||
</Box>
|
||
|
||
<Box>
|
||
<Heading size="sm" mb={3}>
|
||
服务机构
|
||
</Heading>
|
||
<VStack align="start" spacing={2}>
|
||
<Box>
|
||
<Text fontSize="sm" color="gray.600">
|
||
会计师事务所
|
||
</Text>
|
||
<Text fontSize="sm" fontWeight="medium">
|
||
{basicInfo.accounting_firm}
|
||
</Text>
|
||
</Box>
|
||
<Box>
|
||
<Text fontSize="sm" color="gray.600">
|
||
律师事务所
|
||
</Text>
|
||
<Text fontSize="sm" fontWeight="medium">
|
||
{basicInfo.law_firm}
|
||
</Text>
|
||
</Box>
|
||
</VStack>
|
||
</Box>
|
||
</SimpleGrid>
|
||
|
||
<Divider />
|
||
|
||
<Box>
|
||
<Heading size="sm" mb={3}>
|
||
主营业务
|
||
</Heading>
|
||
<Text fontSize="sm" lineHeight="tall">
|
||
{basicInfo.main_business}
|
||
</Text>
|
||
</Box>
|
||
|
||
<Box>
|
||
<Heading size="sm" mb={3}>
|
||
经营范围
|
||
</Heading>
|
||
<Text fontSize="sm" lineHeight="tall" color="gray.700">
|
||
{basicInfo.business_scope}
|
||
</Text>
|
||
</Box>
|
||
</VStack>
|
||
);
|
||
};
|
||
|
||
// ============================================
|
||
// 主组件
|
||
// ============================================
|
||
|
||
/**
|
||
* 基本信息 Tab 组件(懒加载版本)
|
||
*
|
||
* Props:
|
||
* - stockCode: 股票代码(用于懒加载数据)
|
||
* - basicInfo: 公司基本信息(从父组件传入,用于工商信息 Tab)
|
||
* - cardBg: 卡片背景色
|
||
*
|
||
* 懒加载策略:
|
||
* - 使用 Chakra UI Tabs 的 isLazy 属性
|
||
* - 每个 TabPanel 使用独立子组件,在首次激活时才渲染并加载数据
|
||
*/
|
||
const BasicInfoTab = ({ stockCode, basicInfo, cardBg }) => {
|
||
return (
|
||
<Card bg={cardBg} shadow="md">
|
||
<CardBody>
|
||
<Tabs isLazy variant="enclosed" colorScheme="blue">
|
||
<TabList flexWrap="wrap">
|
||
<Tab>
|
||
<Icon as={FaShareAlt} mr={2} />
|
||
股权结构
|
||
</Tab>
|
||
<Tab>
|
||
<Icon as={FaUserTie} mr={2} />
|
||
管理团队
|
||
</Tab>
|
||
<Tab>
|
||
<Icon as={FaBullhorn} mr={2} />
|
||
公司公告
|
||
</Tab>
|
||
<Tab>
|
||
<Icon as={FaSitemap} mr={2} />
|
||
分支机构
|
||
</Tab>
|
||
<Tab>
|
||
<Icon as={FaInfoCircle} mr={2} />
|
||
工商信息
|
||
</Tab>
|
||
</TabList>
|
||
|
||
<TabPanels>
|
||
{/* 股权结构 - 懒加载 */}
|
||
<TabPanel>
|
||
<ShareholderTabPanel stockCode={stockCode} />
|
||
</TabPanel>
|
||
|
||
{/* 管理团队 - 懒加载 */}
|
||
<TabPanel>
|
||
<ManagementTabPanel stockCode={stockCode} />
|
||
</TabPanel>
|
||
|
||
{/* 公司公告 - 懒加载 */}
|
||
<TabPanel>
|
||
<AnnouncementsTabPanel stockCode={stockCode} />
|
||
</TabPanel>
|
||
|
||
{/* 分支机构 - 懒加载 */}
|
||
<TabPanel>
|
||
<BranchesTabPanel stockCode={stockCode} />
|
||
</TabPanel>
|
||
|
||
{/* 工商信息 - 使用父组件传入的 basicInfo */}
|
||
<TabPanel>
|
||
<BusinessInfoTabPanel basicInfo={basicInfo} />
|
||
</TabPanel>
|
||
</TabPanels>
|
||
</Tabs>
|
||
</CardBody>
|
||
</Card>
|
||
);
|
||
};
|
||
|
||
export default BasicInfoTab;
|