refactor(ValueChainCard): 重构产业链分析卡片布局
- 新增 ProcessNavigation 流程导航组件(上游→核心→下游+副标题) - 新增 ValueChainFilterBar 筛选栏组件(类型/重要度/视图Tab切换) - 重构布局为左右分栏:左侧流程导航,右侧筛选+视图切换 - 移除 DisclaimerBox 免责声明 - ValueChainNodeCard 适配黑金主题 - 移除卡片内部左右边距 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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: 'gray.600',
|
||||||
|
inactiveBg: 'gray.700',
|
||||||
|
},
|
||||||
|
core: {
|
||||||
|
active: 'blue.500',
|
||||||
|
activeBg: 'blue.900',
|
||||||
|
inactive: 'gray.600',
|
||||||
|
inactiveBg: 'gray.700',
|
||||||
|
},
|
||||||
|
downstream: {
|
||||||
|
active: 'green.500',
|
||||||
|
activeBg: 'green.900',
|
||||||
|
inactive: 'gray.600',
|
||||||
|
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 : 'transparent'}
|
||||||
|
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 : colors.inactive}
|
||||||
|
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 ScoreBar } from './ScoreBar';
|
||||||
export { default as BusinessTreeItem } from './BusinessTreeItem';
|
export { default as BusinessTreeItem } from './BusinessTreeItem';
|
||||||
export { default as KeyFactorCard } from './KeyFactorCard';
|
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,184 +2,218 @@
|
|||||||
* 产业链分析卡片
|
* 产业链分析卡片
|
||||||
*
|
*
|
||||||
* 显示产业链层级视图和流向关系
|
* 显示产业链层级视图和流向关系
|
||||||
|
* 黑金主题风格 + 流程式导航
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState, useMemo, memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
VStack,
|
|
||||||
HStack,
|
HStack,
|
||||||
Text,
|
Text,
|
||||||
Heading,
|
Heading,
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
|
||||||
Icon,
|
Icon,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Tabs,
|
|
||||||
TabList,
|
|
||||||
TabPanels,
|
|
||||||
Tab,
|
|
||||||
TabPanel,
|
|
||||||
Center,
|
Center,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { FaNetworkWired } from 'react-icons/fa';
|
import { FaNetworkWired } from 'react-icons/fa';
|
||||||
import ReactECharts from 'echarts-for-react';
|
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 ValueChainNodeCard from '../organisms/ValueChainNodeCard';
|
||||||
import { getSankeyChartOption } from '../utils/chartOptions';
|
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 {
|
interface ValueChainCardProps {
|
||||||
valueChainData: ValueChainData;
|
valueChainData: ValueChainData;
|
||||||
|
companyName?: string;
|
||||||
cardBg?: string;
|
cardBg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ValueChainCard: React.FC<ValueChainCardProps> = ({
|
const ValueChainCard: React.FC<ValueChainCardProps> = memo(({
|
||||||
valueChainData,
|
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 nodesByLevel = valueChainData.value_chain_structure?.nodes_by_level;
|
||||||
|
|
||||||
// 获取上游节点
|
// 获取上游节点
|
||||||
const upstreamNodes = [
|
const upstreamNodes = useMemo(() => [
|
||||||
...(nodesByLevel?.['level_-2'] || []),
|
...(nodesByLevel?.['level_-2'] || []),
|
||||||
...(nodesByLevel?.['level_-1'] || []),
|
...(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_1'] || []),
|
||||||
...(nodesByLevel?.['level_2'] || []),
|
...(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 (
|
return (
|
||||||
<Card bg={cardBg} shadow="md">
|
<Card bg={THEME.cardBg} shadow="md">
|
||||||
|
{/* 头部区域 */}
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<HStack>
|
<HStack flexWrap="wrap" gap={2}>
|
||||||
<Icon as={FaNetworkWired} color="teal.500" />
|
<Icon as={FaNetworkWired} color={THEME.gold} />
|
||||||
<Heading size="sm">产业链分析</Heading>
|
<Heading size="sm" color={THEME.textPrimary}>
|
||||||
<HStack spacing={2}>
|
产业链分析
|
||||||
<Badge colorScheme="orange">
|
</Heading>
|
||||||
上游 {valueChainData.analysis_summary?.upstream_nodes || 0}
|
<Text color={THEME.textSecondary} fontSize="sm">
|
||||||
</Badge>
|
| {companyName}供应链图谱
|
||||||
<Badge colorScheme="blue">
|
</Text>
|
||||||
核心 {valueChainData.analysis_summary?.company_nodes || 0}
|
<Badge bg={THEME.gold} color="gray.900">
|
||||||
</Badge>
|
节点 {totalNodes}
|
||||||
<Badge colorScheme="green">
|
</Badge>
|
||||||
下游 {valueChainData.analysis_summary?.downstream_nodes || 0}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
|
||||||
<DisclaimerBox />
|
|
||||||
<Tabs variant="soft-rounded" colorScheme="teal">
|
|
||||||
<TabList>
|
|
||||||
<Tab>层级视图</Tab>
|
|
||||||
<Tab>流向关系</Tab>
|
|
||||||
</TabList>
|
|
||||||
|
|
||||||
<TabPanels>
|
<CardBody px={0}>
|
||||||
{/* 层级视图 */}
|
{/* 工具栏:左侧流程导航 + 右侧筛选 */}
|
||||||
<TabPanel>
|
<Flex
|
||||||
<VStack spacing={8} align="stretch">
|
px={0}
|
||||||
{/* 上游供应链 */}
|
py={3}
|
||||||
{upstreamNodes.length > 0 && (
|
borderBottom="1px solid"
|
||||||
<Box>
|
borderColor="gray.700"
|
||||||
<HStack mb={4}>
|
justify="space-between"
|
||||||
<Badge colorScheme="orange" fontSize="md" px={3} py={1}>
|
align="center"
|
||||||
上游供应链
|
flexWrap="wrap"
|
||||||
</Badge>
|
gap={3}
|
||||||
<Text fontSize="sm" color="gray.600">
|
>
|
||||||
原材料与供应商
|
{/* 左侧:流程式导航 */}
|
||||||
</Text>
|
<ProcessNavigation
|
||||||
</HStack>
|
activeTab={activeTab}
|
||||||
<SimpleGrid columns={{ base: 2, md: 3, lg: 4 }} spacing={4}>
|
onTabChange={setActiveTab}
|
||||||
{upstreamNodes.map((node, idx) => (
|
upstreamCount={upstreamNodes.length}
|
||||||
<ValueChainNodeCard
|
coreCount={coreNodes.length}
|
||||||
key={idx}
|
downstreamCount={downstreamNodes.length}
|
||||||
node={node}
|
/>
|
||||||
level={node.node_level}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 核心企业 */}
|
{/* 右侧:筛选与视图切换 */}
|
||||||
{coreNodes.length > 0 && (
|
<ValueChainFilterBar
|
||||||
<Box>
|
typeFilter={typeFilter}
|
||||||
<HStack mb={4}>
|
onTypeChange={setTypeFilter}
|
||||||
<Badge colorScheme="blue" fontSize="md" px={3} py={1}>
|
importanceFilter={importanceFilter}
|
||||||
核心企业
|
onImportanceChange={setImportanceFilter}
|
||||||
</Badge>
|
viewMode={viewMode}
|
||||||
<Text fontSize="sm" color="gray.600">
|
onViewModeChange={setViewMode}
|
||||||
公司主体与产品
|
/>
|
||||||
</Text>
|
</Flex>
|
||||||
</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 px={0} pt={4}>
|
||||||
<Box>
|
{viewMode === 'hierarchy' ? (
|
||||||
<HStack mb={4}>
|
filteredNodes.length > 0 ? (
|
||||||
<Badge colorScheme="green" fontSize="md" px={3} py={1}>
|
<SimpleGrid columns={{ base: 2, md: 3, lg: 4 }} spacing={4}>
|
||||||
下游客户
|
{filteredNodes.map((node, idx) => (
|
||||||
</Badge>
|
<ValueChainNodeCard
|
||||||
<Text fontSize="sm" color="gray.600">
|
key={idx}
|
||||||
客户与终端市场
|
node={node}
|
||||||
</Text>
|
isCompany={node.node_type === 'company'}
|
||||||
</HStack>
|
level={node.node_level}
|
||||||
<SimpleGrid columns={{ base: 2, md: 3, lg: 4 }} spacing={4}>
|
/>
|
||||||
{downstreamNodes.map((node, idx) => (
|
))}
|
||||||
<ValueChainNodeCard
|
</SimpleGrid>
|
||||||
key={idx}
|
) : (
|
||||||
node={node}
|
<Center h="200px">
|
||||||
level={node.node_level}
|
<Text color={THEME.textSecondary}>暂无匹配的节点数据</Text>
|
||||||
/>
|
</Center>
|
||||||
))}
|
)
|
||||||
</SimpleGrid>
|
) : sankeyOption ? (
|
||||||
</Box>
|
<ReactECharts
|
||||||
)}
|
option={sankeyOption}
|
||||||
</VStack>
|
style={{ height: '500px' }}
|
||||||
</TabPanel>
|
theme="dark"
|
||||||
|
/>
|
||||||
{/* 流向关系 */}
|
) : (
|
||||||
<TabPanel>
|
<Center h="200px">
|
||||||
{sankeyOption ? (
|
<Text color={THEME.textSecondary}>暂无流向数据</Text>
|
||||||
<ReactECharts
|
</Center>
|
||||||
option={sankeyOption}
|
)}
|
||||||
style={{ height: '500px' }}
|
</Box>
|
||||||
theme="light"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Center h="200px">
|
|
||||||
<Text color="gray.500">暂无流向数据</Text>
|
|
||||||
</Center>
|
|
||||||
)}
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ValueChainCard.displayName = 'ValueChainCard';
|
||||||
|
|
||||||
export default ValueChainCard;
|
export default ValueChainCard;
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
* 产业链节点卡片组件
|
* 产业链节点卡片组件
|
||||||
*
|
*
|
||||||
* 显示产业链中的单个节点,点击可展开查看相关公司
|
* 显示产业链中的单个节点,点击可展开查看相关公司
|
||||||
|
* 黑金主题风格
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState, memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
@@ -37,6 +38,36 @@ import type { ValueChainNodeCardProps, RelatedCompany } from '../../types';
|
|||||||
|
|
||||||
const API_BASE_URL = getApiBase();
|
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,
|
service: FaCog,
|
||||||
channel: FaNetworkWired,
|
channel: FaNetworkWired,
|
||||||
raw_material: FaFlask,
|
raw_material: FaFlask,
|
||||||
|
regulator: FaBuilding,
|
||||||
|
end_user: FaUserTie,
|
||||||
};
|
};
|
||||||
return icons[type] || FaBuilding;
|
return icons[type] || FaBuilding;
|
||||||
};
|
};
|
||||||
@@ -64,7 +97,7 @@ const getImportanceColor = (score?: number): string => {
|
|||||||
return 'green';
|
return 'green';
|
||||||
};
|
};
|
||||||
|
|
||||||
const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = memo(({
|
||||||
node,
|
node,
|
||||||
isCompany = false,
|
isCompany = false,
|
||||||
level = 0,
|
level = 0,
|
||||||
@@ -74,17 +107,14 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
|||||||
const [loadingRelated, setLoadingRelated] = useState(false);
|
const [loadingRelated, setLoadingRelated] = useState(false);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// 根据层级和是否为核心企业确定颜色方案
|
// 根据层级确定颜色方案
|
||||||
const getColorScheme = (): string => {
|
const getColorConfig = () => {
|
||||||
if (isCompany) return 'blue';
|
if (isCompany || level === 0) return THEME.core;
|
||||||
if (level < 0) return 'orange';
|
if (level < 0) return THEME.upstream;
|
||||||
if (level > 0) return 'green';
|
return THEME.downstream;
|
||||||
return 'gray';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorScheme = getColorScheme();
|
const colorConfig = getColorConfig();
|
||||||
const bgColor = `${colorScheme}.50`;
|
|
||||||
const borderColor = `${colorScheme}.200`;
|
|
||||||
|
|
||||||
// 获取相关公司数据
|
// 获取相关公司数据
|
||||||
const fetchRelatedCompanies = async () => {
|
const fetchRelatedCompanies = async () => {
|
||||||
@@ -135,16 +165,16 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
|||||||
<>
|
<>
|
||||||
<ScaleFade in={true} initialScale={0.9}>
|
<ScaleFade in={true} initialScale={0.9}>
|
||||||
<Card
|
<Card
|
||||||
bg={bgColor}
|
bg={colorConfig.bg}
|
||||||
borderColor={borderColor}
|
borderColor={colorConfig.border}
|
||||||
borderWidth={isCompany ? 3 : 1}
|
borderWidth={isCompany ? 2 : 1}
|
||||||
shadow={isCompany ? 'lg' : 'sm'}
|
shadow={isCompany ? 'lg' : 'sm'}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
_hover={{
|
_hover={{
|
||||||
shadow: 'xl',
|
shadow: 'xl',
|
||||||
transform: 'translateY(-4px)',
|
transform: 'translateY(-4px)',
|
||||||
borderColor: `${colorScheme}.400`,
|
borderColor: THEME.gold,
|
||||||
}}
|
}}
|
||||||
transition="all 0.3s ease"
|
transition="all 0.3s ease"
|
||||||
minH="140px"
|
minH="140px"
|
||||||
@@ -155,11 +185,11 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
|||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
<Icon
|
<Icon
|
||||||
as={getNodeTypeIcon(node.node_type)}
|
as={getNodeTypeIcon(node.node_type)}
|
||||||
color={`${colorScheme}.500`}
|
color={colorConfig.icon}
|
||||||
boxSize={5}
|
boxSize={5}
|
||||||
/>
|
/>
|
||||||
{isCompany && (
|
{isCompany && (
|
||||||
<Badge colorScheme="blue" variant="solid">
|
<Badge colorScheme={colorConfig.badge} variant="solid">
|
||||||
核心企业
|
核心企业
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -168,28 +198,28 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
|||||||
node.importance_score >= 70 && (
|
node.importance_score >= 70 && (
|
||||||
<Tooltip label="重要节点">
|
<Tooltip label="重要节点">
|
||||||
<span>
|
<span>
|
||||||
<Icon as={FaStar} color="orange.400" boxSize={4} />
|
<Icon as={FaStar} color={THEME.gold} boxSize={4} />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<Text fontWeight="bold" fontSize="sm" noOfLines={2}>
|
<Text fontWeight="bold" fontSize="sm" color={THEME.textPrimary} noOfLines={2}>
|
||||||
{node.node_name}
|
{node.node_name}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{node.node_description && (
|
{node.node_description && (
|
||||||
<Text fontSize="xs" color="gray.600" noOfLines={2}>
|
<Text fontSize="xs" color={THEME.textSecondary} noOfLines={2}>
|
||||||
{node.node_description}
|
{node.node_description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<HStack spacing={2} flexWrap="wrap">
|
<HStack spacing={2} flexWrap="wrap">
|
||||||
<Badge variant="subtle" size="sm" colorScheme={colorScheme}>
|
<Badge variant="subtle" size="sm" colorScheme={colorConfig.badge}>
|
||||||
{node.node_type}
|
{node.node_type}
|
||||||
</Badge>
|
</Badge>
|
||||||
{node.market_share !== undefined && (
|
{node.market_share !== undefined && (
|
||||||
<Badge variant="outline" size="sm">
|
<Badge variant="outline" size="sm" color={THEME.goldLight}>
|
||||||
份额 {node.market_share}%
|
份额 {node.market_share}%
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -198,10 +228,10 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
|||||||
{node.importance_score !== undefined && (
|
{node.importance_score !== undefined && (
|
||||||
<Box>
|
<Box>
|
||||||
<HStack justify="space-between" mb={1}>
|
<HStack justify="space-between" mb={1}>
|
||||||
<Text fontSize="xs" color="gray.500">
|
<Text fontSize="xs" color={THEME.textSecondary}>
|
||||||
重要度
|
重要度
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" fontWeight="bold">
|
<Text fontSize="xs" fontWeight="bold" color={THEME.goldLight}>
|
||||||
{node.importance_score}
|
{node.importance_score}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -210,6 +240,7 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
|||||||
size="xs"
|
size="xs"
|
||||||
colorScheme={getImportanceColor(node.importance_score)}
|
colorScheme={getImportanceColor(node.importance_score)}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
|
bg="gray.600"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -223,12 +254,14 @@ const ValueChainNodeCard: React.FC<ValueChainNodeCardProps> = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
node={node}
|
node={node}
|
||||||
isCompany={isCompany}
|
isCompany={isCompany}
|
||||||
colorScheme={colorScheme}
|
colorScheme={colorConfig.badge}
|
||||||
relatedCompanies={relatedCompanies}
|
relatedCompanies={relatedCompanies}
|
||||||
loadingRelated={loadingRelated}
|
loadingRelated={loadingRelated}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
ValueChainNodeCard.displayName = 'ValueChainNodeCard';
|
||||||
|
|
||||||
export default ValueChainNodeCard;
|
export default ValueChainNodeCard;
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ export interface AnalysisSummary {
|
|||||||
upstream_nodes?: number;
|
upstream_nodes?: number;
|
||||||
company_nodes?: number;
|
company_nodes?: number;
|
||||||
downstream_nodes?: number;
|
downstream_nodes?: number;
|
||||||
|
total_nodes?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValueChainData {
|
export interface ValueChainData {
|
||||||
|
|||||||
Reference in New Issue
Block a user