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