refactor(DeepAnalysisTab): 模块化拆分为 21 个 TypeScript 文件
将 1,796 行单文件拆分为原子设计模式结构: **atoms/** - 原子组件 - DisclaimerBox: 免责声明警告框 - ScoreBar: 评分进度条 - BusinessTreeItem: 业务树形项 - KeyFactorCard: 关键因素卡片 **components/** - Card 容器组件 - CorePositioningCard: 核心定位 - CompetitiveAnalysisCard: 竞争地位分析(含雷达图) - BusinessStructureCard: 业务结构 - ValueChainCard: 产业链分析 - KeyFactorsCard: 关键因素 - TimelineCard: 发展时间线 - BusinessSegmentsCard: 业务板块详情 - StrategyAnalysisCard: 战略分析 **organisms/** - 复杂组件 - ValueChainNodeCard: 产业链节点(含 RelatedCompaniesModal) - TimelineComponent: 时间线(含 EventDetailModal) **utils/** - chartOptions.ts: ECharts 图表配置 优化效果:主文件从 1,796 行减少到 117 行(-93%) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 业务结构树形项组件
|
||||
*
|
||||
* 递归显示业务结构层级
|
||||
* 使用位置:业务结构分析卡片
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, HStack, VStack, Text, Badge, Tag, TagLabel } from '@chakra-ui/react';
|
||||
import { formatPercentage, formatBusinessRevenue } from '@utils/priceFormatters';
|
||||
import type { BusinessTreeItemProps } from '../types';
|
||||
|
||||
const BusinessTreeItem: React.FC<BusinessTreeItemProps> = ({ business, depth = 0 }) => {
|
||||
const bgColor = 'gray.50';
|
||||
|
||||
// 获取营收显示
|
||||
const getRevenueDisplay = (): string => {
|
||||
const revenue = business.revenue || business.financial_metrics?.revenue;
|
||||
const unit = business.revenue_unit;
|
||||
if (revenue !== undefined && revenue !== null) {
|
||||
return formatBusinessRevenue(revenue, unit);
|
||||
}
|
||||
return '-';
|
||||
};
|
||||
|
||||
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 &&
|
||||
business.financial_metrics.revenue_ratio > 30 && (
|
||||
<Badge colorScheme="purple" size="sm">
|
||||
核心业务
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack spacing={4} flexWrap="wrap">
|
||||
<Tag size="sm" variant="subtle">
|
||||
营收占比: {formatPercentage(business.financial_metrics?.revenue_ratio)}
|
||||
</Tag>
|
||||
<Tag size="sm" variant="subtle">
|
||||
毛利率: {formatPercentage(business.financial_metrics?.gross_margin)}
|
||||
</Tag>
|
||||
{business.growth_metrics?.revenue_growth !== undefined && (
|
||||
<Tag
|
||||
size="sm"
|
||||
colorScheme={
|
||||
business.growth_metrics.revenue_growth > 0 ? 'red' : 'green'
|
||||
}
|
||||
>
|
||||
<TagLabel>
|
||||
增长: {business.growth_metrics.revenue_growth > 0 ? '+' : ''}
|
||||
{formatPercentage(business.growth_metrics.revenue_growth)}
|
||||
</TagLabel>
|
||||
</Tag>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
<VStack align="end" spacing={0}>
|
||||
<Text fontSize="lg" fontWeight="bold" color="blue.500">
|
||||
{getRevenueDisplay()}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
营业收入
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessTreeItem;
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 免责声明组件
|
||||
*
|
||||
* 显示 AI 分析内容的免责声明警告框
|
||||
* 使用位置:深度分析各 Card 底部(共 6 处)
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Alert, AlertIcon, Box, Text } from '@chakra-ui/react';
|
||||
|
||||
const DisclaimerBox: React.FC = () => {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisclaimerBox;
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 关键因素卡片组件
|
||||
*
|
||||
* 显示单个关键因素的详细信息
|
||||
* 使用位置:关键因素 Accordion 内
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
Tag,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaArrowUp, FaArrowDown } from 'react-icons/fa';
|
||||
import type { KeyFactorCardProps, ImpactDirection } from '../types';
|
||||
|
||||
/**
|
||||
* 获取影响方向对应的颜色
|
||||
*/
|
||||
const getImpactColor = (direction?: ImpactDirection): string => {
|
||||
const colorMap: Record<ImpactDirection, string> = {
|
||||
positive: 'red',
|
||||
negative: 'green',
|
||||
neutral: 'gray',
|
||||
mixed: 'yellow',
|
||||
};
|
||||
return colorMap[direction || 'neutral'] || 'gray';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取影响方向的中文标签
|
||||
*/
|
||||
const getImpactLabel = (direction?: ImpactDirection): string => {
|
||||
const labelMap: Record<ImpactDirection, string> = {
|
||||
positive: '正面',
|
||||
negative: '负面',
|
||||
neutral: '中性',
|
||||
mixed: '混合',
|
||||
};
|
||||
return labelMap[direction || 'neutral'] || '中性';
|
||||
};
|
||||
|
||||
const KeyFactorCard: React.FC<KeyFactorCardProps> = ({ factor }) => {
|
||||
const impactColor = getImpactColor(factor.impact_direction);
|
||||
const bgColor = 'white';
|
||||
const borderColor = 'gray.200';
|
||||
|
||||
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">
|
||||
{getImpactLabel(factor.impact_direction)}
|
||||
</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 !== undefined && (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyFactorCard;
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 评分进度条组件
|
||||
*
|
||||
* 显示带图标的评分进度条
|
||||
* 使用位置:竞争力分析区域(共 8 处)
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, HStack, Text, Badge, Progress, Icon } from '@chakra-ui/react';
|
||||
import type { ScoreBarProps } from '../types';
|
||||
|
||||
/**
|
||||
* 根据分数百分比获取颜色方案
|
||||
*/
|
||||
const getColorScheme = (percentage: number): string => {
|
||||
if (percentage >= 80) return 'purple';
|
||||
if (percentage >= 60) return 'blue';
|
||||
if (percentage >= 40) return 'yellow';
|
||||
return 'orange';
|
||||
};
|
||||
|
||||
const ScoreBar: React.FC<ScoreBarProps> = ({ label, score, icon }) => {
|
||||
const percentage = ((score || 0) / 100) * 100;
|
||||
const colorScheme = getColorScheme(percentage);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<HStack justify="space-between" mb={1}>
|
||||
<HStack>
|
||||
{icon && (
|
||||
<Icon as={icon} boxSize={4} color={`${colorScheme}.500`} />
|
||||
)}
|
||||
<Text fontSize="sm" fontWeight="medium">
|
||||
{label}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Badge colorScheme={colorScheme}>{score || 0}</Badge>
|
||||
</HStack>
|
||||
<Progress
|
||||
value={percentage}
|
||||
size="sm"
|
||||
colorScheme={colorScheme}
|
||||
borderRadius="full"
|
||||
hasStripe
|
||||
isAnimated
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScoreBar;
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 原子组件导出
|
||||
*
|
||||
* DeepAnalysisTab 内部使用的基础 UI 组件
|
||||
*/
|
||||
|
||||
export { default as DisclaimerBox } from './DisclaimerBox';
|
||||
export { default as ScoreBar } from './ScoreBar';
|
||||
export { default as BusinessTreeItem } from './BusinessTreeItem';
|
||||
export { default as KeyFactorCard } from './KeyFactorCard';
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* 业务板块详情卡片
|
||||
*
|
||||
* 显示公司各业务板块的详细信息
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Heading,
|
||||
Badge,
|
||||
Box,
|
||||
Icon,
|
||||
SimpleGrid,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaIndustry, FaExpandAlt, FaCompressAlt } from 'react-icons/fa';
|
||||
import { DisclaimerBox } from '../atoms';
|
||||
import type { BusinessSegment } from '../types';
|
||||
|
||||
interface BusinessSegmentsCardProps {
|
||||
businessSegments: BusinessSegment[];
|
||||
expandedSegments: Record<number, boolean>;
|
||||
onToggleSegment: (index: number) => void;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const BusinessSegmentsCard: React.FC<BusinessSegmentsCardProps> = ({
|
||||
businessSegments,
|
||||
expandedSegments,
|
||||
onToggleSegment,
|
||||
cardBg,
|
||||
}) => {
|
||||
if (!businessSegments || businessSegments.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} shadow="md">
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FaIndustry} color="indigo.500" />
|
||||
<Heading size="sm">业务板块详情</Heading>
|
||||
<Badge>{businessSegments.length} 个板块</Badge>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DisclaimerBox />
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
||||
{businessSegments.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={() => onToggleSegment(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 !== undefined && (
|
||||
<Box>
|
||||
<Text fontSize="xs" color="gray.500" mb={1}>
|
||||
市场份额
|
||||
</Text>
|
||||
<Badge colorScheme="purple" fontSize="sm">
|
||||
{segment.market_share}%
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isExpanded && segment.revenue_contribution !== undefined && (
|
||||
<Box>
|
||||
<Text fontSize="xs" color="gray.500" mb={1}>
|
||||
营收贡献
|
||||
</Text>
|
||||
<Badge colorScheme="orange" fontSize="sm">
|
||||
{segment.revenue_contribution}%
|
||||
</Badge>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessSegmentsCard;
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 业务结构分析卡片
|
||||
*
|
||||
* 显示公司业务结构树形图
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
VStack,
|
||||
HStack,
|
||||
Heading,
|
||||
Badge,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaChartPie } from 'react-icons/fa';
|
||||
import { DisclaimerBox, BusinessTreeItem } from '../atoms';
|
||||
import type { BusinessStructure } from '../types';
|
||||
|
||||
interface BusinessStructureCardProps {
|
||||
businessStructure: BusinessStructure[];
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const BusinessStructureCard: React.FC<BusinessStructureCardProps> = ({
|
||||
businessStructure,
|
||||
cardBg,
|
||||
}) => {
|
||||
if (!businessStructure || businessStructure.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} shadow="md">
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FaChartPie} color="purple.500" />
|
||||
<Heading size="sm">业务结构分析</Heading>
|
||||
<Badge>{businessStructure[0]?.report_period}</Badge>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DisclaimerBox />
|
||||
<VStack spacing={3} align="stretch">
|
||||
{businessStructure.map((business, idx) => (
|
||||
<BusinessTreeItem
|
||||
key={idx}
|
||||
business={business}
|
||||
depth={business.business_level - 1}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessStructureCard;
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* 竞争地位分析卡片
|
||||
*
|
||||
* 显示竞争力评分、雷达图和竞争分析
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Heading,
|
||||
Badge,
|
||||
Tag,
|
||||
TagLabel,
|
||||
Grid,
|
||||
GridItem,
|
||||
Box,
|
||||
Icon,
|
||||
Divider,
|
||||
SimpleGrid,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
FaTrophy,
|
||||
FaCog,
|
||||
FaStar,
|
||||
FaChartLine,
|
||||
FaDollarSign,
|
||||
FaFlask,
|
||||
FaShieldAlt,
|
||||
FaRocket,
|
||||
FaUsers,
|
||||
} from 'react-icons/fa';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { DisclaimerBox, ScoreBar } from '../atoms';
|
||||
import { getRadarChartOption } from '../utils/chartOptions';
|
||||
import type { ComprehensiveData } from '../types';
|
||||
|
||||
interface CompetitiveAnalysisCardProps {
|
||||
comprehensiveData: ComprehensiveData;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({
|
||||
comprehensiveData,
|
||||
cardBg,
|
||||
}) => {
|
||||
const competitivePosition = comprehensiveData.competitive_position;
|
||||
if (!competitivePosition) return null;
|
||||
|
||||
const radarOption = getRadarChartOption(comprehensiveData);
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} shadow="md">
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FaTrophy} color="gold" />
|
||||
<Heading size="sm">竞争地位分析</Heading>
|
||||
{competitivePosition.ranking && (
|
||||
<Badge colorScheme="purple" ml={2}>
|
||||
行业排名 {competitivePosition.ranking.industry_rank}/
|
||||
{competitivePosition.ranking.total_companies}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DisclaimerBox />
|
||||
|
||||
{/* 主要竞争对手 */}
|
||||
{competitivePosition.analysis?.main_competitors && (
|
||||
<Box mb={4}>
|
||||
<Text fontWeight="bold" fontSize="sm" mb={2} color="gray.600">
|
||||
主要竞争对手
|
||||
</Text>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{competitivePosition.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={competitivePosition.scores?.market_position}
|
||||
icon={FaTrophy}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="技术实力"
|
||||
score={competitivePosition.scores?.technology}
|
||||
icon={FaCog}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="品牌价值"
|
||||
score={competitivePosition.scores?.brand}
|
||||
icon={FaStar}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="运营效率"
|
||||
score={competitivePosition.scores?.operation}
|
||||
icon={FaChartLine}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="财务健康"
|
||||
score={competitivePosition.scores?.finance}
|
||||
icon={FaDollarSign}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="创新能力"
|
||||
score={competitivePosition.scores?.innovation}
|
||||
icon={FaFlask}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="风险控制"
|
||||
score={competitivePosition.scores?.risk}
|
||||
icon={FaShieldAlt}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="成长潜力"
|
||||
score={competitivePosition.scores?.growth}
|
||||
icon={FaRocket}
|
||||
/>
|
||||
</VStack>
|
||||
</GridItem>
|
||||
|
||||
<GridItem colSpan={{ base: 2, lg: 1 }}>
|
||||
{radarOption && (
|
||||
<ReactECharts
|
||||
option={radarOption}
|
||||
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">
|
||||
{competitivePosition.analysis?.competitive_advantages || '暂无数据'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontWeight="bold" fontSize="sm" mb={2} color="green.600">
|
||||
竞争劣势
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
{competitivePosition.analysis?.competitive_disadvantages || '暂无数据'}
|
||||
</Text>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompetitiveAnalysisCard;
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 核心定位卡片
|
||||
*
|
||||
* 显示公司的核心定位、投资亮点和商业模式
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Heading,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Grid,
|
||||
GridItem,
|
||||
Box,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaLightbulb } from 'react-icons/fa';
|
||||
import { DisclaimerBox } from '../atoms';
|
||||
import type { QualitativeAnalysis } from '../types';
|
||||
|
||||
interface CorePositioningCardProps {
|
||||
qualitativeAnalysis: QualitativeAnalysis;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const CorePositioningCard: React.FC<CorePositioningCardProps> = ({
|
||||
qualitativeAnalysis,
|
||||
cardBg,
|
||||
}) => {
|
||||
const blueBg = 'blue.50';
|
||||
const greenBg = 'green.50';
|
||||
|
||||
return (
|
||||
<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">
|
||||
{qualitativeAnalysis.core_positioning?.one_line_intro && (
|
||||
<Alert status="info" variant="left-accent">
|
||||
<AlertIcon />
|
||||
<Text fontWeight="bold">
|
||||
{qualitativeAnalysis.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={blueBg} borderRadius="md">
|
||||
<Text fontSize="sm" whiteSpace="pre-wrap">
|
||||
{qualitativeAnalysis.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={greenBg} borderRadius="md">
|
||||
<Text fontSize="sm" whiteSpace="pre-wrap">
|
||||
{qualitativeAnalysis.core_positioning?.business_model_desc ||
|
||||
'暂无数据'}
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CorePositioningCard;
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 关键因素卡片
|
||||
*
|
||||
* 显示影响公司的关键因素列表
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Heading,
|
||||
Badge,
|
||||
Box,
|
||||
Icon,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionIcon,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaBalanceScale } from 'react-icons/fa';
|
||||
import { DisclaimerBox, KeyFactorCard } from '../atoms';
|
||||
import type { KeyFactors } from '../types';
|
||||
|
||||
interface KeyFactorsCardProps {
|
||||
keyFactors: KeyFactors;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const KeyFactorsCard: React.FC<KeyFactorsCardProps> = ({
|
||||
keyFactors,
|
||||
cardBg,
|
||||
}) => {
|
||||
return (
|
||||
<Card bg={cardBg} shadow="md" h="full">
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FaBalanceScale} color="orange.500" />
|
||||
<Heading size="sm">关键因素</Heading>
|
||||
<Badge>{keyFactors.total_factors} 项</Badge>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DisclaimerBox />
|
||||
<Accordion allowMultiple>
|
||||
{keyFactors.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>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyFactorsCard;
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 战略分析卡片
|
||||
*
|
||||
* 显示公司战略方向和战略举措
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Heading,
|
||||
Box,
|
||||
Icon,
|
||||
Grid,
|
||||
GridItem,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaRocket } from 'react-icons/fa';
|
||||
import { DisclaimerBox } from '../atoms';
|
||||
import type { Strategy } from '../types';
|
||||
|
||||
interface StrategyAnalysisCardProps {
|
||||
strategy: Strategy;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const StrategyAnalysisCard: React.FC<StrategyAnalysisCardProps> = ({
|
||||
strategy,
|
||||
cardBg,
|
||||
}) => {
|
||||
const purpleBg = 'purple.50';
|
||||
const orangeBg = 'orange.50';
|
||||
|
||||
return (
|
||||
<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={purpleBg} borderRadius="md">
|
||||
<Text fontSize="sm">
|
||||
{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={orangeBg} borderRadius="md">
|
||||
<Text fontSize="sm">
|
||||
{strategy.strategic_initiatives || '暂无数据'}
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyAnalysisCard;
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 发展时间线卡片
|
||||
*
|
||||
* 显示公司发展历程时间线
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
HStack,
|
||||
Heading,
|
||||
Badge,
|
||||
Box,
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaHistory } from 'react-icons/fa';
|
||||
import { DisclaimerBox } from '../atoms';
|
||||
import TimelineComponent from '../organisms/TimelineComponent';
|
||||
import type { DevelopmentTimeline } from '../types';
|
||||
|
||||
interface TimelineCardProps {
|
||||
developmentTimeline: DevelopmentTimeline;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const TimelineCard: React.FC<TimelineCardProps> = ({
|
||||
developmentTimeline,
|
||||
cardBg,
|
||||
}) => {
|
||||
return (
|
||||
<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">
|
||||
正面 {developmentTimeline.statistics?.positive_events || 0}
|
||||
</Badge>
|
||||
<Badge colorScheme="green">
|
||||
负面 {developmentTimeline.statistics?.negative_events || 0}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DisclaimerBox />
|
||||
<Box maxH="600px" overflowY="auto" pr={2}>
|
||||
<TimelineComponent events={developmentTimeline.events} />
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineCard;
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* 产业链分析卡片
|
||||
*
|
||||
* 显示产业链层级视图和流向关系
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Heading,
|
||||
Badge,
|
||||
Box,
|
||||
Icon,
|
||||
SimpleGrid,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Center,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaNetworkWired } from 'react-icons/fa';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { DisclaimerBox } from '../atoms';
|
||||
import ValueChainNodeCard from '../organisms/ValueChainNodeCard';
|
||||
import { getSankeyChartOption } from '../utils/chartOptions';
|
||||
import type { ValueChainData } from '../types';
|
||||
|
||||
interface ValueChainCardProps {
|
||||
valueChainData: ValueChainData;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const ValueChainCard: React.FC<ValueChainCardProps> = ({
|
||||
valueChainData,
|
||||
cardBg,
|
||||
}) => {
|
||||
const sankeyOption = getSankeyChartOption(valueChainData);
|
||||
const nodesByLevel = valueChainData.value_chain_structure?.nodes_by_level;
|
||||
|
||||
// 获取上游节点
|
||||
const upstreamNodes = [
|
||||
...(nodesByLevel?.['level_-2'] || []),
|
||||
...(nodesByLevel?.['level_-1'] || []),
|
||||
];
|
||||
|
||||
// 获取核心节点
|
||||
const coreNodes = nodesByLevel?.['level_0'] || [];
|
||||
|
||||
// 获取下游节点
|
||||
const downstreamNodes = [
|
||||
...(nodesByLevel?.['level_1'] || []),
|
||||
...(nodesByLevel?.['level_2'] || []),
|
||||
];
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* 上游供应链 */}
|
||||
{upstreamNodes.length > 0 && (
|
||||
<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}>
|
||||
{upstreamNodes.map((node, idx) => (
|
||||
<ValueChainNodeCard
|
||||
key={idx}
|
||||
node={node}
|
||||
level={node.node_level}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 核心企业 */}
|
||||
{coreNodes.length > 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}>
|
||||
{coreNodes.map((node, idx) => (
|
||||
<ValueChainNodeCard
|
||||
key={idx}
|
||||
node={node}
|
||||
isCompany={node.node_type === 'company'}
|
||||
level={0}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 下游客户 */}
|
||||
{downstreamNodes.length > 0 && (
|
||||
<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}>
|
||||
{downstreamNodes.map((node, idx) => (
|
||||
<ValueChainNodeCard
|
||||
key={idx}
|
||||
node={node}
|
||||
level={node.node_level}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
|
||||
{/* 流向关系 */}
|
||||
<TabPanel>
|
||||
{sankeyOption ? (
|
||||
<ReactECharts
|
||||
option={sankeyOption}
|
||||
style={{ height: '500px' }}
|
||||
theme="light"
|
||||
/>
|
||||
) : (
|
||||
<Center h="200px">
|
||||
<Text color="gray.500">暂无流向数据</Text>
|
||||
</Center>
|
||||
)}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ValueChainCard;
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Card 子组件导出
|
||||
*
|
||||
* DeepAnalysisTab 的各个区块组件
|
||||
*/
|
||||
|
||||
export { default as CorePositioningCard } from './CorePositioningCard';
|
||||
export { default as CompetitiveAnalysisCard } from './CompetitiveAnalysisCard';
|
||||
export { default as BusinessStructureCard } from './BusinessStructureCard';
|
||||
export { default as ValueChainCard } from './ValueChainCard';
|
||||
export { default as KeyFactorsCard } from './KeyFactorsCard';
|
||||
export { default as TimelineCard } from './TimelineCard';
|
||||
export { default as BusinessSegmentsCard } from './BusinessSegmentsCard';
|
||||
export { default as StrategyAnalysisCard } from './StrategyAnalysisCard';
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 深度分析 Tab 主组件
|
||||
*
|
||||
* 组合所有子组件,显示公司深度分析内容
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { VStack, Center, Text, Spinner, Grid, GridItem } from '@chakra-ui/react';
|
||||
import {
|
||||
CorePositioningCard,
|
||||
CompetitiveAnalysisCard,
|
||||
BusinessStructureCard,
|
||||
ValueChainCard,
|
||||
KeyFactorsCard,
|
||||
TimelineCard,
|
||||
BusinessSegmentsCard,
|
||||
StrategyAnalysisCard,
|
||||
} from './components';
|
||||
import type { DeepAnalysisTabProps } from './types';
|
||||
|
||||
const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
|
||||
comprehensiveData,
|
||||
valueChainData,
|
||||
keyFactorsData,
|
||||
loading,
|
||||
cardBg,
|
||||
expandedSegments,
|
||||
onToggleSegment,
|
||||
}) => {
|
||||
// 加载状态
|
||||
if (loading) {
|
||||
return (
|
||||
<Center h="200px">
|
||||
<VStack spacing={4}>
|
||||
<Spinner size="xl" color="blue.500" />
|
||||
<Text>加载深度分析数据...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 核心定位卡片 */}
|
||||
{comprehensiveData?.qualitative_analysis && (
|
||||
<CorePositioningCard
|
||||
qualitativeAnalysis={comprehensiveData.qualitative_analysis}
|
||||
cardBg={cardBg}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 竞争地位分析 */}
|
||||
{comprehensiveData?.competitive_position && (
|
||||
<CompetitiveAnalysisCard
|
||||
comprehensiveData={comprehensiveData}
|
||||
cardBg={cardBg}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 业务结构分析 */}
|
||||
{comprehensiveData?.business_structure &&
|
||||
comprehensiveData.business_structure.length > 0 && (
|
||||
<BusinessStructureCard
|
||||
businessStructure={comprehensiveData.business_structure}
|
||||
cardBg={cardBg}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 产业链分析 */}
|
||||
{valueChainData && (
|
||||
<ValueChainCard valueChainData={valueChainData} cardBg={cardBg} />
|
||||
)}
|
||||
|
||||
{/* 关键因素与发展时间线 */}
|
||||
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
|
||||
<GridItem colSpan={{ base: 2, lg: 1 }}>
|
||||
{keyFactorsData?.key_factors && (
|
||||
<KeyFactorsCard
|
||||
keyFactors={keyFactorsData.key_factors}
|
||||
cardBg={cardBg}
|
||||
/>
|
||||
)}
|
||||
</GridItem>
|
||||
|
||||
<GridItem colSpan={{ base: 2, lg: 1 }}>
|
||||
{keyFactorsData?.development_timeline && (
|
||||
<TimelineCard
|
||||
developmentTimeline={keyFactorsData.development_timeline}
|
||||
cardBg={cardBg}
|
||||
/>
|
||||
)}
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
{/* 业务板块详情 */}
|
||||
{comprehensiveData?.business_segments &&
|
||||
comprehensiveData.business_segments.length > 0 && (
|
||||
<BusinessSegmentsCard
|
||||
businessSegments={comprehensiveData.business_segments}
|
||||
expandedSegments={expandedSegments}
|
||||
onToggleSegment={onToggleSegment}
|
||||
cardBg={cardBg}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 战略分析 */}
|
||||
{comprehensiveData?.qualitative_analysis?.strategy && (
|
||||
<StrategyAnalysisCard
|
||||
strategy={comprehensiveData.qualitative_analysis.strategy}
|
||||
cardBg={cardBg}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeepAnalysisTab;
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 事件详情模态框组件
|
||||
*
|
||||
* 显示时间线事件的详细信息
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
Box,
|
||||
Progress,
|
||||
Icon,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaCheckCircle, FaExclamationCircle } from 'react-icons/fa';
|
||||
import type { TimelineEvent } from '../../types';
|
||||
|
||||
interface EventDetailModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
event: TimelineEvent | null;
|
||||
}
|
||||
|
||||
const EventDetailModal: React.FC<EventDetailModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
event,
|
||||
}) => {
|
||||
if (!event) return null;
|
||||
|
||||
const isPositive = event.impact_metrics?.is_positive;
|
||||
const impactScore = event.impact_metrics?.impact_score || 0;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<HStack>
|
||||
<Icon
|
||||
as={isPositive ? FaCheckCircle : FaExclamationCircle}
|
||||
color={isPositive ? 'red.500' : 'green.500'}
|
||||
boxSize={6}
|
||||
/>
|
||||
<VStack align="start" spacing={0}>
|
||||
<Text>{event.event_title}</Text>
|
||||
<HStack>
|
||||
<Badge colorScheme={isPositive ? 'red' : 'green'}>
|
||||
{event.event_type}
|
||||
</Badge>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
{event.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">
|
||||
{event.event_desc}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{event.related_info?.financial_impact && (
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2} color="gray.600">
|
||||
财务影响
|
||||
</Text>
|
||||
<Text fontSize="sm" lineHeight="1.6" color="blue.600">
|
||||
{event.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={impactScore}
|
||||
size="lg"
|
||||
width="120px"
|
||||
colorScheme={impactScore > 70 ? 'red' : 'orange'}
|
||||
hasStripe
|
||||
isAnimated
|
||||
/>
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
{impactScore}/100
|
||||
</Text>
|
||||
</VStack>
|
||||
<VStack>
|
||||
<Badge
|
||||
size="lg"
|
||||
colorScheme={isPositive ? 'red' : 'green'}
|
||||
px={3}
|
||||
py={1}
|
||||
>
|
||||
{isPositive ? '正面影响' : '负面影响'}
|
||||
</Badge>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme="blue" onClick={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventDetailModal;
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 时间线组件
|
||||
*
|
||||
* 显示公司发展事件时间线
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
Card,
|
||||
CardBody,
|
||||
Icon,
|
||||
Progress,
|
||||
Circle,
|
||||
Fade,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
FaCalendarAlt,
|
||||
FaArrowUp,
|
||||
FaArrowDown,
|
||||
} from 'react-icons/fa';
|
||||
import EventDetailModal from './EventDetailModal';
|
||||
import type { TimelineComponentProps, TimelineEvent } from '../../types';
|
||||
|
||||
const TimelineComponent: React.FC<TimelineComponentProps> = ({ events }) => {
|
||||
const [selectedEvent, setSelectedEvent] = useState<TimelineEvent | null>(null);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
// 背景颜色
|
||||
const positiveBgColor = 'red.50';
|
||||
const negativeBgColor = 'green.50';
|
||||
|
||||
const handleEventClick = (event: TimelineEvent) => {
|
||||
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 = isPositive ? positiveBgColor : negativeBgColor;
|
||||
|
||||
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 || 0) > 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>
|
||||
|
||||
<EventDetailModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
event={selectedEvent}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimelineComponent;
|
||||
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* 相关公司模态框组件
|
||||
*
|
||||
* 显示产业链节点的相关上市公司列表
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
Card,
|
||||
CardBody,
|
||||
Icon,
|
||||
IconButton,
|
||||
Center,
|
||||
Spinner,
|
||||
Divider,
|
||||
SimpleGrid,
|
||||
Box,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
Progress,
|
||||
Tooltip,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
FaBuilding,
|
||||
FaHandshake,
|
||||
FaUserTie,
|
||||
FaIndustry,
|
||||
FaCog,
|
||||
FaNetworkWired,
|
||||
FaFlask,
|
||||
FaStar,
|
||||
FaArrowRight,
|
||||
FaArrowLeft,
|
||||
} from 'react-icons/fa';
|
||||
import type { ValueChainNode, RelatedCompany } from '../../types';
|
||||
|
||||
interface RelatedCompaniesModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
node: ValueChainNode;
|
||||
isCompany: boolean;
|
||||
colorScheme: string;
|
||||
relatedCompanies: RelatedCompany[];
|
||||
loadingRelated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点类型对应的图标
|
||||
*/
|
||||
const getNodeTypeIcon = (type: string) => {
|
||||
const icons: Record<string, React.ComponentType> = {
|
||||
company: FaBuilding,
|
||||
supplier: FaHandshake,
|
||||
customer: FaUserTie,
|
||||
product: FaIndustry,
|
||||
service: FaCog,
|
||||
channel: FaNetworkWired,
|
||||
raw_material: FaFlask,
|
||||
};
|
||||
return icons[type] || FaBuilding;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取重要度对应的颜色
|
||||
*/
|
||||
const getImportanceColor = (score?: number): string => {
|
||||
if (!score) return 'green';
|
||||
if (score >= 80) return 'red';
|
||||
if (score >= 60) return 'orange';
|
||||
if (score >= 40) return 'yellow';
|
||||
return 'green';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取层级标签
|
||||
*/
|
||||
const getLevelLabel = (level?: number): { text: string; color: string } => {
|
||||
if (level === undefined) return { text: '未知', color: 'gray' };
|
||||
if (level < 0) return { text: '上游', color: 'orange' };
|
||||
if (level === 0) return { text: '核心', color: 'blue' };
|
||||
return { text: '下游', color: 'green' };
|
||||
};
|
||||
|
||||
const RelatedCompaniesModal: React.FC<RelatedCompaniesModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
node,
|
||||
isCompany,
|
||||
colorScheme,
|
||||
relatedCompanies,
|
||||
loadingRelated,
|
||||
}) => {
|
||||
return (
|
||||
<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 !== undefined && (
|
||||
<Stat>
|
||||
<StatLabel fontSize="xs">市场份额</StatLabel>
|
||||
<StatNumber fontSize="lg">{node.market_share}%</StatNumber>
|
||||
</Stat>
|
||||
)}
|
||||
|
||||
{node.dependency_degree !== undefined && (
|
||||
<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 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>
|
||||
</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.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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelatedCompaniesModal;
|
||||
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* 产业链节点卡片组件
|
||||
*
|
||||
* 显示产业链中的单个节点,点击可展开查看相关公司
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
Icon,
|
||||
Progress,
|
||||
Box,
|
||||
Tooltip,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
ScaleFade,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
FaBuilding,
|
||||
FaHandshake,
|
||||
FaUserTie,
|
||||
FaIndustry,
|
||||
FaCog,
|
||||
FaNetworkWired,
|
||||
FaFlask,
|
||||
FaStar,
|
||||
} from 'react-icons/fa';
|
||||
import { logger } from '@utils/logger';
|
||||
import { getApiBase } from '@utils/apiConfig';
|
||||
import RelatedCompaniesModal from './RelatedCompaniesModal';
|
||||
import type { ValueChainNodeCardProps, RelatedCompany } from '../../types';
|
||||
|
||||
const API_BASE_URL = getApiBase();
|
||||
|
||||
/**
|
||||
* 获取节点类型对应的图标
|
||||
*/
|
||||
const getNodeTypeIcon = (type: string) => {
|
||||
const icons: Record<string, React.ComponentType> = {
|
||||
company: FaBuilding,
|
||||
supplier: FaHandshake,
|
||||
customer: FaUserTie,
|
||||
product: FaIndustry,
|
||||
service: FaCog,
|
||||
channel: FaNetworkWired,
|
||||
raw_material: FaFlask,
|
||||
};
|
||||
return icons[type] || FaBuilding;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取重要度对应的颜色
|
||||
*/
|
||||
const getImportanceColor = (score?: number): string => {
|
||||
if (!score) return 'green';
|
||||
if (score >= 80) return 'red';
|
||||
if (score >= 60) return 'orange';
|
||||
if (score >= 40) return 'yellow';
|
||||
return 'green';
|
||||
};
|
||||
|
||||
const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
||||
node,
|
||||
isCompany = false,
|
||||
level = 0,
|
||||
}) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [relatedCompanies, setRelatedCompanies] = useState<RelatedCompany[]>([]);
|
||||
const [loadingRelated, setLoadingRelated] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
// 根据层级和是否为核心企业确定颜色方案
|
||||
const getColorScheme = (): string => {
|
||||
if (isCompany) return 'blue';
|
||||
if (level < 0) return 'orange';
|
||||
if (level > 0) return 'green';
|
||||
return 'gray';
|
||||
};
|
||||
|
||||
const colorScheme = getColorScheme();
|
||||
const bgColor = `${colorScheme}.50`;
|
||||
const borderColor = `${colorScheme}.200`;
|
||||
|
||||
// 获取相关公司数据
|
||||
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 instanceof Error ? 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 !== undefined &&
|
||||
node.importance_score >= 70 && (
|
||||
<Tooltip label="重要节点">
|
||||
<span>
|
||||
<Icon as={FaStar} color="orange.400" boxSize={4} />
|
||||
</span>
|
||||
</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 !== undefined && (
|
||||
<Badge variant="outline" size="sm">
|
||||
份额 {node.market_share}%
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{node.importance_score !== undefined && (
|
||||
<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>
|
||||
|
||||
<RelatedCompaniesModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
node={node}
|
||||
isCompany={isCompany}
|
||||
colorScheme={colorScheme}
|
||||
relatedCompanies={relatedCompanies}
|
||||
loadingRelated={loadingRelated}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ValueChainNodeCard;
|
||||
@@ -0,0 +1,326 @@
|
||||
/**
|
||||
* DeepAnalysisTab 组件类型定义
|
||||
*
|
||||
* 深度分析 Tab 所需的所有数据接口类型
|
||||
*/
|
||||
|
||||
// ==================== 格式化工具类型 ====================
|
||||
|
||||
export interface FormatUtils {
|
||||
formatCurrency: (value: number | null | undefined) => string;
|
||||
formatBusinessRevenue: (value: number | null | undefined, unit?: string) => string;
|
||||
formatPercentage: (value: number | null | undefined) => string;
|
||||
}
|
||||
|
||||
// ==================== 竞争力评分类型 ====================
|
||||
|
||||
export interface CompetitiveScores {
|
||||
market_position?: number;
|
||||
technology?: number;
|
||||
brand?: number;
|
||||
operation?: number;
|
||||
finance?: number;
|
||||
innovation?: number;
|
||||
risk?: number;
|
||||
growth?: number;
|
||||
}
|
||||
|
||||
export interface CompetitiveRanking {
|
||||
industry_rank: number;
|
||||
total_companies: number;
|
||||
}
|
||||
|
||||
export interface CompetitiveAnalysis {
|
||||
main_competitors?: string;
|
||||
competitive_advantages?: string;
|
||||
competitive_disadvantages?: string;
|
||||
}
|
||||
|
||||
export interface CompetitivePosition {
|
||||
scores?: CompetitiveScores;
|
||||
ranking?: CompetitiveRanking;
|
||||
analysis?: CompetitiveAnalysis;
|
||||
}
|
||||
|
||||
// ==================== 核心定位类型 ====================
|
||||
|
||||
export interface CorePositioning {
|
||||
one_line_intro?: string;
|
||||
investment_highlights?: string;
|
||||
business_model_desc?: string;
|
||||
}
|
||||
|
||||
export interface Strategy {
|
||||
strategy_description?: string;
|
||||
strategic_initiatives?: string;
|
||||
}
|
||||
|
||||
export interface QualitativeAnalysis {
|
||||
core_positioning?: CorePositioning;
|
||||
strategy?: Strategy;
|
||||
}
|
||||
|
||||
// ==================== 业务结构类型 ====================
|
||||
|
||||
export interface FinancialMetrics {
|
||||
revenue?: number;
|
||||
revenue_ratio?: number;
|
||||
gross_margin?: number;
|
||||
}
|
||||
|
||||
export interface GrowthMetrics {
|
||||
revenue_growth?: number;
|
||||
}
|
||||
|
||||
export interface BusinessStructure {
|
||||
business_name: string;
|
||||
business_level: number;
|
||||
revenue?: number;
|
||||
revenue_unit?: string;
|
||||
financial_metrics?: FinancialMetrics;
|
||||
growth_metrics?: GrowthMetrics;
|
||||
report_period?: string;
|
||||
}
|
||||
|
||||
// ==================== 业务板块类型 ====================
|
||||
|
||||
export interface BusinessSegment {
|
||||
segment_name: string;
|
||||
segment_description?: string;
|
||||
competitive_position?: string;
|
||||
future_potential?: string;
|
||||
key_products?: string;
|
||||
market_share?: number;
|
||||
revenue_contribution?: number;
|
||||
}
|
||||
|
||||
// ==================== 综合数据类型 ====================
|
||||
|
||||
export interface ComprehensiveData {
|
||||
qualitative_analysis?: QualitativeAnalysis;
|
||||
competitive_position?: CompetitivePosition;
|
||||
business_structure?: BusinessStructure[];
|
||||
business_segments?: BusinessSegment[];
|
||||
}
|
||||
|
||||
// ==================== 产业链类型 ====================
|
||||
|
||||
export interface ValueChainNode {
|
||||
node_name: string;
|
||||
node_type: string;
|
||||
node_description?: string;
|
||||
node_level?: number;
|
||||
importance_score?: number;
|
||||
market_share?: number;
|
||||
dependency_degree?: number;
|
||||
}
|
||||
|
||||
export interface ValueChainFlow {
|
||||
source?: { node_name: string };
|
||||
target?: { node_name: string };
|
||||
flow_metrics?: {
|
||||
flow_ratio?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NodesByLevel {
|
||||
[key: string]: ValueChainNode[];
|
||||
}
|
||||
|
||||
export interface ValueChainStructure {
|
||||
nodes_by_level?: NodesByLevel;
|
||||
}
|
||||
|
||||
export interface AnalysisSummary {
|
||||
upstream_nodes?: number;
|
||||
company_nodes?: number;
|
||||
downstream_nodes?: number;
|
||||
}
|
||||
|
||||
export interface ValueChainData {
|
||||
value_chain_flows?: ValueChainFlow[];
|
||||
value_chain_structure?: ValueChainStructure;
|
||||
analysis_summary?: AnalysisSummary;
|
||||
}
|
||||
|
||||
// ==================== 相关公司类型 ====================
|
||||
|
||||
export interface RelatedCompanyRelationship {
|
||||
role: 'source' | 'target';
|
||||
connected_node: string;
|
||||
}
|
||||
|
||||
export interface RelatedCompanyNodeInfo {
|
||||
node_level?: number;
|
||||
node_description?: string;
|
||||
}
|
||||
|
||||
export interface RelatedCompany {
|
||||
stock_code: string;
|
||||
stock_name: string;
|
||||
company_name?: string;
|
||||
node_info?: RelatedCompanyNodeInfo;
|
||||
relationships?: RelatedCompanyRelationship[];
|
||||
}
|
||||
|
||||
// ==================== 关键因素类型 ====================
|
||||
|
||||
export type ImpactDirection = 'positive' | 'negative' | 'neutral' | 'mixed';
|
||||
|
||||
export interface KeyFactor {
|
||||
factor_name: string;
|
||||
factor_value: string | number;
|
||||
factor_unit?: string;
|
||||
factor_desc?: string;
|
||||
impact_direction?: ImpactDirection;
|
||||
impact_weight?: number;
|
||||
year_on_year?: number;
|
||||
report_period?: string;
|
||||
}
|
||||
|
||||
export interface FactorCategory {
|
||||
category_name: string;
|
||||
factors: KeyFactor[];
|
||||
}
|
||||
|
||||
export interface KeyFactors {
|
||||
total_factors?: number;
|
||||
categories: FactorCategory[];
|
||||
}
|
||||
|
||||
// ==================== 时间线事件类型 ====================
|
||||
|
||||
export interface ImpactMetrics {
|
||||
is_positive?: boolean;
|
||||
impact_score?: number;
|
||||
}
|
||||
|
||||
export interface RelatedInfo {
|
||||
financial_impact?: string;
|
||||
}
|
||||
|
||||
export interface TimelineEvent {
|
||||
event_title: string;
|
||||
event_date: string;
|
||||
event_type: string;
|
||||
event_desc: string;
|
||||
impact_metrics?: ImpactMetrics;
|
||||
related_info?: RelatedInfo;
|
||||
}
|
||||
|
||||
export interface TimelineStatistics {
|
||||
positive_events?: number;
|
||||
negative_events?: number;
|
||||
}
|
||||
|
||||
export interface DevelopmentTimeline {
|
||||
events: TimelineEvent[];
|
||||
statistics?: TimelineStatistics;
|
||||
}
|
||||
|
||||
// ==================== 关键因素数据类型 ====================
|
||||
|
||||
export interface KeyFactorsData {
|
||||
key_factors?: KeyFactors;
|
||||
development_timeline?: DevelopmentTimeline;
|
||||
}
|
||||
|
||||
// ==================== 主组件 Props 类型 ====================
|
||||
|
||||
export interface DeepAnalysisTabProps {
|
||||
comprehensiveData?: ComprehensiveData;
|
||||
valueChainData?: ValueChainData;
|
||||
keyFactorsData?: KeyFactorsData;
|
||||
loading?: boolean;
|
||||
cardBg?: string;
|
||||
expandedSegments: Record<number, boolean>;
|
||||
onToggleSegment: (index: number) => void;
|
||||
}
|
||||
|
||||
// ==================== 子组件 Props 类型 ====================
|
||||
|
||||
export interface DisclaimerBoxProps {
|
||||
// 无需 props
|
||||
}
|
||||
|
||||
export interface ScoreBarProps {
|
||||
label: string;
|
||||
score?: number;
|
||||
icon?: React.ComponentType;
|
||||
}
|
||||
|
||||
export interface BusinessTreeItemProps {
|
||||
business: BusinessStructure;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
export interface KeyFactorCardProps {
|
||||
factor: KeyFactor;
|
||||
}
|
||||
|
||||
export interface ValueChainNodeCardProps {
|
||||
node: ValueChainNode;
|
||||
isCompany?: boolean;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export interface TimelineComponentProps {
|
||||
events: TimelineEvent[];
|
||||
}
|
||||
|
||||
// ==================== 图表配置类型 ====================
|
||||
|
||||
export interface RadarIndicator {
|
||||
name: string;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface RadarChartOption {
|
||||
tooltip: { trigger: string };
|
||||
radar: {
|
||||
indicator: RadarIndicator[];
|
||||
shape: string;
|
||||
splitNumber: number;
|
||||
name: { textStyle: { color: string; fontSize: number } };
|
||||
splitLine: { lineStyle: { color: string[] } };
|
||||
splitArea: { show: boolean; areaStyle: { color: string[] } };
|
||||
axisLine: { lineStyle: { color: string } };
|
||||
};
|
||||
series: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
data: Array<{
|
||||
value: number[];
|
||||
name: string;
|
||||
symbol: string;
|
||||
symbolSize: number;
|
||||
lineStyle: { width: number; color: string };
|
||||
areaStyle: { color: string };
|
||||
label: { show: boolean; formatter: (params: { value: number }) => number; color: string; fontSize: number };
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface SankeyNode {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SankeyLink {
|
||||
source: string;
|
||||
target: string;
|
||||
value: number;
|
||||
lineStyle: { color: string; opacity: number };
|
||||
}
|
||||
|
||||
export interface SankeyChartOption {
|
||||
tooltip: { trigger: string; triggerOn: string };
|
||||
series: Array<{
|
||||
type: string;
|
||||
layout: string;
|
||||
emphasis: { focus: string };
|
||||
data: SankeyNode[];
|
||||
links: SankeyLink[];
|
||||
lineStyle: { color: string; curveness: number };
|
||||
label: { color: string; fontSize: number };
|
||||
}>;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* DeepAnalysisTab 图表配置工具
|
||||
*
|
||||
* 生成雷达图和桑基图的 ECharts 配置
|
||||
*/
|
||||
|
||||
import type {
|
||||
ComprehensiveData,
|
||||
ValueChainData,
|
||||
RadarChartOption,
|
||||
SankeyChartOption,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* 生成竞争力雷达图配置
|
||||
* @param comprehensiveData - 综合分析数据
|
||||
* @returns ECharts 雷达图配置,或 null(数据不足时)
|
||||
*/
|
||||
export const getRadarChartOption = (
|
||||
comprehensiveData?: ComprehensiveData
|
||||
): RadarChartOption | null => {
|
||||
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: { value: number }) => params.value,
|
||||
color: '#3182ce',
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成产业链桑基图配置
|
||||
* @param valueChainData - 产业链数据
|
||||
* @returns ECharts 桑基图配置,或 null(数据不足时)
|
||||
*/
|
||||
export const getSankeyChartOption = (
|
||||
valueChainData?: ValueChainData
|
||||
): SankeyChartOption | null => {
|
||||
if (
|
||||
!valueChainData?.value_chain_flows ||
|
||||
valueChainData.value_chain_flows.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodes = new Set<string>();
|
||||
const links: Array<{
|
||||
source: string;
|
||||
target: string;
|
||||
value: number;
|
||||
lineStyle: { color: string; opacity: number };
|
||||
}> = [];
|
||||
|
||||
valueChainData.value_chain_flows.forEach((flow) => {
|
||||
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') || 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 },
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user