feat(CompetitiveAnalysisCard): 竞争优劣势改为 bullet point 列表显示

- 添加 parseToList 函数,支持按换行符或分号/顿号/逗号分割
- 自动清理数字序号(如 "1. xxx")
- 新增 AdvantageList 组件,使用 memo 和 useMemo 优化
- 列表圆点颜色与标题颜色保持一致(绿色/红色)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-26 15:11:23 +08:00
parent b7ad35ba12
commit 3b998b4339

View File

@@ -5,7 +5,7 @@
* 包含行业排名弹窗功能
*/
import React, { memo, useMemo } from 'react';
import React, { memo, useMemo } from "react";
import {
Card,
CardBody,
@@ -28,10 +28,12 @@ import {
ModalOverlay,
ModalContent,
ModalHeader,
UnorderedList,
ListItem,
ModalBody,
ModalCloseButton,
useDisclosure,
} from '@chakra-ui/react';
} from "@chakra-ui/react";
import {
Trophy,
Settings,
@@ -43,47 +45,51 @@ import {
Rocket,
Users,
ExternalLink,
} from 'lucide-react';
import ReactECharts from 'echarts-for-react';
import { ScoreBar } from '../atoms';
import { getRadarChartOption } from '../utils/chartOptions';
import { IndustryRankingView } from '../../../FinancialPanorama/components';
import type { ComprehensiveData, CompetitivePosition, IndustryRankData } from '../types';
} from "lucide-react";
import ReactECharts from "echarts-for-react";
import { ScoreBar } from "../atoms";
import { getRadarChartOption } from "../utils/chartOptions";
import { IndustryRankingView } from "../../../FinancialPanorama/components";
import type {
ComprehensiveData,
CompetitivePosition,
IndustryRankData,
} from "../types";
// 黑金主题弹窗样式
const MODAL_STYLES = {
content: {
bg: 'gray.900',
borderColor: 'rgba(212, 175, 55, 0.3)',
borderWidth: '1px',
maxW: '900px',
bg: "gray.900",
borderColor: "rgba(212, 175, 55, 0.3)",
borderWidth: "1px",
maxW: "900px",
},
header: {
color: 'yellow.500',
borderBottomColor: 'rgba(212, 175, 55, 0.2)',
borderBottomWidth: '1px',
color: "yellow.500",
borderBottomColor: "rgba(212, 175, 55, 0.2)",
borderBottomWidth: "1px",
},
closeButton: {
color: 'yellow.500',
_hover: { bg: 'rgba(212, 175, 55, 0.1)' },
color: "yellow.500",
_hover: { bg: "rgba(212, 175, 55, 0.1)" },
},
} as const;
// 样式常量 - 避免每次渲染创建新对象
const CARD_STYLES = {
bg: 'transparent',
shadow: 'md',
bg: "transparent",
shadow: "md",
} as const;
const CONTENT_BOX_STYLES = {
p: 4,
border: '1px solid',
borderColor: 'yellow.600',
borderRadius: 'md',
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;
const CHART_STYLE = { height: "320px" } as const;
interface CompetitiveAnalysisCardProps {
comprehensiveData: ComprehensiveData;
@@ -118,11 +124,11 @@ const CompetitorTags = memo<CompetitorTagsProps>(({ competitors }) => (
</Box>
));
CompetitorTags.displayName = 'CompetitorTags';
CompetitorTags.displayName = "CompetitorTags";
// 评分区域组件
interface ScoreSectionProps {
scores: CompetitivePosition['scores'];
scores: CompetitivePosition["scores"];
}
const ScoreSection = memo<ScoreSectionProps>(({ scores }) => (
@@ -138,7 +144,52 @@ const ScoreSection = memo<ScoreSectionProps>(({ scores }) => (
</VStack>
));
ScoreSection.displayName = 'ScoreSection';
ScoreSection.displayName = "ScoreSection";
// 将文本按换行符或分号拆分为列表项
const parseToList = (text: string): string[] => {
if (!text) return [];
// 优先按换行符拆分,其次按分号拆分
const items = text.includes("\n")
? text.split("\n")
: text.split(/[;、,]/);
// 清理数字序号(如 "1. ")并过滤空项
return items.map((s) => s.trim().replace(/^\d+\.\s*/, "")).filter(Boolean);
};
// 优劣势列表项组件
interface AdvantageListProps {
title: string;
content?: string;
color: string;
}
const AdvantageList = memo<AdvantageListProps>(({ title, content, color }) => {
const items = useMemo(() => parseToList(content || ""), [content]);
return (
<Box {...CONTENT_BOX_STYLES}>
<Text fontWeight="bold" fontSize="sm" mb={2} color={color}>
{title}
</Text>
{items.length > 0 ? (
<UnorderedList spacing={1} pl={2} styleType="disc" color={color}>
{items.map((item, index) => (
<ListItem key={index} fontSize="sm" color="white">
{item}
</ListItem>
))}
</UnorderedList>
) : (
<Text fontSize="sm" color="white">
</Text>
)}
</Box>
);
});
AdvantageList.displayName = "AdvantageList";
// 竞争优劣势组件
interface AdvantagesSectionProps {
@@ -149,27 +200,13 @@ interface AdvantagesSectionProps {
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>
<AdvantageList title="竞争优势" content={advantages} color="green.400" />
<AdvantageList title="竞争劣势" content={disadvantages} color="red.400" />
</SimpleGrid>
)
);
AdvantagesSection.displayName = 'AdvantagesSection';
AdvantagesSection.displayName = "AdvantagesSection";
const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = memo(
({ comprehensiveData, industryRankData }) => {
@@ -179,16 +216,15 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = memo(
if (!competitivePosition) return null;
// 缓存雷达图配置
const radarOption = useMemo(
() => getRadarChartOption(comprehensiveData),
[comprehensiveData]
);
const radarOption = useMemo(() => getRadarChartOption(comprehensiveData), [
comprehensiveData,
]);
// 缓存竞争对手列表
const competitors = useMemo(
() =>
competitivePosition.analysis?.main_competitors
?.split(',')
?.split(",")
.map((c) => c.trim()) || [],
[competitivePosition.analysis?.main_competitors]
);
@@ -202,7 +238,9 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = memo(
<CardHeader>
<HStack>
<Icon as={Trophy} color="yellow.500" />
<Heading size="sm" color="yellow.500"></Heading>
<Heading size="sm" color="yellow.500">
</Heading>
{competitivePosition.ranking && (
<Badge
ml={2}
@@ -210,9 +248,13 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = memo(
border="1px solid"
borderColor="yellow.600"
color="yellow.500"
cursor={hasIndustryRankData ? 'pointer' : 'default'}
cursor={hasIndustryRankData ? "pointer" : "default"}
onClick={hasIndustryRankData ? onOpen : undefined}
_hover={hasIndustryRankData ? { bg: 'rgba(212, 175, 55, 0.1)' } : undefined}
_hover={
hasIndustryRankData
? { bg: "rgba(212, 175, 55, 0.1)" }
: undefined
}
>
{competitivePosition.ranking.industry_rank}/
{competitivePosition.ranking.total_companies}
@@ -225,7 +267,7 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = memo(
color="yellow.500"
rightIcon={<Icon as={ExternalLink} boxSize={3} />}
onClick={onOpen}
_hover={{ bg: 'rgba(212, 175, 55, 0.1)' }}
_hover={{ bg: "rgba(212, 175, 55, 0.1)" }}
>
</Button>
@@ -234,7 +276,9 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = memo(
</CardHeader>
<CardBody>
{/* 主要竞争对手 */}
{competitors.length > 0 && <CompetitorTags competitors={competitors} />}
{competitors.length > 0 && (
<CompetitorTags competitors={competitors} />
)}
{/* 评分和雷达图 */}
<Grid templateColumns="repeat(2, 1fr)" gap={6}>
@@ -258,13 +302,20 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = memo(
{/* 竞争优势和劣势 */}
<AdvantagesSection
advantages={competitivePosition.analysis?.competitive_advantages}
disadvantages={competitivePosition.analysis?.competitive_disadvantages}
disadvantages={
competitivePosition.analysis?.competitive_disadvantages
}
/>
</CardBody>
</Card>
{/* 行业排名弹窗 - 黑金主题 */}
<Modal isOpen={isOpen} onClose={onClose} size="4xl" scrollBehavior="inside">
<Modal
isOpen={isOpen}
onClose={onClose}
size="4xl"
scrollBehavior="inside"
>
<ModalOverlay bg="blackAlpha.700" />
<ModalContent {...MODAL_STYLES.content}>
<ModalHeader {...MODAL_STYLES.header}>
@@ -290,6 +341,6 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = memo(
}
);
CompetitiveAnalysisCard.displayName = 'CompetitiveAnalysisCard';
CompetitiveAnalysisCard.displayName = "CompetitiveAnalysisCard";
export default CompetitiveAnalysisCard;