refactor: CompanyOverview 组件 TypeScript 拆分

- 新增 index.tsx: 主组件(组合层,50 行)
- 新增 CompanyHeaderCard.tsx: 头部卡片组件(168 行)
- 新增 hooks/useCompanyOverviewData.ts: 数据加载 Hook
- 删除 index.js: 原 330 行代码精简 85%
- 修复 Company/index.js: 恢复 CompanyTabs 渲染

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-10 11:21:02 +08:00
parent bf8847698b
commit 9a77bb6f0b
5 changed files with 378 additions and 330 deletions

View File

@@ -0,0 +1,167 @@
// src/views/Company/components/CompanyOverview/CompanyHeaderCard.tsx
// 公司头部信息卡片组件
import React from "react";
import {
VStack,
HStack,
Text,
Badge,
Card,
CardBody,
Heading,
SimpleGrid,
Divider,
Icon,
Grid,
GridItem,
Stat,
StatLabel,
StatNumber,
Circle,
Link,
} from "@chakra-ui/react";
import {
FaBuilding,
FaMapMarkerAlt,
FaUserShield,
FaBriefcase,
FaCalendarAlt,
FaGlobe,
FaEnvelope,
FaPhone,
FaCrown,
} from "react-icons/fa";
import { ExternalLinkIcon } from "@chakra-ui/icons";
import type { CompanyHeaderCardProps } from "./types";
import { formatRegisteredCapital, formatDate } from "./utils";
/**
* 公司头部信息卡片组件
*/
const CompanyHeaderCard: React.FC<CompanyHeaderCardProps> = ({ basicInfo }) => {
return (
<Card
bg="white"
shadow="lg"
borderTop="4px solid"
borderTopColor="blue.500"
>
<CardBody>
<Grid templateColumns="repeat(12, 1fr)" gap={6}>
{/* 左侧:公司基本信息 */}
<GridItem colSpan={{ base: 12, lg: 8 }}>
<VStack align="start" spacing={4}>
{/* 公司名称和代码 */}
<HStack spacing={4}>
<Circle size="60px" bg="blue.500">
<Icon as={FaBuilding} color="white" boxSize={8} />
</Circle>
<VStack align="start" spacing={1}>
<HStack>
<Heading size="lg" color="blue.600">
{basicInfo.ORGNAME || basicInfo.SECNAME}
</Heading>
<Badge colorScheme="blue" fontSize="md" px={2} py={1}>
{basicInfo.SECCODE}
</Badge>
</HStack>
<HStack spacing={2}>
<Badge colorScheme="purple" fontSize="xs">
{basicInfo.sw_industry_l1}
</Badge>
<Badge colorScheme="orange" fontSize="xs">
{basicInfo.sw_industry_l2}
</Badge>
{basicInfo.sw_industry_l3 && (
<Badge colorScheme="green" fontSize="xs">
{basicInfo.sw_industry_l3}
</Badge>
)}
</HStack>
</VStack>
</HStack>
<Divider />
{/* 管理层信息 */}
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={3} w="full">
<HStack>
<Icon as={FaUserShield} color="gray.500" boxSize={4} />
<Text fontSize="sm">
<Text as="span" color="gray.500"></Text>
<Text as="span" fontWeight="bold">{basicInfo.legal_representative}</Text>
</Text>
</HStack>
<HStack>
<Icon as={FaCrown} color="gray.500" boxSize={4} />
<Text fontSize="sm">
<Text as="span" color="gray.500"></Text>
<Text as="span" fontWeight="bold">{basicInfo.chairman}</Text>
</Text>
</HStack>
<HStack>
<Icon as={FaBriefcase} color="gray.500" boxSize={4} />
<Text fontSize="sm">
<Text as="span" color="gray.500"></Text>
<Text as="span" fontWeight="bold">{basicInfo.general_manager}</Text>
</Text>
</HStack>
<HStack>
<Icon as={FaCalendarAlt} color="gray.500" boxSize={4} />
<Text fontSize="sm">
<Text as="span" color="gray.500"></Text>
<Text as="span" fontWeight="bold">{formatDate(basicInfo.establish_date)}</Text>
</Text>
</HStack>
</SimpleGrid>
{/* 公司简介 */}
<Text fontSize="sm" color="gray.600" noOfLines={2}>
{basicInfo.company_intro}
</Text>
</VStack>
</GridItem>
{/* 右侧:注册资本和联系方式 */}
<GridItem colSpan={{ base: 12, lg: 4 }}>
<VStack spacing={3} align="stretch">
<Stat>
<StatLabel></StatLabel>
<StatNumber fontSize="2xl" color="blue.500">
{formatRegisteredCapital(basicInfo.reg_capital)}
</StatNumber>
</Stat>
<Divider />
<VStack align="stretch" spacing={1}>
<HStack fontSize="sm">
<Icon as={FaMapMarkerAlt} color="gray.500" boxSize={3} />
<Text noOfLines={1}>{basicInfo.province} {basicInfo.city}</Text>
</HStack>
<HStack fontSize="sm">
<Icon as={FaGlobe} color="gray.500" boxSize={3} />
<Link href={basicInfo.website} isExternal color="blue.500" noOfLines={1}>
{basicInfo.website} <ExternalLinkIcon mx="2px" />
</Link>
</HStack>
<HStack fontSize="sm">
<Icon as={FaEnvelope} color="gray.500" boxSize={3} />
<Text noOfLines={1}>{basicInfo.email}</Text>
</HStack>
<HStack fontSize="sm">
<Icon as={FaPhone} color="gray.500" boxSize={3} />
<Text>{basicInfo.tel}</Text>
</HStack>
</VStack>
</VStack>
</GridItem>
</Grid>
</CardBody>
</Card>
);
};
export default CompanyHeaderCard;

