- 拆分 loadData 为 loadBasicInfoData 和 loadDeepAnalysisData - 首次加载仅请求 9 个基本信息接口(原 12 个) - 深度分析 3 个接口切换 Tab 时按需加载 - 新闻动态 1 个接口切换 Tab 时按需加载 - 调整 Tab 顺序:基本信息 → 深度分析 → 新闻动态 - stockCode 变更时重置 Tab 状态和数据 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
602 lines
20 KiB
JavaScript
602 lines
20 KiB
JavaScript
// src/views/Company/components/CompanyOverview/index.js
|
||
// 公司概览主组件 - 状态管理 + Tab 容器
|
||
|
||
import React, { useState, useEffect } from "react";
|
||
import {
|
||
Box,
|
||
VStack,
|
||
HStack,
|
||
Text,
|
||
Badge,
|
||
Card,
|
||
CardBody,
|
||
Heading,
|
||
SimpleGrid,
|
||
Divider,
|
||
Spinner,
|
||
Center,
|
||
Tabs,
|
||
TabList,
|
||
TabPanels,
|
||
Tab,
|
||
TabPanel,
|
||
useColorModeValue,
|
||
Icon,
|
||
Grid,
|
||
GridItem,
|
||
Stat,
|
||
StatLabel,
|
||
StatNumber,
|
||
Container,
|
||
Circle,
|
||
Link,
|
||
} from "@chakra-ui/react";
|
||
|
||
import {
|
||
FaBuilding,
|
||
FaMapMarkerAlt,
|
||
FaUserShield,
|
||
FaBriefcase,
|
||
FaCalendarAlt,
|
||
FaGlobe,
|
||
FaEnvelope,
|
||
FaPhone,
|
||
FaCrown,
|
||
FaBrain,
|
||
FaInfoCircle,
|
||
FaNewspaper,
|
||
} from "react-icons/fa";
|
||
|
||
import { ExternalLinkIcon } from "@chakra-ui/icons";
|
||
|
||
import { logger } from "@utils/logger";
|
||
import { getApiBase } from "@utils/apiConfig";
|
||
|
||
// 子组件
|
||
import DeepAnalysisTab from "./DeepAnalysisTab";
|
||
import BasicInfoTab from "./BasicInfoTab";
|
||
import NewsEventsTab from "./NewsEventsTab";
|
||
|
||
// 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");
|
||
},
|
||
};
|
||
|
||
// 主组件
|
||
const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
|
||
const [stockCode, setStockCode] = useState(propStockCode || "000001");
|
||
|
||
// Tab 懒加载状态追踪
|
||
const [tabsLoaded, setTabsLoaded] = useState({
|
||
basicInfo: false,
|
||
deepAnalysis: false,
|
||
newsEvents: false,
|
||
});
|
||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||
const [basicInfoLoading, setBasicInfoLoading] = useState(false);
|
||
const [deepAnalysisLoading, setDeepAnalysisLoading] = useState(false);
|
||
|
||
// 监听props中的stockCode变化 - 重置Tab状态
|
||
useEffect(() => {
|
||
if (propStockCode && propStockCode !== stockCode) {
|
||
setStockCode(propStockCode);
|
||
// 重置 Tab 状态
|
||
setTabsLoaded({ basicInfo: false, deepAnalysis: false, newsEvents: false });
|
||
setActiveTabIndex(0);
|
||
// 清空深度分析和新闻数据
|
||
setComprehensiveData(null);
|
||
setValueChainData(null);
|
||
setKeyFactorsData(null);
|
||
setNewsEvents([]);
|
||
}
|
||
}, [propStockCode, stockCode]);
|
||
|
||
// 企业深度分析数据
|
||
const [comprehensiveData, setComprehensiveData] = useState(null);
|
||
const [valueChainData, setValueChainData] = useState(null);
|
||
const [keyFactorsData, setKeyFactorsData] = useState(null);
|
||
|
||
// 股票概览数据
|
||
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 [newsEvents, setNewsEvents] = useState([]);
|
||
const [newsLoading, setNewsLoading] = useState(false);
|
||
const [newsSearchQuery, setNewsSearchQuery] = useState("");
|
||
const [newsPagination, setNewsPagination] = useState({
|
||
page: 1,
|
||
per_page: 10,
|
||
total: 0,
|
||
pages: 0,
|
||
has_next: false,
|
||
has_prev: false,
|
||
});
|
||
|
||
const [_error, setError] = useState(null);
|
||
|
||
const bgColor = useColorModeValue("gray.50", "gray.900");
|
||
const cardBg = useColorModeValue("white", "gray.800");
|
||
|
||
// 业务板块详情展开状态
|
||
const [expandedSegments, setExpandedSegments] = useState({});
|
||
|
||
// 切换业务板块展开状态
|
||
const toggleSegmentExpansion = (segmentIndex) => {
|
||
setExpandedSegments((prev) => ({
|
||
...prev,
|
||
[segmentIndex]: !prev[segmentIndex],
|
||
}));
|
||
};
|
||
|
||
// 加载基本信息数据(9个接口)- 首次加载
|
||
const loadBasicInfoData = async () => {
|
||
if (tabsLoaded.basicInfo) return;
|
||
|
||
setBasicInfoLoading(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);
|
||
|
||
setTabsLoaded((prev) => ({ ...prev, basicInfo: true }));
|
||
} catch (err) {
|
||
setError(err.message);
|
||
logger.error("CompanyOverview", "loadBasicInfoData", err, { stockCode });
|
||
} finally {
|
||
setBasicInfoLoading(false);
|
||
}
|
||
};
|
||
|
||
// 加载深度分析数据(3个接口)- Tab切换时加载
|
||
const loadDeepAnalysisData = async () => {
|
||
if (tabsLoaded.deepAnalysis) return;
|
||
|
||
setDeepAnalysisLoading(true);
|
||
|
||
try {
|
||
const requests = [
|
||
fetch(
|
||
`${API_BASE_URL}/api/company/comprehensive-analysis/${stockCode}`
|
||
).then((r) => r.json()),
|
||
fetch(
|
||
`${API_BASE_URL}/api/company/value-chain-analysis/${stockCode}`
|
||
).then((r) => r.json()),
|
||
fetch(
|
||
`${API_BASE_URL}/api/company/key-factors-timeline/${stockCode}`
|
||
).then((r) => r.json()),
|
||
];
|
||
|
||
const [comprehensiveRes, valueChainRes, keyFactorsRes] =
|
||
await Promise.all(requests);
|
||
|
||
// 设置深度分析数据
|
||
if (comprehensiveRes.success) setComprehensiveData(comprehensiveRes.data);
|
||
if (valueChainRes.success) setValueChainData(valueChainRes.data);
|
||
if (keyFactorsRes.success) setKeyFactorsData(keyFactorsRes.data);
|
||
|
||
setTabsLoaded((prev) => ({ ...prev, deepAnalysis: true }));
|
||
} catch (err) {
|
||
logger.error("CompanyOverview", "loadDeepAnalysisData", err, {
|
||
stockCode,
|
||
});
|
||
} finally {
|
||
setDeepAnalysisLoading(false);
|
||
}
|
||
};
|
||
|
||
// 首次加载 - 只加载基本信息
|
||
useEffect(() => {
|
||
if (stockCode) {
|
||
loadBasicInfoData();
|
||
}
|
||
}, [stockCode]);
|
||
|
||
// 加载新闻事件(1个接口)- Tab切换时加载
|
||
const loadNewsEvents = async (page = 1, searchQuery = "") => {
|
||
setNewsLoading(true);
|
||
try {
|
||
const params = new URLSearchParams({
|
||
page: page.toString(),
|
||
per_page: "10",
|
||
sort: "new",
|
||
include_creator: "true",
|
||
include_stats: "true",
|
||
});
|
||
|
||
const queryText = searchQuery || basicInfo?.SECNAME || "";
|
||
if (queryText) {
|
||
params.append("q", queryText);
|
||
}
|
||
|
||
const response = await fetch(
|
||
`${API_BASE_URL}/api/events?${params.toString()}`
|
||
);
|
||
const data = await response.json();
|
||
|
||
const events = data.data?.events || data.events || [];
|
||
const pagination = data.data?.pagination || {
|
||
page: 1,
|
||
per_page: 10,
|
||
total: 0,
|
||
pages: 0,
|
||
has_next: false,
|
||
has_prev: false,
|
||
};
|
||
|
||
setNewsEvents(events);
|
||
setNewsPagination(pagination);
|
||
|
||
// 首次加载时标记为已加载
|
||
if (page === 1 && !tabsLoaded.newsEvents) {
|
||
setTabsLoaded((prev) => ({ ...prev, newsEvents: true }));
|
||
}
|
||
} catch (err) {
|
||
logger.error("CompanyOverview", "loadNewsEvents", err, {
|
||
stockCode,
|
||
searchQuery,
|
||
page,
|
||
});
|
||
setNewsEvents([]);
|
||
setNewsPagination({
|
||
page: 1,
|
||
per_page: 10,
|
||
total: 0,
|
||
pages: 0,
|
||
has_next: false,
|
||
has_prev: false,
|
||
});
|
||
} finally {
|
||
setNewsLoading(false);
|
||
}
|
||
};
|
||
|
||
// 处理新闻搜索
|
||
const handleNewsSearch = () => {
|
||
loadNewsEvents(1, newsSearchQuery);
|
||
};
|
||
|
||
// 处理新闻分页
|
||
const handleNewsPageChange = (newPage) => {
|
||
loadNewsEvents(newPage, newsSearchQuery);
|
||
};
|
||
|
||
// Tab 切换处理 - 懒加载
|
||
const handleTabChange = (index) => {
|
||
setActiveTabIndex(index);
|
||
// index 0: 基本信息 - 已首次加载
|
||
// index 1: 深度分析 - 切换时加载
|
||
// index 2: 新闻动态 - 切换时加载
|
||
if (index === 1 && !tabsLoaded.deepAnalysis) {
|
||
loadDeepAnalysisData();
|
||
} else if (index === 2 && !tabsLoaded.newsEvents && basicInfo) {
|
||
loadNewsEvents(1);
|
||
}
|
||
};
|
||
|
||
if (basicInfoLoading && !basicInfo) {
|
||
return (
|
||
<Box bg={bgColor} minH="100vh" p={4}>
|
||
<Container maxW="container.xl">
|
||
<Center h="400px">
|
||
<VStack spacing={4}>
|
||
<Spinner size="xl" color="blue.500" thickness="4px" />
|
||
<Text>正在加载企业全景数据...</Text>
|
||
</VStack>
|
||
</Center>
|
||
</Container>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Box bg={bgColor} minH="100vh" py={6}>
|
||
<Container maxW="container.xl">
|
||
<VStack spacing={6} align="stretch">
|
||
{/* 公司头部信息 - 醒目展示 */}
|
||
{basicInfo && (
|
||
<Card
|
||
bg={cardBg}
|
||
shadow="2xl"
|
||
borderTop="6px 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="70px" bg="blue.500">
|
||
<Icon as={FaBuilding} color="white" boxSize={10} />
|
||
</Circle>
|
||
<VStack align="start" spacing={1}>
|
||
<HStack>
|
||
<Heading size="xl" color="blue.600">
|
||
{basicInfo.ORGNAME || basicInfo.SECNAME}
|
||
</Heading>
|
||
<Badge
|
||
colorScheme="blue"
|
||
fontSize="lg"
|
||
px={3}
|
||
py={1}
|
||
>
|
||
{basicInfo.SECCODE}
|
||
</Badge>
|
||
</HStack>
|
||
<HStack spacing={3}>
|
||
<Badge colorScheme="purple" fontSize="sm">
|
||
{basicInfo.sw_industry_l1}
|
||
</Badge>
|
||
<Badge colorScheme="orange" fontSize="sm">
|
||
{basicInfo.sw_industry_l2}
|
||
</Badge>
|
||
{basicInfo.sw_industry_l3 && (
|
||
<Badge colorScheme="green" fontSize="sm">
|
||
{basicInfo.sw_industry_l3}
|
||
</Badge>
|
||
)}
|
||
</HStack>
|
||
</VStack>
|
||
</HStack>
|
||
|
||
<Divider />
|
||
|
||
<SimpleGrid columns={2} spacing={4} w="full">
|
||
<HStack>
|
||
<Icon as={FaUserShield} color="gray.500" />
|
||
<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" />
|
||
<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" />
|
||
<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" />
|
||
<Text fontSize="sm">
|
||
<Text as="span" color="gray.500">
|
||
成立日期:
|
||
</Text>
|
||
<Text as="span" fontWeight="bold">
|
||
{formatUtils.formatDate(basicInfo.establish_date)}
|
||
</Text>
|
||
</Text>
|
||
</HStack>
|
||
</SimpleGrid>
|
||
|
||
<Box>
|
||
<Text fontSize="sm" color="gray.600" noOfLines={3}>
|
||
{basicInfo.company_intro}
|
||
</Text>
|
||
</Box>
|
||
</VStack>
|
||
</GridItem>
|
||
|
||
<GridItem colSpan={{ base: 12, lg: 4 }}>
|
||
<VStack spacing={3} align="stretch">
|
||
<Stat>
|
||
<StatLabel>注册资本</StatLabel>
|
||
<StatNumber fontSize="3xl" color="blue.500">
|
||
{formatUtils.formatRegisteredCapital(
|
||
basicInfo.reg_capital
|
||
)}
|
||
</StatNumber>
|
||
</Stat>
|
||
|
||
<Divider />
|
||
|
||
<VStack align="stretch" spacing={2}>
|
||
<HStack fontSize="sm">
|
||
<Icon as={FaMapMarkerAlt} color="gray.500" />
|
||
<Text noOfLines={1}>
|
||
{basicInfo.province} {basicInfo.city}
|
||
</Text>
|
||
</HStack>
|
||
<HStack fontSize="sm">
|
||
<Icon as={FaGlobe} color="gray.500" />
|
||
<Link
|
||
href={basicInfo.website}
|
||
isExternal
|
||
color="blue.500"
|
||
>
|
||
{basicInfo.website} <ExternalLinkIcon mx="2px" />
|
||
</Link>
|
||
</HStack>
|
||
<HStack fontSize="sm">
|
||
<Icon as={FaEnvelope} color="gray.500" />
|
||
<Text>{basicInfo.email}</Text>
|
||
</HStack>
|
||
<HStack fontSize="sm">
|
||
<Icon as={FaPhone} color="gray.500" />
|
||
<Text>{basicInfo.tel}</Text>
|
||
</HStack>
|
||
</VStack>
|
||
</VStack>
|
||
</GridItem>
|
||
</Grid>
|
||
</CardBody>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 主要内容区 - 分为基本信息、深度分析和新闻动态 */}
|
||
<Tabs
|
||
variant="soft-rounded"
|
||
colorScheme="blue"
|
||
size="lg"
|
||
index={activeTabIndex}
|
||
onChange={handleTabChange}
|
||
>
|
||
<TabList
|
||
bg={cardBg}
|
||
p={4}
|
||
borderRadius="lg"
|
||
shadow="md"
|
||
flexWrap="wrap"
|
||
>
|
||
<Tab fontWeight="bold">
|
||
<Icon as={FaInfoCircle} mr={2} />
|
||
基本信息
|
||
</Tab>
|
||
<Tab fontWeight="bold">
|
||
<Icon as={FaBrain} mr={2} />
|
||
深度分析
|
||
</Tab>
|
||
<Tab fontWeight="bold">
|
||
<Icon as={FaNewspaper} mr={2} />
|
||
新闻动态
|
||
</Tab>
|
||
</TabList>
|
||
|
||
<TabPanels>
|
||
{/* 基本信息标签页 - 默认 Tab */}
|
||
<TabPanel p={0} pt={6}>
|
||
<BasicInfoTab
|
||
basicInfo={basicInfo}
|
||
actualControl={actualControl}
|
||
concentration={concentration}
|
||
topShareholders={topShareholders}
|
||
topCirculationShareholders={topCirculationShareholders}
|
||
management={management}
|
||
announcements={announcements}
|
||
branches={branches}
|
||
disclosureSchedule={disclosureSchedule}
|
||
cardBg={cardBg}
|
||
loading={basicInfoLoading}
|
||
/>
|
||
</TabPanel>
|
||
|
||
{/* 深度分析标签页 - 切换时加载 */}
|
||
<TabPanel p={0} pt={6}>
|
||
<DeepAnalysisTab
|
||
comprehensiveData={comprehensiveData}
|
||
valueChainData={valueChainData}
|
||
keyFactorsData={keyFactorsData}
|
||
loading={deepAnalysisLoading}
|
||
cardBg={cardBg}
|
||
expandedSegments={expandedSegments}
|
||
onToggleSegment={toggleSegmentExpansion}
|
||
/>
|
||
</TabPanel>
|
||
|
||
{/* 新闻动态标签页 - 切换时加载 */}
|
||
<TabPanel p={0} pt={6}>
|
||
<NewsEventsTab
|
||
newsEvents={newsEvents}
|
||
newsLoading={newsLoading}
|
||
newsPagination={newsPagination}
|
||
searchQuery={newsSearchQuery}
|
||
onSearchChange={setNewsSearchQuery}
|
||
onSearch={handleNewsSearch}
|
||
onPageChange={handleNewsPageChange}
|
||
cardBg={cardBg}
|
||
/>
|
||
</TabPanel>
|
||
</TabPanels>
|
||
</Tabs>
|
||
</VStack>
|
||
</Container>
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default CompanyAnalysisComplete;
|