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 数据 */}
);