perf(BasicInfoTab): 性能优化与主题样式提取

- BranchesPanel: 添加 memo 包裹主组件和子组件
- BranchesPanel: 提取 BranchCard、EmptyState、InfoItem 为独立 memo 组件
- BranchesPanel: 预计算状态徽章样式避免每次渲染创建新对象
- BranchesPanel: 使用 useMemo 缓存状态类型计算结果
- config.ts: 扩展主题配置,添加 status、gradients、card、iconBg 配置
- StrategyAnalysisCard: ContentItem 中 parseToList 结果使用 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:04:09 +08:00
parent a7c5a72061
commit 8d609d5fbf
4 changed files with 333 additions and 148 deletions

View File

@@ -1,7 +1,7 @@
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/BranchesPanel.tsx // src/views/Company/components/CompanyOverview/BasicInfoTab/components/BranchesPanel.tsx
// 分支机构 Tab Panel - 黑金风格 // 分支机构 Tab Panel - 黑金风格
import React from "react"; import React, { memo, useMemo } from "react";
import { import {
Box, Box,
VStack, VStack,
@@ -11,7 +11,13 @@ import {
SimpleGrid, SimpleGrid,
Center, Center,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { GitBranch, Building2, CheckCircle, XCircle } from "lucide-react"; import {
GitBranch,
Building2,
CheckCircle,
XCircle,
HelpCircle,
} from "lucide-react";
import { useBranchesData } from "../../hooks/useBranchesData"; import { useBranchesData } from "../../hooks/useBranchesData";
import { THEME } from "../config"; import { THEME } from "../config";
@@ -24,23 +30,42 @@ interface BranchesPanelProps {
isActive?: boolean; isActive?: boolean;
} }
// 黑金卡片样式 // 状态分类关键词
const cardStyles = { const ACTIVE_KEYWORDS = ["存续", "在营", "开业", "在册", "在业"];
bg: "linear-gradient(145deg, rgba(30, 30, 35, 0.95), rgba(20, 20, 25, 0.98))", const INACTIVE_KEYWORDS = ["吊销", "注销", "撤销", "关闭", "歇业", "迁出"];
border: "1px solid",
borderColor: "rgba(212, 175, 55, 0.3)", // 获取状态分类
borderRadius: "12px", type StatusType = "active" | "inactive" | "unknown";
overflow: "hidden", const getStatusType = (status: string | undefined): StatusType => {
transition: "all 0.3s ease", if (!status || status === "其他") return "unknown";
_hover: { // 优先判断异常状态(因为"吊销,未注销"同时包含两个关键词)
borderColor: "rgba(212, 175, 55, 0.6)", if (INACTIVE_KEYWORDS.some((keyword) => status.includes(keyword))) {
boxShadow: "0 4px 20px rgba(212, 175, 55, 0.15), inset 0 1px 0 rgba(212, 175, 55, 0.1)", return "inactive";
transform: "translateY(-2px)", }
if (ACTIVE_KEYWORDS.some((keyword) => status.includes(keyword))) {
return "active";
}
return "unknown";
};
// 状态样式配置(使用 THEME.status 配置)
const STATUS_CONFIG = {
active: {
icon: CheckCircle,
...THEME.status.active,
},
inactive: {
icon: XCircle,
...THEME.status.inactive,
},
unknown: {
icon: HelpCircle,
...THEME.status.unknown,
}, },
}; };
// 状态徽章样式 // 状态徽章基础样式(静态部分)
const getStatusBadgeStyles = (isActive: boolean) => ({ const STATUS_BADGE_BASE = {
display: "inline-flex", display: "inline-flex",
alignItems: "center", alignItems: "center",
gap: "4px", gap: "4px",
@@ -49,14 +74,38 @@ const getStatusBadgeStyles = (isActive: boolean) => ({
borderRadius: "full", borderRadius: "full",
fontSize: "xs", fontSize: "xs",
fontWeight: "medium", fontWeight: "medium",
bg: isActive ? "rgba(212, 175, 55, 0.15)" : "rgba(255, 100, 100, 0.15)",
color: isActive ? THEME.gold : "#ff6b6b",
border: "1px solid", border: "1px solid",
borderColor: isActive ? "rgba(212, 175, 55, 0.3)" : "rgba(255, 100, 100, 0.3)", } as const;
});
// 信息项组件 // 预计算各状态的完整徽章样式,避免每次渲染创建新对象
const InfoItem: React.FC<{ label: string; value: string | number }> = ({ label, value }) => ( const STATUS_BADGE_STYLES = {
active: {
...STATUS_BADGE_BASE,
bg: STATUS_CONFIG.active.bgColor,
color: STATUS_CONFIG.active.color,
borderColor: STATUS_CONFIG.active.borderColor,
},
inactive: {
...STATUS_BADGE_BASE,
bg: STATUS_CONFIG.inactive.bgColor,
color: STATUS_CONFIG.inactive.color,
borderColor: STATUS_CONFIG.inactive.borderColor,
},
unknown: {
...STATUS_BADGE_BASE,
bg: STATUS_CONFIG.unknown.bgColor,
color: STATUS_CONFIG.unknown.color,
borderColor: STATUS_CONFIG.unknown.borderColor,
},
} as const;
// 信息项组件 - memo 优化
interface InfoItemProps {
label: string;
value: string | number;
}
const InfoItem = memo<InfoItemProps>(({ label, value }) => (
<VStack align="start" spacing={0.5}> <VStack align="start" spacing={0.5}>
<Text fontSize="xs" color={THEME.textSecondary} letterSpacing="0.5px"> <Text fontSize="xs" color={THEME.textSecondary} letterSpacing="0.5px">
{label} {label}
@@ -65,106 +114,127 @@ const InfoItem: React.FC<{ label: string; value: string | number }> = ({ label,
{value || "-"} {value || "-"}
</Text> </Text>
</VStack> </VStack>
); ));
const BranchesPanel: React.FC<BranchesPanelProps> = ({ stockCode, isActive = true }) => { InfoItem.displayName = "BranchInfoItem";
const { branches, loading } = useBranchesData({ stockCode, enabled: isActive });
if (loading) { // 空状态组件 - 独立 memo 避免重复渲染
return <BranchesSkeleton />; const EmptyState = memo(() => (
} <Center h="200px">
<VStack spacing={3}>
<Box
p={4}
borderRadius="full"
bg={THEME.iconBg}
border="1px solid"
borderColor={THEME.iconBgLight}
>
<Icon as={GitBranch} boxSize={10} color={THEME.gold} opacity={0.6} />
</Box>
<Text color={THEME.textSecondary} fontSize="sm">
</Text>
</VStack>
</Center>
));
if (branches.length === 0) { EmptyState.displayName = "BranchesEmptyState";
return (
<Center h="200px"> // 分支卡片组件 - 独立 memo 优化列表渲染
<VStack spacing={3}> interface BranchCardProps {
<Box branch: any;
p={4} }
borderRadius="full"
bg="rgba(212, 175, 55, 0.1)" const BranchCard = memo<BranchCardProps>(({ branch }) => {
border="1px solid" const statusType = useMemo(() => getStatusType(branch.business_status), [
borderColor="rgba(212, 175, 55, 0.2)" branch.business_status,
> ]);
<Icon as={GitBranch} boxSize={10} color={THEME.gold} opacity={0.6} /> const StatusIcon = STATUS_CONFIG[statusType].icon;
</Box>
<Text color={THEME.textSecondary} fontSize="sm"> // 缓存关联企业显示值
const relatedCompanyValue = useMemo(
</Text> () => `${branch.related_company_count || 0}`,
</VStack> [branch.related_company_count]
</Center> );
);
}
return ( return (
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}> <Box sx={THEME.card}>
{branches.map((branch: any, idx: number) => { {/* 顶部金色装饰线 */}
const isActive = branch.business_status === "存续"; <Box h="2px" bgGradient={THEME.gradients.decorLine} />
return ( <Box p={4}>
<Box key={idx} sx={cardStyles}> <VStack align="start" spacing={4}>
{/* 顶部金色装饰线 */} {/* 标题行 */}
<Box <HStack justify="space-between" w="full" align="flex-start">
h="2px" <HStack spacing={2} flex={1}>
bgGradient="linear(to-r, transparent, rgba(212, 175, 55, 0.6), transparent)" <Box p={1.5} borderRadius="md" bg={THEME.iconBg}>
/> <Icon as={Building2} boxSize={3.5} color={THEME.gold} />
</Box>
<Text
fontWeight="bold"
color={THEME.textPrimary}
fontSize="sm"
noOfLines={2}
lineHeight="tall"
>
{branch.branch_name}
</Text>
</HStack>
<Box p={4}> {/* 状态徽章 */}
<VStack align="start" spacing={4}> <Box sx={STATUS_BADGE_STYLES[statusType]}>
{/* 标题行 */} <Icon as={StatusIcon} boxSize={3} />
<HStack justify="space-between" w="full" align="flex-start"> <Text>{branch.business_status || "未知"}</Text>
<HStack spacing={2} flex={1}>
<Box
p={1.5}
borderRadius="md"
bg="rgba(212, 175, 55, 0.1)"
>
<Icon as={Building2} boxSize={3.5} color={THEME.gold} />
</Box>
<Text
fontWeight="bold"
color={THEME.textPrimary}
fontSize="sm"
noOfLines={2}
lineHeight="tall"
>
{branch.branch_name}
</Text>
</HStack>
{/* 状态徽章 */}
<Box sx={getStatusBadgeStyles(isActive)}>
<Icon
as={isActive ? CheckCircle : XCircle}
boxSize={3}
/>
<Text>{branch.business_status}</Text>
</Box>
</HStack>
{/* 分隔线 */}
<Box
w="full"
h="1px"
bgGradient="linear(to-r, rgba(212, 175, 55, 0.3), transparent)"
/>
{/* 信息网格 */}
<SimpleGrid columns={2} spacing={3} w="full">
<InfoItem label="注册资本" value={branch.register_capital} />
<InfoItem label="法人代表" value={branch.legal_person} />
<InfoItem label="成立日期" value={formatDate(branch.register_date)} />
<InfoItem
label="关联企业"
value={`${branch.related_company_count || 0}`}
/>
</SimpleGrid>
</VStack>
</Box> </Box>
</Box> </HStack>
);
})} {/* 分隔线 */}
</SimpleGrid> <Box w="full" h="1px" bgGradient={THEME.gradients.divider} />
{/* 信息网格 */}
<SimpleGrid columns={2} spacing={3} w="full">
<InfoItem label="注册资本" value={branch.register_capital} />
<InfoItem label="法人代表" value={branch.legal_person} />
<InfoItem
label="成立日期"
value={formatDate(branch.register_date)}
/>
<InfoItem label="关联企业" value={relatedCompanyValue} />
</SimpleGrid>
</VStack>
</Box>
</Box>
); );
}; });
BranchCard.displayName = "BranchCard";
// 主组件
const BranchesPanel: React.FC<BranchesPanelProps> = memo(
({ stockCode, isActive = true }) => {
const { branches, loading } = useBranchesData({
stockCode,
enabled: isActive,
});
if (loading) {
return <BranchesSkeleton />;
}
if (branches.length === 0) {
return <EmptyState />;
}
return (
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
{branches.map((branch: any, idx: number) => (
<BranchCard key={branch.branch_name || idx} branch={branch} />
))}
</SimpleGrid>
);
}
);
BranchesPanel.displayName = "BranchesPanel";
export default BranchesPanel; export default BranchesPanel;

View File

@@ -44,6 +44,7 @@ const ManagementCard: React.FC<ManagementCardProps> = ({ person, categoryColor }
name={person.name} name={person.name}
size="md" size="md"
bg={categoryColor} bg={categoryColor}
color="black"
/> />
<VStack align="start" spacing={1} flex={1}> <VStack align="start" spacing={1} flex={1}>
{/* 姓名和性别 */} {/* 姓名和性别 */}

View File

@@ -3,6 +3,28 @@
import { LucideIcon, Share2, UserRound, GitBranch, Info } from "lucide-react"; import { LucideIcon, Share2, UserRound, GitBranch, Info } from "lucide-react";
// 状态颜色类型
export interface StatusColors {
color: string;
bgColor: string;
borderColor: string;
}
// 卡片样式类型
export interface CardStyles {
bg: string;
border: string;
borderColor: string;
borderRadius: string;
overflow: string;
transition: string;
_hover: {
borderColor: string;
boxShadow: string;
transform: string;
};
}
// 主题类型定义 // 主题类型定义
export interface Theme { export interface Theme {
bg: string; bg: string;
@@ -21,6 +43,23 @@ export interface Theme {
tabUnselected: { tabUnselected: {
color: string; color: string;
}; };
// 状态颜色配置
status: {
active: StatusColors;
inactive: StatusColors;
unknown: StatusColors;
};
// 渐变配置
gradients: {
cardBg: string;
decorLine: string;
divider: string;
};
// 图标容器背景
iconBg: string;
iconBgLight: string;
// 通用卡片样式
card: CardStyles;
} }
// 黑金主题配置 // 黑金主题配置
@@ -30,17 +69,62 @@ export const THEME: Theme = {
cardBg: "gray.800", cardBg: "gray.800",
tableBg: "gray.700", tableBg: "gray.700",
tableHoverBg: "gray.600", tableHoverBg: "gray.600",
gold: "#F4D03F", // 亮黄金色(用于文字,对比度更好) gold: "#F4D03F", // 亮黄金色(用于文字,对比度更好)
goldLight: "#F0D78C", // 浅金色(用于次要文字) goldLight: "#F0D78C", // 浅金色(用于次要文字)
textPrimary: "white", textPrimary: "white",
textSecondary: "gray.400", textSecondary: "gray.400",
border: "rgba(212, 175, 55, 0.3)", // 边框保持原色 border: "rgba(212, 175, 55, 0.3)", // 边框保持原色
tabSelected: { tabSelected: {
bg: "#D4AF37", // 选中背景保持深金色 bg: "#D4AF37", // 选中背景保持深金色
color: "gray.900", color: "gray.900",
}, },
tabUnselected: { tabUnselected: {
color: "#F4D03F", // 未选中使用亮金色 color: "#F4D03F", // 未选中使用亮金色
},
// 状态颜色配置(用于分支机构等状态显示)
status: {
active: {
color: "#F4D03F",
bgColor: "rgba(212, 175, 55, 0.15)",
borderColor: "rgba(212, 175, 55, 0.3)",
},
inactive: {
color: "#ff6b6b",
bgColor: "rgba(255, 100, 100, 0.15)",
borderColor: "rgba(255, 100, 100, 0.3)",
},
unknown: {
color: "#a0aec0",
bgColor: "rgba(160, 174, 192, 0.15)",
borderColor: "rgba(160, 174, 192, 0.3)",
},
},
// 渐变配置
gradients: {
cardBg:
"linear-gradient(145deg, rgba(30, 30, 35, 0.95), rgba(20, 20, 25, 0.98))",
decorLine:
"linear(to-r, transparent, rgba(212, 175, 55, 0.6), transparent)",
divider: "linear(to-r, rgba(212, 175, 55, 0.3), transparent)",
},
// 图标容器背景
iconBg: "rgba(212, 175, 55, 0.1)",
iconBgLight: "rgba(212, 175, 55, 0.2)",
// 通用卡片样式
card: {
bg:
"linear-gradient(145deg, rgba(30, 30, 35, 0.95), rgba(20, 20, 25, 0.98))",
border: "1px solid",
borderColor: "rgba(212, 175, 55, 0.3)",
borderRadius: "12px",
overflow: "hidden",
transition: "all 0.3s ease",
_hover: {
borderColor: "rgba(212, 175, 55, 0.6)",
boxShadow:
"0 4px 20px rgba(212, 175, 55, 0.15), inset 0 1px 0 rgba(212, 175, 55, 0.1)",
transform: "translateY(-2px)",
},
}, },
}; };

View File

@@ -4,7 +4,7 @@
* 显示公司战略方向和战略举措 * 显示公司战略方向和战略举措
*/ */
import React, { memo, useMemo } from 'react'; import React, { memo, useMemo } from "react";
import { import {
Card, Card,
CardBody, CardBody,
@@ -18,27 +18,29 @@ import {
Grid, Grid,
GridItem, GridItem,
Center, Center,
} from '@chakra-ui/react'; UnorderedList,
import { Rocket, BarChart2 } from 'lucide-react'; ListItem,
import type { Strategy } from '../types'; } from "@chakra-ui/react";
import { Rocket, BarChart2 } from "lucide-react";
import type { Strategy } from "../types";
// 样式常量 - 避免每次渲染创建新对象 // 样式常量 - 避免每次渲染创建新对象
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 EMPTY_BOX_STYLES = { const EMPTY_BOX_STYLES = {
border: '1px dashed', border: "1px dashed",
borderColor: 'yellow.600', borderColor: "yellow.600",
borderRadius: 'md', borderRadius: "md",
py: 12, py: 12,
} as const; } as const;
@@ -64,7 +66,17 @@ const EmptyState = memo(() => (
</Box> </Box>
)); ));
EmptyState.displayName = 'StrategyEmptyState'; EmptyState.displayName = "StrategyEmptyState";
// 将文本按分号拆分为列表项
const parseToList = (text: string): string[] => {
if (!text) return [];
// 按中英文分号拆分,过滤空项
return text
.split(/[;]/)
.map((s) => s.trim())
.filter(Boolean);
};
// 内容项组件 - 复用结构 // 内容项组件 - 复用结构
interface ContentItemProps { interface ContentItemProps {
@@ -72,24 +84,40 @@ interface ContentItemProps {
content: string; content: string;
} }
const ContentItem = memo<ContentItemProps>(({ title, content }) => ( const ContentItem = memo<ContentItemProps>(({ title, content }) => {
<VStack align="stretch" spacing={2}> // 缓存解析结果,避免每次渲染重新计算
<Text fontWeight="bold" fontSize="sm" color="yellow.500"> const items = useMemo(() => parseToList(content), [content]);
{title}
</Text>
<Text fontSize="sm" color="white">
{content}
</Text>
</VStack>
));
ContentItem.displayName = 'StrategyContentItem'; return (
<VStack align="stretch" spacing={2}>
<Text fontWeight="bold" fontSize="sm" color="yellow.500">
{title}
</Text>
{items.length > 1 ? (
<UnorderedList spacing={1} pl={2} styleType="disc" color="yellow.500">
{items.map((item, index) => (
<ListItem key={index} fontSize="sm" color="white">
{item}
</ListItem>
))}
</UnorderedList>
) : (
<Text fontSize="sm" color="white">
{content || "暂无数据"}
</Text>
)}
</VStack>
);
});
ContentItem.displayName = "StrategyContentItem";
const StrategyAnalysisCard: React.FC<StrategyAnalysisCardProps> = memo( const StrategyAnalysisCard: React.FC<StrategyAnalysisCardProps> = memo(
({ strategy }) => { ({ strategy }) => {
// 缓存数据检测结果 // 缓存数据检测结果
const hasData = useMemo( const hasData = useMemo(
() => !!(strategy?.strategy_description || strategy?.strategic_initiatives), () =>
!!(strategy?.strategy_description || strategy?.strategic_initiatives),
[strategy?.strategy_description, strategy?.strategic_initiatives] [strategy?.strategy_description, strategy?.strategic_initiatives]
); );
@@ -98,7 +126,9 @@ const StrategyAnalysisCard: React.FC<StrategyAnalysisCardProps> = memo(
<CardHeader> <CardHeader>
<HStack> <HStack>
<Icon as={Rocket} color="yellow.500" /> <Icon as={Rocket} color="yellow.500" />
<Heading size="sm" color="yellow.500"></Heading> <Heading size="sm" color="yellow.500">
</Heading>
</HStack> </HStack>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
@@ -110,13 +140,13 @@ const StrategyAnalysisCard: React.FC<StrategyAnalysisCardProps> = memo(
<GridItem colSpan={GRID_RESPONSIVE_COLSPAN}> <GridItem colSpan={GRID_RESPONSIVE_COLSPAN}>
<ContentItem <ContentItem
title="战略方向" title="战略方向"
content={strategy.strategy_description || '暂无数据'} content={strategy.strategy_description || "暂无数据"}
/> />
</GridItem> </GridItem>
<GridItem colSpan={GRID_RESPONSIVE_COLSPAN}> <GridItem colSpan={GRID_RESPONSIVE_COLSPAN}>
<ContentItem <ContentItem
title="战略举措" title="战略举措"
content={strategy.strategic_initiatives || '暂无数据'} content={strategy.strategic_initiatives || "暂无数据"}
/> />
</GridItem> </GridItem>
</Grid> </Grid>
@@ -128,6 +158,6 @@ const StrategyAnalysisCard: React.FC<StrategyAnalysisCardProps> = memo(
} }
); );
StrategyAnalysisCard.displayName = 'StrategyAnalysisCard'; StrategyAnalysisCard.displayName = "StrategyAnalysisCard";
export default StrategyAnalysisCard; export default StrategyAnalysisCard;