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
// 分支机构 Tab Panel - 黑金风格
import React from "react";
import React, { memo, useMemo } from "react";
import {
Box,
VStack,
@@ -11,7 +11,13 @@ import {
SimpleGrid,
Center,
} 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 { THEME } from "../config";
@@ -24,23 +30,42 @@ interface BranchesPanelProps {
isActive?: boolean;
}
// 黑金卡片样式
const cardStyles = {
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)",
// 状态分类关键词
const ACTIVE_KEYWORDS = ["存续", "在营", "开业", "在册", "在业"];
const INACTIVE_KEYWORDS = ["吊销", "注销", "撤销", "关闭", "歇业", "迁出"];
// 获取状态分类
type StatusType = "active" | "inactive" | "unknown";
const getStatusType = (status: string | undefined): StatusType => {
if (!status || status === "其他") return "unknown";
// 优先判断异常状态(因为"吊销,未注销"同时包含两个关键词)
if (INACTIVE_KEYWORDS.some((keyword) => status.includes(keyword))) {
return "inactive";
}
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",
alignItems: "center",
gap: "4px",
@@ -49,14 +74,38 @@ const getStatusBadgeStyles = (isActive: boolean) => ({
borderRadius: "full",
fontSize: "xs",
fontWeight: "medium",
bg: isActive ? "rgba(212, 175, 55, 0.15)" : "rgba(255, 100, 100, 0.15)",
color: isActive ? THEME.gold : "#ff6b6b",
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}>
<Text fontSize="xs" color={THEME.textSecondary} letterSpacing="0.5px">
{label}
@@ -65,106 +114,127 @@ const InfoItem: React.FC<{ label: string; value: string | number }> = ({ label,
{value || "-"}
</Text>
</VStack>
);
));
const BranchesPanel: React.FC<BranchesPanelProps> = ({ stockCode, isActive = true }) => {
const { branches, loading } = useBranchesData({ stockCode, enabled: isActive });
InfoItem.displayName = "BranchInfoItem";
if (loading) {
return <BranchesSkeleton />;
}
// 空状态组件 - 独立 memo 避免重复渲染
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) {
return (
<Center h="200px">
<VStack spacing={3}>
<Box
p={4}
borderRadius="full"
bg="rgba(212, 175, 55, 0.1)"
border="1px solid"
borderColor="rgba(212, 175, 55, 0.2)"
>
<Icon as={GitBranch} boxSize={10} color={THEME.gold} opacity={0.6} />
</Box>
<Text color={THEME.textSecondary} fontSize="sm">
</Text>
</VStack>
</Center>
);
}
EmptyState.displayName = "BranchesEmptyState";
// 分支卡片组件 - 独立 memo 优化列表渲染
interface BranchCardProps {
branch: any;
}
const BranchCard = memo<BranchCardProps>(({ branch }) => {
const statusType = useMemo(() => getStatusType(branch.business_status), [
branch.business_status,
]);
const StatusIcon = STATUS_CONFIG[statusType].icon;
// 缓存关联企业显示值
const relatedCompanyValue = useMemo(
() => `${branch.related_company_count || 0}`,
[branch.related_company_count]
);
return (
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
{branches.map((branch: any, idx: number) => {
const isActive = branch.business_status === "存续";
<Box sx={THEME.card}>
{/* 顶部金色装饰线 */}
<Box h="2px" bgGradient={THEME.gradients.decorLine} />
return (
<Box key={idx} sx={cardStyles}>
{/* 顶部金色装饰线 */}
<Box
h="2px"
bgGradient="linear(to-r, transparent, rgba(212, 175, 55, 0.6), transparent)"
/>
<Box p={4}>
<VStack align="start" spacing={4}>
{/* 标题行 */}
<HStack justify="space-between" w="full" align="flex-start">
<HStack spacing={2} flex={1}>
<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}>
{/* 标题行 */}
<HStack justify="space-between" w="full" align="flex-start">
<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 sx={STATUS_BADGE_STYLES[statusType]}>
<Icon as={StatusIcon} boxSize={3} />
<Text>{branch.business_status || "未知"}</Text>
</Box>
</Box>
);
})}
</SimpleGrid>
</HStack>
{/* 分隔线 */}
<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;

View File

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

View File

@@ -3,6 +3,28 @@
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 {
bg: string;
@@ -21,6 +43,23 @@ export interface Theme {
tabUnselected: {
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",
tableBg: "gray.700",
tableHoverBg: "gray.600",
gold: "#F4D03F", // 亮黄金色(用于文字,对比度更好)
goldLight: "#F0D78C", // 浅金色(用于次要文字)
gold: "#F4D03F", // 亮黄金色(用于文字,对比度更好)
goldLight: "#F0D78C", // 浅金色(用于次要文字)
textPrimary: "white",
textSecondary: "gray.400",
border: "rgba(212, 175, 55, 0.3)", // 边框保持原色
border: "rgba(212, 175, 55, 0.3)", // 边框保持原色
tabSelected: {
bg: "#D4AF37", // 选中背景保持深金色
bg: "#D4AF37", // 选中背景保持深金色
color: "gray.900",
},
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 {
Card,
CardBody,
@@ -18,27 +18,29 @@ import {
Grid,
GridItem,
Center,
} from '@chakra-ui/react';
import { Rocket, BarChart2 } from 'lucide-react';
import type { Strategy } from '../types';
UnorderedList,
ListItem,
} from "@chakra-ui/react";
import { Rocket, BarChart2 } from "lucide-react";
import type { Strategy } from "../types";
// 样式常量 - 避免每次渲染创建新对象
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 EMPTY_BOX_STYLES = {
border: '1px dashed',
borderColor: 'yellow.600',
borderRadius: 'md',
border: "1px dashed",
borderColor: "yellow.600",
borderRadius: "md",
py: 12,
} as const;
@@ -64,7 +66,17 @@ const EmptyState = memo(() => (
</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 {
@@ -72,24 +84,40 @@ interface ContentItemProps {
content: string;
}
const ContentItem = memo<ContentItemProps>(({ title, content }) => (
<VStack align="stretch" spacing={2}>
<Text fontWeight="bold" fontSize="sm" color="yellow.500">
{title}
</Text>
<Text fontSize="sm" color="white">
{content}
</Text>
</VStack>
));
const ContentItem = memo<ContentItemProps>(({ title, content }) => {
// 缓存解析结果,避免每次渲染重新计算
const items = useMemo(() => parseToList(content), [content]);
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(
({ strategy }) => {
// 缓存数据检测结果
const hasData = useMemo(
() => !!(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>
<HStack>
<Icon as={Rocket} color="yellow.500" />
<Heading size="sm" color="yellow.500"></Heading>
<Heading size="sm" color="yellow.500">
</Heading>
</HStack>
</CardHeader>
<CardBody>
@@ -110,13 +140,13 @@ const StrategyAnalysisCard: React.FC<StrategyAnalysisCardProps> = memo(
<GridItem colSpan={GRID_RESPONSIVE_COLSPAN}>
<ContentItem
title="战略方向"
content={strategy.strategy_description || '暂无数据'}
content={strategy.strategy_description || "暂无数据"}
/>
</GridItem>
<GridItem colSpan={GRID_RESPONSIVE_COLSPAN}>
<ContentItem
title="战略举措"
content={strategy.strategic_initiatives || '暂无数据'}
content={strategy.strategic_initiatives || "暂无数据"}
/>
</GridItem>
</Grid>
@@ -128,6 +158,6 @@ const StrategyAnalysisCard: React.FC<StrategyAnalysisCardProps> = memo(
}
);
StrategyAnalysisCard.displayName = 'StrategyAnalysisCard';
StrategyAnalysisCard.displayName = "StrategyAnalysisCard";
export default StrategyAnalysisCard;