View File

@@ -0,0 +1,140 @@
// src/views/Company/components/CompanyOverview/hooks/useCompanyOverviewData.ts
// 公司概览数据加载 Hook
import { useState, useEffect, useCallback } from "react";
import { logger } from "@utils/logger";
import { getApiBase } from "@utils/apiConfig";
import type {
BasicInfo,
ActualControl,
Concentration,
Management,
Shareholder,
Branch,
Announcement,
DisclosureSchedule,
CompanyOverviewData,
} from "../types";
const API_BASE_URL = getApiBase();
interface ApiResponse<T> {
success: boolean;
data: T;
}
/**
* 公司概览数据加载 Hook
* @param propStockCode - 股票代码
* @returns 公司概览数据
*/
export const useCompanyOverviewData = (propStockCode?: string): CompanyOverviewData => {
const [stockCode, setStockCode] = useState(propStockCode || "000001");
const [loading, setLoading] = useState(false);
const [dataLoaded, setDataLoaded] = useState(false);
// 基本信息数据
const [basicInfo, setBasicInfo] = useState<BasicInfo | null>(null);
const [actualControl, setActualControl] = useState<ActualControl[]>([]);
const [concentration, setConcentration] = useState<Concentration[]>([]);
const [management, setManagement] = useState<Management[]>([]);
const [topCirculationShareholders, setTopCirculationShareholders] = useState<Shareholder[]>([]);
const [topShareholders, setTopShareholders] = useState<Shareholder[]>([]);
const [branches, setBranches] = useState<Branch[]>([]);
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
const [disclosureSchedule, setDisclosureSchedule] = useState<DisclosureSchedule[]>([]);
// 监听 props 中的 stockCode 变化
useEffect(() => {
if (propStockCode && propStockCode !== stockCode) {
setStockCode(propStockCode);
setDataLoaded(false);
}
}, [propStockCode, stockCode]);
// 加载基本信息数据9个接口
const loadBasicInfoData = useCallback(async () => {
if (dataLoaded) return;
setLoading(true);
try {
const [
basicRes,
actualRes,
concentrationRes,
managementRes,
circulationRes,
shareholdersRes,
branchesRes,
announcementsRes,
disclosureRes,
] = await Promise.all([
fetch(`${API_BASE_URL}/api/stock/${stockCode}/basic-info`).then((r) =>
r.json()
) as Promise<ApiResponse<BasicInfo>>,
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}/management?active_only=true`).then((r) =>
r.json()
) as Promise<ApiResponse<Management[]>>,
fetch(`${API_BASE_URL}/api/stock/${stockCode}/top-circulation-shareholders?limit=10`).then((r) =>
r.json()
) as Promise<ApiResponse<Shareholder[]>>,
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}/branches`).then((r) =>
r.json()
) as Promise<ApiResponse<Branch[]>>,
fetch(`${API_BASE_URL}/api/stock/${stockCode}/announcements?limit=20`).then((r) =>
r.json()
) as Promise<ApiResponse<Announcement[]>>,
fetch(`${API_BASE_URL}/api/stock/${stockCode}/disclosure-schedule`).then((r) =>
r.json()
) as Promise<ApiResponse<DisclosureSchedule[]>>,
]);
if (basicRes.success) setBasicInfo(basicRes.data);
if (actualRes.success) setActualControl(actualRes.data);
if (concentrationRes.success) setConcentration(concentrationRes.data);
if (managementRes.success) setManagement(managementRes.data);
if (circulationRes.success) setTopCirculationShareholders(circulationRes.data);
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
if (branchesRes.success) setBranches(branchesRes.data);
if (announcementsRes.success) setAnnouncements(announcementsRes.data);
if (disclosureRes.success) setDisclosureSchedule(disclosureRes.data);
setDataLoaded(true);
} catch (err) {
logger.error("useCompanyOverviewData", "loadBasicInfoData", err, { stockCode });
} finally {
setLoading(false);
}
}, [stockCode, dataLoaded]);
// 首次加载
useEffect(() => {
if (stockCode) {
loadBasicInfoData();
}
}, [stockCode, loadBasicInfoData]);
return {
basicInfo,
actualControl,
concentration,
management,
topCirculationShareholders,
topShareholders,
branches,
announcements,
disclosureSchedule,
loading,
dataLoaded,
};
};

