Compare commits
3 Commits
e049429b09
...
e92cc09e06
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e92cc09e06 | ||
|
|
23112db115 | ||
|
|
7c7c70c4d9 |
@@ -147,7 +147,8 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
|
||||
bg={theme.bg}
|
||||
borderBottom="1px solid"
|
||||
borderColor={theme.borderColor}
|
||||
px={4}
|
||||
pl={0}
|
||||
pr={4}
|
||||
py={2}
|
||||
flexWrap="wrap"
|
||||
gap={2}
|
||||
|
||||
@@ -20,7 +20,8 @@ const TabNavigation: React.FC<TabNavigationProps> = ({
|
||||
borderColor={themeColors.dividerColor}
|
||||
borderTopLeftRadius={borderRadius}
|
||||
borderTopRightRadius={borderRadius}
|
||||
px={4}
|
||||
pl={0}
|
||||
pr={4}
|
||||
py={2}
|
||||
flexWrap="wrap"
|
||||
gap={2}
|
||||
|
||||
@@ -473,147 +473,65 @@ export const PINGAN_BANK_DATA = {
|
||||
},
|
||||
business_structure: [
|
||||
{
|
||||
business_name: '零售金融',
|
||||
business_name: '舒泰清(复方聚乙二醇电解质散IV)',
|
||||
business_level: 1,
|
||||
revenue: 812300,
|
||||
revenue: 17900,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 50.1,
|
||||
gross_margin: 42.5
|
||||
revenue_ratio: 55.16,
|
||||
gross_margin: 78.21
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: 11.2
|
||||
revenue_growth: -8.20
|
||||
},
|
||||
report_period: '2024Q3'
|
||||
report_period: '2024年报'
|
||||
},
|
||||
{
|
||||
business_name: '信用卡业务',
|
||||
business_level: 2,
|
||||
revenue: 325000,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 20.1,
|
||||
gross_margin: 38.2
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: 15.8
|
||||
},
|
||||
report_period: '2024Q3'
|
||||
},
|
||||
{
|
||||
business_name: '财富管理',
|
||||
business_level: 2,
|
||||
revenue: 280500,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 17.3,
|
||||
gross_margin: 52.1
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: 22.5
|
||||
},
|
||||
report_period: '2024Q3'
|
||||
},
|
||||
{
|
||||
business_name: '消费信贷',
|
||||
business_level: 2,
|
||||
revenue: 206800,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 12.7,
|
||||
gross_margin: 35.8
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: 8.6
|
||||
},
|
||||
report_period: '2024Q3'
|
||||
},
|
||||
{
|
||||
business_name: '对公金融',
|
||||
business_name: '苏肽生(注射用鼠神经生长因子)',
|
||||
business_level: 1,
|
||||
revenue: 685400,
|
||||
revenue: 13400,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 42.2,
|
||||
gross_margin: 38.6
|
||||
revenue_ratio: 41.21,
|
||||
gross_margin: 89.11
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: 6.8
|
||||
revenue_growth: -17.30
|
||||
},
|
||||
report_period: '2024Q3'
|
||||
report_period: '2024年报'
|
||||
},
|
||||
{
|
||||
business_name: '公司贷款',
|
||||
business_level: 2,
|
||||
revenue: 412000,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 25.4,
|
||||
gross_margin: 36.2
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: 5.2
|
||||
},
|
||||
report_period: '2024Q3'
|
||||
},
|
||||
{
|
||||
business_name: '供应链金融',
|
||||
business_level: 2,
|
||||
revenue: 185600,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 11.4,
|
||||
gross_margin: 41.5
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: 18.3
|
||||
},
|
||||
report_period: '2024Q3'
|
||||
},
|
||||
{
|
||||
business_name: '投资银行',
|
||||
business_level: 2,
|
||||
revenue: 87800,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 5.4,
|
||||
gross_margin: 45.2
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: -2.3
|
||||
},
|
||||
report_period: '2024Q3'
|
||||
},
|
||||
{
|
||||
business_name: '资金同业',
|
||||
business_name: '舒斯通(复方聚乙二醇(3350)电解质散)',
|
||||
business_level: 1,
|
||||
revenue: 125800,
|
||||
revenue: 771,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 7.7,
|
||||
gross_margin: 28.2
|
||||
revenue_ratio: 2.37
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: 3.5
|
||||
report_period: '2024年报'
|
||||
},
|
||||
{
|
||||
business_name: '阿司匹林肠溶片',
|
||||
business_level: 1,
|
||||
revenue: 396,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 1.22
|
||||
},
|
||||
report_period: '2024Q3'
|
||||
report_period: '2024年报'
|
||||
},
|
||||
{
|
||||
business_name: '研发业务',
|
||||
business_level: 1,
|
||||
report_period: '2024年报'
|
||||
}
|
||||
],
|
||||
business_segments: [
|
||||
{
|
||||
segment_name: '信用卡业务',
|
||||
description: '国内领先的信用卡发卡银行,流通卡量超7000万张',
|
||||
key_metrics: { cards_issued: 7200, transaction_volume: 28500, market_share: 8.5 }
|
||||
},
|
||||
{
|
||||
segment_name: '财富管理',
|
||||
description: '私人银行及财富管理业务快速发展,AUM突破4万亿',
|
||||
key_metrics: { aum: 42000, private_banking_customers: 125000, wealth_customers: 1200000 }
|
||||
},
|
||||
{
|
||||
segment_name: '供应链金融',
|
||||
description: '依托科技平台打造智慧供应链金融生态',
|
||||
key_metrics: { platform_customers: 35000, financing_balance: 5600, digitization_rate: 95 }
|
||||
segment_name: '已上市药品营销',
|
||||
segment_description: '舒泰神已上市药品营销业务主要包括舒泰清(复方聚乙二醇电解质散IV)和苏肽生(注射用鼠神经生长因子)两大核心产品。2024年实现营业收入3.25亿元,其中舒泰清贡献1.79亿元(55.16%),苏肽生贡献1.34亿元(41.21%)。尽管面临市场竞争压力,产品毛利率保持高位,综合毛利率达80.83%,其中苏肽生毛利率高达89.11%。',
|
||||
competitive_position: '舒泰清为《中国消化内镜诊疗肠道准备指南》和《慢性便秘诊治指南》一线用药,苏肽生是国内首个国药准字鼠神经生长因子产品。公司医保目录产品舒斯通已落地,并布局舒亦清、舒常轻等系列产品形成梯队,构建了一定市场竞争优势。然而,2024年集采中同类(III型)产品中选,对舒泰清(IV型)形成潜在价格压力。',
|
||||
future_potential: '公司正在构建系列化产品线应对市场变化,研发投入保持高强度(1.62亿元,占营收49.97%)。在研管线中,STSP-0601血友病药物获FDA孤儿药资格,BDB-001被纳入突破性治疗品种,创新药研发持续推进。国家政策支持创新药发展,行业环境向好,同时国际化布局已有初步进展,未来3-5年有望通过新产品上市实现业绩突破。'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1336,11 +1254,68 @@ export const generateCompanyData = (stockCode, stockName = '示例公司') => {
|
||||
}
|
||||
},
|
||||
business_structure: [
|
||||
{ business_name: '核心产品', revenue: baseRevenue * 0.6, ratio: 60, growth: 12.5, report_period: '2024Q3' },
|
||||
{ business_name: '增值服务', revenue: baseRevenue * 0.25, ratio: 25, growth: 18.2, report_period: '2024Q3' },
|
||||
{ business_name: '其他业务', revenue: baseRevenue * 0.15, ratio: 15, growth: 5.8, report_period: '2024Q3' }
|
||||
{
|
||||
business_name: '舒泰清(复方聚乙二醇电解质散IV)',
|
||||
business_level: 1,
|
||||
revenue: 17900,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 55.16,
|
||||
gross_margin: 78.21
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: -8.20
|
||||
},
|
||||
report_period: '2024年报'
|
||||
},
|
||||
{
|
||||
business_name: '苏肽生(注射用鼠神经生长因子)',
|
||||
business_level: 1,
|
||||
revenue: 13400,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 41.21,
|
||||
gross_margin: 89.11
|
||||
},
|
||||
growth_metrics: {
|
||||
revenue_growth: -17.30
|
||||
},
|
||||
report_period: '2024年报'
|
||||
},
|
||||
{
|
||||
business_name: '舒斯通(复方聚乙二醇(3350)电解质散)',
|
||||
business_level: 1,
|
||||
revenue: 771,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 2.37
|
||||
},
|
||||
report_period: '2024年报'
|
||||
},
|
||||
{
|
||||
business_name: '阿司匹林肠溶片',
|
||||
business_level: 1,
|
||||
revenue: 396,
|
||||
revenue_unit: '万元',
|
||||
financial_metrics: {
|
||||
revenue_ratio: 1.22
|
||||
},
|
||||
report_period: '2024年报'
|
||||
},
|
||||
{
|
||||
business_name: '研发业务',
|
||||
business_level: 1,
|
||||
report_period: '2024年报'
|
||||
}
|
||||
],
|
||||
business_segments: []
|
||||
business_segments: [
|
||||
{
|
||||
segment_name: '已上市药品营销',
|
||||
segment_description: '舒泰神已上市药品营销业务主要包括舒泰清(复方聚乙二醇电解质散IV)和苏肽生(注射用鼠神经生长因子)两大核心产品。2024年实现营业收入3.25亿元,其中舒泰清贡献1.79亿元(55.16%),苏肽生贡献1.34亿元(41.21%)。尽管面临市场竞争压力,产品毛利率保持高位,综合毛利率达80.83%,其中苏肽生毛利率高达89.11%。',
|
||||
competitive_position: '舒泰清为《中国消化内镜诊疗肠道准备指南》和《慢性便秘诊治指南》一线用药,苏肽生是国内首个国药准字鼠神经生长因子产品。公司医保目录产品舒斯通已落地,并布局舒亦清、舒常轻等系列产品形成梯队,构建了一定市场竞争优势。然而,2024年集采中同类(III型)产品中选,对舒泰清(IV型)形成潜在价格压力。',
|
||||
future_potential: '公司正在构建系列化产品线应对市场变化,研发投入保持高强度(1.62亿元,占营收49.97%)。在研管线中,STSP-0601血友病药物获FDA孤儿药资格,BDB-001被纳入突破性治疗品种,创新药研发持续推进。国家政策支持创新药发展,行业环境向好,同时国际化布局已有初步进展,未来3-5年有望通过新产品上市实现业绩突破。'
|
||||
}
|
||||
]
|
||||
},
|
||||
valueChainAnalysis: {
|
||||
value_chain_flows: [
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* 递归显示业务结构层级
|
||||
* 使用位置:业务结构分析卡片
|
||||
* 黑金主题风格
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
@@ -10,9 +11,17 @@ import { Box, HStack, VStack, Text, Badge, Tag, TagLabel } from '@chakra-ui/reac
|
||||
import { formatPercentage, formatBusinessRevenue } from '@utils/priceFormatters';
|
||||
import type { BusinessTreeItemProps } from '../types';
|
||||
|
||||
const BusinessTreeItem: React.FC<BusinessTreeItemProps> = ({ business, depth = 0 }) => {
|
||||
const bgColor = 'gray.50';
|
||||
// 黑金主题配置
|
||||
const THEME = {
|
||||
bg: 'gray.700',
|
||||
gold: '#D4AF37',
|
||||
goldLight: '#F0D78C',
|
||||
textPrimary: '#D4AF37',
|
||||
textSecondary: 'gray.400',
|
||||
border: 'rgba(212, 175, 55, 0.5)',
|
||||
};
|
||||
|
||||
const BusinessTreeItem: React.FC<BusinessTreeItemProps> = ({ business, depth = 0 }) => {
|
||||
// 获取营收显示
|
||||
const getRevenueDisplay = (): string => {
|
||||
const revenue = business.revenue || business.financial_metrics?.revenue;
|
||||
@@ -27,40 +36,39 @@ const BusinessTreeItem: React.FC<BusinessTreeItemProps> = ({ business, depth = 0
|
||||
<Box
|
||||
ml={depth * 6}
|
||||
p={3}
|
||||
bg={bgColor}
|
||||
bg={THEME.bg}
|
||||
borderLeft={depth > 0 ? '4px solid' : 'none'}
|
||||
borderLeftColor="blue.400"
|
||||
borderLeftColor={THEME.gold}
|
||||
borderRadius="md"
|
||||
mb={2}
|
||||
_hover={{ shadow: 'md' }}
|
||||
_hover={{ shadow: 'md', bg: 'gray.600' }}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing={1}>
|
||||
<HStack>
|
||||
<Text fontWeight="bold" fontSize={depth === 0 ? 'md' : 'sm'}>
|
||||
<Text fontWeight="bold" fontSize={depth === 0 ? 'md' : 'sm'} color={THEME.textPrimary}>
|
||||
{business.business_name}
|
||||
</Text>
|
||||
{business.financial_metrics?.revenue_ratio &&
|
||||
business.financial_metrics.revenue_ratio > 30 && (
|
||||
<Badge colorScheme="purple" size="sm">
|
||||
<Badge bg={THEME.gold} color="gray.900" size="sm">
|
||||
核心业务
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
<HStack spacing={4} flexWrap="wrap">
|
||||
<Tag size="sm" variant="subtle">
|
||||
<Tag size="sm" bg="gray.600" color={THEME.textPrimary}>
|
||||
营收占比: {formatPercentage(business.financial_metrics?.revenue_ratio)}
|
||||
</Tag>
|
||||
<Tag size="sm" variant="subtle">
|
||||
<Tag size="sm" bg="gray.600" color={THEME.textPrimary}>
|
||||
毛利率: {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'
|
||||
}
|
||||
bg={business.growth_metrics.revenue_growth > 0 ? 'red.600' : 'green.600'}
|
||||
color="white"
|
||||
>
|
||||
<TagLabel>
|
||||
增长: {business.growth_metrics.revenue_growth > 0 ? '+' : ''}
|
||||
@@ -71,10 +79,10 @@ const BusinessTreeItem: React.FC<BusinessTreeItemProps> = ({ business, depth = 0
|
||||
</HStack>
|
||||
</VStack>
|
||||
<VStack align="end" spacing={0}>
|
||||
<Text fontSize="lg" fontWeight="bold" color="blue.500">
|
||||
<Text fontSize="lg" fontWeight="bold" color={THEME.gold}>
|
||||
{getRevenueDisplay()}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
<Text fontSize="xs" color={THEME.textSecondary}>
|
||||
营业收入
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* 显示单个关键因素的详细信息
|
||||
* 使用位置:关键因素 Accordion 内
|
||||
* 黑金主题设计
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
@@ -19,6 +20,13 @@ import {
|
||||
import { FaArrowUp, FaArrowDown } from 'react-icons/fa';
|
||||
import type { KeyFactorCardProps, ImpactDirection } from '../types';
|
||||
|
||||
// 黑金主题样式常量
|
||||
const THEME = {
|
||||
cardBg: '#252D3A',
|
||||
textColor: '#E2E8F0',
|
||||
subtextColor: '#A0AEC0',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 获取影响方向对应的颜色
|
||||
*/
|
||||
@@ -47,31 +55,43 @@ const getImpactLabel = (direction?: ImpactDirection): string => {
|
||||
|
||||
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">
|
||||
<Card
|
||||
bg={THEME.cardBg}
|
||||
border="1px solid"
|
||||
borderColor="whiteAlpha.100"
|
||||
size="sm"
|
||||
>
|
||||
<CardBody p={3}>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
<HStack justify="space-between">
|
||||
<Text fontWeight="medium" fontSize="sm">
|
||||
<Text fontWeight="medium" fontSize="sm" color={THEME.textColor}>
|
||||
{factor.factor_name}
|
||||
</Text>
|
||||
<Badge colorScheme={impactColor} size="sm">
|
||||
<Badge
|
||||
bg="transparent"
|
||||
border="1px solid"
|
||||
borderColor={`${impactColor}.400`}
|
||||
color={`${impactColor}.400`}
|
||||
size="sm"
|
||||
>
|
||||
{getImpactLabel(factor.impact_direction)}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<HStack spacing={2}>
|
||||
<Text fontSize="lg" fontWeight="bold" color={`${impactColor}.500`}>
|
||||
<Text fontSize="lg" fontWeight="bold" color={`${impactColor}.400`}>
|
||||
{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'}
|
||||
bg="transparent"
|
||||
border="1px solid"
|
||||
borderColor={factor.year_on_year > 0 ? 'red.400' : 'green.400'}
|
||||
color={factor.year_on_year > 0 ? 'red.400' : 'green.400'}
|
||||
>
|
||||
<Icon
|
||||
as={factor.year_on_year > 0 ? FaArrowUp : FaArrowDown}
|
||||
@@ -84,17 +104,17 @@ const KeyFactorCard: React.FC<KeyFactorCardProps> = ({ factor }) => {
|
||||
</HStack>
|
||||
|
||||
{factor.factor_desc && (
|
||||
<Text fontSize="xs" color="gray.600" noOfLines={2}>
|
||||
<Text fontSize="xs" color={THEME.subtextColor} noOfLines={2}>
|
||||
{factor.factor_desc}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
<Text fontSize="xs" color={THEME.subtextColor}>
|
||||
影响权重: {factor.impact_weight}
|
||||
</Text>
|
||||
{factor.report_period && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
<Text fontSize="xs" color={THEME.subtextColor}>
|
||||
{factor.report_period}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 产业链流程式导航组件
|
||||
*
|
||||
* 显示上游 → 核心 → 下游的流程式导航
|
||||
* 带图标箭头连接符
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { HStack, VStack, Box, Text, Icon, Badge } from '@chakra-ui/react';
|
||||
import { FaArrowRight } from 'react-icons/fa';
|
||||
|
||||
// 黑金主题配置
|
||||
const THEME = {
|
||||
gold: '#D4AF37',
|
||||
textSecondary: 'gray.400',
|
||||
upstream: {
|
||||
active: 'orange.500',
|
||||
activeBg: 'orange.900',
|
||||
inactive: 'white',
|
||||
inactiveBg: 'gray.700',
|
||||
},
|
||||
core: {
|
||||
active: 'blue.500',
|
||||
activeBg: 'blue.900',
|
||||
inactive: 'white',
|
||||
inactiveBg: 'gray.700',
|
||||
},
|
||||
downstream: {
|
||||
active: 'green.500',
|
||||
activeBg: 'green.900',
|
||||
inactive: 'white',
|
||||
inactiveBg: 'gray.700',
|
||||
},
|
||||
};
|
||||
|
||||
export type TabType = 'upstream' | 'core' | 'downstream';
|
||||
|
||||
interface ProcessNavigationProps {
|
||||
activeTab: TabType;
|
||||
onTabChange: (tab: TabType) => void;
|
||||
upstreamCount: number;
|
||||
coreCount: number;
|
||||
downstreamCount: number;
|
||||
}
|
||||
|
||||
interface NavItemProps {
|
||||
label: string;
|
||||
subtitle: string;
|
||||
count: number;
|
||||
isActive: boolean;
|
||||
colorKey: 'upstream' | 'core' | 'downstream';
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const NavItem: React.FC<NavItemProps> = memo(({
|
||||
label,
|
||||
subtitle,
|
||||
count,
|
||||
isActive,
|
||||
colorKey,
|
||||
onClick,
|
||||
}) => {
|
||||
const colors = THEME[colorKey];
|
||||
|
||||
return (
|
||||
<Box
|
||||
px={4}
|
||||
py={2}
|
||||
borderRadius="lg"
|
||||
cursor="pointer"
|
||||
bg={isActive ? colors.activeBg : colors.inactiveBg}
|
||||
borderWidth={2}
|
||||
borderColor={isActive ? colors.active : 'gray.600'}
|
||||
onClick={onClick}
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
borderColor: colors.active,
|
||||
transform: 'translateY(-2px)',
|
||||
}}
|
||||
>
|
||||
<VStack spacing={1} align="center">
|
||||
<HStack spacing={2}>
|
||||
<Text
|
||||
fontWeight={isActive ? 'bold' : 'medium'}
|
||||
color={isActive ? colors.active : colors.inactive}
|
||||
fontSize="sm"
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Badge
|
||||
bg={isActive ? colors.active : 'gray.600'}
|
||||
color="white"
|
||||
borderRadius="full"
|
||||
px={2}
|
||||
fontSize="xs"
|
||||
>
|
||||
{count}
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Text
|
||||
fontSize="xs"
|
||||
color={THEME.textSecondary}
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
NavItem.displayName = 'NavItem';
|
||||
|
||||
const ProcessNavigation: React.FC<ProcessNavigationProps> = memo(({
|
||||
activeTab,
|
||||
onTabChange,
|
||||
upstreamCount,
|
||||
coreCount,
|
||||
downstreamCount,
|
||||
}) => {
|
||||
return (
|
||||
<HStack
|
||||
spacing={2}
|
||||
flexWrap="wrap"
|
||||
gap={2}
|
||||
>
|
||||
<NavItem
|
||||
label="上游供应链"
|
||||
subtitle="原材料与供应商"
|
||||
count={upstreamCount}
|
||||
isActive={activeTab === 'upstream'}
|
||||
colorKey="upstream"
|
||||
onClick={() => onTabChange('upstream')}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
as={FaArrowRight}
|
||||
color={THEME.textSecondary}
|
||||
boxSize={4}
|
||||
/>
|
||||
|
||||
<NavItem
|
||||
label="核心企业"
|
||||
subtitle="公司主体与产品"
|
||||
count={coreCount}
|
||||
isActive={activeTab === 'core'}
|
||||
colorKey="core"
|
||||
onClick={() => onTabChange('core')}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
as={FaArrowRight}
|
||||
color={THEME.textSecondary}
|
||||
boxSize={4}
|
||||
/>
|
||||
|
||||
<NavItem
|
||||
label="下游客户"
|
||||
subtitle="客户与终端市场"
|
||||
count={downstreamCount}
|
||||
isActive={activeTab === 'downstream'}
|
||||
colorKey="downstream"
|
||||
onClick={() => onTabChange('downstream')}
|
||||
/>
|
||||
</HStack>
|
||||
);
|
||||
});
|
||||
|
||||
ProcessNavigation.displayName = 'ProcessNavigation';
|
||||
|
||||
export default ProcessNavigation;
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* 产业链筛选栏组件
|
||||
*
|
||||
* 提供类型筛选、重要度筛选和视图切换功能
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
HStack,
|
||||
Select,
|
||||
Tabs,
|
||||
TabList,
|
||||
Tab,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
// 黑金主题配置
|
||||
const THEME = {
|
||||
gold: '#D4AF37',
|
||||
textPrimary: '#D4AF37',
|
||||
textSecondary: 'gray.400',
|
||||
inputBg: 'gray.700',
|
||||
inputBorder: 'gray.600',
|
||||
};
|
||||
|
||||
export type ViewMode = 'hierarchy' | 'flow';
|
||||
|
||||
// 节点类型选项
|
||||
const TYPE_OPTIONS = [
|
||||
{ value: 'all', label: '全部类型' },
|
||||
{ value: 'company', label: '公司' },
|
||||
{ value: 'supplier', label: '供应商' },
|
||||
{ value: 'customer', label: '客户' },
|
||||
{ value: 'regulator', label: '监管机构' },
|
||||
{ value: 'product', label: '产品' },
|
||||
{ value: 'service', label: '服务' },
|
||||
{ value: 'channel', label: '渠道' },
|
||||
{ value: 'raw_material', label: '原材料' },
|
||||
{ value: 'end_user', label: '终端用户' },
|
||||
];
|
||||
|
||||
// 重要度选项
|
||||
const IMPORTANCE_OPTIONS = [
|
||||
{ value: 'all', label: '全部重要度' },
|
||||
{ value: 'high', label: '高 (≥80)' },
|
||||
{ value: 'medium', label: '中 (50-79)' },
|
||||
{ value: 'low', label: '低 (<50)' },
|
||||
];
|
||||
|
||||
interface ValueChainFilterBarProps {
|
||||
typeFilter: string;
|
||||
onTypeChange: (value: string) => void;
|
||||
importanceFilter: string;
|
||||
onImportanceChange: (value: string) => void;
|
||||
viewMode: ViewMode;
|
||||
onViewModeChange: (value: ViewMode) => void;
|
||||
}
|
||||
|
||||
const ValueChainFilterBar: React.FC<ValueChainFilterBarProps> = memo(({
|
||||
typeFilter,
|
||||
onTypeChange,
|
||||
importanceFilter,
|
||||
onImportanceChange,
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
}) => {
|
||||
return (
|
||||
<HStack
|
||||
spacing={3}
|
||||
flexWrap="wrap"
|
||||
gap={3}
|
||||
>
|
||||
{/* 左侧筛选区 */}
|
||||
{/* <HStack spacing={3}>
|
||||
<Select
|
||||
value={typeFilter}
|
||||
onChange={(e) => onTypeChange(e.target.value)}
|
||||
size="sm"
|
||||
w="140px"
|
||||
bg={THEME.inputBg}
|
||||
borderColor={THEME.inputBorder}
|
||||
color={THEME.textPrimary}
|
||||
_hover={{ borderColor: THEME.gold }}
|
||||
_focus={{ borderColor: THEME.gold, boxShadow: 'none' }}
|
||||
>
|
||||
{TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value} style={{ background: '#2D3748' }}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={importanceFilter}
|
||||
onChange={(e) => onImportanceChange(e.target.value)}
|
||||
size="sm"
|
||||
w="140px"
|
||||
bg={THEME.inputBg}
|
||||
borderColor={THEME.inputBorder}
|
||||
color={THEME.textPrimary}
|
||||
_hover={{ borderColor: THEME.gold }}
|
||||
_focus={{ borderColor: THEME.gold, boxShadow: 'none' }}
|
||||
>
|
||||
{IMPORTANCE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value} style={{ background: '#2D3748' }}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</HStack> */}
|
||||
|
||||
{/* 右侧视图切换 */}
|
||||
<Tabs
|
||||
index={viewMode === 'hierarchy' ? 0 : 1}
|
||||
onChange={(index) => onViewModeChange(index === 0 ? 'hierarchy' : 'flow')}
|
||||
variant="soft-rounded"
|
||||
size="sm"
|
||||
>
|
||||
<TabList>
|
||||
<Tab
|
||||
color={THEME.textSecondary}
|
||||
_selected={{
|
||||
bg: THEME.gold,
|
||||
color: 'gray.900',
|
||||
}}
|
||||
_hover={{
|
||||
bg: 'gray.600',
|
||||
}}
|
||||
>
|
||||
层级视图
|
||||
</Tab>
|
||||
<Tab
|
||||
color={THEME.textSecondary}
|
||||
_selected={{
|
||||
bg: THEME.gold,
|
||||
color: 'gray.900',
|
||||
}}
|
||||
_hover={{
|
||||
bg: 'gray.600',
|
||||
}}
|
||||
>
|
||||
流向关系
|
||||
</Tab>
|
||||
</TabList>
|
||||
</Tabs>
|
||||
</HStack>
|
||||
);
|
||||
});
|
||||
|
||||
ValueChainFilterBar.displayName = 'ValueChainFilterBar';
|
||||
|
||||
export default ValueChainFilterBar;
|
||||
@@ -8,3 +8,7 @@ export { default as DisclaimerBox } from './DisclaimerBox';
|
||||
export { default as ScoreBar } from './ScoreBar';
|
||||
export { default as BusinessTreeItem } from './BusinessTreeItem';
|
||||
export { default as KeyFactorCard } from './KeyFactorCard';
|
||||
export { default as ProcessNavigation } from './ProcessNavigation';
|
||||
export { default as ValueChainFilterBar } from './ValueChainFilterBar';
|
||||
export type { TabType } from './ProcessNavigation';
|
||||
export type { ViewMode } from './ValueChainFilterBar';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* 业务板块详情卡片
|
||||
*
|
||||
* 显示公司各业务板块的详细信息
|
||||
* 黑金主题风格
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
@@ -20,9 +21,19 @@ import {
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaIndustry, FaExpandAlt, FaCompressAlt } from 'react-icons/fa';
|
||||
import { DisclaimerBox } from '../atoms';
|
||||
import type { BusinessSegment } from '../types';
|
||||
|
||||
// 黑金主题配置
|
||||
const THEME = {
|
||||
cardBg: 'gray.800',
|
||||
innerCardBg: 'gray.700',
|
||||
gold: '#D4AF37',
|
||||
goldLight: '#F0D78C',
|
||||
textPrimary: '#D4AF37',
|
||||
textSecondary: 'gray.400',
|
||||
border: 'rgba(212, 175, 55, 0.3)',
|
||||
};
|
||||
|
||||
interface BusinessSegmentsCardProps {
|
||||
businessSegments: BusinessSegment[];
|
||||
expandedSegments: Record<number, boolean>;
|
||||
@@ -34,31 +45,29 @@ const BusinessSegmentsCard: React.FC<BusinessSegmentsCardProps> = ({
|
||||
businessSegments,
|
||||
expandedSegments,
|
||||
onToggleSegment,
|
||||
cardBg,
|
||||
}) => {
|
||||
if (!businessSegments || businessSegments.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} shadow="md">
|
||||
<Card bg={THEME.cardBg} shadow="md">
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FaIndustry} color="indigo.500" />
|
||||
<Heading size="sm">业务板块详情</Heading>
|
||||
<Badge>{businessSegments.length} 个板块</Badge>
|
||||
<Icon as={FaIndustry} color={THEME.gold} />
|
||||
<Heading size="sm" color={THEME.textPrimary}>业务板块详情</Heading>
|
||||
<Badge bg={THEME.gold} color="gray.900">{businessSegments.length} 个板块</Badge>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DisclaimerBox />
|
||||
<CardBody px={2}>
|
||||
<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>
|
||||
<Card key={idx} bg={THEME.innerCardBg}>
|
||||
<CardBody px={2}>
|
||||
<VStack align="stretch" spacing={3}>
|
||||
<HStack justify="space-between">
|
||||
<Text fontWeight="bold" fontSize="md">
|
||||
<Text fontWeight="bold" fontSize="md" color={THEME.textPrimary}>
|
||||
{segment.segment_name}
|
||||
</Text>
|
||||
<Button
|
||||
@@ -68,18 +77,20 @@ const BusinessSegmentsCard: React.FC<BusinessSegmentsCardProps> = ({
|
||||
<Icon as={isExpanded ? FaCompressAlt : FaExpandAlt} />
|
||||
}
|
||||
onClick={() => onToggleSegment(idx)}
|
||||
colorScheme="blue"
|
||||
color={THEME.gold}
|
||||
_hover={{ bg: 'gray.600' }}
|
||||
>
|
||||
{isExpanded ? '折叠' : '展开'}
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
<Box>
|
||||
<Text fontSize="xs" color="gray.500" mb={1}>
|
||||
<Text fontSize="xs" color={THEME.textSecondary} mb={1}>
|
||||
业务描述
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={THEME.textPrimary}
|
||||
noOfLines={isExpanded ? undefined : 3}
|
||||
>
|
||||
{segment.segment_description || '暂无描述'}
|
||||
@@ -87,11 +98,12 @@ const BusinessSegmentsCard: React.FC<BusinessSegmentsCardProps> = ({
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fontSize="xs" color="gray.500" mb={1}>
|
||||
<Text fontSize="xs" color={THEME.textSecondary} mb={1}>
|
||||
竞争地位
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={THEME.textPrimary}
|
||||
noOfLines={isExpanded ? undefined : 2}
|
||||
>
|
||||
{segment.competitive_position || '暂无数据'}
|
||||
@@ -99,13 +111,13 @@ const BusinessSegmentsCard: React.FC<BusinessSegmentsCardProps> = ({
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Text fontSize="xs" color="gray.500" mb={1}>
|
||||
<Text fontSize="xs" color={THEME.textSecondary} mb={1}>
|
||||
未来潜力
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
noOfLines={isExpanded ? undefined : 2}
|
||||
color="blue.600"
|
||||
color={THEME.goldLight}
|
||||
>
|
||||
{segment.future_potential || '暂无数据'}
|
||||
</Text>
|
||||
@@ -113,10 +125,10 @@ const BusinessSegmentsCard: React.FC<BusinessSegmentsCardProps> = ({
|
||||
|
||||
{isExpanded && segment.key_products && (
|
||||
<Box>
|
||||
<Text fontSize="xs" color="gray.500" mb={1}>
|
||||
<Text fontSize="xs" color={THEME.textSecondary} mb={1}>
|
||||
主要产品
|
||||
</Text>
|
||||
<Text fontSize="sm" color="green.600">
|
||||
<Text fontSize="sm" color="green.300">
|
||||
{segment.key_products}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -124,10 +136,10 @@ const BusinessSegmentsCard: React.FC<BusinessSegmentsCardProps> = ({
|
||||
|
||||
{isExpanded && segment.market_share !== undefined && (
|
||||
<Box>
|
||||
<Text fontSize="xs" color="gray.500" mb={1}>
|
||||
<Text fontSize="xs" color={THEME.textSecondary} mb={1}>
|
||||
市场份额
|
||||
</Text>
|
||||
<Badge colorScheme="purple" fontSize="sm">
|
||||
<Badge bg="purple.600" color="white" fontSize="sm">
|
||||
{segment.market_share}%
|
||||
</Badge>
|
||||
</Box>
|
||||
@@ -135,10 +147,10 @@ const BusinessSegmentsCard: React.FC<BusinessSegmentsCardProps> = ({
|
||||
|
||||
{isExpanded && segment.revenue_contribution !== undefined && (
|
||||
<Box>
|
||||
<Text fontSize="xs" color="gray.500" mb={1}>
|
||||
<Text fontSize="xs" color={THEME.textSecondary} mb={1}>
|
||||
营收贡献
|
||||
</Text>
|
||||
<Badge colorScheme="orange" fontSize="sm">
|
||||
<Badge bg={THEME.gold} color="gray.900" fontSize="sm">
|
||||
{segment.revenue_contribution}%
|
||||
</Badge>
|
||||
</Box>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* 业务结构分析卡片
|
||||
*
|
||||
* 显示公司业务结构树形图
|
||||
* 黑金主题风格
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
@@ -16,9 +17,17 @@ import {
|
||||
Icon,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaChartPie } from 'react-icons/fa';
|
||||
import { DisclaimerBox, BusinessTreeItem } from '../atoms';
|
||||
import { BusinessTreeItem } from '../atoms';
|
||||
import type { BusinessStructure } from '../types';
|
||||
|
||||
// 黑金主题配置
|
||||
const THEME = {
|
||||
cardBg: 'gray.800',
|
||||
gold: '#D4AF37',
|
||||
textPrimary: '#D4AF37',
|
||||
border: 'rgba(212, 175, 55, 0.3)',
|
||||
};
|
||||
|
||||
interface BusinessStructureCardProps {
|
||||
businessStructure: BusinessStructure[];
|
||||
cardBg?: string;
|
||||
@@ -26,21 +35,19 @@ interface BusinessStructureCardProps {
|
||||
|
||||
const BusinessStructureCard: React.FC<BusinessStructureCardProps> = ({
|
||||
businessStructure,
|
||||
cardBg,
|
||||
}) => {
|
||||
if (!businessStructure || businessStructure.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} shadow="md">
|
||||
<Card bg={THEME.cardBg} shadow="md">
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FaChartPie} color="purple.500" />
|
||||
<Heading size="sm">业务结构分析</Heading>
|
||||
<Badge>{businessStructure[0]?.report_period}</Badge>
|
||||
<Icon as={FaChartPie} color={THEME.gold} />
|
||||
<Heading size="sm" color={THEME.textPrimary}>业务结构分析</Heading>
|
||||
<Badge bg={THEME.gold} color="gray.900">{businessStructure[0]?.report_period}</Badge>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DisclaimerBox />
|
||||
<CardBody px={0}>
|
||||
<VStack spacing={3} align="stretch">
|
||||
{businessStructure.map((business, idx) => (
|
||||
<BusinessTreeItem
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* 关键因素卡片
|
||||
*
|
||||
* 显示影响公司的关键因素列表
|
||||
* 黑金主题设计
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
@@ -23,42 +24,87 @@ import {
|
||||
AccordionIcon,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaBalanceScale } from 'react-icons/fa';
|
||||
import { DisclaimerBox, KeyFactorCard } from '../atoms';
|
||||
import { KeyFactorCard } from '../atoms';
|
||||
import type { KeyFactors } from '../types';
|
||||
|
||||
// 黑金主题样式常量
|
||||
const THEME = {
|
||||
bg: '#1A202C',
|
||||
cardBg: '#252D3A',
|
||||
border: '#C9A961',
|
||||
borderGradient: 'linear-gradient(90deg, #C9A961, #8B7355)',
|
||||
titleColor: '#C9A961',
|
||||
textColor: '#E2E8F0',
|
||||
subtextColor: '#A0AEC0',
|
||||
} as const;
|
||||
|
||||
const CARD_STYLES = {
|
||||
bg: THEME.bg,
|
||||
shadow: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'whiteAlpha.100',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
_before: {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: THEME.borderGradient,
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface KeyFactorsCardProps {
|
||||
keyFactors: KeyFactors;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const KeyFactorsCard: React.FC<KeyFactorsCardProps> = ({
|
||||
keyFactors,
|
||||
cardBg,
|
||||
}) => {
|
||||
const KeyFactorsCard: React.FC<KeyFactorsCardProps> = ({ keyFactors }) => {
|
||||
return (
|
||||
<Card bg={cardBg} shadow="md" h="full">
|
||||
<Card {...CARD_STYLES} h="full">
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FaBalanceScale} color="orange.500" />
|
||||
<Heading size="sm">关键因素</Heading>
|
||||
<Badge>{keyFactors.total_factors} 项</Badge>
|
||||
<Icon as={FaBalanceScale} color="yellow.500" />
|
||||
<Heading size="sm" color={THEME.titleColor}>
|
||||
关键因素
|
||||
</Heading>
|
||||
<Badge
|
||||
bg="transparent"
|
||||
border="1px solid"
|
||||
borderColor="yellow.600"
|
||||
color="yellow.500"
|
||||
>
|
||||
{keyFactors.total_factors} 项
|
||||
</Badge>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DisclaimerBox />
|
||||
<Accordion allowMultiple>
|
||||
{keyFactors.categories.map((category, idx) => (
|
||||
<AccordionItem key={idx}>
|
||||
<AccordionButton>
|
||||
<AccordionItem key={idx} border="none">
|
||||
<AccordionButton
|
||||
bg={THEME.cardBg}
|
||||
borderRadius="md"
|
||||
mb={2}
|
||||
_hover={{ bg: 'whiteAlpha.100' }}
|
||||
>
|
||||
<Box flex="1" textAlign="left">
|
||||
<HStack>
|
||||
<Text fontWeight="medium">{category.category_name}</Text>
|
||||
<Badge size="sm" variant="subtle">
|
||||
<Text fontWeight="medium" color={THEME.textColor}>
|
||||
{category.category_name}
|
||||
</Text>
|
||||
<Badge
|
||||
bg="whiteAlpha.100"
|
||||
color={THEME.subtextColor}
|
||||
size="sm"
|
||||
>
|
||||
{category.factors.length}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
<AccordionIcon color={THEME.subtextColor} />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4}>
|
||||
<VStack spacing={3} align="stretch">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* 发展时间线卡片
|
||||
*
|
||||
* 显示公司发展历程时间线
|
||||
* 黑金主题设计
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
@@ -16,37 +17,73 @@ import {
|
||||
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';
|
||||
|
||||
// 黑金主题样式常量
|
||||
const THEME = {
|
||||
bg: '#1A202C',
|
||||
cardBg: '#252D3A',
|
||||
border: '#C9A961',
|
||||
borderGradient: 'linear-gradient(90deg, #C9A961, #8B7355)',
|
||||
titleColor: '#C9A961',
|
||||
textColor: '#E2E8F0',
|
||||
subtextColor: '#A0AEC0',
|
||||
} as const;
|
||||
|
||||
const CARD_STYLES = {
|
||||
bg: THEME.bg,
|
||||
shadow: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'whiteAlpha.100',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
_before: {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '3px',
|
||||
background: THEME.borderGradient,
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface TimelineCardProps {
|
||||
developmentTimeline: DevelopmentTimeline;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const TimelineCard: React.FC<TimelineCardProps> = ({
|
||||
developmentTimeline,
|
||||
cardBg,
|
||||
}) => {
|
||||
const TimelineCard: React.FC<TimelineCardProps> = ({ developmentTimeline }) => {
|
||||
return (
|
||||
<Card bg={cardBg} shadow="md" h="full">
|
||||
<Card {...CARD_STYLES} h="full">
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FaHistory} color="cyan.500" />
|
||||
<Heading size="sm">发展时间线</Heading>
|
||||
<Icon as={FaHistory} color="yellow.500" />
|
||||
<Heading size="sm" color={THEME.titleColor}>
|
||||
发展时间线
|
||||
</Heading>
|
||||
<HStack spacing={1}>
|
||||
<Badge colorScheme="red">
|
||||
<Badge
|
||||
bg="transparent"
|
||||
border="1px solid"
|
||||
borderColor="red.400"
|
||||
color="red.400"
|
||||
>
|
||||
正面 {developmentTimeline.statistics?.positive_events || 0}
|
||||
</Badge>
|
||||
<Badge colorScheme="green">
|
||||
<Badge
|
||||
bg="transparent"
|
||||
border="1px solid"
|
||||
borderColor="green.400"
|
||||
color="green.400"
|
||||
>
|
||||
负面 {developmentTimeline.statistics?.negative_events || 0}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DisclaimerBox />
|
||||
<Box maxH="600px" overflowY="auto" pr={2}>
|
||||
<TimelineComponent events={developmentTimeline.events} />
|
||||
</Box>
|
||||
|
||||
@@ -2,184 +2,215 @@
|
||||
* 产业链分析卡片
|
||||
*
|
||||
* 显示产业链层级视图和流向关系
|
||||
* 黑金主题风格 + 流程式导航
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useMemo, memo } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Heading,
|
||||
Badge,
|
||||
Box,
|
||||
Icon,
|
||||
SimpleGrid,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanels,
|
||||
Tab,
|
||||
TabPanel,
|
||||
Center,
|
||||
Box,
|
||||
Flex,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaNetworkWired } from 'react-icons/fa';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { DisclaimerBox } from '../atoms';
|
||||
import {
|
||||
ProcessNavigation,
|
||||
ValueChainFilterBar,
|
||||
} from '../atoms';
|
||||
import type { TabType, ViewMode } from '../atoms';
|
||||
import ValueChainNodeCard from '../organisms/ValueChainNodeCard';
|
||||
import { getSankeyChartOption } from '../utils/chartOptions';
|
||||
import type { ValueChainData } from '../types';
|
||||
import type { ValueChainData, ValueChainNode } from '../types';
|
||||
|
||||
// 黑金主题配置
|
||||
const THEME = {
|
||||
cardBg: 'gray.800',
|
||||
gold: '#D4AF37',
|
||||
goldLight: '#F0D78C',
|
||||
textPrimary: '#D4AF37',
|
||||
textSecondary: 'gray.400',
|
||||
};
|
||||
|
||||
interface ValueChainCardProps {
|
||||
valueChainData: ValueChainData;
|
||||
companyName?: string;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const ValueChainCard: React.FC<ValueChainCardProps> = ({
|
||||
const ValueChainCard: React.FC<ValueChainCardProps> = memo(({
|
||||
valueChainData,
|
||||
cardBg,
|
||||
companyName = '目标公司',
|
||||
}) => {
|
||||
const sankeyOption = getSankeyChartOption(valueChainData);
|
||||
// 状态管理
|
||||
const [activeTab, setActiveTab] = useState<TabType>('upstream');
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all');
|
||||
const [importanceFilter, setImportanceFilter] = useState<string>('all');
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('hierarchy');
|
||||
|
||||
// 解析节点数据
|
||||
const nodesByLevel = valueChainData.value_chain_structure?.nodes_by_level;
|
||||
|
||||
// 获取上游节点
|
||||
const upstreamNodes = [
|
||||
const upstreamNodes = useMemo(() => [
|
||||
...(nodesByLevel?.['level_-2'] || []),
|
||||
...(nodesByLevel?.['level_-1'] || []),
|
||||
];
|
||||
], [nodesByLevel]);
|
||||
|
||||
// 获取核心节点
|
||||
const coreNodes = nodesByLevel?.['level_0'] || [];
|
||||
const coreNodes = useMemo(() =>
|
||||
nodesByLevel?.['level_0'] || [],
|
||||
[nodesByLevel]);
|
||||
|
||||
// 获取下游节点
|
||||
const downstreamNodes = [
|
||||
const downstreamNodes = useMemo(() => [
|
||||
...(nodesByLevel?.['level_1'] || []),
|
||||
...(nodesByLevel?.['level_2'] || []),
|
||||
];
|
||||
], [nodesByLevel]);
|
||||
|
||||
// 计算总节点数
|
||||
const totalNodes = valueChainData.analysis_summary?.total_nodes ||
|
||||
(upstreamNodes.length + coreNodes.length + downstreamNodes.length);
|
||||
|
||||
// 根据 activeTab 获取当前节点
|
||||
const currentNodes = useMemo(() => {
|
||||
switch (activeTab) {
|
||||
case 'upstream':
|
||||
return upstreamNodes;
|
||||
case 'core':
|
||||
return coreNodes;
|
||||
case 'downstream':
|
||||
return downstreamNodes;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}, [activeTab, upstreamNodes, coreNodes, downstreamNodes]);
|
||||
|
||||
// 筛选节点
|
||||
const filteredNodes = useMemo(() => {
|
||||
let nodes = [...currentNodes];
|
||||
|
||||
// 类型筛选
|
||||
if (typeFilter !== 'all') {
|
||||
nodes = nodes.filter((n: ValueChainNode) => n.node_type === typeFilter);
|
||||
}
|
||||
|
||||
// 重要度筛选
|
||||
if (importanceFilter !== 'all') {
|
||||
nodes = nodes.filter((n: ValueChainNode) => {
|
||||
const score = n.importance_score || 0;
|
||||
switch (importanceFilter) {
|
||||
case 'high':
|
||||
return score >= 80;
|
||||
case 'medium':
|
||||
return score >= 50 && score < 80;
|
||||
case 'low':
|
||||
return score < 50;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}, [currentNodes, typeFilter, importanceFilter]);
|
||||
|
||||
// Sankey 图配置
|
||||
const sankeyOption = useMemo(() =>
|
||||
getSankeyChartOption(valueChainData),
|
||||
[valueChainData]);
|
||||
|
||||
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>
|
||||
<Card bg={THEME.cardBg} shadow="md">
|
||||
{/* 头部区域 */}
|
||||
<CardHeader py={0}>
|
||||
<HStack flexWrap="wrap" gap={0}>
|
||||
<Icon as={FaNetworkWired} color={THEME.gold} />
|
||||
<Heading size="sm" color={THEME.textPrimary}>
|
||||
产业链分析
|
||||
</Heading>
|
||||
<Text color={THEME.textSecondary} fontSize="sm">
|
||||
| {companyName}供应链图谱
|
||||
</Text>
|
||||
<Badge bg={THEME.gold} color="gray.900">
|
||||
节点 {totalNodes}
|
||||
</Badge>
|
||||
</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>
|
||||
)}
|
||||
<CardBody px={2}>
|
||||
{/* 工具栏:左侧流程导航 + 右侧筛选 */}
|
||||
<Flex
|
||||
borderBottom="1px solid"
|
||||
borderColor="gray.700"
|
||||
justify="space-between"
|
||||
align="center"
|
||||
flexWrap="wrap"
|
||||
>
|
||||
{/* 左侧:流程式导航 */}
|
||||
<ProcessNavigation
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
upstreamCount={upstreamNodes.length}
|
||||
coreCount={coreNodes.length}
|
||||
downstreamCount={downstreamNodes.length}
|
||||
/>
|
||||
|
||||
{/* 核心企业 */}
|
||||
{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>
|
||||
)}
|
||||
{/* 右侧:筛选与视图切换 */}
|
||||
<ValueChainFilterBar
|
||||
typeFilter={typeFilter}
|
||||
onTypeChange={setTypeFilter}
|
||||
importanceFilter={importanceFilter}
|
||||
onImportanceChange={setImportanceFilter}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{/* 下游客户 */}
|
||||
{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>
|
||||
{/* 内容区域 */}
|
||||
<Box px={0} pt={4}>
|
||||
{viewMode === 'hierarchy' ? (
|
||||
filteredNodes.length > 0 ? (
|
||||
<SimpleGrid columns={{ base: 2, md: 3, lg: 4 }} spacing={4}>
|
||||
{filteredNodes.map((node, idx) => (
|
||||
<ValueChainNodeCard
|
||||
key={idx}
|
||||
node={node}
|
||||
isCompany={node.node_type === 'company'}
|
||||
level={node.node_level}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<Center h="200px">
|
||||
<Text color={THEME.textSecondary}>暂无匹配的节点数据</Text>
|
||||
</Center>
|
||||
)
|
||||
) : sankeyOption ? (
|
||||
<ReactECharts
|
||||
option={sankeyOption}
|
||||
style={{ height: '500px' }}
|
||||
theme="dark"
|
||||
/>
|
||||
) : (
|
||||
<Center h="200px">
|
||||
<Text color={THEME.textSecondary}>暂无流向数据</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ValueChainCard.displayName = 'ValueChainCard';
|
||||
|
||||
export default ValueChainCard;
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
* 产业链节点卡片组件
|
||||
*
|
||||
* 显示产业链中的单个节点,点击可展开查看相关公司
|
||||
* 黑金主题风格
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, memo } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
@@ -37,6 +38,36 @@ import type { ValueChainNodeCardProps, RelatedCompany } from '../../types';
|
||||
|
||||
const API_BASE_URL = getApiBase();
|
||||
|
||||
// 黑金主题配置
|
||||
const THEME = {
|
||||
cardBg: 'gray.700',
|
||||
gold: '#D4AF37',
|
||||
goldLight: '#F0D78C',
|
||||
textPrimary: 'white',
|
||||
textSecondary: 'gray.400',
|
||||
// 上游颜色
|
||||
upstream: {
|
||||
bg: 'rgba(237, 137, 54, 0.1)',
|
||||
border: 'orange.600',
|
||||
badge: 'orange',
|
||||
icon: 'orange.400',
|
||||
},
|
||||
// 核心企业颜色
|
||||
core: {
|
||||
bg: 'rgba(66, 153, 225, 0.15)',
|
||||
border: 'blue.500',
|
||||
badge: 'blue',
|
||||
icon: 'blue.400',
|
||||
},
|
||||
// 下游颜色
|
||||
downstream: {
|
||||
bg: 'rgba(72, 187, 120, 0.1)',
|
||||
border: 'green.600',
|
||||
badge: 'green',
|
||||
icon: 'green.400',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取节点类型对应的图标
|
||||
*/
|
||||
@@ -49,6 +80,8 @@ const getNodeTypeIcon = (type: string) => {
|
||||
service: FaCog,
|
||||
channel: FaNetworkWired,
|
||||
raw_material: FaFlask,
|
||||
regulator: FaBuilding,
|
||||
end_user: FaUserTie,
|
||||
};
|
||||
return icons[type] || FaBuilding;
|
||||
};
|
||||
@@ -64,7 +97,7 @@ const getImportanceColor = (score?: number): string => {
|
||||
return 'green';
|
||||
};
|
||||
|
||||
const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
||||
const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = memo(({
|
||||
node,
|
||||
isCompany = false,
|
||||
level = 0,
|
||||
@@ -74,17 +107,14 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
||||
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 getColorConfig = () => {
|
||||
if (isCompany || level === 0) return THEME.core;
|
||||
if (level < 0) return THEME.upstream;
|
||||
return THEME.downstream;
|
||||
};
|
||||
|
||||
const colorScheme = getColorScheme();
|
||||
const bgColor = `${colorScheme}.50`;
|
||||
const borderColor = `${colorScheme}.200`;
|
||||
const colorConfig = getColorConfig();
|
||||
|
||||
// 获取相关公司数据
|
||||
const fetchRelatedCompanies = async () => {
|
||||
@@ -135,16 +165,16 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
||||
<>
|
||||
<ScaleFade in={true} initialScale={0.9}>
|
||||
<Card
|
||||
bg={bgColor}
|
||||
borderColor={borderColor}
|
||||
borderWidth={isCompany ? 3 : 1}
|
||||
bg={colorConfig.bg}
|
||||
borderColor={colorConfig.border}
|
||||
borderWidth={isCompany ? 2 : 1}
|
||||
shadow={isCompany ? 'lg' : 'sm'}
|
||||
cursor="pointer"
|
||||
onClick={handleCardClick}
|
||||
_hover={{
|
||||
shadow: 'xl',
|
||||
transform: 'translateY(-4px)',
|
||||
borderColor: `${colorScheme}.400`,
|
||||
borderColor: THEME.gold,
|
||||
}}
|
||||
transition="all 0.3s ease"
|
||||
minH="140px"
|
||||
@@ -155,11 +185,11 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
||||
<HStack spacing={2}>
|
||||
<Icon
|
||||
as={getNodeTypeIcon(node.node_type)}
|
||||
color={`${colorScheme}.500`}
|
||||
color={colorConfig.icon}
|
||||
boxSize={5}
|
||||
/>
|
||||
{isCompany && (
|
||||
<Badge colorScheme="blue" variant="solid">
|
||||
<Badge colorScheme={colorConfig.badge} variant="solid">
|
||||
核心企业
|
||||
</Badge>
|
||||
)}
|
||||
@@ -168,28 +198,28 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
||||
node.importance_score >= 70 && (
|
||||
<Tooltip label="重要节点">
|
||||
<span>
|
||||
<Icon as={FaStar} color="orange.400" boxSize={4} />
|
||||
<Icon as={FaStar} color={THEME.gold} boxSize={4} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<Text fontWeight="bold" fontSize="sm" noOfLines={2}>
|
||||
<Text fontWeight="bold" fontSize="sm" color={THEME.textPrimary} noOfLines={2}>
|
||||
{node.node_name}
|
||||
</Text>
|
||||
|
||||
{node.node_description && (
|
||||
<Text fontSize="xs" color="gray.600" noOfLines={2}>
|
||||
<Text fontSize="xs" color={THEME.textSecondary} noOfLines={2}>
|
||||
{node.node_description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
<Badge variant="subtle" size="sm" colorScheme={colorScheme}>
|
||||
<Badge variant="subtle" size="sm" colorScheme={colorConfig.badge}>
|
||||
{node.node_type}
|
||||
</Badge>
|
||||
{node.market_share !== undefined && (
|
||||
<Badge variant="outline" size="sm">
|
||||
<Badge variant="outline" size="sm" color={THEME.goldLight}>
|
||||
份额 {node.market_share}%
|
||||
</Badge>
|
||||
)}
|
||||
@@ -198,10 +228,10 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
||||
{node.importance_score !== undefined && (
|
||||
<Box>
|
||||
<HStack justify="space-between" mb={1}>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
<Text fontSize="xs" color={THEME.textSecondary}>
|
||||
重要度
|
||||
</Text>
|
||||
<Text fontSize="xs" fontWeight="bold">
|
||||
<Text fontSize="xs" fontWeight="bold" color={THEME.goldLight}>
|
||||
{node.importance_score}
|
||||
</Text>
|
||||
</HStack>
|
||||
@@ -210,6 +240,7 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
||||
size="xs"
|
||||
colorScheme={getImportanceColor(node.importance_score)}
|
||||
borderRadius="full"
|
||||
bg="gray.600"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -223,12 +254,14 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
||||
onClose={onClose}
|
||||
node={node}
|
||||
isCompany={isCompany}
|
||||
colorScheme={colorScheme}
|
||||
colorScheme={colorConfig.badge}
|
||||
relatedCompanies={relatedCompanies}
|
||||
loadingRelated={loadingRelated}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
ValueChainNodeCard.displayName = 'ValueChainNodeCard';
|
||||
|
||||
export default ValueChainNodeCard;
|
||||
|
||||
@@ -174,6 +174,7 @@ export interface AnalysisSummary {
|
||||
upstream_nodes?: number;
|
||||
company_nodes?: number;
|
||||
downstream_nodes?: number;
|
||||
total_nodes?: number;
|
||||
}
|
||||
|
||||
export interface ValueChainData {
|
||||
|
||||
Reference in New Issue
Block a user