From 23112db11509cb97c852f757739169362be1a228 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Fri, 12 Dec 2025 14:04:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ValueChainCard):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BA=A7=E4=B8=9A=E9=93=BE=E5=88=86=E6=9E=90=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ProcessNavigation 流程导航组件(上游→核心→下游+副标题) - 新增 ValueChainFilterBar 筛选栏组件(类型/重要度/视图Tab切换) - 重构布局为左右分栏:左侧流程导航,右侧筛选+视图切换 - 移除 DisclaimerBox 免责声明 - ValueChainNodeCard 适配黑金主题 - 移除卡片内部左右边距 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../atoms/ProcessNavigation.tsx | 170 ++++++++++ .../atoms/ValueChainFilterBar.tsx | 151 +++++++++ .../DeepAnalysisTab/atoms/index.ts | 4 + .../components/ValueChainCard.tsx | 296 ++++++++++-------- .../organisms/ValueChainNodeCard/index.tsx | 85 +++-- .../CompanyOverview/DeepAnalysisTab/types.ts | 1 + 6 files changed, 550 insertions(+), 157 deletions(-) create mode 100644 src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/ProcessNavigation.tsx create mode 100644 src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/ValueChainFilterBar.tsx diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/ProcessNavigation.tsx b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/ProcessNavigation.tsx new file mode 100644 index 00000000..836393b3 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/ProcessNavigation.tsx @@ -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 = memo(({ + label, + subtitle, + count, + isActive, + colorKey, + onClick, +}) => { + const colors = THEME[colorKey]; + + return ( + + + + + {label} + + + {count} + + + + {subtitle} + + + + ); +}); + +NavItem.displayName = 'NavItem'; + +const ProcessNavigation: React.FC = memo(({ + activeTab, + onTabChange, + upstreamCount, + coreCount, + downstreamCount, +}) => { + return ( + + onTabChange('upstream')} + /> + + + + onTabChange('core')} + /> + + + + onTabChange('downstream')} + /> + + ); +}); + +ProcessNavigation.displayName = 'ProcessNavigation'; + +export default ProcessNavigation; diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/ValueChainFilterBar.tsx b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/ValueChainFilterBar.tsx new file mode 100644 index 00000000..0e7f63e4 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/ValueChainFilterBar.tsx @@ -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 = memo(({ + typeFilter, + onTypeChange, + importanceFilter, + onImportanceChange, + viewMode, + onViewModeChange, +}) => { + return ( + + {/* 左侧筛选区 */} + + + + + + + {/* 右侧视图切换 */} + onViewModeChange(index === 0 ? 'hierarchy' : 'flow')} + variant="soft-rounded" + size="sm" + > + + + 层级视图 + + + 流向关系 + + + + + ); +}); + +ValueChainFilterBar.displayName = 'ValueChainFilterBar'; + +export default ValueChainFilterBar; diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/index.ts b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/index.ts index 11267f56..0db9bd87 100644 --- a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/index.ts +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/atoms/index.ts @@ -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'; diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/components/ValueChainCard.tsx b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/components/ValueChainCard.tsx index 217aa9bb..3e46b8f5 100644 --- a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/components/ValueChainCard.tsx +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/components/ValueChainCard.tsx @@ -2,184 +2,218 @@ * 产业链分析卡片 * * 显示产业链层级视图和流向关系 + * 黑金主题风格 + 流程式导航 */ -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 = ({ +const ValueChainCard: React.FC = memo(({ valueChainData, - cardBg, + companyName = '目标公司', }) => { - const sankeyOption = getSankeyChartOption(valueChainData); + // 状态管理 + const [activeTab, setActiveTab] = useState('upstream'); + const [typeFilter, setTypeFilter] = useState('all'); + const [importanceFilter, setImportanceFilter] = useState('all'); + const [viewMode, setViewMode] = useState('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 ( - + + {/* 头部区域 */} - - - 产业链分析 - - - 上游 {valueChainData.analysis_summary?.upstream_nodes || 0} - - - 核心 {valueChainData.analysis_summary?.company_nodes || 0} - - - 下游 {valueChainData.analysis_summary?.downstream_nodes || 0} - - + + + + 产业链分析 + + + | {companyName}供应链图谱 + + + 节点 {totalNodes} + - - - - - 层级视图 - 流向关系 - - - {/* 层级视图 */} - - - {/* 上游供应链 */} - {upstreamNodes.length > 0 && ( - - - - 上游供应链 - - - 原材料与供应商 - - - - {upstreamNodes.map((node, idx) => ( - - ))} - - - )} + + {/* 工具栏:左侧流程导航 + 右侧筛选 */} + + {/* 左侧:流程式导航 */} + - {/* 核心企业 */} - {coreNodes.length > 0 && ( - - - - 核心企业 - - - 公司主体与产品 - - - - {coreNodes.map((node, idx) => ( - - ))} - - - )} + {/* 右侧:筛选与视图切换 */} + + - {/* 下游客户 */} - {downstreamNodes.length > 0 && ( - - - - 下游客户 - - - 客户与终端市场 - - - - {downstreamNodes.map((node, idx) => ( - - ))} - - - )} - - - - {/* 流向关系 */} - - {sankeyOption ? ( - - ) : ( -
- 暂无流向数据 -
- )} -
-
-
+ {/* 内容区域 */} + + {viewMode === 'hierarchy' ? ( + filteredNodes.length > 0 ? ( + + {filteredNodes.map((node, idx) => ( + + ))} + + ) : ( +
+ 暂无匹配的节点数据 +
+ ) + ) : sankeyOption ? ( + + ) : ( +
+ 暂无流向数据 +
+ )} +
); -}; +}); + +ValueChainCard.displayName = 'ValueChainCard'; export default ValueChainCard; diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/organisms/ValueChainNodeCard/index.tsx b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/organisms/ValueChainNodeCard/index.tsx index 5a29c0df..3f6245e4 100644 --- a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/organisms/ValueChainNodeCard/index.tsx +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/organisms/ValueChainNodeCard/index.tsx @@ -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 = ({ +const ValueChainNodeCard: React.FC = memo(({ node, isCompany = false, level = 0, @@ -74,17 +107,14 @@ const ValueChainNodeCard: React.FC = ({ 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 = ({ <> = ({ {isCompany && ( - + 核心企业 )} @@ -168,28 +198,28 @@ const ValueChainNodeCard: React.FC = ({ node.importance_score >= 70 && ( - + )} - + {node.node_name} {node.node_description && ( - + {node.node_description} )} - + {node.node_type} {node.market_share !== undefined && ( - + 份额 {node.market_share}% )} @@ -198,10 +228,10 @@ const ValueChainNodeCard: React.FC = ({ {node.importance_score !== undefined && ( - + 重要度 - + {node.importance_score} @@ -210,6 +240,7 @@ const ValueChainNodeCard: React.FC = ({ size="xs" colorScheme={getImportanceColor(node.importance_score)} borderRadius="full" + bg="gray.600" /> )} @@ -223,12 +254,14 @@ const ValueChainNodeCard: React.FC = ({ onClose={onClose} node={node} isCompany={isCompany} - colorScheme={colorScheme} + colorScheme={colorConfig.badge} relatedCompanies={relatedCompanies} loadingRelated={loadingRelated} /> ); -}; +}); + +ValueChainNodeCard.displayName = 'ValueChainNodeCard'; export default ValueChainNodeCard; diff --git a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/types.ts b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/types.ts index f6d756a6..3d5c5d96 100644 --- a/src/views/Company/components/CompanyOverview/DeepAnalysisTab/types.ts +++ b/src/views/Company/components/CompanyOverview/DeepAnalysisTab/types.ts @@ -174,6 +174,7 @@ export interface AnalysisSummary { upstream_nodes?: number; company_nodes?: number; downstream_nodes?: number; + total_nodes?: number; } export interface ValueChainData {