From 9aaf4400c185183ecd370ac3cd77cfa6a6157ec6 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 10 Dec 2025 16:28:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20BasicInfoTab=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E4=B8=BA=20TypeScript=20=E6=A8=A1=E5=9D=97=E5=8C=96=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除旧的 BasicInfoTab.js (~1000行) - 新建 BasicInfoTab/ 目录,拆分为 10 个 TypeScript 文件: - index.tsx: 主组件(可配置 Tab) - config.ts: Tab 配置 + 黑金主题 - utils.ts: 格式化工具函数 - components/: 5 个面板组件 + LoadingState - 主组件支持 enabledTabs、defaultTabIndex、onTabChange - 应用黑金主题,支持懒加载 (isLazy) - 更新 types.ts 添加 ActualControl、Concentration 等类型字段 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../CompanyOverview/BasicInfoTab.js | 994 ------------------ .../components/AnnouncementsPanel.tsx | 210 ++++ .../BasicInfoTab/components/BranchesPanel.tsx | 95 ++ .../components/BusinessInfoPanel.tsx | 109 ++ .../BasicInfoTab/components/LoadingState.tsx | 32 + .../components/ManagementPanel.tsx | 179 ++++ .../components/ShareholderPanel.tsx | 313 ++++++ .../BasicInfoTab/components/index.ts | 9 + .../CompanyOverview/BasicInfoTab/config.ts | 103 ++ .../CompanyOverview/BasicInfoTab/index.tsx | 145 +++ .../CompanyOverview/BasicInfoTab/utils.ts | 52 + .../components/CompanyOverview/index.tsx | 1 - .../components/CompanyOverview/types.ts | 21 + 13 files changed, 1268 insertions(+), 995 deletions(-) delete mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab.js create mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab/components/AnnouncementsPanel.tsx create mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab/components/BranchesPanel.tsx create mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab/components/BusinessInfoPanel.tsx create mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab/components/LoadingState.tsx create mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab/components/ManagementPanel.tsx create mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx create mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab/components/index.ts create mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab/config.ts create mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx create mode 100644 src/views/Company/components/CompanyOverview/BasicInfoTab/utils.ts 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; } /**