2678 lines
126 KiB
JavaScript
2678 lines
126 KiB
JavaScript
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;
|