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