diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab.js b/src/views/Company/components/CompanyOverview/BasicInfoTab.js
deleted file mode 100644
index 163a665c..00000000
--- a/src/views/Company/components/CompanyOverview/BasicInfoTab.js
+++ /dev/null
@@ -1,994 +0,0 @@
-// 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;
diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/AnnouncementsPanel.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/AnnouncementsPanel.tsx
new file mode 100644
index 00000000..cd940387
--- /dev/null
+++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/AnnouncementsPanel.tsx
@@ -0,0 +1,210 @@
+// src/views/Company/components/CompanyOverview/BasicInfoTab/components/AnnouncementsPanel.tsx
+// 公司公告 Tab Panel
+
+import React, { useState } from "react";
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Badge,
+ Icon,
+ Card,
+ CardBody,
+ SimpleGrid,
+ Divider,
+ IconButton,
+ Button,
+ Tag,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalCloseButton,
+ ModalBody,
+ ModalFooter,
+ useDisclosure,
+} from "@chakra-ui/react";
+import { FaCalendarAlt, FaBullhorn } from "react-icons/fa";
+import { ExternalLinkIcon } from "@chakra-ui/icons";
+
+import { useAnnouncementsData } from "../../hooks/useAnnouncementsData";
+import { useDisclosureData } from "../../hooks/useDisclosureData";
+import { THEME } from "../config";
+import { formatDate } from "../utils";
+import LoadingState from "./LoadingState";
+
+interface AnnouncementsPanelProps {
+ stockCode: string;
+}
+
+const AnnouncementsPanel: React.FC = ({ stockCode }) => {
+ const { announcements, loading: announcementsLoading } = useAnnouncementsData(stockCode);
+ const { disclosureSchedule, loading: disclosureLoading } = useDisclosureData(stockCode);
+
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [selectedAnnouncement, setSelectedAnnouncement] = useState(null);
+
+ const handleAnnouncementClick = (announcement: any) => {
+ setSelectedAnnouncement(announcement);
+ onOpen();
+ };
+
+ const loading = announcementsLoading || disclosureLoading;
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+ <>
+
+ {/* 财报披露日程 */}
+ {disclosureSchedule.length > 0 && (
+
+
+
+ 财报披露日程
+
+
+ {disclosureSchedule.slice(0, 4).map((schedule: any, idx: number) => (
+
+
+
+
+ {schedule.report_name}
+
+
+ {schedule.is_disclosed ? "已披露" : "预计"}
+
+
+ {formatDate(
+ schedule.is_disclosed
+ ? schedule.actual_date
+ : schedule.latest_scheduled_date
+ )}
+
+
+
+
+ ))}
+
+
+ )}
+
+
+
+ {/* 最新公告 */}
+
+
+
+ 最新公告
+
+
+ {announcements.map((announcement: any, idx: number) => (
+ handleAnnouncementClick(announcement)}
+ _hover={{ bg: THEME.tableHoverBg }}
+ >
+
+
+
+
+
+ {announcement.info_type || "公告"}
+
+
+ {formatDate(announcement.announce_date)}
+
+
+
+ {announcement.title}
+
+
+
+ {announcement.format && (
+
+ {announcement.format}
+
+ )}
+ }
+ variant="ghost"
+ color={THEME.goldLight}
+ aria-label="查看原文"
+ onClick={(e) => {
+ e.stopPropagation();
+ window.open(announcement.url, "_blank");
+ }}
+ />
+
+
+
+
+ ))}
+
+
+
+
+ {/* 公告详情模态框 */}
+
+
+
+
+
+ {selectedAnnouncement?.title}
+
+
+ {selectedAnnouncement?.info_type || "公告"}
+
+
+ {formatDate(selectedAnnouncement?.announce_date)}
+
+
+
+
+
+
+
+
+ 文件格式:{selectedAnnouncement?.format || "-"}
+
+
+ 文件大小:{selectedAnnouncement?.file_size || "-"} KB
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default AnnouncementsPanel;
diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/BranchesPanel.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/BranchesPanel.tsx
new file mode 100644
index 00000000..ff49c720
--- /dev/null
+++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/BranchesPanel.tsx
@@ -0,0 +1,95 @@
+// src/views/Company/components/CompanyOverview/BasicInfoTab/components/BranchesPanel.tsx
+// 分支机构 Tab Panel
+
+import React from "react";
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Badge,
+ Icon,
+ Card,
+ CardBody,
+ SimpleGrid,
+ Center,
+} from "@chakra-ui/react";
+import { FaSitemap } from "react-icons/fa";
+
+import { useBranchesData } from "../../hooks/useBranchesData";
+import { THEME } from "../config";
+import { formatDate } from "../utils";
+import LoadingState from "./LoadingState";
+
+interface BranchesPanelProps {
+ stockCode: string;
+}
+
+const BranchesPanel: React.FC = ({ stockCode }) => {
+ const { branches, loading } = useBranchesData(stockCode);
+
+ if (loading) {
+ return ;
+ }
+
+ if (branches.length === 0) {
+ return (
+
+
+
+ 暂无分支机构信息
+
+
+ );
+ }
+
+ return (
+
+ {branches.map((branch: any, idx: number) => (
+
+
+
+
+ {branch.branch_name}
+
+ {branch.business_status}
+
+
+
+
+
+ 注册资本
+
+ {branch.register_capital || "-"}
+
+
+
+ 法人代表
+
+ {branch.legal_person || "-"}
+
+
+
+ 成立日期
+
+ {formatDate(branch.register_date)}
+
+
+
+ 关联企业
+
+ {branch.related_company_count || 0} 家
+
+
+
+
+
+
+ ))}
+
+ );
+};
+
+export default BranchesPanel;
diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/BusinessInfoPanel.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/BusinessInfoPanel.tsx
new file mode 100644
index 00000000..0752847c
--- /dev/null
+++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/BusinessInfoPanel.tsx
@@ -0,0 +1,109 @@
+// src/views/Company/components/CompanyOverview/BasicInfoTab/components/BusinessInfoPanel.tsx
+// 工商信息 Tab Panel
+
+import React from "react";
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Heading,
+ SimpleGrid,
+ Divider,
+ Center,
+ Code,
+} from "@chakra-ui/react";
+
+import { THEME } from "../config";
+
+interface BusinessInfoPanelProps {
+ basicInfo: any;
+}
+
+const BusinessInfoPanel: React.FC = ({ 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}
+
+
+
+ );
+};
+
+export default BusinessInfoPanel;
diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/LoadingState.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/LoadingState.tsx
new file mode 100644
index 00000000..450cefef
--- /dev/null
+++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/LoadingState.tsx
@@ -0,0 +1,32 @@
+// src/views/Company/components/CompanyOverview/BasicInfoTab/components/LoadingState.tsx
+// 复用的加载状态组件
+
+import React from "react";
+import { Center, VStack, Spinner, Text } from "@chakra-ui/react";
+import { THEME } from "../config";
+
+interface LoadingStateProps {
+ message?: string;
+ height?: string;
+}
+
+/**
+ * 加载状态组件(黑金主题)
+ */
+const LoadingState: React.FC = ({
+ message = "加载中...",
+ height = "200px",
+}) => {
+ return (
+
+
+
+
+ {message}
+
+
+
+ );
+};
+
+export default LoadingState;
diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ManagementPanel.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ManagementPanel.tsx
new file mode 100644
index 00000000..84293aa0
--- /dev/null
+++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ManagementPanel.tsx
@@ -0,0 +1,179 @@
+// src/views/Company/components/CompanyOverview/BasicInfoTab/components/ManagementPanel.tsx
+// 管理团队 Tab Panel
+
+import React from "react";
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Heading,
+ Badge,
+ Icon,
+ Card,
+ CardBody,
+ SimpleGrid,
+ Avatar,
+ Tag,
+} from "@chakra-ui/react";
+import {
+ FaUserTie,
+ FaCrown,
+ FaEye,
+ FaUsers,
+ FaVenusMars,
+ FaGraduationCap,
+ FaPassport,
+} from "react-icons/fa";
+
+import { useManagementData } from "../../hooks/useManagementData";
+import { THEME } from "../config";
+import { formatDate } from "../utils";
+import LoadingState from "./LoadingState";
+
+interface ManagementPanelProps {
+ stockCode: string;
+}
+
+const ManagementPanel: React.FC = ({ stockCode }) => {
+ const { management, loading } = useManagementData(stockCode);
+
+ // 管理层职位分类
+ const getManagementByCategory = () => {
+ const categories: Record = {
+ 高管: [],
+ 董事: [],
+ 监事: [],
+ 其他: [],
+ };
+
+ management.forEach((person: any) => {
+ 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;
+ };
+
+ const getCategoryIcon = (category: string) => {
+ switch (category) {
+ case "高管":
+ return FaUserTie;
+ case "董事":
+ return FaCrown;
+ case "监事":
+ return FaEye;
+ default:
+ return FaUsers;
+ }
+ };
+
+ const getCategoryColor = (category: string) => {
+ switch (category) {
+ case "高管":
+ return THEME.gold;
+ case "董事":
+ return THEME.goldLight;
+ case "监事":
+ return "green.400";
+ default:
+ return THEME.textSecondary;
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+ {Object.entries(getManagementByCategory()).map(
+ ([category, people]) =>
+ people.length > 0 && (
+
+
+
+ {category}
+ {people.length}人
+
+
+
+ {people.map((person: any, idx: number) => (
+
+
+
+
+
+
+ {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}
+
+ )}
+
+
+ 任职日期:{formatDate(person.start_date)}
+
+
+
+
+
+ ))}
+
+
+ )
+ )}
+
+ );
+};
+
+export default ManagementPanel;
diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx
new file mode 100644
index 00000000..e87ec1b4
--- /dev/null
+++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx
@@ -0,0 +1,313 @@
+// src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx
+// 股权结构 Tab Panel
+
+import React from "react";
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ Heading,
+ Badge,
+ Icon,
+ Card,
+ CardBody,
+ CardHeader,
+ SimpleGrid,
+ Table,
+ Thead,
+ Tbody,
+ Tr,
+ Th,
+ Td,
+ TableContainer,
+ Tooltip,
+ Stat,
+ StatLabel,
+ StatNumber,
+ StatHelpText,
+} from "@chakra-ui/react";
+import {
+ FaCrown,
+ FaChartPie,
+ FaUsers,
+ FaChartLine,
+ FaArrowUp,
+ FaArrowDown,
+ FaChartBar,
+ FaBuilding,
+ FaGlobe,
+ FaShieldAlt,
+ FaBriefcase,
+ FaCircle,
+ FaUserTie,
+} from "react-icons/fa";
+
+import { useShareholderData } from "../../hooks/useShareholderData";
+import { THEME } from "../config";
+import { formatPercentage, formatShares, formatDate } from "../utils";
+import LoadingState from "./LoadingState";
+
+interface ShareholderPanelProps {
+ stockCode: string;
+}
+
+// 股东类型标签组件
+const ShareholderTypeBadge: React.FC<{ type: string }> = ({ type }) => {
+ const typeConfig: Record = {
+ 基金: { 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}
+
+ );
+};
+
+const ShareholderPanel: React.FC = ({ stockCode }) => {
+ const {
+ actualControl,
+ concentration,
+ topShareholders,
+ topCirculationShareholders,
+ loading,
+ } = useShareholderData(stockCode);
+
+ // 计算股权集中度变化
+ const getConcentrationTrend = () => {
+ const grouped: Record> = {};
+ concentration.forEach((item: any) => {
+ 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}
+
+
+ 截至 {formatDate(actualControl[0].end_date)}
+
+
+
+
+ 控制比例
+
+ {formatPercentage(actualControl[0].holding_ratio)}
+
+
+ {formatShares(actualControl[0].holding_shares)}
+
+
+
+
+
+
+ )}
+
+ {/* 股权集中度 */}
+ {concentration.length > 0 && (
+
+
+
+ 股权集中度
+
+
+ {getConcentrationTrend()
+ .slice(0, 1)
+ .map(([date, items]) => (
+
+
+
+ {formatDate(date)}
+
+
+
+
+ {Object.entries(items).map(([key, item]: [string, any]) => (
+
+ {item.stat_item}
+
+
+ {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 && (
+
+
+
+ 十大股东
+
+ {formatDate(topShareholders[0].end_date)}
+
+
+
+
+
+
+ | 排名 |
+ 股东名称 |
+ 股东类型 |
+ 持股数量 |
+ 持股比例 |
+ 股份性质 |
+
+
+
+ {topShareholders.slice(0, 10).map((shareholder: any, idx: number) => (
+
+ |
+
+ {shareholder.shareholder_rank}
+
+ |
+
+
+
+ {shareholder.shareholder_name}
+
+
+ |
+
+
+ |
+
+ {formatShares(shareholder.holding_shares)}
+ |
+
+
+ {formatPercentage(shareholder.total_share_ratio)}
+
+ |
+
+
+ {shareholder.share_nature || "流通股"}
+
+ |
+
+ ))}
+
+
+
+
+ )}
+
+ {/* 十大流通股东 */}
+ {topCirculationShareholders.length > 0 && (
+
+
+
+ 十大流通股东
+
+ {formatDate(topCirculationShareholders[0].end_date)}
+
+
+
+
+
+
+ | 排名 |
+ 股东名称 |
+ 股东类型 |
+ 持股数量 |
+ 流通股比例 |
+
+
+
+ {topCirculationShareholders.slice(0, 10).map((shareholder: any, idx: number) => (
+
+ |
+
+ {shareholder.shareholder_rank}
+
+ |
+
+
+
+ {shareholder.shareholder_name}
+
+
+ |
+
+
+ |
+
+ {formatShares(shareholder.holding_shares)}
+ |
+
+
+ {formatPercentage(shareholder.circulation_share_ratio)}
+
+ |
+
+ ))}
+
+
+
+
+ )}
+
+ );
+};
+
+export default ShareholderPanel;
diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/index.ts b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/index.ts
new file mode 100644
index 00000000..f3cc4334
--- /dev/null
+++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/index.ts
@@ -0,0 +1,9 @@
+// src/views/Company/components/CompanyOverview/BasicInfoTab/components/index.ts
+// 组件导出
+
+export { default as LoadingState } from "./LoadingState";
+export { default as ShareholderPanel } from "./ShareholderPanel";
+export { default as ManagementPanel } from "./ManagementPanel";
+export { default as AnnouncementsPanel } from "./AnnouncementsPanel";
+export { default as BranchesPanel } from "./BranchesPanel";
+export { default as BusinessInfoPanel } from "./BusinessInfoPanel";
diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/config.ts b/src/views/Company/components/CompanyOverview/BasicInfoTab/config.ts
new file mode 100644
index 00000000..269368ff
--- /dev/null
+++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/config.ts
@@ -0,0 +1,103 @@
+// src/views/Company/components/CompanyOverview/BasicInfoTab/config.ts
+// Tab 配置 + 黑金主题配置
+
+import { IconType } from "react-icons";
+import {
+ FaShareAlt,
+ FaUserTie,
+ FaBullhorn,
+ FaSitemap,
+ FaInfoCircle,
+} from "react-icons/fa";
+
+// 主题类型定义
+export interface Theme {
+ bg: string;
+ cardBg: string;
+ tableBg: string;
+ tableHoverBg: string;
+ gold: string;
+ goldLight: string;
+ textPrimary: string;
+ textSecondary: string;
+ border: string;
+ tabSelected: {
+ bg: string;
+ color: string;
+ };
+ tabUnselected: {
+ color: string;
+ };
+}
+
+// 黑金主题配置
+export const THEME: Theme = {
+ bg: "gray.900",
+ cardBg: "gray.800",
+ tableBg: "gray.700",
+ tableHoverBg: "gray.600",
+ gold: "#D4AF37",
+ goldLight: "#F0D78C",
+ textPrimary: "white",
+ textSecondary: "gray.400",
+ border: "rgba(212, 175, 55, 0.3)",
+ tabSelected: {
+ bg: "#D4AF37",
+ color: "gray.900",
+ },
+ tabUnselected: {
+ color: "#D4AF37",
+ },
+};
+
+// Tab 配置类型
+export interface TabConfig {
+ key: string;
+ name: string;
+ icon: IconType;
+ enabled: boolean;
+}
+
+// Tab 配置
+export const TAB_CONFIG: TabConfig[] = [
+ {
+ key: "shareholder",
+ name: "股权结构",
+ icon: FaShareAlt,
+ enabled: true,
+ },
+ {
+ key: "management",
+ name: "管理团队",
+ icon: FaUserTie,
+ enabled: true,
+ },
+ {
+ key: "announcements",
+ name: "公司公告",
+ icon: FaBullhorn,
+ enabled: true,
+ },
+ {
+ key: "branches",
+ name: "分支机构",
+ icon: FaSitemap,
+ enabled: true,
+ },
+ {
+ key: "business",
+ name: "工商信息",
+ icon: FaInfoCircle,
+ enabled: true,
+ },
+];
+
+// 获取启用的 Tab 列表
+export const getEnabledTabs = (enabledKeys?: string[]): TabConfig[] => {
+ if (!enabledKeys || enabledKeys.length === 0) {
+ return TAB_CONFIG.filter((tab) => tab.enabled);
+ }
+ return TAB_CONFIG.filter(
+ (tab) => tab.enabled && enabledKeys.includes(tab.key)
+ );
+};
diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx
new file mode 100644
index 00000000..7c3ecca3
--- /dev/null
+++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx
@@ -0,0 +1,145 @@
+// src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx
+// 基本信息 Tab 组件 - 可配置版本(黑金主题)
+
+import React from "react";
+import {
+ Card,
+ CardBody,
+ Tabs,
+ TabList,
+ TabPanels,
+ Tab,
+ TabPanel,
+ Icon,
+ HStack,
+ Text,
+} from "@chakra-ui/react";
+
+import { THEME, TAB_CONFIG, getEnabledTabs, type TabConfig } from "./config";
+import {
+ ShareholderPanel,
+ ManagementPanel,
+ AnnouncementsPanel,
+ BranchesPanel,
+ BusinessInfoPanel,
+} from "./components";
+
+// Props 类型定义
+export interface BasicInfoTabProps {
+ stockCode: string;
+ basicInfo?: any;
+
+ // 可配置项
+ enabledTabs?: string[]; // 指定显示哪些 Tab(通过 key)
+ defaultTabIndex?: number; // 默认选中 Tab
+ onTabChange?: (index: number, tabKey: string) => void;
+}
+
+// Tab 组件映射
+const TAB_COMPONENTS: Record> = {
+ shareholder: ShareholderPanel,
+ management: ManagementPanel,
+ announcements: AnnouncementsPanel,
+ branches: BranchesPanel,
+ business: BusinessInfoPanel,
+};
+
+/**
+ * 基本信息 Tab 组件
+ *
+ * 特性:
+ * - 可配置显示哪些 Tab(enabledTabs)
+ * - 黑金主题
+ * - 懒加载(isLazy)
+ * - 支持 Tab 变更回调
+ */
+const BasicInfoTab: React.FC = ({
+ stockCode,
+ basicInfo,
+ enabledTabs,
+ defaultTabIndex = 0,
+ onTabChange,
+}) => {
+ // 获取启用的 Tab 配置
+ const tabs = getEnabledTabs(enabledTabs);
+
+ // 处理 Tab 变更
+ const handleTabChange = (index: number) => {
+ if (onTabChange && tabs[index]) {
+ onTabChange(index, tabs[index].key);
+ }
+ };
+
+ // 渲染单个 Tab 内容
+ const renderTabContent = (tab: TabConfig) => {
+ const Component = TAB_COMPONENTS[tab.key];
+ if (!Component) return null;
+
+ // business Tab 需要 basicInfo,其他需要 stockCode
+ if (tab.key === "business") {
+ return ;
+ }
+ return ;
+ };
+
+ return (
+
+
+
+
+ {tabs.map((tab) => (
+
+
+
+ {tab.name}
+
+
+ ))}
+
+
+
+ {tabs.map((tab) => (
+
+ {renderTabContent(tab)}
+
+ ))}
+
+
+
+
+ );
+};
+
+export default BasicInfoTab;
+
+// 导出配置和工具,供外部使用
+export { THEME, TAB_CONFIG, getEnabledTabs } from "./config";
+export * from "./utils";
diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/utils.ts b/src/views/Company/components/CompanyOverview/BasicInfoTab/utils.ts
new file mode 100644
index 00000000..35358861
--- /dev/null
+++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/utils.ts
@@ -0,0 +1,52 @@
+// src/views/Company/components/CompanyOverview/BasicInfoTab/utils.ts
+// 格式化工具函数
+
+/**
+ * 格式化百分比
+ */
+export const formatPercentage = (value: number | null | undefined): string => {
+ if (value === null || value === undefined) return "-";
+ return `${(value * 100).toFixed(2)}%`;
+};
+
+/**
+ * 格式化数字(自动转换亿/万)
+ */
+export const formatNumber = (value: number | null | undefined): string => {
+ 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();
+};
+
+/**
+ * 格式化股数(自动转换亿股/万股)
+ */
+export const formatShares = (value: number | null | undefined): string => {
+ 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()}股`;
+};
+
+/**
+ * 格式化日期(去掉时间部分)
+ */
+export const formatDate = (dateStr: string | null | undefined): string => {
+ if (!dateStr) return "-";
+ return dateStr.split("T")[0];
+};
+
+// 导出工具对象(兼容旧代码)
+export const formatUtils = {
+ formatPercentage,
+ formatNumber,
+ formatShares,
+ formatDate,
+};
diff --git a/src/views/Company/components/CompanyOverview/index.tsx b/src/views/Company/components/CompanyOverview/index.tsx
index c1f4f22a..8a499a42 100644
--- a/src/views/Company/components/CompanyOverview/index.tsx
+++ b/src/views/Company/components/CompanyOverview/index.tsx
@@ -56,7 +56,6 @@ const CompanyOverview: React.FC = ({ stockCode }) => {
);
diff --git a/src/views/Company/components/CompanyOverview/types.ts b/src/views/Company/components/CompanyOverview/types.ts
index c274f8a8..23836a59 100644
--- a/src/views/Company/components/CompanyOverview/types.ts
+++ b/src/views/Company/components/CompanyOverview/types.ts
@@ -28,9 +28,13 @@ export interface BasicInfo {
* 实际控制人
*/
export interface ActualControl {
+ actual_controller_name?: string;
controller_name?: string;
+ control_type?: string;
controller_type?: string;
holding_ratio?: number;
+ holding_shares?: number;
+ end_date?: string;
}
/**
@@ -40,6 +44,10 @@ export interface Concentration {
top1_ratio?: number;
top5_ratio?: number;
top10_ratio?: number;
+ stat_item?: string;
+ holding_ratio?: number;
+ ratio_change?: number;
+ end_date?: string;
}
/**
@@ -48,8 +56,14 @@ export interface Concentration {
export interface Management {
name?: string;
position?: string;
+ position_name?: string;
+ position_category?: string;
start_date?: string;
end_date?: string;
+ gender?: string;
+ education?: string;
+ birth_year?: string;
+ nationality?: string;
}
/**
@@ -57,8 +71,15 @@ export interface Management {
*/
export interface Shareholder {
shareholder_name?: string;
+ shareholder_type?: string;
+ shareholder_rank?: number;
holding_ratio?: number;
holding_amount?: number;
+ holding_shares?: number;
+ total_share_ratio?: number;
+ circulation_share_ratio?: number;
+ share_nature?: string;
+ end_date?: string;
}
/**