refactor: Company 页面一级 Tab 重构为 6 个

- 新增深度分析 Tab(从 CompanyOverview 提取为独立组件)
- 新增动态跟踪 Tab(占位组件,后续添加内容)
- Tab 顺序:公司概览 | 深度分析 | 股票行情 | 财务全景 | 盈利预测 | 动态跟踪
- 简化 CompanyOverview:移除内部 Tabs,只保留头部卡片 + 基本信息
- DeepAnalysis 组件独立管理深度分析数据加载(3个接口)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-09 17:52:23 +08:00
parent 04ce16df56
commit 2dd7dd755a
6 changed files with 314 additions and 441 deletions

View File

@@ -1,5 +1,5 @@
// src/views/Company/components/CompanyOverview/index.js // src/views/Company/components/CompanyOverview/index.js
// 公司概览主组件 - 状态管理 + Tab 容器 // 公司概览 - 头部卡片 + 基本信息
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { import {
@@ -15,19 +15,12 @@ import {
Divider, Divider,
Spinner, Spinner,
Center, Center,
Tabs,
TabList,
TabPanels,
Tab,
TabPanel,
useColorModeValue,
Icon, Icon,
Grid, Grid,
GridItem, GridItem,
Stat, Stat,
StatLabel, StatLabel,
StatNumber, StatNumber,
Container,
Circle, Circle,
Link, Link,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
@@ -42,9 +35,6 @@ import {
FaEnvelope, FaEnvelope,
FaPhone, FaPhone,
FaCrown, FaCrown,
FaBrain,
FaInfoCircle,
FaNewspaper,
} from "react-icons/fa"; } from "react-icons/fa";
import { ExternalLinkIcon } from "@chakra-ui/icons"; import { ExternalLinkIcon } from "@chakra-ui/icons";
@@ -53,9 +43,7 @@ import { logger } from "@utils/logger";
import { getApiBase } from "@utils/apiConfig"; import { getApiBase } from "@utils/apiConfig";
// 子组件 // 子组件
import DeepAnalysisTab from "./DeepAnalysisTab";
import BasicInfoTab from "./BasicInfoTab"; import BasicInfoTab from "./BasicInfoTab";
import NewsEventsTab from "./NewsEventsTab";
// API配置 // API配置
const API_BASE_URL = getApiBase(); const API_BASE_URL = getApiBase();
@@ -76,87 +64,47 @@ const formatUtils = {
}, },
}; };
// 主组件 /**
const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => { * 公司概览组件
*
* 功能:
* - 显示公司头部信息卡片
* - 显示基本信息(股权结构、管理层、公告等)
*
* @param {Object} props
* @param {string} props.stockCode - 股票代码
*/
const CompanyOverview = ({ stockCode: propStockCode }) => {
const [stockCode, setStockCode] = useState(propStockCode || "000001"); const [stockCode, setStockCode] = useState(propStockCode || "000001");
const [loading, setLoading] = useState(false);
const [dataLoaded, setDataLoaded] = useState(false);
// Tab 懒加载状态追踪 // 监听 props 中的 stockCode 变化
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(() => { useEffect(() => {
if (propStockCode && propStockCode !== stockCode) { if (propStockCode && propStockCode !== stockCode) {
setStockCode(propStockCode); setStockCode(propStockCode);
// 重置 Tab 状态 setDataLoaded(false);
setTabsLoaded({ basicInfo: false, deepAnalysis: false, newsEvents: false });
setActiveTabIndex(0);
// 清空深度分析和新闻数据
setComprehensiveData(null);
setValueChainData(null);
setKeyFactorsData(null);
setNewsEvents([]);
} }
}, [propStockCode, stockCode]); }, [propStockCode, stockCode]);
// 企业深度分析数据 // 基本信息数据
const [comprehensiveData, setComprehensiveData] = useState(null);
const [valueChainData, setValueChainData] = useState(null);
const [keyFactorsData, setKeyFactorsData] = useState(null);
// 股票概览数据
const [basicInfo, setBasicInfo] = useState(null); const [basicInfo, setBasicInfo] = useState(null);
const [actualControl, setActualControl] = useState([]); const [actualControl, setActualControl] = useState([]);
const [concentration, setConcentration] = useState([]); const [concentration, setConcentration] = useState([]);
const [management, setManagement] = useState([]); const [management, setManagement] = useState([]);
const [topCirculationShareholders, setTopCirculationShareholders] = useState( const [topCirculationShareholders, setTopCirculationShareholders] = useState([]);
[]
);
const [topShareholders, setTopShareholders] = useState([]); const [topShareholders, setTopShareholders] = useState([]);
const [branches, setBranches] = useState([]); const [branches, setBranches] = useState([]);
const [announcements, setAnnouncements] = useState([]); const [announcements, setAnnouncements] = useState([]);
const [disclosureSchedule, setDisclosureSchedule] = 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 [_error, setError] = useState(null);
const bgColor = useColorModeValue("gray.50", "gray.900"); // 加载基本信息数据9个接口
const cardBg = useColorModeValue("white", "gray.800");
// 业务板块详情展开状态
const [expandedSegments, setExpandedSegments] = useState({});
// 切换业务板块展开状态
const toggleSegmentExpansion = (segmentIndex) => {
setExpandedSegments((prev) => ({
...prev,
[segmentIndex]: !prev[segmentIndex],
}));
};
// 加载基本信息数据9个接口- 首次加载
const loadBasicInfoData = async () => { const loadBasicInfoData = async () => {
if (tabsLoaded.basicInfo) return; if (dataLoaded) return;
setBasicInfoLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
@@ -202,7 +150,6 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
disclosureRes, disclosureRes,
] = await Promise.all(requests); ] = await Promise.all(requests);
// 设置股票概览数据
if (basicRes.success) setBasicInfo(basicRes.data); if (basicRes.success) setBasicInfo(basicRes.data);
if (actualRes.success) setActualControl(actualRes.data); if (actualRes.success) setActualControl(actualRes.data);
if (concentrationRes.success) setConcentration(concentrationRes.data); if (concentrationRes.success) setConcentration(concentrationRes.data);
@@ -214,388 +161,169 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
if (announcementsRes.success) setAnnouncements(announcementsRes.data); if (announcementsRes.success) setAnnouncements(announcementsRes.data);
if (disclosureRes.success) setDisclosureSchedule(disclosureRes.data); if (disclosureRes.success) setDisclosureSchedule(disclosureRes.data);
setTabsLoaded((prev) => ({ ...prev, basicInfo: true })); setDataLoaded(true);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
logger.error("CompanyOverview", "loadBasicInfoData", err, { stockCode }); logger.error("CompanyOverview", "loadBasicInfoData", err, { stockCode });
} finally { } finally {
setBasicInfoLoading(false); setLoading(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(() => { useEffect(() => {
if (stockCode) { if (stockCode) {
loadBasicInfoData(); loadBasicInfoData();
} }
}, [stockCode]); }, [stockCode]);
// 加载新闻事件1个接口- Tab切换时加载 if (loading && !basicInfo) {
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 ( return (
<Box bg={bgColor} minH="100vh" p={4}> <Center h="300px">
<Container maxW="container.xl"> <VStack spacing={4}>
<Center h="400px"> <Spinner size="xl" color="blue.500" thickness="4px" />
<VStack spacing={4}> <Text>正在加载公司概览数据...</Text>
<Spinner size="xl" color="blue.500" thickness="4px" /> </VStack>
<Text>正在加载企业全景数据...</Text> </Center>
</VStack>
</Center>
</Container>
</Box>
); );
} }
return ( return (
<Box bg={bgColor} minH="100vh" py={6}> <VStack spacing={6} align="stretch">
<Container maxW="container.xl"> {/* 公司头部信息卡片 */}
<VStack spacing={6} align="stretch"> {basicInfo && (
{/* 公司头部信息 - 醒目展示 */} <Card
{basicInfo && ( bg="white"
<Card shadow="lg"
bg={cardBg} borderTop="4px solid"
shadow="2xl" borderTopColor="blue.500"
borderTop="6px solid" >
borderTopColor="blue.500" <CardBody>
> <Grid templateColumns="repeat(12, 1fr)" gap={6}>
<CardBody> <GridItem colSpan={{ base: 12, lg: 8 }}>
<Grid templateColumns="repeat(12, 1fr)" gap={6}> <VStack align="start" spacing={4}>
<GridItem colSpan={{ base: 12, lg: 8 }}> <HStack spacing={4}>
<VStack align="start" spacing={4}> <Circle size="60px" bg="blue.500">
<HStack spacing={4}> <Icon as={FaBuilding} color="white" boxSize={8} />
<Circle size="70px" bg="blue.500"> </Circle>
<Icon as={FaBuilding} color="white" boxSize={10} /> <VStack align="start" spacing={1}>
</Circle> <HStack>
<VStack align="start" spacing={1}> <Heading size="lg" color="blue.600">
<HStack> {basicInfo.ORGNAME || basicInfo.SECNAME}
<Heading size="xl" color="blue.600"> </Heading>
{basicInfo.ORGNAME || basicInfo.SECNAME} <Badge colorScheme="blue" fontSize="md" px={2} py={1}>
</Heading> {basicInfo.SECCODE}
<Badge </Badge>
colorScheme="blue" </HStack>
fontSize="lg" <HStack spacing={2}>
px={3} <Badge colorScheme="purple" fontSize="xs">
py={1} {basicInfo.sw_industry_l1}
> </Badge>
{basicInfo.SECCODE} <Badge colorScheme="orange" fontSize="xs">
</Badge> {basicInfo.sw_industry_l2}
</HStack> </Badge>
<HStack spacing={3}> {basicInfo.sw_industry_l3 && (
<Badge colorScheme="purple" fontSize="sm"> <Badge colorScheme="green" fontSize="xs">
{basicInfo.sw_industry_l1} {basicInfo.sw_industry_l3}
</Badge> </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> </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> </VStack>
</GridItem> </HStack>
<GridItem colSpan={{ base: 12, lg: 4 }}> <Divider />
<VStack spacing={3} align="stretch">
<Stat>
<StatLabel>注册资本</StatLabel>
<StatNumber fontSize="3xl" color="blue.500">
{formatUtils.formatRegisteredCapital(
basicInfo.reg_capital
)}
</StatNumber>
</Stat>
<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>
<VStack align="stretch" spacing={2}> <Text fontSize="sm" color="gray.600" noOfLines={2}>
<HStack fontSize="sm"> {basicInfo.company_intro}
<Icon as={FaMapMarkerAlt} color="gray.500" /> </Text>
<Text noOfLines={1}> </VStack>
{basicInfo.province} {basicInfo.city} </GridItem>
</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>
)}
{/* 主要内容区 - 分为基本信息、深度分析和新闻动态 */} <GridItem colSpan={{ base: 12, lg: 4 }}>
<Tabs <VStack spacing={3} align="stretch">
variant="soft-rounded" <Stat>
colorScheme="blue" <StatLabel>注册资本</StatLabel>
size="lg" <StatNumber fontSize="2xl" color="blue.500">
index={activeTabIndex} {formatUtils.formatRegisteredCapital(basicInfo.reg_capital)}
onChange={handleTabChange} </StatNumber>
> </Stat>
<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> <Divider />
{/* 基本信息标签页 - 默认 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>
{/* 深度分析标签页 - 切换时加载 */} <VStack align="stretch" spacing={1}>
<TabPanel p={0} pt={6}> <HStack fontSize="sm">
<DeepAnalysisTab <Icon as={FaMapMarkerAlt} color="gray.500" boxSize={3} />
comprehensiveData={comprehensiveData} <Text noOfLines={1}>{basicInfo.province} {basicInfo.city}</Text>
valueChainData={valueChainData} </HStack>
keyFactorsData={keyFactorsData} <HStack fontSize="sm">
loading={deepAnalysisLoading} <Icon as={FaGlobe} color="gray.500" boxSize={3} />
cardBg={cardBg} <Link href={basicInfo.website} isExternal color="blue.500" noOfLines={1}>
expandedSegments={expandedSegments} {basicInfo.website} <ExternalLinkIcon mx="2px" />
onToggleSegment={toggleSegmentExpansion} </Link>
/> </HStack>
</TabPanel> <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>
)}
{/* 新闻动态标签页 - 切换时加载 */} {/* 基本信息内容 */}
<TabPanel p={0} pt={6}> <BasicInfoTab
<NewsEventsTab basicInfo={basicInfo}
newsEvents={newsEvents} actualControl={actualControl}
newsLoading={newsLoading} concentration={concentration}
newsPagination={newsPagination} topShareholders={topShareholders}
searchQuery={newsSearchQuery} topCirculationShareholders={topCirculationShareholders}
onSearchChange={setNewsSearchQuery} management={management}
onSearch={handleNewsSearch} announcements={announcements}
onPageChange={handleNewsPageChange} branches={branches}
cardBg={cardBg} disclosureSchedule={disclosureSchedule}
/> cardBg="white"
</TabPanel> loading={loading}
</TabPanels> />
</Tabs> </VStack>
</VStack>
</Container>
</Box>
); );
}; };
export default CompanyAnalysisComplete; export default CompanyOverview;

View File

@@ -9,7 +9,6 @@ import {
TabPanels, TabPanels,
TabPanel, TabPanel,
Divider, Divider,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import TabNavigation from './TabNavigation'; import TabNavigation from './TabNavigation';
@@ -17,9 +16,11 @@ import { COMPANY_TABS, getTabNameByIndex } from '../../constants';
// 子组件导入Tab 内容组件) // 子组件导入Tab 内容组件)
import CompanyOverview from '../CompanyOverview'; import CompanyOverview from '../CompanyOverview';
import DeepAnalysis from '../DeepAnalysis';
import MarketDataView from '../MarketDataView'; import MarketDataView from '../MarketDataView';
import FinancialPanorama from '../FinancialPanorama'; import FinancialPanorama from '../FinancialPanorama';
import ForecastReport from '../ForecastReport'; import ForecastReport from '../ForecastReport';
import DynamicTracking from '../DynamicTracking';
/** /**
* Tab 组件映射 * Tab 组件映射
@@ -27,9 +28,11 @@ import ForecastReport from '../ForecastReport';
*/ */
const TAB_COMPONENTS = { const TAB_COMPONENTS = {
overview: CompanyOverview, overview: CompanyOverview,
analysis: DeepAnalysis,
market: MarketDataView, market: MarketDataView,
financial: FinancialPanorama, financial: FinancialPanorama,
forecast: ForecastReport, forecast: ForecastReport,
tracking: DynamicTracking,
}; };
/** /**
@@ -48,10 +51,6 @@ const TAB_COMPONENTS = {
const CompanyTabs = ({ stockCode, onTabChange, bgColor }) => { const CompanyTabs = ({ stockCode, onTabChange, bgColor }) => {
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
// 主题相关颜色
const tabBg = useColorModeValue('gray.50', 'gray.700');
const activeBg = useColorModeValue('blue.500', 'blue.400');
/** /**
* 处理 Tab 切换 * 处理 Tab 切换
*/ */
@@ -76,7 +75,7 @@ const CompanyTabs = ({ stockCode, onTabChange, bgColor }) => {
onChange={handleTabChange} onChange={handleTabChange}
> >
{/* Tab 导航 */} {/* Tab 导航 */}
<TabNavigation tabBg={tabBg} activeBg={activeBg} /> <TabNavigation tabBg="gray.50" activeBg="blue.500" />
<Divider /> <Divider />

View File

@@ -0,0 +1,100 @@
// src/views/Company/components/DeepAnalysis/index.js
// 深度分析 - 独立一级 Tab 组件
import React, { useState, useEffect } from "react";
import { logger } from "@utils/logger";
import { getApiBase } from "@utils/apiConfig";
// 复用原有的展示组件
import DeepAnalysisTab from "../CompanyOverview/DeepAnalysisTab";
const API_BASE_URL = getApiBase();
/**
* 深度分析组件
*
* 功能:
* - 加载深度分析数据3个接口
* - 管理展开状态
* - 渲染 DeepAnalysisTab 展示组件
*
* @param {Object} props
* @param {string} props.stockCode - 股票代码
*/
const DeepAnalysis = ({ stockCode }) => {
// 数据状态
const [comprehensiveData, setComprehensiveData] = useState(null);
const [valueChainData, setValueChainData] = useState(null);
const [keyFactorsData, setKeyFactorsData] = useState(null);
const [loading, setLoading] = useState(false);
// 业务板块展开状态
const [expandedSegments, setExpandedSegments] = useState({});
// 切换业务板块展开状态
const toggleSegmentExpansion = (segmentIndex) => {
setExpandedSegments((prev) => ({
...prev,
[segmentIndex]: !prev[segmentIndex],
}));
};
// 加载深度分析数据3个接口
const loadDeepAnalysisData = async () => {
if (!stockCode) return;
setLoading(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);
} catch (err) {
logger.error("DeepAnalysis", "loadDeepAnalysisData", err, { stockCode });
} finally {
setLoading(false);
}
};
// stockCode 变更时重新加载数据
useEffect(() => {
if (stockCode) {
// 重置数据
setComprehensiveData(null);
setValueChainData(null);
setKeyFactorsData(null);
setExpandedSegments({});
// 加载新数据
loadDeepAnalysisData();
}
}, [stockCode]);
return (
<DeepAnalysisTab
comprehensiveData={comprehensiveData}
valueChainData={valueChainData}
keyFactorsData={keyFactorsData}
loading={loading}
cardBg="white"
expandedSegments={expandedSegments}
onToggleSegment={toggleSegmentExpansion}
/>
);
};
export default DeepAnalysis;

View File

@@ -0,0 +1,46 @@
// src/views/Company/components/DynamicTracking/index.js
// 动态跟踪 - 独立一级 Tab 组件
import React from "react";
import {
Box,
VStack,
Text,
Icon,
Card,
CardBody,
} from "@chakra-ui/react";
import { FaNewspaper } from "react-icons/fa";
/**
* 动态跟踪组件
*
* 功能:
* - 预留二级 Tab 结构
* - 后续放入新闻动态等
*
* @param {Object} props
* @param {string} props.stockCode - 股票代码
*/
const DynamicTracking = ({ stockCode }) => {
return (
<Card bg="white" shadow="sm">
<CardBody>
<VStack spacing={4} py={12}>
<Icon as={FaNewspaper} boxSize={12} color="gray.300" />
<Text fontSize="lg" color="gray.500">
动态跟踪
</Text>
<Text fontSize="sm" color="gray.400">
后续将添加新闻动态等内容
</Text>
<Text fontSize="xs" color="gray.300">
股票代码: {stockCode}
</Text>
</VStack>
</CardBody>
</Card>
);
};
export default DynamicTracking;

View File

@@ -1,7 +1,7 @@
// src/views/Company/constants/index.js // src/views/Company/constants/index.js
// 公司详情页面常量配置 // 公司详情页面常量配置
import { FaChartLine, FaMoneyBillWave, FaChartBar, FaInfoCircle } from 'react-icons/fa'; import { FaChartLine, FaMoneyBillWave, FaChartBar, FaInfoCircle, FaBrain, FaNewspaper } from 'react-icons/fa';
/** /**
* Tab 配置 * Tab 配置
@@ -9,9 +9,11 @@ import { FaChartLine, FaMoneyBillWave, FaChartBar, FaInfoCircle } from 'react-ic
*/ */
export const COMPANY_TABS = [ export const COMPANY_TABS = [
{ key: 'overview', name: '公司概览', icon: FaInfoCircle }, { key: 'overview', name: '公司概览', icon: FaInfoCircle },
{ key: 'analysis', name: '深度分析', icon: FaBrain },
{ key: 'market', name: '股票行情', icon: FaChartLine }, { key: 'market', name: '股票行情', icon: FaChartLine },
{ key: 'financial', name: '财务全景', icon: FaMoneyBillWave }, { key: 'financial', name: '财务全景', icon: FaMoneyBillWave },
{ key: 'forecast', name: '盈利预测', icon: FaChartBar }, { key: 'forecast', name: '盈利预测', icon: FaChartBar },
{ key: 'tracking', name: '动态跟踪', icon: FaNewspaper },
]; ];
/** /**

View File

@@ -2,7 +2,7 @@
// 公司详情页面入口 - 纯组合层 // 公司详情页面入口 - 纯组合层
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { Container, VStack, useColorModeValue } from '@chakra-ui/react'; import { Container, VStack } from '@chakra-ui/react';
// 自定义 Hooks // 自定义 Hooks
import { useCompanyStock } from './hooks/useCompanyStock'; import { useCompanyStock } from './hooks/useCompanyStock';
@@ -24,8 +24,6 @@ import CompanyTabs from './components/CompanyTabs';
* - PostHog 事件追踪 * - PostHog 事件追踪
*/ */
const CompanyIndex = () => { const CompanyIndex = () => {
const bgColor = useColorModeValue('white', 'gray.800');
// 1. 先获取股票代码(不带追踪回调) // 1. 先获取股票代码(不带追踪回调)
const { const {
stockCode, stockCode,
@@ -78,7 +76,7 @@ const CompanyIndex = () => {
isInWatchlist={isInWatchlist} isInWatchlist={isInWatchlist}
isWatchlistLoading={isWatchlistLoading} isWatchlistLoading={isWatchlistLoading}
onWatchlistToggle={handleWatchlistToggle} onWatchlistToggle={handleWatchlistToggle}
bgColor={bgColor} bgColor="white"
/> />
{/* 股票行情卡片:价格、关键指标、主力动态 */} {/* 股票行情卡片:价格、关键指标、主力动态 */}
@@ -88,7 +86,7 @@ const CompanyIndex = () => {
<CompanyTabs <CompanyTabs
stockCode={stockCode} stockCode={stockCode}
onTabChange={trackTabChanged} onTabChange={trackTabChanged}
bgColor={bgColor} bgColor="white"
/> />
</VStack> </VStack>
</Container> </Container>