View File

@@ -1,329 +0,0 @@
// src/views/Company/components/CompanyOverview/index.js
// 公司概览 - 头部卡片 + 基本信息
import React, { useState, useEffect } from "react";
import {
Box,
VStack,
HStack,
Text,
Badge,
Card,
CardBody,
Heading,
SimpleGrid,
Divider,
Spinner,
Center,
Icon,
Grid,
GridItem,
Stat,
StatLabel,
StatNumber,
Circle,
Link,
} from "@chakra-ui/react";
import {
FaBuilding,
FaMapMarkerAlt,
FaUserShield,
FaBriefcase,
FaCalendarAlt,
FaGlobe,
FaEnvelope,
FaPhone,
FaCrown,
} from "react-icons/fa";
import { ExternalLinkIcon } from "@chakra-ui/icons";
import { logger } from "@utils/logger";
import { getApiBase } from "@utils/apiConfig";
// 子组件
import BasicInfoTab from "./BasicInfoTab";
// API配置
const API_BASE_URL = getApiBase();
// 格式化工具
const formatUtils = {
formatRegisteredCapital: (value) => {
if (!value && value !== 0) return "-";
const absValue = Math.abs(value);
if (absValue >= 100000) {
return (value / 10000).toFixed(2) + "亿元";
}
return value.toFixed(2) + "万元";
},
formatDate: (dateString) => {
if (!dateString) return "-";
return new Date(dateString).toLocaleDateString("zh-CN");
},
};
/**
* 公司概览组件
*
* 功能:
* - 显示公司头部信息卡片
* - 显示基本信息(股权结构、管理层、公告等)
*
* @param {Object} props
* @param {string} props.stockCode - 股票代码
*/
const CompanyOverview = ({ stockCode: propStockCode }) => {
const [stockCode, setStockCode] = useState(propStockCode || "000001");
const [loading, setLoading] = useState(false);
const [dataLoaded, setDataLoaded] = useState(false);
// 监听 props 中的 stockCode 变化
useEffect(() => {
if (propStockCode && propStockCode !== stockCode) {
setStockCode(propStockCode);
setDataLoaded(false);
}
}, [propStockCode, stockCode]);
// 基本信息数据
const [basicInfo, setBasicInfo] = useState(null);
const [actualControl, setActualControl] = useState([]);
const [concentration, setConcentration] = useState([]);
const [management, setManagement] = useState([]);
const [topCirculationShareholders, setTopCirculationShareholders] = useState([]);
const [topShareholders, setTopShareholders] = useState([]);
const [branches, setBranches] = useState([]);
const [announcements, setAnnouncements] = useState([]);
const [disclosureSchedule, setDisclosureSchedule] = useState([]);
const [_error, setError] = useState(null);
// 加载基本信息数据9个接口
const loadBasicInfoData = async () => {
if (dataLoaded) return;
setLoading(true);
setError(null);
try {
const requests = [
fetch(`${API_BASE_URL}/api/stock/${stockCode}/basic-info`).then((r) =>
r.json()
),
fetch(
`${API_BASE_URL}/api/stock/${stockCode}/actual-control`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/stock/${stockCode}/concentration`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/stock/${stockCode}/management?active_only=true`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/stock/${stockCode}/top-circulation-shareholders?limit=10`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/stock/${stockCode}/top-shareholders?limit=10`
).then((r) => r.json()),
fetch(`${API_BASE_URL}/api/stock/${stockCode}/branches`).then((r) =>
r.json()
),
fetch(
`${API_BASE_URL}/api/stock/${stockCode}/announcements?limit=20`
).then((r) => r.json()),
fetch(
`${API_BASE_URL}/api/stock/${stockCode}/disclosure-schedule`
).then((r) => r.json()),
];
const [
basicRes,
actualRes,
concentrationRes,
managementRes,
circulationRes,
shareholdersRes,
branchesRes,
announcementsRes,
disclosureRes,
] = await Promise.all(requests);
if (basicRes.success) setBasicInfo(basicRes.data);
if (actualRes.success) setActualControl(actualRes.data);
if (concentrationRes.success) setConcentration(concentrationRes.data);
if (managementRes.success) setManagement(managementRes.data);
if (circulationRes.success)
setTopCirculationShareholders(circulationRes.data);
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
if (branchesRes.success) setBranches(branchesRes.data);
if (announcementsRes.success) setAnnouncements(announcementsRes.data);
if (disclosureRes.success) setDisclosureSchedule(disclosureRes.data);
setDataLoaded(true);
} catch (err) {
setError(err.message);
logger.error("CompanyOverview", "loadBasicInfoData", err, { stockCode });
} finally {
setLoading(false);
}
};
// 首次加载
useEffect(() => {
if (stockCode) {
loadBasicInfoData();
}
}, [stockCode]);
if (loading && !basicInfo) {
return (
<Center h="300px">
<VStack spacing={4}>
<Spinner size="xl" color="blue.500" thickness="4px" />
<Text>正在加载公司概览数据...</Text>
</VStack>
</Center>
);
}
return (
<VStack spacing={6} align="stretch">
{/* 公司头部信息卡片 */}
{basicInfo && (
<Card
bg="white"
shadow="lg"
borderTop="4px solid"
borderTopColor="blue.500"
>
<CardBody>
<Grid templateColumns="repeat(12, 1fr)" gap={6}>
<GridItem colSpan={{ base: 12, lg: 8 }}>
<VStack align="start" spacing={4}>
<HStack spacing={4}>
<Circle size="60px" bg="blue.500">
<Icon as={FaBuilding} color="white" boxSize={8} />
</Circle>
<VStack align="start" spacing={1}>
<HStack>
<Heading size="lg" color="blue.600">
{basicInfo.ORGNAME || basicInfo.SECNAME}
</Heading>
<Badge colorScheme="blue" fontSize="md" px={2} py={1}>
{basicInfo.SECCODE}
</Badge>
</HStack>
<HStack spacing={2}>
<Badge colorScheme="purple" fontSize="xs">
{basicInfo.sw_industry_l1}
</Badge>
<Badge colorScheme="orange" fontSize="xs">
{basicInfo.sw_industry_l2}
</Badge>
{basicInfo.sw_industry_l3 && (
<Badge colorScheme="green" fontSize="xs">
{basicInfo.sw_industry_l3}
</Badge>
)}
</HStack>
</VStack>
</HStack>
<Divider />
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={3} w="full">
<HStack>
<Icon as={FaUserShield} color="gray.500" boxSize={4} />
<Text fontSize="sm">
<Text as="span" color="gray.500">法定代表人</Text>
<Text as="span" fontWeight="bold">{basicInfo.legal_representative}</Text>
</Text>
</HStack>
<HStack>
<Icon as={FaCrown} color="gray.500" boxSize={4} />
<Text fontSize="sm">
<Text as="span" color="gray.500">董事长</Text>
<Text as="span" fontWeight="bold">{basicInfo.chairman}</Text>
</Text>
</HStack>
<HStack>
<Icon as={FaBriefcase} color="gray.500" boxSize={4} />
<Text fontSize="sm">
<Text as="span" color="gray.500">总经理</Text>
<Text as="span" fontWeight="bold">{basicInfo.general_manager}</Text>
</Text>
</HStack>
<HStack>
<Icon as={FaCalendarAlt} color="gray.500" boxSize={4} />
<Text fontSize="sm">
<Text as="span" color="gray.500">成立日期</Text>
<Text as="span" fontWeight="bold">{formatUtils.formatDate(basicInfo.establish_date)}</Text>
</Text>
</HStack>
</SimpleGrid>
<Text fontSize="sm" color="gray.600" noOfLines={2}>
{basicInfo.company_intro}
</Text>
</VStack>
</GridItem>
<GridItem colSpan={{ base: 12, lg: 4 }}>
<VStack spacing={3} align="stretch">
<Stat>
<StatLabel>注册资本</StatLabel>
<StatNumber fontSize="2xl" color="blue.500">
{formatUtils.formatRegisteredCapital(basicInfo.reg_capital)}
</StatNumber>
</Stat>
<Divider />
<VStack align="stretch" spacing={1}>
<HStack fontSize="sm">
<Icon as={FaMapMarkerAlt} color="gray.500" boxSize={3} />
<Text noOfLines={1}>{basicInfo.province} {basicInfo.city}</Text>
</HStack>
<HStack fontSize="sm">
<Icon as={FaGlobe} color="gray.500" boxSize={3} />
<Link href={basicInfo.website} isExternal color="blue.500" noOfLines={1}>
{basicInfo.website} <ExternalLinkIcon mx="2px" />
</Link>
</HStack>
<HStack fontSize="sm">
<Icon as={FaEnvelope} color="gray.500" boxSize={3} />
<Text noOfLines={1}>{basicInfo.email}</Text>
</HStack>
<HStack fontSize="sm">
<Icon as={FaPhone} color="gray.500" boxSize={3} />
<Text>{basicInfo.tel}</Text>
</HStack>
</VStack>
</VStack>
</GridItem>
</Grid>
</CardBody>
</Card>
)}
{/* 基本信息内容 */}
<BasicInfoTab
basicInfo={basicInfo}
actualControl={actualControl}
concentration={concentration}
topShareholders={topShareholders}
topCirculationShareholders={topCirculationShareholders}
management={management}
announcements={announcements}
branches={branches}
disclosureSchedule={disclosureSchedule}
cardBg="white"
loading={loading}
/>
</VStack>
);
};
export default CompanyOverview;

