Files
vf_react/src/views/Company/components/CompanyOverview/index.js
zdl 776c7a0f98 perf: CompanyOverview Tab 懒加载优化
- 拆分 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>
2025-12-09 17:37:11 +08:00

602 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;