perf(CompetitiveAnalysisCard): 渲染优化与黑金 UI
- 渲染优化: React.memo, useMemo, 样式常量提取 - 子组件拆分: CompetitorTags, ScoreSection, AdvantagesSection - 黑金 UI: 金色边框、金色标题、白色内容、深色雷达图主题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* 显示竞争力评分、雷达图和竞争分析
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
@@ -35,148 +35,187 @@ import {
|
||||
FaUsers,
|
||||
} from 'react-icons/fa';
|
||||
import ReactECharts from 'echarts-for-react';
|
||||
import { DisclaimerBox, ScoreBar } from '../atoms';
|
||||
import { ScoreBar } from '../atoms';
|
||||
import { getRadarChartOption } from '../utils/chartOptions';
|
||||
import type { ComprehensiveData } from '../types';
|
||||
import type { ComprehensiveData, CompetitivePosition } from '../types';
|
||||
|
||||
// 样式常量 - 避免每次渲染创建新对象
|
||||
const CARD_STYLES = {
|
||||
bg: 'transparent',
|
||||
border: '1px solid',
|
||||
borderColor: 'yellow.600',
|
||||
shadow: 'md',
|
||||
} as const;
|
||||
|
||||
const CONTENT_BOX_STYLES = {
|
||||
p: 4,
|
||||
border: '1px solid',
|
||||
borderColor: 'yellow.600',
|
||||
borderRadius: 'md',
|
||||
} as const;
|
||||
|
||||
const GRID_COLSPAN = { base: 2, lg: 1 } as const;
|
||||
const CHART_STYLE = { height: '320px' } as const;
|
||||
|
||||
interface CompetitiveAnalysisCardProps {
|
||||
comprehensiveData: ComprehensiveData;
|
||||
cardBg?: string;
|
||||
}
|
||||
|
||||
const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({
|
||||
comprehensiveData,
|
||||
cardBg,
|
||||
}) => {
|
||||
const competitivePosition = comprehensiveData.competitive_position;
|
||||
if (!competitivePosition) return null;
|
||||
// 竞争对手标签组件
|
||||
interface CompetitorTagsProps {
|
||||
competitors: string[];
|
||||
}
|
||||
|
||||
const radarOption = getRadarChartOption(comprehensiveData);
|
||||
const CompetitorTags = memo<CompetitorTagsProps>(({ competitors }) => (
|
||||
<Box mb={4}>
|
||||
<Text fontWeight="bold" fontSize="sm" mb={2} color="yellow.500">
|
||||
主要竞争对手
|
||||
</Text>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{competitors.map((competitor, idx) => (
|
||||
<Tag
|
||||
key={idx}
|
||||
size="md"
|
||||
variant="outline"
|
||||
borderColor="yellow.600"
|
||||
color="yellow.500"
|
||||
borderRadius="full"
|
||||
>
|
||||
<Icon as={FaUsers} mr={1} />
|
||||
<TagLabel>{competitor}</TagLabel>
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
</Box>
|
||||
));
|
||||
|
||||
return (
|
||||
<Card bg={cardBg} shadow="md">
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FaTrophy} color="gold" />
|
||||
<Heading size="sm">竞争地位分析</Heading>
|
||||
{competitivePosition.ranking && (
|
||||
<Badge colorScheme="purple" ml={2}>
|
||||
行业排名 {competitivePosition.ranking.industry_rank}/
|
||||
{competitivePosition.ranking.total_companies}
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DisclaimerBox />
|
||||
CompetitorTags.displayName = 'CompetitorTags';
|
||||
|
||||
{/* 主要竞争对手 */}
|
||||
{competitivePosition.analysis?.main_competitors && (
|
||||
<Box mb={4}>
|
||||
<Text fontWeight="bold" fontSize="sm" mb={2} color="gray.600">
|
||||
主要竞争对手
|
||||
</Text>
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{competitivePosition.analysis.main_competitors
|
||||
.split(',')
|
||||
.map((competitor, idx) => (
|
||||
<Tag
|
||||
key={idx}
|
||||
size="md"
|
||||
colorScheme="purple"
|
||||
variant="outline"
|
||||
borderRadius="full"
|
||||
>
|
||||
<Icon as={FaUsers} mr={1} />
|
||||
<TagLabel>{competitor.trim()}</TagLabel>
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
// 评分区域组件
|
||||
interface ScoreSectionProps {
|
||||
scores: CompetitivePosition['scores'];
|
||||
}
|
||||
|
||||
{/* 评分和雷达图 */}
|
||||
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
|
||||
<GridItem colSpan={{ base: 2, lg: 1 }}>
|
||||
<VStack spacing={4} align="stretch">
|
||||
<ScoreBar
|
||||
label="市场地位"
|
||||
score={competitivePosition.scores?.market_position}
|
||||
icon={FaTrophy}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="技术实力"
|
||||
score={competitivePosition.scores?.technology}
|
||||
icon={FaCog}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="品牌价值"
|
||||
score={competitivePosition.scores?.brand}
|
||||
icon={FaStar}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="运营效率"
|
||||
score={competitivePosition.scores?.operation}
|
||||
icon={FaChartLine}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="财务健康"
|
||||
score={competitivePosition.scores?.finance}
|
||||
icon={FaDollarSign}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="创新能力"
|
||||
score={competitivePosition.scores?.innovation}
|
||||
icon={FaFlask}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="风险控制"
|
||||
score={competitivePosition.scores?.risk}
|
||||
icon={FaShieldAlt}
|
||||
/>
|
||||
<ScoreBar
|
||||
label="成长潜力"
|
||||
score={competitivePosition.scores?.growth}
|
||||
icon={FaRocket}
|
||||
/>
|
||||
</VStack>
|
||||
</GridItem>
|
||||
const ScoreSection = memo<ScoreSectionProps>(({ scores }) => (
|
||||
<VStack spacing={4} align="stretch">
|
||||
<ScoreBar label="市场地位" score={scores?.market_position} icon={FaTrophy} />
|
||||
<ScoreBar label="技术实力" score={scores?.technology} icon={FaCog} />
|
||||
<ScoreBar label="品牌价值" score={scores?.brand} icon={FaStar} />
|
||||
<ScoreBar label="运营效率" score={scores?.operation} icon={FaChartLine} />
|
||||
<ScoreBar label="财务健康" score={scores?.finance} icon={FaDollarSign} />
|
||||
<ScoreBar label="创新能力" score={scores?.innovation} icon={FaFlask} />
|
||||
<ScoreBar label="风险控制" score={scores?.risk} icon={FaShieldAlt} />
|
||||
<ScoreBar label="成长潜力" score={scores?.growth} icon={FaRocket} />
|
||||
</VStack>
|
||||
));
|
||||
|
||||
<GridItem colSpan={{ base: 2, lg: 1 }}>
|
||||
{radarOption && (
|
||||
<ReactECharts
|
||||
option={radarOption}
|
||||
style={{ height: '320px' }}
|
||||
theme="light"
|
||||
/>
|
||||
ScoreSection.displayName = 'ScoreSection';
|
||||
|
||||
// 竞争优劣势组件
|
||||
interface AdvantagesSectionProps {
|
||||
advantages?: string;
|
||||
disadvantages?: string;
|
||||
}
|
||||
|
||||
const AdvantagesSection = memo<AdvantagesSectionProps>(
|
||||
({ advantages, disadvantages }) => (
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
<Box {...CONTENT_BOX_STYLES}>
|
||||
<Text fontWeight="bold" fontSize="sm" mb={2} color="green.400">
|
||||
竞争优势
|
||||
</Text>
|
||||
<Text fontSize="sm" color="white">
|
||||
{advantages || '暂无数据'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box {...CONTENT_BOX_STYLES}>
|
||||
<Text fontWeight="bold" fontSize="sm" mb={2} color="red.400">
|
||||
竞争劣势
|
||||
</Text>
|
||||
<Text fontSize="sm" color="white">
|
||||
{disadvantages || '暂无数据'}
|
||||
</Text>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
)
|
||||
);
|
||||
|
||||
AdvantagesSection.displayName = 'AdvantagesSection';
|
||||
|
||||
const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = memo(
|
||||
({ comprehensiveData }) => {
|
||||
const competitivePosition = comprehensiveData.competitive_position;
|
||||
if (!competitivePosition) return null;
|
||||
|
||||
// 缓存雷达图配置
|
||||
const radarOption = useMemo(
|
||||
() => getRadarChartOption(comprehensiveData),
|
||||
[comprehensiveData]
|
||||
);
|
||||
|
||||
// 缓存竞争对手列表
|
||||
const competitors = useMemo(
|
||||
() =>
|
||||
competitivePosition.analysis?.main_competitors
|
||||
?.split(',')
|
||||
.map((c) => c.trim()) || [],
|
||||
[competitivePosition.analysis?.main_competitors]
|
||||
);
|
||||
|
||||
return (
|
||||
<Card {...CARD_STYLES}>
|
||||
<CardHeader>
|
||||
<HStack>
|
||||
<Icon as={FaTrophy} color="yellow.500" />
|
||||
<Heading size="sm" color="yellow.500">竞争地位分析</Heading>
|
||||
{competitivePosition.ranking && (
|
||||
<Badge
|
||||
ml={2}
|
||||
bg="transparent"
|
||||
border="1px solid"
|
||||
borderColor="yellow.600"
|
||||
color="yellow.500"
|
||||
>
|
||||
行业排名 {competitivePosition.ranking.industry_rank}/
|
||||
{competitivePosition.ranking.total_companies}
|
||||
</Badge>
|
||||
)}
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
{/* 主要竞争对手 */}
|
||||
{competitors.length > 0 && <CompetitorTags competitors={competitors} />}
|
||||
|
||||
<Divider my={4} />
|
||||
{/* 评分和雷达图 */}
|
||||
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
|
||||
<GridItem colSpan={GRID_COLSPAN}>
|
||||
<ScoreSection scores={competitivePosition.scores} />
|
||||
</GridItem>
|
||||
|
||||
{/* 竞争优势和劣势 */}
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
<Box>
|
||||
<Text fontWeight="bold" fontSize="sm" mb={2} color="red.600">
|
||||
竞争优势
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
{competitivePosition.analysis?.competitive_advantages || '暂无数据'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontWeight="bold" fontSize="sm" mb={2} color="green.600">
|
||||
竞争劣势
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
{competitivePosition.analysis?.competitive_disadvantages || '暂无数据'}
|
||||
</Text>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
<GridItem colSpan={GRID_COLSPAN}>
|
||||
{radarOption && (
|
||||
<ReactECharts
|
||||
option={radarOption}
|
||||
style={CHART_STYLE}
|
||||
theme="dark"
|
||||
/>
|
||||
)}
|
||||
</GridItem>
|
||||
</Grid>
|
||||
|
||||
<Divider my={4} borderColor="yellow.600" />
|
||||
|
||||
{/* 竞争优势和劣势 */}
|
||||
<AdvantagesSection
|
||||
advantages={competitivePosition.analysis?.competitive_advantages}
|
||||
disadvantages={competitivePosition.analysis?.competitive_disadvantages}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CompetitiveAnalysisCard.displayName = 'CompetitiveAnalysisCard';
|
||||
|
||||
export default CompetitiveAnalysisCard;
|
||||
|
||||
Reference in New Issue
Block a user