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:
zdl
2025-12-11 10:59:05 +08:00
parent 7819b4f8a2
commit 32a73efb55
22 changed files with 2664 additions and 1795 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 };
}>;
}

View File

@@ -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 },
},
],
};
};