View File

@@ -0,0 +1,70 @@
// src/views/Company/components/CompanyOverview/index.tsx
// 公司概览 - 主组件(组合层)
import React from "react";
import { VStack, Spinner, Center, Text } from "@chakra-ui/react";
import { useCompanyOverviewData } from "./hooks/useCompanyOverviewData";
import CompanyHeaderCard from "./CompanyHeaderCard";
import type { CompanyOverviewProps } from "./types";
// 子组件(暂保持 JS
import BasicInfoTab from "./BasicInfoTab";
/**
* 公司概览组件
*
* 功能:
* - 显示公司头部信息卡片
* - 显示基本信息(股权结构、管理层、公告等)
*/
const CompanyOverview: React.FC<CompanyOverviewProps> = ({ stockCode }) => {
const {
basicInfo,
actualControl,
concentration,
management,
topCirculationShareholders,
topShareholders,
branches,
announcements,
disclosureSchedule,
loading,
} = useCompanyOverviewData(stockCode);
// 加载状态
if (loading && !basicInfo) {
return (
<Center h="300px">
<VStack spacing={4}>
<Spinner size="xl" color="blue.500" thickness="4px" />
<Text>...</Text>
</VStack>
</Center>
);
}
return (
<VStack spacing={6} align="stretch">
{/* 公司头部信息卡片 */}
{basicInfo && <CompanyHeaderCard basicInfo={basicInfo} />}
{/* 基本信息内容 */}
<BasicInfoTab
basicInfo={basicInfo}
actualControl={actualControl}
concentration={concentration}
topShareholders={topShareholders}
topCirculationShareholders={topCirculationShareholders}
management={management}
announcements={announcements}
branches={branches}
disclosureSchedule={disclosureSchedule}
cardBg="white"
loading={loading}
/>
</VStack>
);
};
export default CompanyOverview;

View File

@@ -98,7 +98,7 @@ const CompanyIndex = () => {
/>
{/* Tab 切换区域:概览、行情、财务、预测 */}
{/* <CompanyTabs stockCode={stockCode} onTabChange={trackTabChanged} bgColor="#1A202C"/> */}
<CompanyTabs stockCode={stockCode} onTabChange={trackTabChanged} bgColor="#1A202C"/>
</VStack>
</Container>
);