perf: CompanyOverview 内层 Tab 懒加载优化
- 将 useCompanyOverviewData(9个API)拆分为独立 Hooks: - useBasicInfo: 基本信息(首屏唯一加载) - useShareholderData: 股东信息(4个API) - useManagementData: 管理层信息 - useAnnouncementsData: 公告数据 - useBranchesData: 分支机构 - useDisclosureData: 披露日程 - BasicInfoTab 使用子组件实现真正的懒加载: - ShareholderTabPanel、ManagementTabPanel 等 - 配合 Chakra UI isLazy,切换 Tab 时才加载数据 - 首屏 API 请求从 9 个减少到 1 个 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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<T> {
|
||||||
|
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<Announcement[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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<Announcement[]>;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
};
|
||||||
@@ -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<T> {
|
||||||
|
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<BasicInfo | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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<BasicInfo>;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
};
|
||||||
@@ -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<T> {
|
||||||
|
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<Branch[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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<Branch[]>;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
};
|
||||||
@@ -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<T> {
|
||||||
|
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<DisclosureSchedule[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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<DisclosureSchedule[]>;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
};
|
||||||
@@ -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<T> {
|
||||||
|
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<Management[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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<Management[]>;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
};
|
||||||
@@ -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<T> {
|
||||||
|
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<ActualControl[]>([]);
|
||||||
|
const [concentration, setConcentration] = useState<Concentration[]>([]);
|
||||||
|
const [topShareholders, setTopShareholders] = useState<Shareholder[]>([]);
|
||||||
|
const [topCirculationShareholders, setTopCirculationShareholders] = useState<Shareholder[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(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<ApiResponse<ActualControl[]>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/concentration`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<Concentration[]>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/top-shareholders?limit=10`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<Shareholder[]>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/top-circulation-shareholders?limit=10`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<Shareholder[]>>,
|
||||||
|
]);
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
// src/views/Company/components/CompanyOverview/index.tsx
|
// src/views/Company/components/CompanyOverview/index.tsx
|
||||||
// 公司概览 - 主组件(组合层)
|
// 公司概览 - 主组件(组合层)
|
||||||
|
// 懒加载优化:只加载头部卡片数据,BasicInfoTab 内部懒加载各 Tab 数据
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { VStack, Spinner, Center, Text } from "@chakra-ui/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 CompanyHeaderCard from "./CompanyHeaderCard";
|
||||||
import type { CompanyOverviewProps } from "./types";
|
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<CompanyOverviewProps> = ({ stockCode }) => {
|
const CompanyOverview: React.FC<CompanyOverviewProps> = ({ stockCode }) => {
|
||||||
const {
|
const { basicInfo, loading, error } = useBasicInfo(stockCode);
|
||||||
basicInfo,
|
|
||||||
actualControl,
|
|
||||||
concentration,
|
|
||||||
management,
|
|
||||||
topCirculationShareholders,
|
|
||||||
topShareholders,
|
|
||||||
branches,
|
|
||||||
announcements,
|
|
||||||
disclosureSchedule,
|
|
||||||
loading,
|
|
||||||
} = useCompanyOverviewData(stockCode);
|
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
if (loading && !basicInfo) {
|
if (loading && !basicInfo) {
|
||||||
@@ -44,24 +38,25 @@ const CompanyOverview: React.FC<CompanyOverviewProps> = ({ stockCode }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 错误状态
|
||||||
|
if (error && !basicInfo) {
|
||||||
|
return (
|
||||||
|
<Center h="300px">
|
||||||
|
<Text color="red.500">{error}</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack spacing={6} align="stretch">
|
<VStack spacing={6} align="stretch">
|
||||||
{/* 公司头部信息卡片 */}
|
{/* 公司头部信息卡片 */}
|
||||||
{basicInfo && <CompanyHeaderCard basicInfo={basicInfo} />}
|
{basicInfo && <CompanyHeaderCard basicInfo={basicInfo} />}
|
||||||
|
|
||||||
{/* 基本信息内容 */}
|
{/* 基本信息内容 - 传入 stockCode,内部懒加载各 Tab 数据 */}
|
||||||
<BasicInfoTab
|
<BasicInfoTab
|
||||||
|
stockCode={stockCode}
|
||||||
basicInfo={basicInfo}
|
basicInfo={basicInfo}
|
||||||
actualControl={actualControl}
|
|
||||||
concentration={concentration}
|
|
||||||
topShareholders={topShareholders}
|
|
||||||
topCirculationShareholders={topCirculationShareholders}
|
|
||||||
management={management}
|
|
||||||
announcements={announcements}
|
|
||||||
branches={branches}
|
|
||||||
disclosureSchedule={disclosureSchedule}
|
|
||||||
cardBg="white"
|
cardBg="white"
|
||||||
loading={loading}
|
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user