// 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 (
{type}
);
};
// ============================================
// 懒加载 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 (
加载股权结构数据...
);
}
return (
{actualControl.length > 0 && (
实际控制人
{actualControl[0].actual_controller_name}
{actualControl[0].control_type}
截至 {formatUtils.formatDate(actualControl[0].end_date)}
控制比例
{formatUtils.formatPercentage(actualControl[0].holding_ratio)}
{formatUtils.formatShares(actualControl[0].holding_shares)}
)}
{concentration.length > 0 && (
股权集中度
{getConcentrationTrend()
.slice(0, 1)
.map(([date, items]) => (
{formatUtils.formatDate(date)}
{Object.entries(items).map(([key, item]) => (
{item.stat_item}
{formatUtils.formatPercentage(item.holding_ratio)}
{item.ratio_change && (
0 ? "red" : "green"
}
>
0 ? FaArrowUp : FaArrowDown
}
mr={1}
boxSize={3}
/>
{Math.abs(item.ratio_change).toFixed(2)}%
)}
))}
))}
)}
{topShareholders.length > 0 && (
十大股东
{formatUtils.formatDate(topShareholders[0].end_date)}
| 排名 |
股东名称 |
股东类型 |
持股数量 |
持股比例 |
股份性质 |
{topShareholders.slice(0, 10).map((shareholder, idx) => (
|
{shareholder.shareholder_rank}
|
{shareholder.shareholder_name}
|
|
{formatUtils.formatShares(shareholder.holding_shares)}
|
{formatUtils.formatPercentage(
shareholder.total_share_ratio
)}
|
{shareholder.share_nature || "流通股"}
|
))}
)}
{topCirculationShareholders.length > 0 && (
十大流通股东
{formatUtils.formatDate(topCirculationShareholders[0].end_date)}
| 排名 |
股东名称 |
股东类型 |
持股数量 |
流通股比例 |
{topCirculationShareholders.slice(0, 10).map((shareholder, idx) => (
|
{shareholder.shareholder_rank}
|
{shareholder.shareholder_name}
|
|
{formatUtils.formatShares(shareholder.holding_shares)}
|
{formatUtils.formatPercentage(
shareholder.circulation_share_ratio
)}
|
))}
)}
);
};
/**
* 管理团队 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 (
加载管理团队数据...
);
}
return (
{Object.entries(getManagementByCategory()).map(
([category, people]) =>
people.length > 0 && (
{category}
{people.length}人
{people.map((person, idx) => (
{person.name}
{person.gender && (
)}
{person.position_name}
{person.education && (
{person.education}
)}
{person.birth_year && (
{new Date().getFullYear() -
parseInt(person.birth_year)}
岁
)}
{person.nationality &&
person.nationality !== "中国" && (
{person.nationality}
)}
任职日期:{formatUtils.formatDate(person.start_date)}
))}
)
)}
);
};
/**
* 公司公告 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 (
加载公告数据...
);
}
return (
<>
{disclosureSchedule.length > 0 && (
财报披露日程
{disclosureSchedule.slice(0, 4).map((schedule, idx) => (
{schedule.report_name}
{schedule.is_disclosed ? "已披露" : "预计"}
{formatUtils.formatDate(
schedule.is_disclosed
? schedule.actual_date
: schedule.latest_scheduled_date
)}
))}
)}
最新公告
{announcements.map((announcement, idx) => (
handleAnnouncementClick(announcement)}
_hover={{ bg: "gray.50" }}
>
{announcement.info_type || "公告"}
{formatUtils.formatDate(announcement.announce_date)}
{announcement.title}
{announcement.format && (
{announcement.format}
)}
}
variant="ghost"
onClick={(e) => {
e.stopPropagation();
window.open(announcement.url, "_blank");
}}
/>
))}
{/* 公告详情模态框 */}
{selectedAnnouncement?.title}
{selectedAnnouncement?.info_type || "公告"}
{formatUtils.formatDate(selectedAnnouncement?.announce_date)}
文件格式:{selectedAnnouncement?.format || "-"}
文件大小:{selectedAnnouncement?.file_size || "-"} KB
>
);
};
/**
* 分支机构 Tab Panel - 懒加载子组件
*/
const BranchesTabPanel = ({ stockCode }) => {
const { branches, loading } = useBranchesData(stockCode);
if (loading) {
return (
加载分支机构数据...
);
}
if (branches.length === 0) {
return (
暂无分支机构信息
);
}
return (
{branches.map((branch, idx) => (
{branch.branch_name}
{branch.business_status}
注册资本
{branch.register_capital || "-"}
法人代表
{branch.legal_person || "-"}
成立日期
{formatUtils.formatDate(branch.register_date)}
关联企业
{branch.related_company_count || 0} 家
))}
);
};
/**
* 工商信息 Tab Panel - 使用父组件传入的 basicInfo
*/
const BusinessInfoTabPanel = ({ basicInfo }) => {
if (!basicInfo) {
return (
暂无工商信息
);
}
return (
工商信息
统一信用代码
{basicInfo.credit_code}
公司规模
{basicInfo.company_size}
注册地址
{basicInfo.reg_address}
办公地址
{basicInfo.office_address}
服务机构
会计师事务所
{basicInfo.accounting_firm}
律师事务所
{basicInfo.law_firm}
主营业务
{basicInfo.main_business}
经营范围
{basicInfo.business_scope}
);
};
// ============================================
// 主组件
// ============================================
/**
* 基本信息 Tab 组件(懒加载版本)
*
* Props:
* - stockCode: 股票代码(用于懒加载数据)
* - basicInfo: 公司基本信息(从父组件传入,用于工商信息 Tab)
* - cardBg: 卡片背景色
*
* 懒加载策略:
* - 使用 Chakra UI Tabs 的 isLazy 属性
* - 每个 TabPanel 使用独立子组件,在首次激活时才渲染并加载数据
*/
const BasicInfoTab = ({ stockCode, basicInfo, cardBg }) => {
return (
股权结构
管理团队
公司公告
分支机构
工商信息
{/* 股权结构 - 懒加载 */}
{/* 管理团队 - 懒加载 */}
{/* 公司公告 - 懒加载 */}
{/* 分支机构 - 懒加载 */}
{/* 工商信息 - 使用父组件传入的 basicInfo */}
);
};
export default BasicInfoTab;