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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user