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