diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab.js b/src/views/Company/components/CompanyOverview/BasicInfoTab.js index cbf2d82b..163a665c 100644 --- a/src/views/Company/components/CompanyOverview/BasicInfoTab.js +++ b/src/views/Company/components/CompanyOverview/BasicInfoTab.js @@ -1,5 +1,6 @@ // src/views/Company/components/CompanyOverview/BasicInfoTab.js // 基本信息 Tab - 股权结构、管理团队、公司公告、分支机构、工商信息 +// 懒加载优化:使用 isLazy + 独立 Hooks,点击 Tab 时才加载对应数据 import React from "react"; import { @@ -46,7 +47,15 @@ import { 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, @@ -128,37 +137,267 @@ const ShareholderTypeBadge = ({ type }) => { ); }; +// ============================================ +// 懒加载 TabPanel 子组件 +// 每个子组件独立调用 Hook,配合 isLazy 实现真正的懒加载 +// ============================================ + /** - * 基本信息 Tab 组件 - * - * Props: - * - basicInfo: 公司基本信息 - * - actualControl: 实际控制人数组 - * - concentration: 股权集中度数组 - * - topShareholders: 前十大股东数组 - * - topCirculationShareholders: 前十大流通股东数组 - * - management: 管理层数组 - * - announcements: 公告列表数组 - * - branches: 分支机构数组 - * - disclosureSchedule: 披露日程数组 - * - cardBg: 卡片背景色 - * - onAnnouncementClick: 公告点击回调 (announcement) => void + * 股权结构 Tab Panel - 懒加载子组件 */ -const BasicInfoTab = ({ - basicInfo, - actualControl = [], - concentration = [], - topShareholders = [], - topCirculationShareholders = [], - management = [], - announcements = [], - branches = [], - disclosureSchedule = [], - cardBg, - loading = false, -}) => { - const { isOpen, onOpen, onClose } = useDisclosure(); - const [selectedAnnouncement, setSelectedAnnouncement] = React.useState(null); +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 = () => { @@ -193,705 +432,254 @@ const BasicInfoTab = ({ return categories; }; - // 计算股权集中度变化 - 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 ( + + {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 + )} + + + + + ))} + + + )} - - {/* 股权结构标签页 */} - - - {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)} + + + + 最新公告 + + + {announcements.map((announcement, idx) => ( + handleAnnouncementClick(announcement)} + _hover={{ bg: "gray.50" }} + > + + + + + + {announcement.info_type || "公告"} + + {formatUtils.formatDate(announcement.announce_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 - )} - -
-
-
- )} -
-
- - {/* 管理团队标签页 */} - - - {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 - )} - - - - - - ))} - - - ) - )} - - - - {/* 公司公告标签页 */} - - - {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 - )} - - - - - ))} - - - )} - - - - - - - 最新公告 + + {announcement.title} + + + + {announcement.format && ( + + {announcement.format} + + )} + } + variant="ghost" + onClick={(e) => { + e.stopPropagation(); + window.open(announcement.url, "_blank"); + }} + /> - - {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"); - }} - /> - - - - - ))} - - -
- - - {/* 分支机构标签页 */} - - {branches.length > 0 ? ( - - {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} 家 - - - - - - - ))} - - ) : ( -
- - - 暂无分支机构信息 - -
- )} -
- - {/* 工商信息标签页 */} - - {basicInfo && ( - - - - - 工商信息 - - - - - 统一信用代码 - - {basicInfo.credit_code} - - - - 公司规模 - - {basicInfo.company_size} - - - - 注册地址 - - - {basicInfo.reg_address} - - - - - 办公地址 - - - {basicInfo.office_address} - - - - - - - - 服务机构 - - - - - 会计师事务所 - - - {basicInfo.accounting_firm} - - - - - 律师事务所 - - - {basicInfo.law_firm} - - - - - - - - - - - 主营业务 - - - {basicInfo.main_business} - - - - - - 经营范围 - - - {basicInfo.business_scope} - - - - )} - - -
-
-
+ + + + ))} + + + {/* 公告详情模态框 */} @@ -939,4 +727,268 @@ const BasicInfoTab = ({ ); }; +/** + * 分支机构 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/hooks/useAnnouncementsData.ts b/src/views/Company/components/CompanyOverview/hooks/useAnnouncementsData.ts new file mode 100644 index 00000000..4c0fb953 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/hooks/useAnnouncementsData.ts @@ -0,0 +1,61 @@ +// src/views/Company/components/CompanyOverview/hooks/useAnnouncementsData.ts +// 公告数据 Hook - 用于公司公告 Tab + +import { useState, useEffect, useCallback } from "react"; +import { logger } from "@utils/logger"; +import { getApiBase } from "@utils/apiConfig"; +import type { Announcement } from "../types"; + +const API_BASE_URL = getApiBase(); + +interface ApiResponse { + success: boolean; + data: T; +} + +interface UseAnnouncementsDataResult { + announcements: Announcement[]; + loading: boolean; + error: string | null; +} + +/** + * 公告数据 Hook + * @param stockCode - 股票代码 + */ +export const useAnnouncementsData = (stockCode?: string): UseAnnouncementsDataResult => { + const [announcements, setAnnouncements] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadData = useCallback(async () => { + if (!stockCode) return; + + setLoading(true); + setError(null); + + try { + const response = await fetch( + `${API_BASE_URL}/api/stock/${stockCode}/announcements?limit=20` + ); + const result = (await response.json()) as ApiResponse; + + if (result.success) { + setAnnouncements(result.data); + } else { + setError("加载公告数据失败"); + } + } catch (err) { + logger.error("useAnnouncementsData", "loadData", err, { stockCode }); + setError("网络请求失败"); + } finally { + setLoading(false); + } + }, [stockCode]); + + useEffect(() => { + loadData(); + }, [loadData]); + + return { announcements, loading, error }; +}; diff --git a/src/views/Company/components/CompanyOverview/hooks/useBasicInfo.ts b/src/views/Company/components/CompanyOverview/hooks/useBasicInfo.ts new file mode 100644 index 00000000..a3c9bd2f --- /dev/null +++ b/src/views/Company/components/CompanyOverview/hooks/useBasicInfo.ts @@ -0,0 +1,59 @@ +// src/views/Company/components/CompanyOverview/hooks/useBasicInfo.ts +// 公司基本信息 Hook - 用于 CompanyHeaderCard + +import { useState, useEffect, useCallback } from "react"; +import { logger } from "@utils/logger"; +import { getApiBase } from "@utils/apiConfig"; +import type { BasicInfo } from "../types"; + +const API_BASE_URL = getApiBase(); + +interface ApiResponse { + success: boolean; + data: T; +} + +interface UseBasicInfoResult { + basicInfo: BasicInfo | null; + loading: boolean; + error: string | null; +} + +/** + * 公司基本信息 Hook + * @param stockCode - 股票代码 + */ +export const useBasicInfo = (stockCode?: string): UseBasicInfoResult => { + const [basicInfo, setBasicInfo] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadData = useCallback(async () => { + if (!stockCode) return; + + setLoading(true); + setError(null); + + try { + const response = await fetch(`${API_BASE_URL}/api/stock/${stockCode}/basic-info`); + const result = (await response.json()) as ApiResponse; + + if (result.success) { + setBasicInfo(result.data); + } else { + setError("加载基本信息失败"); + } + } catch (err) { + logger.error("useBasicInfo", "loadData", err, { stockCode }); + setError("网络请求失败"); + } finally { + setLoading(false); + } + }, [stockCode]); + + useEffect(() => { + loadData(); + }, [loadData]); + + return { basicInfo, loading, error }; +}; diff --git a/src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts b/src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts new file mode 100644 index 00000000..42a18560 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts @@ -0,0 +1,59 @@ +// src/views/Company/components/CompanyOverview/hooks/useBranchesData.ts +// 分支机构数据 Hook - 用于分支机构 Tab + +import { useState, useEffect, useCallback } from "react"; +import { logger } from "@utils/logger"; +import { getApiBase } from "@utils/apiConfig"; +import type { Branch } from "../types"; + +const API_BASE_URL = getApiBase(); + +interface ApiResponse { + success: boolean; + data: T; +} + +interface UseBranchesDataResult { + branches: Branch[]; + loading: boolean; + error: string | null; +} + +/** + * 分支机构数据 Hook + * @param stockCode - 股票代码 + */ +export const useBranchesData = (stockCode?: string): UseBranchesDataResult => { + const [branches, setBranches] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadData = useCallback(async () => { + if (!stockCode) return; + + setLoading(true); + setError(null); + + try { + const response = await fetch(`${API_BASE_URL}/api/stock/${stockCode}/branches`); + const result = (await response.json()) as ApiResponse; + + if (result.success) { + setBranches(result.data); + } else { + setError("加载分支机构数据失败"); + } + } catch (err) { + logger.error("useBranchesData", "loadData", err, { stockCode }); + setError("网络请求失败"); + } finally { + setLoading(false); + } + }, [stockCode]); + + useEffect(() => { + loadData(); + }, [loadData]); + + return { branches, loading, error }; +}; diff --git a/src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts b/src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts new file mode 100644 index 00000000..a803e08b --- /dev/null +++ b/src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts @@ -0,0 +1,61 @@ +// src/views/Company/components/CompanyOverview/hooks/useDisclosureData.ts +// 披露日程数据 Hook - 用于工商信息 Tab + +import { useState, useEffect, useCallback } from "react"; +import { logger } from "@utils/logger"; +import { getApiBase } from "@utils/apiConfig"; +import type { DisclosureSchedule } from "../types"; + +const API_BASE_URL = getApiBase(); + +interface ApiResponse { + success: boolean; + data: T; +} + +interface UseDisclosureDataResult { + disclosureSchedule: DisclosureSchedule[]; + loading: boolean; + error: string | null; +} + +/** + * 披露日程数据 Hook + * @param stockCode - 股票代码 + */ +export const useDisclosureData = (stockCode?: string): UseDisclosureDataResult => { + const [disclosureSchedule, setDisclosureSchedule] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadData = useCallback(async () => { + if (!stockCode) return; + + setLoading(true); + setError(null); + + try { + const response = await fetch( + `${API_BASE_URL}/api/stock/${stockCode}/disclosure-schedule` + ); + const result = (await response.json()) as ApiResponse; + + if (result.success) { + setDisclosureSchedule(result.data); + } else { + setError("加载披露日程数据失败"); + } + } catch (err) { + logger.error("useDisclosureData", "loadData", err, { stockCode }); + setError("网络请求失败"); + } finally { + setLoading(false); + } + }, [stockCode]); + + useEffect(() => { + loadData(); + }, [loadData]); + + return { disclosureSchedule, loading, error }; +}; diff --git a/src/views/Company/components/CompanyOverview/hooks/useManagementData.ts b/src/views/Company/components/CompanyOverview/hooks/useManagementData.ts new file mode 100644 index 00000000..cbf2e2b2 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/hooks/useManagementData.ts @@ -0,0 +1,61 @@ +// src/views/Company/components/CompanyOverview/hooks/useManagementData.ts +// 管理团队数据 Hook - 用于管理团队 Tab + +import { useState, useEffect, useCallback } from "react"; +import { logger } from "@utils/logger"; +import { getApiBase } from "@utils/apiConfig"; +import type { Management } from "../types"; + +const API_BASE_URL = getApiBase(); + +interface ApiResponse { + success: boolean; + data: T; +} + +interface UseManagementDataResult { + management: Management[]; + loading: boolean; + error: string | null; +} + +/** + * 管理团队数据 Hook + * @param stockCode - 股票代码 + */ +export const useManagementData = (stockCode?: string): UseManagementDataResult => { + const [management, setManagement] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadData = useCallback(async () => { + if (!stockCode) return; + + setLoading(true); + setError(null); + + try { + const response = await fetch( + `${API_BASE_URL}/api/stock/${stockCode}/management?active_only=true` + ); + const result = (await response.json()) as ApiResponse; + + if (result.success) { + setManagement(result.data); + } else { + setError("加载管理团队数据失败"); + } + } catch (err) { + logger.error("useManagementData", "loadData", err, { stockCode }); + setError("网络请求失败"); + } finally { + setLoading(false); + } + }, [stockCode]); + + useEffect(() => { + loadData(); + }, [loadData]); + + return { management, loading, error }; +}; diff --git a/src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts b/src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts new file mode 100644 index 00000000..a4047690 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts @@ -0,0 +1,83 @@ +// src/views/Company/components/CompanyOverview/hooks/useShareholderData.ts +// 股权结构数据 Hook - 用于股权结构 Tab + +import { useState, useEffect, useCallback } from "react"; +import { logger } from "@utils/logger"; +import { getApiBase } from "@utils/apiConfig"; +import type { ActualControl, Concentration, Shareholder } from "../types"; + +const API_BASE_URL = getApiBase(); + +interface ApiResponse { + success: boolean; + data: T; +} + +interface UseShareholderDataResult { + actualControl: ActualControl[]; + concentration: Concentration[]; + topShareholders: Shareholder[]; + topCirculationShareholders: Shareholder[]; + loading: boolean; + error: string | null; +} + +/** + * 股权结构数据 Hook + * @param stockCode - 股票代码 + */ +export const useShareholderData = (stockCode?: string): UseShareholderDataResult => { + const [actualControl, setActualControl] = useState([]); + const [concentration, setConcentration] = useState([]); + const [topShareholders, setTopShareholders] = useState([]); + const [topCirculationShareholders, setTopCirculationShareholders] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadData = useCallback(async () => { + if (!stockCode) return; + + setLoading(true); + setError(null); + + try { + const [actualRes, concentrationRes, shareholdersRes, circulationRes] = await Promise.all([ + fetch(`${API_BASE_URL}/api/stock/${stockCode}/actual-control`).then((r) => + r.json() + ) as Promise>, + fetch(`${API_BASE_URL}/api/stock/${stockCode}/concentration`).then((r) => + r.json() + ) as Promise>, + fetch(`${API_BASE_URL}/api/stock/${stockCode}/top-shareholders?limit=10`).then((r) => + r.json() + ) as Promise>, + fetch(`${API_BASE_URL}/api/stock/${stockCode}/top-circulation-shareholders?limit=10`).then((r) => + r.json() + ) as Promise>, + ]); + + if (actualRes.success) setActualControl(actualRes.data); + if (concentrationRes.success) setConcentration(concentrationRes.data); + if (shareholdersRes.success) setTopShareholders(shareholdersRes.data); + if (circulationRes.success) setTopCirculationShareholders(circulationRes.data); + } catch (err) { + logger.error("useShareholderData", "loadData", err, { stockCode }); + setError("加载股权结构数据失败"); + } finally { + setLoading(false); + } + }, [stockCode]); + + useEffect(() => { + loadData(); + }, [loadData]); + + return { + actualControl, + concentration, + topShareholders, + topCirculationShareholders, + loading, + error, + }; +}; diff --git a/src/views/Company/components/CompanyOverview/index.tsx b/src/views/Company/components/CompanyOverview/index.tsx index 1ea383ff..c1f4f22a 100644 --- a/src/views/Company/components/CompanyOverview/index.tsx +++ b/src/views/Company/components/CompanyOverview/index.tsx @@ -1,10 +1,11 @@ // src/views/Company/components/CompanyOverview/index.tsx // 公司概览 - 主组件(组合层) +// 懒加载优化:只加载头部卡片数据,BasicInfoTab 内部懒加载各 Tab 数据 import React from "react"; import { VStack, Spinner, Center, Text } from "@chakra-ui/react"; -import { useCompanyOverviewData } from "./hooks/useCompanyOverviewData"; +import { useBasicInfo } from "./hooks/useBasicInfo"; import CompanyHeaderCard from "./CompanyHeaderCard"; import type { CompanyOverviewProps } from "./types"; @@ -15,22 +16,15 @@ import BasicInfoTab from "./BasicInfoTab"; * 公司概览组件 * * 功能: - * - 显示公司头部信息卡片 - * - 显示基本信息(股权结构、管理层、公告等) + * - 显示公司头部信息卡片(useBasicInfo) + * - 显示基本信息 Tab(内部懒加载各子 Tab 数据) + * + * 懒加载策略: + * - 主组件只加载 basicInfo(1 个 API) + * - BasicInfoTab 内部根据 Tab 切换懒加载其他数据 */ const CompanyOverview: React.FC = ({ stockCode }) => { - const { - basicInfo, - actualControl, - concentration, - management, - topCirculationShareholders, - topShareholders, - branches, - announcements, - disclosureSchedule, - loading, - } = useCompanyOverviewData(stockCode); + const { basicInfo, loading, error } = useBasicInfo(stockCode); // 加载状态 if (loading && !basicInfo) { @@ -44,24 +38,25 @@ const CompanyOverview: React.FC = ({ stockCode }) => { ); } + // 错误状态 + if (error && !basicInfo) { + return ( +
+ {error} +
+ ); + } + return ( {/* 公司头部信息卡片 */} {basicInfo && } - {/* 基本信息内容 */} + {/* 基本信息内容 - 传入 stockCode,内部懒加载各 Tab 数据 */} );