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:
@@ -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,25 +114,20 @@ 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 />;
|
||||
}
|
||||
|
||||
if (branches.length === 0) {
|
||||
return (
|
||||
// 空状态组件 - 独立 memo 避免重复渲染
|
||||
const EmptyState = memo(() => (
|
||||
<Center h="200px">
|
||||
<VStack spacing={3}>
|
||||
<Box
|
||||
p={4}
|
||||
borderRadius="full"
|
||||
bg="rgba(212, 175, 55, 0.1)"
|
||||
bg={THEME.iconBg}
|
||||
border="1px solid"
|
||||
borderColor="rgba(212, 175, 55, 0.2)"
|
||||
borderColor={THEME.iconBgLight}
|
||||
>
|
||||
<Icon as={GitBranch} boxSize={10} color={THEME.gold} opacity={0.6} />
|
||||
</Box>
|
||||
@@ -92,32 +136,38 @@ const BranchesPanel: React.FC<BranchesPanelProps> = ({ stockCode, isActive = tru
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
));
|
||||
|
||||
EmptyState.displayName = "BranchesEmptyState";
|
||||
|
||||
// 分支卡片组件 - 独立 memo 优化列表渲染
|
||||
interface BranchCardProps {
|
||||
branch: any;
|
||||
}
|
||||
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||
{branches.map((branch: any, idx: number) => {
|
||||
const isActive = branch.business_status === "存续";
|
||||
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 (
|
||||
<Box key={idx} sx={cardStyles}>
|
||||
<Box sx={THEME.card}>
|
||||
{/* 顶部金色装饰线 */}
|
||||
<Box
|
||||
h="2px"
|
||||
bgGradient="linear(to-r, transparent, rgba(212, 175, 55, 0.6), transparent)"
|
||||
/>
|
||||
<Box h="2px" bgGradient={THEME.gradients.decorLine} />
|
||||
|
||||
<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)"
|
||||
>
|
||||
<Box p={1.5} borderRadius="md" bg={THEME.iconBg}>
|
||||
<Icon as={Building2} boxSize={3.5} color={THEME.gold} />
|
||||
</Box>
|
||||
<Text
|
||||
@@ -132,39 +182,59 @@ const BranchesPanel: React.FC<BranchesPanelProps> = ({ stockCode, isActive = tru
|
||||
</HStack>
|
||||
|
||||
{/* 状态徽章 */}
|
||||
<Box sx={getStatusBadgeStyles(isActive)}>
|
||||
<Icon
|
||||
as={isActive ? CheckCircle : XCircle}
|
||||
boxSize={3}
|
||||
/>
|
||||
<Text>{branch.business_status}</Text>
|
||||
<Box sx={STATUS_BADGE_STYLES[statusType]}>
|
||||
<Icon as={StatusIcon} 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)"
|
||||
/>
|
||||
<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={`${branch.related_company_count || 0} 家`}
|
||||
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;
|
||||
|
||||
@@ -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}>
|
||||
{/* 姓名和性别 */}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// 黑金主题配置
|
||||
@@ -42,6 +81,51 @@ export const THEME: Theme = {
|
||||
tabUnselected: {
|
||||
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)",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Tab 配置类型
|
||||
|
||||
@@ -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 }) => (
|
||||
const ContentItem = memo<ContentItemProps>(({ title, content }) => {
|
||||
// 缓存解析结果,避免每次渲染重新计算
|
||||
const items = useMemo(() => parseToList(content), [content]);
|
||||
|
||||
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}
|
||||
{content || "暂无数据"}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
ContentItem.displayName = 'StrategyContentItem';
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user