Files
vf_react/src/views/Company/CompanyOverview.js
2025-12-09 15:16:02 +08:00

2678 lines
126 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.

import React, { useState, useEffect } from 'react';
import {
Box, VStack, HStack, Text, Badge, Card, CardBody, CardHeader,
Heading, SimpleGrid, Divider, Spinner, Center, Alert, AlertIcon,
Tabs, TabList, TabPanels, Tab, TabPanel, Button, useColorModeValue,
Tag, TagLabel, Icon, Tooltip, Flex, Grid, GridItem, useToast,
Table, Thead, Tbody, Tr, Th, Td, TableContainer, IconButton,
Skeleton, SkeletonText, Progress, Stack, Stat, StatLabel, StatNumber,
StatHelpText, Container, Wrap, WrapItem, List, ListItem,
ListIcon, Accordion, AccordionItem, AccordionButton, AccordionPanel,
AccordionIcon, Fade, ScaleFade, useDisclosure, Modal, ModalOverlay,
ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton,
Circle, Square, Avatar, AvatarGroup, Input, InputGroup, InputLeftElement,
Link, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Image, Code,
chakra
} from '@chakra-ui/react';
import {
FaBuilding, FaMapMarkerAlt, FaChartLine, FaLightbulb, FaRocket,
FaNetworkWired, FaChevronDown, FaChevronUp, FaChevronLeft, FaChevronRight,
FaCog, FaTrophy, FaShieldAlt, FaBrain, FaChartPie, FaHistory, FaCheckCircle,
FaExclamationCircle, FaArrowUp, FaArrowDown, FaArrowRight, FaArrowLeft,
FaLink, FaStar, FaUserTie, FaIndustry, FaDollarSign, FaBalanceScale, FaChartBar,
FaEye, FaFlask, FaHandshake, FaUsers, FaClock, FaCalendarAlt,
FaCircle, FaGlobe, FaEnvelope, FaPhone, FaFax, FaBriefcase,
FaUniversity, FaGraduationCap, FaVenusMars, FaPassport, FaFileAlt,
FaNewspaper, FaBullhorn, FaUserShield, FaShareAlt, FaSitemap,
FaSearch, FaDownload, FaExternalLinkAlt, FaInfoCircle, FaCrown,
FaCertificate, FaAward, FaExpandAlt, FaCompressAlt, FaGavel, FaFire
} from 'react-icons/fa';
import {
RepeatIcon, InfoIcon, ChevronRightIcon, TimeIcon, EmailIcon,
PhoneIcon, ExternalLinkIcon, AttachmentIcon, CalendarIcon, SearchIcon,
WarningIcon, CheckIcon
} from '@chakra-ui/icons';
import ReactECharts from 'echarts-for-react';
import { logger } from '../../utils/logger';
import { getApiBase } from '../../utils/apiConfig';
// API配置
const API_BASE_URL = getApiBase();
// 格式化工具
const formatUtils = {
formatCurrency: (value) => {
if (!value && value !== 0) return '-';
const absValue = Math.abs(value);
if (absValue >= 100000000) {
return (value / 100000000).toFixed(2) + '亿元';
} else if (absValue >= 10000) {
return (value / 10000).toFixed(2) + '万元';
}
return value.toFixed(2) + '元';
},
formatRegisteredCapital: (value) => {
// 注册资本字段,数据库存储的是万元为单位的数值
if (!value && value !== 0) return '-';
const absValue = Math.abs(value);
if (absValue >= 100000) { // 10亿万元 = 10亿元
return (value / 10000).toFixed(2) + '亿元';
}
return value.toFixed(2) + '万元';
},
formatBusinessRevenue: (value, unit) => {
// 业务收入格式化,考虑数据库中的单位字段
if (!value && value !== 0) return '-';
if (unit) {
// 根据数据库中的单位进行智能格式化
if (unit === '元') {
// 元为单位时,自动转换为合适的单位显示
const absValue = Math.abs(value);
if (absValue >= 100000000) {
return (value / 100000000).toFixed(2) + '亿元';
} else if (absValue >= 10000) {
return (value / 10000).toFixed(2) + '万元';
}
return value.toFixed(0) + '元';
} else if (unit === '万元') {
// 万元为单位时,可能需要转换为亿元
const absValue = Math.abs(value);
if (absValue >= 10000) {
return (value / 10000).toFixed(2) + '亿元';
}
return value.toFixed(2) + '万元';
} else if (unit === '亿元') {
// 亿元为单位时,直接显示
return value.toFixed(2) + '亿元';
} else {
// 其他单位直接显示
return value.toFixed(2) + unit;
}
}
// 没有单位字段时,使用默认的货币格式化
const absValue = Math.abs(value);
if (absValue >= 100000000) {
return (value / 100000000).toFixed(2) + '亿元';
} else if (absValue >= 10000) {
return (value / 10000).toFixed(2) + '万元';
}
return value.toFixed(2) + '元';
},
formatPercentage: (value) => {
if (!value && value !== 0) return '-';
return value.toFixed(2) + '%';
},
formatNumber: (value) => {
if (!value && value !== 0) return '-';
return value.toLocaleString('zh-CN');
},
formatDate: (dateString) => {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('zh-CN');
},
formatShares: (value) => {
if (!value && value !== 0) return '-';
const absValue = Math.abs(value);
if (absValue >= 100000000) {
return (value / 100000000).toFixed(2) + '亿股';
} else if (absValue >= 10000) {
return (value / 10000).toFixed(2) + '万股';
}
return value.toFixed(0) + '股';
}
};
// 免责声明组件
const DisclaimerBox = () => {
return (
<Alert status="warning" variant="left-accent" mb={4}>
<AlertIcon />
<Box fontSize="xs" lineHeight="1.4">
<Text fontWeight="medium" mb={1}>免责声明</Text>
<Text>
本内容由AI模型基于新闻公告研报等公开信息自动分析和生成未经许可严禁转载
所有内容仅供参考不构成任何投资建议请投资者注意风险独立审慎决策
</Text>
</Box>
</Alert>
);
};
// 评分进度条组件
const ScoreBar = ({ label, score, maxScore = 100, colorScheme = 'blue', icon }) => {
const percentage = (score / maxScore) * 100;
const getColorScheme = () => {
if (percentage >= 80) return 'purple';
if (percentage >= 60) return 'blue';
if (percentage >= 40) return 'yellow';
return 'orange';
};
return (
<Box>
<HStack justify="space-between" mb={1}>
<HStack>
{icon && <Icon as={icon} boxSize={4} color={`${getColorScheme()}.500`} />}
<Text fontSize="sm" fontWeight="medium">{label}</Text>
</HStack>
<Badge colorScheme={getColorScheme()}>{score || 0}</Badge>
</HStack>
<Progress
value={percentage}
size="sm"
colorScheme={getColorScheme()}
borderRadius="full"
hasStripe
isAnimated
/>
</Box>
);
};
// 业务结构树形图组件
const BusinessTreeItem = ({ business, depth = 0 }) => {
const bgColor = useColorModeValue('gray.50', 'gray.700');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const profitColor = business.financial_metrics?.profit_growth > 0 ? 'red.500' : 'green.500';
return (
<Box
ml={depth * 6}
p={3}
bg={bgColor}
borderLeft={depth > 0 ? `4px solid` : 'none'}
borderLeftColor="blue.400"
borderRadius="md"
mb={2}
_hover={{ shadow: 'md' }}
transition="all 0.2s"
>
<HStack justify="space-between">
<VStack align="start" spacing={1}>
<HStack>
<Text fontWeight="bold" fontSize={depth === 0 ? 'md' : 'sm'}>
{business.business_name}
</Text>
{business.financial_metrics?.revenue_ratio > 30 && (
<Badge colorScheme="purple" size="sm">核心业务</Badge>
)}
</HStack>
<HStack spacing={4} flexWrap="wrap">
<Tag size="sm" variant="subtle">
营收占比: {formatUtils.formatPercentage(business.financial_metrics?.revenue_ratio)}
</Tag>
<Tag size="sm" variant="subtle">
毛利率: {formatUtils.formatPercentage(business.financial_metrics?.gross_margin)}
</Tag>
{business.growth_metrics?.revenue_growth && (
<Tag size="sm" colorScheme={business.growth_metrics.revenue_growth > 0 ? 'red' : 'green'}>
<TagLabel>
增长: {business.growth_metrics.revenue_growth > 0 ? '+' : ''}{formatUtils.formatPercentage(business.growth_metrics.revenue_growth)}
</TagLabel>
</Tag>
)}
</HStack>
</VStack>
<VStack align="end" spacing={0}>
<Text fontSize="lg" fontWeight="bold" color="blue.500">
{(() => {
// 优先使用business.revenue如果没有则使用financial_metrics.revenue
const revenue = business.revenue || business.financial_metrics?.revenue;
const unit = business.revenue_unit;
if (revenue || revenue === 0) {
return formatUtils.formatBusinessRevenue(revenue, unit);
}
return '-';
})()}
</Text>
<Text fontSize="xs" color="gray.500">营业收入</Text>
</VStack>
</HStack>
</Box>
);
};
// 产业链节点卡片
const ValueChainNodeCard = ({ node, isCompany = false, level = 0 }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [relatedCompanies, setRelatedCompanies] = useState([]);
const [loadingRelated, setLoadingRelated] = useState(false);
const toast = useToast();
const getColorScheme = () => {
if (isCompany) return 'blue';
if (level < 0) return 'orange';
if (level > 0) return 'green';
return 'gray';
};
const colorScheme = getColorScheme();
const bgColor = useColorModeValue(`${colorScheme}.50`, `${colorScheme}.900`);
const borderColor = useColorModeValue(`${colorScheme}.200`, `${colorScheme}.600`);
const getNodeTypeIcon = (type) => {
const icons = {
'company': FaBuilding,
'supplier': FaHandshake,
'customer': FaUserTie,
'product': FaIndustry,
'service': FaCog,
'channel': FaNetworkWired,
'raw_material': FaFlask
};
return icons[type] || FaBuilding;
};
const getImportanceColor = (score) => {
if (score >= 80) return 'red';
if (score >= 60) return 'orange';
if (score >= 40) return 'yellow';
return 'green';
};
// 获取相关公司
const fetchRelatedCompanies = async () => {
setLoadingRelated(true);
try {
const response = await fetch(
`${API_BASE_URL}/api/company/value-chain/related-companies?node_name=${encodeURIComponent(node.node_name)}`
);
const data = await response.json();
if (data.success) {
setRelatedCompanies(data.data || []);
} else {
toast({
title: '获取相关公司失败',
description: data.message,
status: 'error',
duration: 3000,
isClosable: true,
});
}
} catch (error) {
logger.error('ValueChainNodeCard', 'fetchRelatedCompanies', error, { node_name: node.node_name });
toast({
title: '获取相关公司失败',
description: error.message,
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setLoadingRelated(false);
}
};
const handleCardClick = () => {
onOpen();
if (relatedCompanies.length === 0) {
fetchRelatedCompanies();
}
};
return (
<>
<ScaleFade in={true} initialScale={0.9}>
<Card
bg={bgColor}
borderColor={borderColor}
borderWidth={isCompany ? 3 : 1}
shadow={isCompany ? 'lg' : 'sm'}
cursor="pointer"
onClick={handleCardClick}
_hover={{
shadow: 'xl',
transform: 'translateY(-4px)',
borderColor: `${colorScheme}.400`
}}
transition="all 0.3s ease"
minH="140px"
>
<CardBody p={4}>
<VStack spacing={3} align="stretch">
<HStack justify="space-between">
<HStack spacing={2}>
<Icon
as={getNodeTypeIcon(node.node_type)}
color={`${colorScheme}.500`}
boxSize={5}
/>
{isCompany && (
<Badge colorScheme="blue" variant="solid">核心企业</Badge>
)}
</HStack>
{node.importance_score >= 70 && (
<Tooltip label="重要节点">
<Icon as={FaStar} color="orange.400" boxSize={4} />
</Tooltip>
)}
</HStack>
<Text fontWeight="bold" fontSize="sm" noOfLines={2}>
{node.node_name}
</Text>
{node.node_description && (
<Text fontSize="xs" color="gray.600" noOfLines={2}>
{node.node_description}
</Text>
)}
<HStack spacing={2} flexWrap="wrap">
<Badge variant="subtle" size="sm" colorScheme={colorScheme}>
{node.node_type}
</Badge>
{node.market_share && (
<Badge variant="outline" size="sm">
份额 {node.market_share}%
</Badge>
)}
</HStack>
{(node.importance_score || node.importance_score === 0) && (
<Box>
<HStack justify="space-between" mb={1}>
<Text fontSize="xs" color="gray.500">重要度</Text>
<Text fontSize="xs" fontWeight="bold">{node.importance_score}</Text>
</HStack>
<Progress
value={node.importance_score}
size="xs"
colorScheme={getImportanceColor(node.importance_score)}
borderRadius="full"
/>
</Box>
)}
</VStack>
</CardBody>
</Card>
</ScaleFade>
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<HStack>
<Icon as={getNodeTypeIcon(node.node_type)} color={`${colorScheme}.500`} boxSize={6} />
<VStack align="start" spacing={0}>
<Text>{node.node_name}</Text>
<HStack>
<Badge colorScheme={colorScheme}>{node.node_type}</Badge>
{isCompany && <Badge colorScheme="blue" variant="solid">核心企业</Badge>}
</HStack>
</VStack>
</HStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack align="stretch" spacing={4}>
{node.node_description && (
<Box>
<Text fontWeight="bold" mb={2} color="gray.600">节点描述</Text>
<Text fontSize="sm" lineHeight="1.6">{node.node_description}</Text>
</Box>
)}
<SimpleGrid columns={3} spacing={4}>
<Stat>
<StatLabel fontSize="xs">重要度评分</StatLabel>
<StatNumber fontSize="lg">{node.importance_score || 0}</StatNumber>
<StatHelpText>
<Progress
value={node.importance_score}
size="sm"
colorScheme={getImportanceColor(node.importance_score)}
borderRadius="full"
/>
</StatHelpText>
</Stat>
{node.market_share && (
<Stat>
<StatLabel fontSize="xs">市场份额</StatLabel>
<StatNumber fontSize="lg">{node.market_share}%</StatNumber>
</Stat>
)}
{node.dependency_degree && (
<Stat>
<StatLabel fontSize="xs">依赖程度</StatLabel>
<StatNumber fontSize="lg">{node.dependency_degree}%</StatNumber>
<StatHelpText>
<Progress
value={node.dependency_degree}
size="sm"
colorScheme={node.dependency_degree > 50 ? 'orange' : 'green'}
borderRadius="full"
/>
</StatHelpText>
</Stat>
)}
</SimpleGrid>
<Divider />
{/* 相关公司列表 */}
<Box>
<HStack mb={3} justify="space-between">
<Text fontWeight="bold" color="gray.600">相关公司</Text>
{loadingRelated && <Spinner size="sm" />}
</HStack>
{loadingRelated ? (
<Center py={4}>
<Spinner size="md" />
</Center>
) : relatedCompanies.length > 0 ? (
<VStack align="stretch" spacing={3} maxH="400px" overflowY="auto">
{relatedCompanies.map((company, idx) => {
// 获取节点层级标签
const getLevelLabel = (level) => {
if (level < 0) return { text: '上游', color: 'orange' };
if (level === 0) return { text: '核心', color: 'blue' };
if (level > 0) return { text: '下游', color: 'green' };
return { text: '未知', color: 'gray' };
};
const levelInfo = getLevelLabel(company.node_info?.node_level);
return (
<Card key={idx} variant="outline" size="sm">
<CardBody p={3}>
<VStack align="stretch" spacing={2}>
{/* 公司基本信息 */}
<HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}>
<HStack flexWrap="wrap">
<Text fontSize="sm" fontWeight="bold">{company.stock_name}</Text>
<Badge size="sm" colorScheme="blue">{company.stock_code}</Badge>
<Badge size="sm" colorScheme={levelInfo.color} variant="solid">
{levelInfo.text}
</Badge>
{company.node_info?.node_type && (
<Badge size="sm" colorScheme="purple" variant="outline">
{company.node_info.node_type}
</Badge>
)}
</HStack>
{company.company_name && (
<Text fontSize="xs" color="gray.500" noOfLines={1}>
{company.company_name}
</Text>
)}
</VStack>
<IconButton
size="sm"
icon={<ExternalLinkIcon />}
variant="ghost"
colorScheme="blue"
onClick={() => {
window.location.href = `/company?stock_code=${company.stock_code}`;
}}
aria-label="查看公司详情"
/>
</HStack>
{/* 节点描述 */}
{company.node_info?.node_description && (
<Text fontSize="xs" color="gray.600" noOfLines={2}>
{company.node_info.node_description}
</Text>
)}
{/* 节点指标 */}
{(company.node_info?.importance_score || company.node_info?.market_share || company.node_info?.dependency_degree) && (
<HStack spacing={3} pt={1} borderTop="1px" borderColor="gray.100">
{company.node_info.importance_score && (
<HStack spacing={1}>
<Text fontSize="xs" color="gray.500">重要度:</Text>
<Badge size="xs" colorScheme="red">{company.node_info.importance_score}</Badge>
</HStack>
)}
{company.node_info.market_share && (
<HStack spacing={1}>
<Text fontSize="xs" color="gray.500">市场份额:</Text>
<Text fontSize="xs" fontWeight="medium">{company.node_info.market_share}%</Text>
</HStack>
)}
{company.node_info.dependency_degree && (
<HStack spacing={1}>
<Text fontSize="xs" color="gray.500">依赖度:</Text>
<Text fontSize="xs" fontWeight="medium">{company.node_info.dependency_degree}%</Text>
</HStack>
)}
</HStack>
)}
{/* 流向关系 */}
{company.relationships && company.relationships.length > 0 && (
<Box pt={2} borderTop="1px" borderColor="gray.100">
<Text fontSize="xs" fontWeight="bold" color="gray.600" mb={1}>
产业链关系:
</Text>
<VStack align="stretch" spacing={1}>
{company.relationships.map((rel, ridx) => (
<HStack key={ridx} fontSize="xs" spacing={2}>
<Icon
as={rel.role === 'source' ? FaArrowRight : FaArrowLeft}
color={rel.role === 'source' ? 'green.500' : 'orange.500'}
boxSize={3}
/>
<Text color="gray.700" noOfLines={1}>
{rel.role === 'source' ? '流向' : '来自'}
<Text as="span" fontWeight="medium" mx={1}>
{rel.connected_node}
</Text>
</Text>
{rel.relationship_desc && (
<Badge size="xs" colorScheme="cyan" variant="subtle" noOfLines={1}>
{rel.relationship_desc}
</Badge>
)}
{rel.flow_ratio && (
<Text color="purple.600" fontWeight="medium">
{rel.flow_ratio}%
</Text>
)}
</HStack>
))}
</VStack>
</Box>
)}
</VStack>
</CardBody>
</Card>
);
})}
</VStack>
) : (
<Center py={4}>
<VStack spacing={2}>
<Icon as={FaBuilding} boxSize={8} color="gray.300" />
<Text fontSize="sm" color="gray.500">暂无相关公司</Text>
</VStack>
</Center>
)}
</Box>
</VStack>
</ModalBody>
<ModalFooter>
<Button colorScheme="blue" onClick={onClose}>关闭</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
// 关键因素卡片
const KeyFactorCard = ({ factor }) => {
const impactColor = {
positive: 'red',
negative: 'green',
neutral: 'gray',
mixed: 'yellow'
}[factor.impact_direction] || 'gray';
const bgColor = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.600');
return (
<Card bg={bgColor} borderColor={borderColor} size="sm">
<CardBody p={3}>
<VStack align="stretch" spacing={2}>
<HStack justify="space-between">
<Text fontWeight="medium" fontSize="sm">{factor.factor_name}</Text>
<Badge colorScheme={impactColor} size="sm">
{factor.impact_direction === 'positive' ? '正面' :
factor.impact_direction === 'negative' ? '负面' :
factor.impact_direction === 'mixed' ? '混合' : '中性'}
</Badge>
</HStack>
<HStack spacing={2}>
<Text fontSize="lg" fontWeight="bold" color={`${impactColor}.500`}>
{factor.factor_value}
{factor.factor_unit && ` ${factor.factor_unit}`}
</Text>
{factor.year_on_year && (
<Tag size="sm" colorScheme={factor.year_on_year > 0 ? 'red' : 'green'}>
<Icon as={factor.year_on_year > 0 ? FaArrowUp : FaArrowDown} mr={1} boxSize={3} />
{Math.abs(factor.year_on_year)}%
</Tag>
)}
</HStack>
{factor.factor_desc && (
<Text fontSize="xs" color="gray.600" noOfLines={2}>
{factor.factor_desc}
</Text>
)}
<HStack justify="space-between">
<Text fontSize="xs" color="gray.500">
影响权重: {factor.impact_weight}
</Text>
{factor.report_period && (
<Text fontSize="xs" color="gray.500">{factor.report_period}</Text>
)}
</HStack>
</VStack>
</CardBody>
</Card>
);
};
// 时间线组件
const TimelineComponent = ({ events }) => {
const [selectedEvent, setSelectedEvent] = useState(null);
const { isOpen, onOpen, onClose } = useDisclosure();
const handleEventClick = (event) => {
setSelectedEvent(event);
onOpen();
};
return (
<>
<Box position="relative" pl={8}>
<Box position="absolute" left="15px" top="20px" bottom="20px" width="2px" bg="gray.300" />
<VStack align="stretch" spacing={6}>
{events.map((event, idx) => {
const isPositive = event.impact_metrics?.is_positive;
const iconColor = isPositive ? 'red.500' : 'green.500';
const bgColor = useColorModeValue(
isPositive ? 'red.50' : 'green.50',
isPositive ? 'red.900' : 'green.900'
);
return (
<Fade in={true} key={idx}>
<Box position="relative">
<Circle
size="30px"
bg={iconColor}
position="absolute"
left="-15px"
top="20px"
zIndex={2}
border="3px solid white"
shadow="md"
>
<Icon as={isPositive ? FaArrowUp : FaArrowDown} color="white" boxSize={3} />
</Circle>
<Box position="absolute" left="15px" top="35px" width="20px" height="2px" bg="gray.300" />
<Card
ml={10}
bg={bgColor}
cursor="pointer"
onClick={() => handleEventClick(event)}
_hover={{ shadow: 'lg', transform: 'translateX(4px)' }}
transition="all 0.3s ease"
>
<CardBody p={4}>
<VStack align="stretch" spacing={2}>
<HStack justify="space-between">
<VStack align="start" spacing={0}>
<Text fontWeight="bold" fontSize="sm">{event.event_title}</Text>
<HStack spacing={2}>
<Icon as={FaCalendarAlt} boxSize={3} color="gray.500" />
<Text fontSize="xs" color="gray.500" fontWeight="medium">
{event.event_date}
</Text>
</HStack>
</VStack>
<Badge colorScheme={isPositive ? 'red' : 'green'} size="sm">
{event.event_type}
</Badge>
</HStack>
<Text fontSize="sm" color="gray.600" noOfLines={2}>
{event.event_desc}
</Text>
<HStack>
<Text fontSize="xs" color="gray.500">影响度:</Text>
<Progress
value={event.impact_metrics?.impact_score}
size="xs"
width="60px"
colorScheme={event.impact_metrics?.impact_score > 70 ? 'red' : 'orange'}
borderRadius="full"
/>
<Text fontSize="xs" color="gray.500" fontWeight="bold">
{event.impact_metrics?.impact_score || 0}
</Text>
</HStack>
</VStack>
</CardBody>
</Card>
</Box>
</Fade>
);
})}
</VStack>
</Box>
{selectedEvent && (
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<HStack>
<Icon
as={selectedEvent.impact_metrics?.is_positive ? FaCheckCircle : FaExclamationCircle}
color={selectedEvent.impact_metrics?.is_positive ? 'red.500' : 'green.500'}
boxSize={6}
/>
<VStack align="start" spacing={0}>
<Text>{selectedEvent.event_title}</Text>
<HStack>
<Badge colorScheme={selectedEvent.impact_metrics?.is_positive ? 'red' : 'green'}>
{selectedEvent.event_type}
</Badge>
<Text fontSize="sm" color="gray.500">{selectedEvent.event_date}</Text>
</HStack>
</VStack>
</HStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack align="stretch" spacing={4}>
<Box>
<Text fontWeight="bold" mb={2} color="gray.600">事件详情</Text>
<Text fontSize="sm" lineHeight="1.6">{selectedEvent.event_desc}</Text>
</Box>
{selectedEvent.related_info?.financial_impact && (
<Box>
<Text fontWeight="bold" mb={2} color="gray.600">财务影响</Text>
<Text fontSize="sm" lineHeight="1.6" color="blue.600">
{selectedEvent.related_info.financial_impact}
</Text>
</Box>
)}
<Box>
<Text fontWeight="bold" mb={2} color="gray.600">影响评估</Text>
<HStack spacing={4}>
<VStack spacing={1}>
<Text fontSize="xs" color="gray.500">影响度</Text>
<Progress
value={selectedEvent.impact_metrics?.impact_score}
size="lg"
width="120px"
colorScheme={selectedEvent.impact_metrics?.impact_score > 70 ? 'red' : 'orange'}
hasStripe
isAnimated
/>
<Text fontSize="sm" fontWeight="bold">
{selectedEvent.impact_metrics?.impact_score || 0}/100
</Text>
</VStack>
<VStack>
<Badge
size="lg"
colorScheme={selectedEvent.impact_metrics?.is_positive ? 'red' : 'green'}
px={3} py={1}
>
{selectedEvent.impact_metrics?.is_positive ? '正面影响' : '负面影响'}
</Badge>
</VStack>
</HStack>
</Box>
</VStack>
</ModalBody>
<ModalFooter>
<Button colorScheme="blue" onClick={onClose}>关闭</Button>
</ModalFooter>
</ModalContent>
</Modal>
)}
</>
);
};
// 股东类型标签组件
const ShareholderTypeBadge = ({ type }) => {
const typeConfig = {
'基金': { color: 'blue', icon: FaChartBar },
'个人': { color: 'green', icon: FaUserTie },
'法人': { color: 'purple', icon: FaBuilding },
'QFII': { color: 'orange', icon: FaGlobe },
'社保': { color: 'red', icon: FaShieldAlt },
'保险': { color: 'teal', icon: FaShieldAlt },
'信托': { color: 'cyan', icon: FaBriefcase },
'券商': { color: 'pink', icon: FaChartLine }
};
const config = Object.entries(typeConfig).find(([key]) => type?.includes(key))?.[1] ||
{ color: 'gray', icon: FaCircle };
return (
<Badge colorScheme={config.color} size="sm">
<Icon as={config.icon} mr={1} boxSize={3} />
{type}
</Badge>
);
};
// 主组件 - 完整版
const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
const [stockCode, setStockCode] = useState(propStockCode || '000001');
const [loading, setLoading] = useState(false);
// 监听props中的stockCode变化
useEffect(() => {
if (propStockCode && propStockCode !== stockCode) {
setStockCode(propStockCode);
}
}, [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 toast = useToast();
const bgColor = useColorModeValue('gray.50', 'gray.900');
const cardBg = useColorModeValue('white', 'gray.800');
const { isOpen: isAnnouncementOpen, onOpen: onAnnouncementOpen, onClose: onAnnouncementClose } = useDisclosure();
const [selectedAnnouncement, setSelectedAnnouncement] = useState(null);
// 业务板块详情展开状态
const [expandedSegments, setExpandedSegments] = useState({});
// 切换业务板块展开状态
const toggleSegmentExpansion = (segmentIndex) => {
setExpandedSegments(prev => ({
...prev,
[segmentIndex]: !prev[segmentIndex]
}));
};
// 加载数据
const loadData = async () => {
setLoading(true);
setError(null);
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()),
// 股票概览数据
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 [
comprehensiveRes, valueChainRes, keyFactorsRes,
basicRes, actualRes, concentrationRes, managementRes,
circulationRes, shareholdersRes, branchesRes, announcementsRes, disclosureRes
] = await Promise.all(requests);
// 设置深度分析数据
if (comprehensiveRes.success) setComprehensiveData(comprehensiveRes.data);
if (valueChainRes.success) setValueChainData(valueChainRes.data);
if (keyFactorsRes.success) setKeyFactorsData(keyFactorsRes.data);
// 设置股票概览数据
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);
} catch (err) {
setError(err.message);
logger.error('CompanyOverview', 'loadData', err, { stockCode });
// ❌ 移除数据加载失败toast
// toast({
// title: '数据加载失败',
// description: err.message,
// status: 'error',
// duration: 3000,
// isClosable: true,
// });
} finally {
setLoading(false);
}
};
useEffect(() => {
if (stockCode) {
loadData();
}
}, [stockCode]);
// 加载新闻事件
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'
});
// 搜索关键词优先级:
// 1. 用户输入的搜索关键词
// 2. 股票简称
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();
// API返回 data.data.events
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);
} 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);
}
};
// 当基本信息加载完成后,加载新闻事件
useEffect(() => {
if (basicInfo) {
loadNewsEvents(1);
}
}, [basicInfo]);
// 处理搜索
const handleNewsSearch = () => {
loadNewsEvents(1, newsSearchQuery);
};
// 处理分页
const handleNewsPageChange = (newPage) => {
loadNewsEvents(newPage, newsSearchQuery);
// 滚动到新闻列表顶部
document.getElementById('news-list-top')?.scrollIntoView({ behavior: 'smooth' });
};
// 管理层职位分类
const getManagementByCategory = () => {
const categories = {
'高管': [],
'董事': [],
'监事': [],
'其他': []
};
management.forEach(person => {
if (person.position_category === '高管' || person.position_name?.includes('总')) {
categories['高管'].push(person);
} else if (person.position_category === '董事' || person.position_name?.includes('董事')) {
categories['董事'].push(person);
} else if (person.position_category === '监事' || person.position_name?.includes('监事')) {
categories['监事'].push(person);
} else {
categories['其他'].push(person);
}
});
return categories;
};
// 计算股权集中度变化
const getConcentrationTrend = () => {
const grouped = {};
concentration.forEach(item => {
if (!grouped[item.end_date]) {
grouped[item.end_date] = {};
}
grouped[item.end_date][item.stat_item] = item;
});
return Object.entries(grouped).sort((a, b) => b[0].localeCompare(a[0])).slice(0, 5);
};
// 生成雷达图配置
const getRadarChartOption = () => {
if (!comprehensiveData?.competitive_position?.scores) return null;
const scores = comprehensiveData.competitive_position.scores;
const indicators = [
{ name: '市场地位', max: 100 },
{ name: '技术实力', max: 100 },
{ name: '品牌价值', max: 100 },
{ name: '运营效率', max: 100 },
{ name: '财务健康', max: 100 },
{ name: '创新能力', max: 100 },
{ name: '风险控制', max: 100 },
{ name: '成长潜力', max: 100 }
];
const data = [
scores.market_position || 0,
scores.technology || 0,
scores.brand || 0,
scores.operation || 0,
scores.finance || 0,
scores.innovation || 0,
scores.risk || 0,
scores.growth || 0
];
return {
tooltip: { trigger: 'item' },
radar: {
indicator: indicators,
shape: 'polygon',
splitNumber: 4,
name: {
textStyle: { color: '#666', fontSize: 12 }
},
splitLine: {
lineStyle: {
color: ['#e8e8e8', '#e0e0e0', '#d0d0d0', '#c0c0c0']
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(250,250,250,0.3)', 'rgba(200,200,200,0.3)']
}
},
axisLine: {
lineStyle: { color: '#ddd' }
}
},
series: [{
name: '竞争力评分',
type: 'radar',
data: [{
value: data,
name: '当前评分',
symbol: 'circle',
symbolSize: 5,
lineStyle: { width: 2, color: '#3182ce' },
areaStyle: { color: 'rgba(49, 130, 206, 0.3)' },
label: {
show: true,
formatter: (params) => params.value,
color: '#3182ce',
fontSize: 10
}
}]
}]
};
};
// 生成产业链桑基图配置
const getSankeyChartOption = () => {
if (!valueChainData?.value_chain_flows || valueChainData.value_chain_flows.length === 0) return null;
const nodes = new Set();
const links = [];
valueChainData.value_chain_flows.forEach(flow => {
// 检查 source 和 target 是否存在
if (!flow?.source?.node_name || !flow?.target?.node_name) return;
nodes.add(flow.source.node_name);
nodes.add(flow.target.node_name);
links.push({
source: flow.source.node_name,
target: flow.target.node_name,
value: parseFloat(flow.flow_metrics?.flow_ratio) || 1,
lineStyle: { color: 'source', opacity: 0.6 }
});
});
return {
tooltip: { trigger: 'item', triggerOn: 'mousemove' },
series: [{
type: 'sankey',
layout: 'none',
emphasis: { focus: 'adjacency' },
data: Array.from(nodes).map(name => ({ name })),
links: links,
lineStyle: { color: 'gradient', curveness: 0.5 },
label: { color: '#333', fontSize: 10 }
}]
};
};
if (loading) {
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" defaultIndex={0}>
<TabList bg={cardBg} p={4} borderRadius="lg" shadow="md" flexWrap="wrap">
<Tab fontWeight="bold"><Icon as={FaBrain} mr={2} />深度分析</Tab>
<Tab fontWeight="bold"><Icon as={FaInfoCircle} mr={2} />基本信息</Tab>
<Tab fontWeight="bold"><Icon as={FaNewspaper} mr={2} />新闻动态</Tab>
</TabList>
<TabPanels>
{/* 深度分析标签页 */}
<TabPanel p={0} pt={6}>
<VStack spacing={6} align="stretch">
{/* 核心定位卡片 */}
{comprehensiveData?.qualitative_analysis && (
<Card bg={cardBg} shadow="md">
<CardHeader>
<HStack>
<Icon as={FaLightbulb} color="yellow.500" />
<Heading size="sm">核心定位</Heading>
</HStack>
</CardHeader>
<CardBody>
<DisclaimerBox />
<VStack spacing={4} align="stretch">
{comprehensiveData.qualitative_analysis.core_positioning?.one_line_intro && (
<Alert status="info" variant="left-accent">
<AlertIcon />
<Text fontWeight="bold">{comprehensiveData.qualitative_analysis.core_positioning.one_line_intro}</Text>
</Alert>
)}
<Grid templateColumns="repeat(2, 1fr)" gap={4}>
<GridItem colSpan={{ base: 2, md: 1 }}>
<VStack align="stretch" spacing={3}>
<Text fontWeight="bold" fontSize="sm" color="gray.600">投资亮点</Text>
<Box p={3} bg={useColorModeValue('blue.50', 'blue.900')} borderRadius="md">
<Text fontSize="sm" whiteSpace="pre-wrap">
{comprehensiveData.qualitative_analysis.core_positioning?.investment_highlights || '暂无数据'}
</Text>
</Box>
</VStack>
</GridItem>
<GridItem colSpan={{ base: 2, md: 1 }}>
<VStack align="stretch" spacing={3}>
<Text fontWeight="bold" fontSize="sm" color="gray.600">商业模式</Text>
<Box p={3} bg={useColorModeValue('green.50', 'green.900')} borderRadius="md">
<Text fontSize="sm" whiteSpace="pre-wrap">
{comprehensiveData.qualitative_analysis.core_positioning?.business_model_desc || '暂无数据'}
</Text>
</Box>
</VStack>
</GridItem>
</Grid>
</VStack>
</CardBody>
</Card>
)}
{/* 竞争地位分析 */}
{comprehensiveData?.competitive_position && (
<Card bg={cardBg} shadow="md">
<CardHeader>
<HStack>
<Icon as={FaTrophy} color="gold" />
<Heading size="sm">竞争地位分析</Heading>
{comprehensiveData.competitive_position.ranking && (
<Badge colorScheme="purple" ml={2}>
行业排名 {comprehensiveData.competitive_position.ranking.industry_rank}/{comprehensiveData.competitive_position.ranking.total_companies}
</Badge>
)}
</HStack>
</CardHeader>
<CardBody>
<DisclaimerBox />
{comprehensiveData.competitive_position.analysis?.main_competitors && (
<Box mb={4}>
<Text fontWeight="bold" fontSize="sm" mb={2} color="gray.600">主要竞争对手</Text>
<HStack spacing={2} flexWrap="wrap">
{comprehensiveData.competitive_position.analysis.main_competitors
.split(',')
.map((competitor, idx) => (
<Tag key={idx} size="md" colorScheme="purple" variant="outline" borderRadius="full">
<Icon as={FaUsers} mr={1} />
<TagLabel>{competitor.trim()}</TagLabel>
</Tag>
))}
</HStack>
</Box>
)}
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
<GridItem colSpan={{ base: 2, lg: 1 }}>
<VStack spacing={4} align="stretch">
<ScoreBar label="市场地位" score={comprehensiveData.competitive_position.scores?.market_position} icon={FaTrophy} />
<ScoreBar label="技术实力" score={comprehensiveData.competitive_position.scores?.technology} icon={FaCog} />
<ScoreBar label="品牌价值" score={comprehensiveData.competitive_position.scores?.brand} icon={FaStar} />
<ScoreBar label="运营效率" score={comprehensiveData.competitive_position.scores?.operation} icon={FaChartLine} />
<ScoreBar label="财务健康" score={comprehensiveData.competitive_position.scores?.finance} icon={FaDollarSign} />
<ScoreBar label="创新能力" score={comprehensiveData.competitive_position.scores?.innovation} icon={FaFlask} />
<ScoreBar label="风险控制" score={comprehensiveData.competitive_position.scores?.risk} icon={FaShieldAlt} />
<ScoreBar label="成长潜力" score={comprehensiveData.competitive_position.scores?.growth} icon={FaRocket} />
</VStack>
</GridItem>
<GridItem colSpan={{ base: 2, lg: 1 }}>
{getRadarChartOption() && (
<ReactECharts
option={getRadarChartOption()}
style={{ height: '320px' }}
theme="light"
/>
)}
</GridItem>
</Grid>
<Divider my={4} />
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
<Box>
<Text fontWeight="bold" fontSize="sm" mb={2} color="red.600">竞争优势</Text>
<Text fontSize="sm">
{comprehensiveData.competitive_position.analysis?.competitive_advantages || '暂无数据'}
</Text>
</Box>
<Box>
<Text fontWeight="bold" fontSize="sm" mb={2} color="green.600">竞争劣势</Text>
<Text fontSize="sm">
{comprehensiveData.competitive_position.analysis?.competitive_disadvantages || '暂无数据'}
</Text>
</Box>
</SimpleGrid>
</CardBody>
</Card>
)}
{/* 业务结构分析 */}
{comprehensiveData?.business_structure && comprehensiveData.business_structure.length > 0 && (
<Card bg={cardBg} shadow="md">
<CardHeader>
<HStack>
<Icon as={FaChartPie} color="purple.500" />
<Heading size="sm">业务结构分析</Heading>
<Badge>{comprehensiveData.business_structure[0]?.report_period}</Badge>
</HStack>
</CardHeader>
<CardBody>
<DisclaimerBox />
<VStack spacing={3} align="stretch">
{comprehensiveData.business_structure.map((business, idx) => (
<BusinessTreeItem key={idx} business={business} depth={business.business_level - 1} />
))}
</VStack>
</CardBody>
</Card>
)}
{/* 产业链分析 */}
{valueChainData && (
<Card bg={cardBg} shadow="md">
<CardHeader>
<HStack>
<Icon as={FaNetworkWired} color="teal.500" />
<Heading size="sm">产业链分析</Heading>
<HStack spacing={2}>
<Badge colorScheme="orange">
上游 {valueChainData.analysis_summary?.upstream_nodes || 0}
</Badge>
<Badge colorScheme="blue">
核心 {valueChainData.analysis_summary?.company_nodes || 0}
</Badge>
<Badge colorScheme="green">
下游 {valueChainData.analysis_summary?.downstream_nodes || 0}
</Badge>
</HStack>
</HStack>
</CardHeader>
<CardBody>
<DisclaimerBox />
<Tabs variant="soft-rounded" colorScheme="teal">
<TabList>
<Tab>层级视图</Tab>
<Tab>流向关系</Tab>
</TabList>
<TabPanels>
<TabPanel>
<VStack spacing={8} align="stretch">
{(valueChainData.value_chain_structure?.nodes_by_level?.['level_-2'] ||
valueChainData.value_chain_structure?.nodes_by_level?.['level_-1']) && (
<Box>
<HStack mb={4}>
<Badge colorScheme="orange" fontSize="md" px={3} py={1}>上游供应链</Badge>
<Text fontSize="sm" color="gray.600">原材料与供应商</Text>
</HStack>
<SimpleGrid columns={{ base: 2, md: 3, lg: 4 }} spacing={4}>
{[
...(valueChainData.value_chain_structure?.nodes_by_level?.['level_-2'] || []),
...(valueChainData.value_chain_structure?.nodes_by_level?.['level_-1'] || [])
].map((node, idx) => (
<ValueChainNodeCard key={idx} node={node} level={node.node_level} />
))}
</SimpleGrid>
</Box>
)}
{valueChainData.value_chain_structure?.nodes_by_level?.['level_0'] && (
<Box>
<HStack mb={4}>
<Badge colorScheme="blue" fontSize="md" px={3} py={1}>核心企业</Badge>
<Text fontSize="sm" color="gray.600">公司主体与产品</Text>
</HStack>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
{valueChainData.value_chain_structure.nodes_by_level['level_0'].map((node, idx) => (
<ValueChainNodeCard
key={idx}
node={node}
isCompany={node.node_type === 'company'}
level={0}
/>
))}
</SimpleGrid>
</Box>
)}
{(valueChainData.value_chain_structure?.nodes_by_level?.['level_1'] ||
valueChainData.value_chain_structure?.nodes_by_level?.['level_2']) && (
<Box>
<HStack mb={4}>
<Badge colorScheme="green" fontSize="md" px={3} py={1}>下游客户</Badge>
<Text fontSize="sm" color="gray.600">客户与终端市场</Text>
</HStack>
<SimpleGrid columns={{ base: 2, md: 3, lg: 4 }} spacing={4}>
{[
...(valueChainData.value_chain_structure?.nodes_by_level?.['level_1'] || []),
...(valueChainData.value_chain_structure?.nodes_by_level?.['level_2'] || [])
].map((node, idx) => (
<ValueChainNodeCard key={idx} node={node} level={node.node_level} />
))}
</SimpleGrid>
</Box>
)}
</VStack>
</TabPanel>
<TabPanel>
{getSankeyChartOption() ? (
<ReactECharts
option={getSankeyChartOption()}
style={{ height: '500px' }}
theme="light"
/>
) : (
<Center h="200px">
<Text color="gray.500">暂无流向数据</Text>
</Center>
)}
</TabPanel>
</TabPanels>
</Tabs>
</CardBody>
</Card>
)}
{/* 关键因素与发展时间线 */}
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
<GridItem colSpan={{ base: 2, lg: 1 }}>
{keyFactorsData?.key_factors && (
<Card bg={cardBg} shadow="md" h="full">
<CardHeader>
<HStack>
<Icon as={FaBalanceScale} color="orange.500" />
<Heading size="sm">关键因素</Heading>
<Badge>{keyFactorsData.key_factors.total_factors} </Badge>
</HStack>
</CardHeader>
<CardBody>
<DisclaimerBox />
<Accordion allowMultiple>
{keyFactorsData.key_factors.categories.map((category, idx) => (
<AccordionItem key={idx}>
<AccordionButton>
<Box flex="1" textAlign="left">
<HStack>
<Text fontWeight="medium">{category.category_name}</Text>
<Badge size="sm" variant="subtle">
{category.factors.length}
</Badge>
</HStack>
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
<VStack spacing={3} align="stretch">
{category.factors.map((factor, fidx) => (
<KeyFactorCard key={fidx} factor={factor} />
))}
</VStack>
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
</CardBody>
</Card>
)}
</GridItem>
<GridItem colSpan={{ base: 2, lg: 1 }}>
{keyFactorsData?.development_timeline && (
<Card bg={cardBg} shadow="md" h="full">
<CardHeader>
<HStack>
<Icon as={FaHistory} color="cyan.500" />
<Heading size="sm">发展时间线</Heading>
<HStack spacing={1}>
<Badge colorScheme="red">
正面 {keyFactorsData.development_timeline.statistics?.positive_events || 0}
</Badge>
<Badge colorScheme="green">
负面 {keyFactorsData.development_timeline.statistics?.negative_events || 0}
</Badge>
</HStack>
</HStack>
</CardHeader>
<CardBody>
<DisclaimerBox />
<Box maxH="600px" overflowY="auto" pr={2}>
<TimelineComponent events={keyFactorsData.development_timeline.events} />
</Box>
</CardBody>
</Card>
)}
</GridItem>
</Grid>
{/* 业务板块详情 */}
{comprehensiveData?.business_segments && comprehensiveData.business_segments.length > 0 && (
<Card bg={cardBg} shadow="md">
<CardHeader>
<HStack>
<Icon as={FaIndustry} color="indigo.500" />
<Heading size="sm">业务板块详情</Heading>
<Badge>{comprehensiveData.business_segments.length} 个板块</Badge>
</HStack>
</CardHeader>
<CardBody>
<DisclaimerBox />
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
{comprehensiveData.business_segments.map((segment, idx) => {
const isExpanded = expandedSegments[idx];
return (
<Card key={idx} variant="outline">
<CardBody>
<VStack align="stretch" spacing={3}>
<HStack justify="space-between">
<Text fontWeight="bold" fontSize="md">{segment.segment_name}</Text>
<Button
size="sm"
variant="ghost"
leftIcon={<Icon as={isExpanded ? FaCompressAlt : FaExpandAlt} />}
onClick={() => toggleSegmentExpansion(idx)}
colorScheme="blue"
>
{isExpanded ? '折叠' : '展开'}
</Button>
</HStack>
<Box>
<Text fontSize="xs" color="gray.500" mb={1}>业务描述</Text>
<Text fontSize="sm" noOfLines={isExpanded ? undefined : 3}>
{segment.segment_description || '暂无描述'}
</Text>
</Box>
<Box>
<Text fontSize="xs" color="gray.500" mb={1}>竞争地位</Text>
<Text fontSize="sm" noOfLines={isExpanded ? undefined : 2}>
{segment.competitive_position || '暂无数据'}
</Text>
</Box>
<Box>
<Text fontSize="xs" color="gray.500" mb={1}>未来潜力</Text>
<Text fontSize="sm" noOfLines={isExpanded ? undefined : 2} color="blue.600">
{segment.future_potential || '暂无数据'}
</Text>
</Box>
{isExpanded && segment.key_products && (
<Box>
<Text fontSize="xs" color="gray.500" mb={1}>主要产品</Text>
<Text fontSize="sm" color="green.600">
{segment.key_products}
</Text>
</Box>
)}
{isExpanded && segment.market_share && (
<Box>
<Text fontSize="xs" color="gray.500" mb={1}>市场份额</Text>
<HStack>
<Badge colorScheme="purple" fontSize="sm">
{segment.market_share}%
</Badge>
</HStack>
</Box>
)}
{isExpanded && segment.revenue_contribution && (
<Box>
<Text fontSize="xs" color="gray.500" mb={1}>营收贡献</Text>
<HStack>
<Badge colorScheme="orange" fontSize="sm">
{segment.revenue_contribution}%
</Badge>
</HStack>
</Box>
)}
</VStack>
</CardBody>
</Card>
);
})}
</SimpleGrid>
</CardBody>
</Card>
)}
{/* 战略分析 */}
{comprehensiveData?.qualitative_analysis?.strategy && (
<Card bg={cardBg} shadow="md">
<CardHeader>
<HStack>
<Icon as={FaRocket} color="red.500" />
<Heading size="sm">战略分析</Heading>
</HStack>
</CardHeader>
<CardBody>
<DisclaimerBox />
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
<GridItem colSpan={{ base: 2, md: 1 }}>
<VStack align="stretch" spacing={3}>
<Text fontWeight="bold" fontSize="sm" color="gray.600">战略方向</Text>
<Box p={4} bg={useColorModeValue('purple.50', 'purple.900')} borderRadius="md">
<Text fontSize="sm">
{comprehensiveData.qualitative_analysis.strategy.strategy_description || '暂无数据'}
</Text>
</Box>
</VStack>
</GridItem>
<GridItem colSpan={{ base: 2, md: 1 }}>
<VStack align="stretch" spacing={3}>
<Text fontWeight="bold" fontSize="sm" color="gray.600">战略举措</Text>
<Box p={4} bg={useColorModeValue('orange.50', 'orange.900')} borderRadius="md">
<Text fontSize="sm">
{comprehensiveData.qualitative_analysis.strategy.strategic_initiatives || '暂无数据'}
</Text>
</Box>
</VStack>
</GridItem>
</Grid>
</CardBody>
</Card>
)}
</VStack>
</TabPanel>
{/* 基本信息标签页 */}
<TabPanel p={0} pt={6}>
<Card bg={cardBg} shadow="md">
<CardBody>
<Tabs variant="enclosed" colorScheme="blue">
<TabList flexWrap="wrap">
<Tab><Icon as={FaShareAlt} mr={2} />股权结构</Tab>
<Tab><Icon as={FaUserTie} mr={2} />管理团队</Tab>
<Tab><Icon as={FaBullhorn} mr={2} />公司公告</Tab>
<Tab><Icon as={FaSitemap} mr={2} />分支机构</Tab>
<Tab><Icon as={FaInfoCircle} mr={2} />工商信息</Tab>
</TabList>
<TabPanels>
{/* 股权结构标签页 */}
<TabPanel>
<VStack spacing={6} align="stretch">
{actualControl.length > 0 && (
<Box>
<HStack mb={4}>
<Icon as={FaCrown} color="gold" boxSize={5} />
<Heading size="sm">实际控制人</Heading>
</HStack>
<Card variant="outline">
<CardBody>
<HStack justify="space-between">
<VStack align="start">
<Text fontWeight="bold" fontSize="lg">
{actualControl[0].actual_controller_name}
</Text>
<HStack>
<Badge colorScheme="purple">{actualControl[0].control_type}</Badge>
<Text fontSize="sm" color="gray.500">
截至 {formatUtils.formatDate(actualControl[0].end_date)}
</Text>
</HStack>
</VStack>
<Stat textAlign="right">
<StatLabel>控制比例</StatLabel>
<StatNumber color="purple.500">
{formatUtils.formatPercentage(actualControl[0].holding_ratio)}
</StatNumber>
<StatHelpText>
{formatUtils.formatShares(actualControl[0].holding_shares)}
</StatHelpText>
</Stat>
</HStack>
</CardBody>
</Card>
</Box>
)}
{concentration.length > 0 && (
<Box>
<HStack mb={4}>
<Icon as={FaChartPie} color="blue.500" boxSize={5} />
<Heading size="sm">股权集中度</Heading>
</HStack>
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
{getConcentrationTrend().slice(0, 1).map(([date, items]) => (
<Card key={date} variant="outline">
<CardHeader pb={2}>
<Text fontSize="sm" color="gray.500">
{formatUtils.formatDate(date)}
</Text>
</CardHeader>
<CardBody pt={2}>
<VStack spacing={3} align="stretch">
{Object.entries(items).map(([key, item]) => (
<HStack key={key} justify="space-between">
<Text fontSize="sm">{item.stat_item}</Text>
<HStack>
<Text fontWeight="bold" color="blue.500">
{formatUtils.formatPercentage(item.holding_ratio)}
</Text>
{item.ratio_change && (
<Badge colorScheme={item.ratio_change > 0 ? 'red' : 'green'}>
<Icon as={item.ratio_change > 0 ? FaArrowUp : FaArrowDown} mr={1} boxSize={3} />
{Math.abs(item.ratio_change).toFixed(2)}%
</Badge>
)}
</HStack>
</HStack>
))}
</VStack>
</CardBody>
</Card>
))}
</SimpleGrid>
</Box>
)}
{topShareholders.length > 0 && (
<Box>
<HStack mb={4}>
<Icon as={FaUsers} color="green.500" boxSize={5} />
<Heading size="sm">十大股东</Heading>
<Badge>{formatUtils.formatDate(topShareholders[0].end_date)}</Badge>
</HStack>
<TableContainer>
<Table size="sm" variant="striped">
<Thead>
<Tr>
<Th>排名</Th>
<Th>股东名称</Th>
<Th>股东类型</Th>
<Th isNumeric>持股数量</Th>
<Th isNumeric>持股比例</Th>
<Th>股份性质</Th>
</Tr>
</Thead>
<Tbody>
{topShareholders.slice(0, 10).map((shareholder, idx) => (
<Tr key={idx}>
<Td>
<Badge colorScheme={idx < 3 ? 'red' : 'gray'}>
{shareholder.shareholder_rank}
</Badge>
</Td>
<Td>
<Tooltip label={shareholder.shareholder_name}>
<Text noOfLines={1} maxW="200px">
{shareholder.shareholder_name}
</Text>
</Tooltip>
</Td>
<Td>
<ShareholderTypeBadge type={shareholder.shareholder_type} />
</Td>
<Td isNumeric fontWeight="medium">
{formatUtils.formatShares(shareholder.holding_shares)}
</Td>
<Td isNumeric>
<Text color="blue.500" fontWeight="bold">
{formatUtils.formatPercentage(shareholder.total_share_ratio)}
</Text>
</Td>
<Td>
<Badge size="sm" variant="outline">
{shareholder.share_nature || '流通股'}
</Badge>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
)}
{topCirculationShareholders.length > 0 && (
<Box>
<HStack mb={4}>
<Icon as={FaChartLine} color="purple.500" boxSize={5} />
<Heading size="sm">十大流通股东</Heading>
<Badge>{formatUtils.formatDate(topCirculationShareholders[0].end_date)}</Badge>
</HStack>
<TableContainer>
<Table size="sm" variant="striped">
<Thead>
<Tr>
<Th>排名</Th>
<Th>股东名称</Th>
<Th>股东类型</Th>
<Th isNumeric>持股数量</Th>
<Th isNumeric>流通股比例</Th>
</Tr>
</Thead>
<Tbody>
{topCirculationShareholders.slice(0, 10).map((shareholder, idx) => (
<Tr key={idx}>
<Td>
<Badge colorScheme={idx < 3 ? 'orange' : 'gray'}>
{shareholder.shareholder_rank}
</Badge>
</Td>
<Td>
<Tooltip label={shareholder.shareholder_name}>
<Text noOfLines={1} maxW="250px">
{shareholder.shareholder_name}
</Text>
</Tooltip>
</Td>
<Td>
<ShareholderTypeBadge type={shareholder.shareholder_type} />
</Td>
<Td isNumeric fontWeight="medium">
{formatUtils.formatShares(shareholder.holding_shares)}
</Td>
<Td isNumeric>
<Text color="purple.500" fontWeight="bold">
{formatUtils.formatPercentage(shareholder.circulation_share_ratio)}
</Text>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
)}
</VStack>
</TabPanel>
{/* 管理团队标签页 */}
<TabPanel>
<VStack spacing={6} align="stretch">
{Object.entries(getManagementByCategory()).map(([category, people]) => (
people.length > 0 && (
<Box key={category}>
<HStack mb={4}>
<Icon
as={category === '高管' ? FaUserTie :
category === '董事' ? FaCrown :
category === '监事' ? FaEye : FaUsers}
color={category === '高管' ? 'blue.500' :
category === '董事' ? 'purple.500' :
category === '监事' ? 'green.500' : 'gray.500'}
boxSize={5}
/>
<Heading size="sm">{category}</Heading>
<Badge>{people.length}</Badge>
</HStack>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
{people.map((person, idx) => (
<Card key={idx} variant="outline" size="sm">
<CardBody>
<HStack spacing={3} align="start">
<Avatar
name={person.name}
size="md"
bg={category === '高管' ? 'blue.500' :
category === '董事' ? 'purple.500' :
category === '监事' ? 'green.500' : 'gray.500'}
/>
<VStack align="start" spacing={1} flex={1}>
<HStack>
<Text fontWeight="bold">{person.name}</Text>
{person.gender && (
<Icon
as={FaVenusMars}
color={person.gender === '男' ? 'blue.400' : 'pink.400'}
boxSize={3}
/>
)}
</HStack>
<Text fontSize="sm" color="blue.600">
{person.position_name}
</Text>
<HStack spacing={2} flexWrap="wrap">
{person.education && (
<Tag size="sm" variant="subtle">
<Icon as={FaGraduationCap} mr={1} boxSize={3} />
{person.education}
</Tag>
)}
{person.birth_year && (
<Tag size="sm" variant="subtle">
{new Date().getFullYear() - parseInt(person.birth_year)}
</Tag>
)}
{person.nationality && person.nationality !== '中国' && (
<Tag size="sm" colorScheme="orange">
<Icon as={FaPassport} mr={1} boxSize={3} />
{person.nationality}
</Tag>
)}
</HStack>
<Text fontSize="xs" color="gray.500">
任职日期{formatUtils.formatDate(person.start_date)}
</Text>
</VStack>
</HStack>
</CardBody>
</Card>
))}
</SimpleGrid>
</Box>
)
))}
</VStack>
</TabPanel>
{/* 公司公告标签页 */}
<TabPanel>
<VStack spacing={4} align="stretch">
{disclosureSchedule.length > 0 && (
<Box>
<HStack mb={3}>
<Icon as={FaCalendarAlt} color="orange.500" />
<Text fontWeight="bold">财报披露日程</Text>
</HStack>
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={3}>
{disclosureSchedule.slice(0, 4).map((schedule, idx) => (
<Card
key={idx}
variant="outline"
size="sm"
bg={schedule.is_disclosed ? 'green.50' : 'orange.50'}
>
<CardBody p={3}>
<VStack spacing={1}>
<Badge colorScheme={schedule.is_disclosed ? 'green' : 'orange'}>
{schedule.report_name}
</Badge>
<Text fontSize="sm" fontWeight="bold">
{schedule.is_disclosed ? '已披露' : '预计'}
</Text>
<Text fontSize="xs">
{formatUtils.formatDate(
schedule.is_disclosed ? schedule.actual_date : schedule.latest_scheduled_date
)}
</Text>
</VStack>
</CardBody>
</Card>
))}
</SimpleGrid>
</Box>
)}
<Divider />
<Box>
<HStack mb={3}>
<Icon as={FaBullhorn} color="blue.500" />
<Text fontWeight="bold">最新公告</Text>
</HStack>
<VStack spacing={2} align="stretch">
{announcements.map((announcement, idx) => (
<Card
key={idx}
variant="outline"
size="sm"
cursor="pointer"
onClick={() => {
setSelectedAnnouncement(announcement);
onAnnouncementOpen();
}}
_hover={{ bg: 'gray.50' }}
>
<CardBody p={3}>
<HStack justify="space-between">
<VStack align="start" spacing={1} flex={1}>
<HStack>
<Badge size="sm" colorScheme="blue">
{announcement.info_type || '公告'}
</Badge>
<Text fontSize="xs" color="gray.500">
{formatUtils.formatDate(announcement.announce_date)}
</Text>
</HStack>
<Text fontSize="sm" fontWeight="medium" noOfLines={1}>
{announcement.title}
</Text>
</VStack>
<HStack>
{announcement.format && (
<Tag size="sm" variant="subtle">{announcement.format}</Tag>
)}
<IconButton
size="sm"
icon={<ExternalLinkIcon />}
variant="ghost"
onClick={(e) => {
e.stopPropagation();
window.open(announcement.url, '_blank');
}}
/>
</HStack>
</HStack>
</CardBody>
</Card>
))}
</VStack>
</Box>
</VStack>
</TabPanel>
{/* 分支机构标签页 */}
<TabPanel>
{branches.length > 0 ? (
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
{branches.map((branch, idx) => (
<Card key={idx} variant="outline">
<CardBody>
<VStack align="start" spacing={3}>
<HStack justify="space-between" w="full">
<Text fontWeight="bold">{branch.branch_name}</Text>
<Badge
colorScheme={branch.business_status === '存续' ? 'green' : 'red'}
>
{branch.business_status}
</Badge>
</HStack>
<SimpleGrid columns={2} spacing={2} w="full">
<VStack align="start" spacing={1}>
<Text fontSize="xs" color="gray.500">注册资本</Text>
<Text fontSize="sm" fontWeight="medium">
{branch.register_capital || '-'}
</Text>
</VStack>
<VStack align="start" spacing={1}>
<Text fontSize="xs" color="gray.500">法人代表</Text>
<Text fontSize="sm" fontWeight="medium">
{branch.legal_person || '-'}
</Text>
</VStack>
<VStack align="start" spacing={1}>
<Text fontSize="xs" color="gray.500">成立日期</Text>
<Text fontSize="sm" fontWeight="medium">
{formatUtils.formatDate(branch.register_date)}
</Text>
</VStack>
<VStack align="start" spacing={1}>
<Text fontSize="xs" color="gray.500">关联企业</Text>
<Text fontSize="sm" fontWeight="medium">
{branch.related_company_count || 0}
</Text>
</VStack>
</SimpleGrid>
</VStack>
</CardBody>
</Card>
))}
</SimpleGrid>
) : (
<Center h="200px">
<VStack>
<Icon as={FaSitemap} boxSize={12} color="gray.300" />
<Text color="gray.500">暂无分支机构信息</Text>
</VStack>
</Center>
)}
</TabPanel>
{/* 工商信息标签页 */}
<TabPanel>
{basicInfo && (
<VStack spacing={4} align="stretch">
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={6}>
<Box>
<Heading size="sm" mb={3}>工商信息</Heading>
<VStack align="start" spacing={2}>
<HStack w="full">
<Text fontSize="sm" color="gray.600" minW="80px">统一信用代码</Text>
<Code fontSize="xs">{basicInfo.credit_code}</Code>
</HStack>
<HStack w="full">
<Text fontSize="sm" color="gray.600" minW="80px">公司规模</Text>
<Text fontSize="sm">{basicInfo.company_size}</Text>
</HStack>
<HStack w="full">
<Text fontSize="sm" color="gray.600" minW="80px">注册地址</Text>
<Text fontSize="sm" noOfLines={2}>{basicInfo.reg_address}</Text>
</HStack>
<HStack w="full">
<Text fontSize="sm" color="gray.600" minW="80px">办公地址</Text>
<Text fontSize="sm" noOfLines={2}>{basicInfo.office_address}</Text>
</HStack>
</VStack>
</Box>
<Box>
<Heading size="sm" mb={3}>服务机构</Heading>
<VStack align="start" spacing={2}>
<Box>
<Text fontSize="sm" color="gray.600">会计师事务所</Text>
<Text fontSize="sm" fontWeight="medium">{basicInfo.accounting_firm}</Text>
</Box>
<Box>
<Text fontSize="sm" color="gray.600">律师事务所</Text>
<Text fontSize="sm" fontWeight="medium">{basicInfo.law_firm}</Text>
</Box>
</VStack>
</Box>
</SimpleGrid>
<Divider />
<Box>
<Heading size="sm" mb={3}>主营业务</Heading>
<Text fontSize="sm" lineHeight="tall">{basicInfo.main_business}</Text>
</Box>
<Box>
<Heading size="sm" mb={3}>经营范围</Heading>
<Text fontSize="sm" lineHeight="tall" color="gray.700">
{basicInfo.business_scope}
</Text>
</Box>
</VStack>
)}
</TabPanel>
</TabPanels>
</Tabs>
</CardBody>
</Card>
</TabPanel>
{/* 新闻动态标签页 */}
<TabPanel p={0} pt={6}>
<VStack spacing={4} align="stretch">
<Card bg={cardBg} shadow="md">
<CardBody>
<VStack spacing={4} align="stretch">
{/* 搜索框和统计信息 */}
<HStack justify="space-between" flexWrap="wrap">
<HStack flex={1} minW="300px">
<InputGroup>
<InputLeftElement pointerEvents="none">
<SearchIcon color="gray.400" />
</InputLeftElement>
<Input
placeholder="搜索相关新闻..."
value={newsSearchQuery}
onChange={(e) => setNewsSearchQuery(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleNewsSearch()}
/>
</InputGroup>
<Button
colorScheme="blue"
onClick={handleNewsSearch}
isLoading={newsLoading}
minW="80px"
>
搜索
</Button>
</HStack>
{newsPagination.total > 0 && (
<HStack spacing={2}>
<Icon as={FaNewspaper} color="blue.500" />
<Text fontSize="sm" color="gray.600">
共找到 <Text as="span" fontWeight="bold" color="blue.600">{newsPagination.total}</Text>
</Text>
</HStack>
)}
</HStack>
<div id="news-list-top" />
{/* 新闻列表 */}
{newsLoading ? (
<Center h="400px">
<VStack spacing={3}>
<Spinner size="xl" color="blue.500" thickness="4px" />
<Text color="gray.600">正在加载新闻...</Text>
</VStack>
</Center>
) : newsEvents.length > 0 ? (
<>
<VStack spacing={3} align="stretch">
{newsEvents.map((event, idx) => {
const importanceColor = {
'S': 'red',
'A': 'orange',
'B': 'yellow',
'C': 'green'
}[event.importance] || 'gray';
const eventTypeIcon = {
'企业公告': FaBullhorn,
'政策': FaGavel,
'技术突破': FaFlask,
'企业融资': FaDollarSign,
'政策监管': FaShieldAlt,
'政策动态': FaFileAlt,
'行业事件': FaIndustry
}[event.event_type] || FaNewspaper;
return (
<Card
key={event.id || idx}
variant="outline"
_hover={{ bg: useColorModeValue('gray.50', 'gray.700'), shadow: 'md', borderColor: 'blue.300' }}
transition="all 0.2s"
>
<CardBody p={4}>
<VStack align="stretch" spacing={3}>
{/* 标题栏 */}
<HStack justify="space-between" align="start">
<VStack align="start" spacing={2} flex={1}>
<HStack>
<Icon as={eventTypeIcon} color="blue.500" boxSize={5} />
<Text fontWeight="bold" fontSize="lg" lineHeight="1.3">
{event.title}
</Text>
</HStack>
{/* 标签栏 */}
<HStack spacing={2} flexWrap="wrap">
{event.importance && (
<Badge colorScheme={importanceColor} variant="solid" px={2}>
{event.importance}
</Badge>
)}
{event.event_type && (
<Badge colorScheme="blue" variant="outline">
{event.event_type}
</Badge>
)}
{event.invest_score && (
<Badge colorScheme="purple" variant="subtle">
投资分: {event.invest_score}
</Badge>
)}
{event.keywords && event.keywords.length > 0 && (
<>
{event.keywords.slice(0, 4).map((keyword, kidx) => (
<Tag key={kidx} size="sm" colorScheme="cyan" variant="subtle">
{typeof keyword === 'string'
? keyword
: (keyword?.concept || keyword?.name || '未知')}
</Tag>
))}
</>
)}
</HStack>
</VStack>
{/* 右侧信息栏 */}
<VStack align="end" spacing={1} minW="100px">
<Text fontSize="xs" color="gray.500">
{event.created_at ? new Date(event.created_at).toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}) : ''}
</Text>
<HStack spacing={3}>
{event.view_count !== undefined && (
<HStack spacing={1}>
<Icon as={FaEye} boxSize={3} color="gray.400" />
<Text fontSize="xs" color="gray.500">{event.view_count}</Text>
</HStack>
)}
{event.hot_score !== undefined && (
<HStack spacing={1}>
<Icon as={FaFire} boxSize={3} color="orange.400" />
<Text fontSize="xs" color="gray.500">{event.hot_score.toFixed(1)}</Text>
</HStack>
)}
</HStack>
{event.creator && (
<Text fontSize="xs" color="gray.400">
@{event.creator.username}
</Text>
)}
</VStack>
</HStack>
{/* 描述 */}
{event.description && (
<Text fontSize="sm" color="gray.700" lineHeight="1.6">
{event.description}
</Text>
)}
{/* 收益率数据 */}
{(event.related_avg_chg !== null || event.related_max_chg !== null || event.related_week_chg !== null) && (
<Box pt={2} borderTop="1px" borderColor="gray.200">
<HStack spacing={6} flexWrap="wrap">
<HStack spacing={1}>
<Icon as={FaChartLine} boxSize={3} color="gray.500" />
<Text fontSize="xs" color="gray.500" fontWeight="medium">相关涨跌:</Text>
</HStack>
{event.related_avg_chg !== null && event.related_avg_chg !== undefined && (
<HStack spacing={1}>
<Text fontSize="xs" color="gray.500">平均</Text>
<Text
fontSize="sm"
fontWeight="bold"
color={event.related_avg_chg > 0 ? 'red.500' : 'green.500'}
>
{event.related_avg_chg > 0 ? '+' : ''}{event.related_avg_chg.toFixed(2)}%
</Text>
</HStack>
)}
{event.related_max_chg !== null && event.related_max_chg !== undefined && (
<HStack spacing={1}>
<Text fontSize="xs" color="gray.500">最大</Text>
<Text
fontSize="sm"
fontWeight="bold"
color={event.related_max_chg > 0 ? 'red.500' : 'green.500'}
>
{event.related_max_chg > 0 ? '+' : ''}{event.related_max_chg.toFixed(2)}%
</Text>
</HStack>
)}
{event.related_week_chg !== null && event.related_week_chg !== undefined && (
<HStack spacing={1}>
<Text fontSize="xs" color="gray.500"></Text>
<Text
fontSize="sm"
fontWeight="bold"
color={event.related_week_chg > 0 ? 'red.500' : 'green.500'}
>
{event.related_week_chg > 0 ? '+' : ''}{event.related_week_chg.toFixed(2)}%
</Text>
</HStack>
)}
</HStack>
</Box>
)}
</VStack>
</CardBody>
</Card>
);
})}
</VStack>
{/* 分页控件 */}
{newsPagination.pages > 1 && (
<Box pt={4}>
<HStack justify="space-between" align="center" flexWrap="wrap">
{/* 分页信息 */}
<Text fontSize="sm" color="gray.600">
{newsPagination.page} / {newsPagination.pages}
</Text>
{/* 分页按钮 */}
<HStack spacing={2}>
<Button
size="sm"
onClick={() => handleNewsPageChange(1)}
isDisabled={!newsPagination.has_prev || newsLoading}
leftIcon={<Icon as={FaChevronLeft} />}
>
首页
</Button>
<Button
size="sm"
onClick={() => handleNewsPageChange(newsPagination.page - 1)}
isDisabled={!newsPagination.has_prev || newsLoading}
>
上一页
</Button>
{/* 页码按钮 */}
{(() => {
const currentPage = newsPagination.page;
const totalPages = newsPagination.pages;
const pageButtons = [];
// 显示当前页及前后各2页
let startPage = Math.max(1, currentPage - 2);
let endPage = Math.min(totalPages, currentPage + 2);
// 如果开始页大于1显示省略号
if (startPage > 1) {
pageButtons.push(
<Text key="start-ellipsis" fontSize="sm" color="gray.400">...</Text>
);
}
for (let i = startPage; i <= endPage; i++) {
pageButtons.push(
<Button
key={i}
size="sm"
variant={i === currentPage ? 'solid' : 'outline'}
colorScheme={i === currentPage ? 'blue' : 'gray'}
onClick={() => handleNewsPageChange(i)}
isDisabled={newsLoading}
>
{i}
</Button>
);
}
// 如果结束页小于总页数,显示省略号
if (endPage < totalPages) {
pageButtons.push(
<Text key="end-ellipsis" fontSize="sm" color="gray.400">...</Text>
);
}
return pageButtons;
})()}
<Button
size="sm"
onClick={() => handleNewsPageChange(newsPagination.page + 1)}
isDisabled={!newsPagination.has_next || newsLoading}
>
下一页
</Button>
<Button
size="sm"
onClick={() => handleNewsPageChange(newsPagination.pages)}
isDisabled={!newsPagination.has_next || newsLoading}
rightIcon={<Icon as={FaChevronRight} />}
>
末页
</Button>
</HStack>
</HStack>
</Box>
)}
</>
) : (
<Center h="400px">
<VStack spacing={3}>
<Icon as={FaNewspaper} boxSize={16} color="gray.300" />
<Text color="gray.500" fontSize="lg" fontWeight="medium">暂无相关新闻</Text>
<Text fontSize="sm" color="gray.400">
{newsSearchQuery ? '尝试修改搜索关键词' : '该公司暂无新闻动态'}
</Text>
</VStack>
</Center>
)}
</VStack>
</CardBody>
</Card>
</VStack>
</TabPanel>
</TabPanels>
</Tabs>
</VStack>
</Container>
{/* 公告详情模态框 */}
<Modal isOpen={isAnnouncementOpen} onClose={onAnnouncementClose} size="xl">
<ModalOverlay />
<ModalContent>
<ModalHeader>
<VStack align="start" spacing={1}>
<Text>{selectedAnnouncement?.title}</Text>
<HStack>
<Badge>{selectedAnnouncement?.info_type}</Badge>
<Text fontSize="sm" color="gray.500">
{formatUtils.formatDate(selectedAnnouncement?.announce_date)}
</Text>
</HStack>
</VStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack align="start" spacing={3}>
<Text fontSize="sm">文件格式{selectedAnnouncement?.format}</Text>
<Text fontSize="sm">文件大小{selectedAnnouncement?.file_size} KB</Text>
</VStack>
</ModalBody>
<ModalFooter>
<Button
colorScheme="blue"
mr={3}
onClick={() => window.open(selectedAnnouncement?.url, '_blank')}
>
查看原文
</Button>
<Button variant="ghost" onClick={onAnnouncementClose}>关闭</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Box>
);
};
export default CompanyAnalysisComplete;