refactor: ManagementPanel 组件拆分重构

- 创建 management/ 子目录,模块化管理
- 拆分为 5 个 TypeScript 文件:types.ts、ManagementPanel.tsx、CategorySection.tsx、ManagementCard.tsx、index.ts
- 添加 useMemo 缓存分类计算结果
- 使用 React.memo 优化 ManagementCard 和 CategorySection
- 添加完整的 TypeScript 类型定义,消除 any
- 更新 STRUCTURE.md 同步目录结构

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-10 19:28:05 +08:00
parent c32091e83e
commit 6d5594556b
8 changed files with 352 additions and 182 deletions

View File

@@ -30,7 +30,12 @@ src/views/Company/
│ │ │ ├── index.ts # 组件统一导出
│ │ │ ├── LoadingState.tsx # 加载状态组件(黑金主题 Spinner
│ │ │ ├── ShareholderPanel.tsx # 股权结构面板(实控人、十大股东、股权集中度)
│ │ │ ├── ManagementPanel.tsx # 管理团队面板(高管列表表格
│ │ │ ├── management/ # 管理团队模块(拆分重构
│ │ │ │ ├── index.ts # 模块导出
│ │ │ │ ├── types.ts # 类型定义ManagementPerson 等)
│ │ │ │ ├── ManagementPanel.tsx # 主组件useMemo 优化)
│ │ │ │ ├── CategorySection.tsx # 分类区块memo 优化)
│ │ │ │ └── ManagementCard.tsx # 人员卡片memo 优化)
│ │ │ ├── AnnouncementsPanel.tsx # 公告信息面板(公告列表 + 披露日程)
│ │ │ ├── BranchesPanel.tsx # 分支机构面板(分支列表表格)
│ │ │ └── BusinessInfoPanel.tsx # 工商信息面板(注册资本、成立日期等)
@@ -469,3 +474,36 @@ MarketDataView/
- **服务层分离**API 调用统一在 `marketService.ts` 中管理
- **图表配置抽离**:复杂的 ECharts 配置集中在 `chartOptions.ts`
- **组件复用**通用组件ThemedCard、MarkdownRenderer可在其他模块使用
### 2025-12-10 ManagementPanel 拆分重构
**改动概述**
- `ManagementPanel.tsx` 从 **180 行** 拆分为 **5 个 TypeScript 文件**
- 创建 `management/` 子目录,模块化管理
- 添加性能优化(`useMemo`、`React.memo`
**拆分后文件结构**
```
components/management/
├── index.ts # 模块导出
├── types.ts # 类型定义(~35 行)
├── ManagementPanel.tsx # 主组件(~105 行useMemo 优化)
├── CategorySection.tsx # 分类区块组件(~65 行memo
└── ManagementCard.tsx # 人员卡片组件(~100 行memo
```
**类型定义**`types.ts`
- `ManagementPerson` - 管理人员信息
- `ManagementCategory` - 分类类型(高管/董事/监事/其他)
- `CategorizedManagement` - 分类后的数据结构
- `CategoryConfig` - 分类配置(图标、颜色)
**性能优化**
- `useMemo` - 缓存 `categorizeManagement()` 分类计算结果
- `React.memo` - `ManagementCard` 和 `CategorySection` 使用 memo 包装
- 常量提取 - `CATEGORY_CONFIG` 和 `CATEGORY_ORDER` 提取到组件外部
**设计原则**
- **职责分离**:卡片渲染、分类区块、数据处理各自独立
- **类型安全**:消除 `any` 类型,完整的 TypeScript 类型定义
- **可复用性**`ManagementCard` 可独立使用

View File

@@ -1,179 +0,0 @@
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/ManagementPanel.tsx
// 管理团队 Tab Panel
import React from "react";
import {
Box,
VStack,
HStack,
Text,
Heading,
Badge,
Icon,
Card,
CardBody,
SimpleGrid,
Avatar,
Tag,
} from "@chakra-ui/react";
import {
FaUserTie,
FaCrown,
FaEye,
FaUsers,
FaVenusMars,
FaGraduationCap,
FaPassport,
} from "react-icons/fa";
import { useManagementData } from "../../hooks/useManagementData";
import { THEME } from "../config";
import { formatDate } from "../utils";
import LoadingState from "./LoadingState";
interface ManagementPanelProps {
stockCode: string;
}
const ManagementPanel: React.FC<ManagementPanelProps> = ({ stockCode }) => {
const { management, loading } = useManagementData(stockCode);
// 管理层职位分类
const getManagementByCategory = () => {
const categories: Record<string, any[]> = {
: [],
: [],
: [],
: [],
};
management.forEach((person: any) => {
if (
person.position_category === "高管" ||
person.position_name?.includes("总")
) {
categories["高管"].push(person);
} else if (
person.position_category === "董事" ||
person.position_name?.includes("董事")
) {
categories["董事"].push(person);
} else if (
person.position_category === "监事" ||
person.position_name?.includes("监事")
) {
categories["监事"].push(person);
} else {
categories["其他"].push(person);
}
});
return categories;
};
const getCategoryIcon = (category: string) => {
switch (category) {
case "高管":
return FaUserTie;
case "董事":
return FaCrown;
case "监事":
return FaEye;
default:
return FaUsers;
}
};
const getCategoryColor = (category: string) => {
switch (category) {
case "高管":
return THEME.gold;
case "董事":
return THEME.goldLight;
case "监事":
return "green.400";
default:
return THEME.textSecondary;
}
};
if (loading) {
return <LoadingState message="加载管理团队数据..." />;
}
return (
<VStack spacing={6} align="stretch">
{Object.entries(getManagementByCategory()).map(
([category, people]) =>
people.length > 0 && (
<Box key={category}>
<HStack mb={4}>
<Icon
as={getCategoryIcon(category)}
color={getCategoryColor(category)}
boxSize={5}
/>
<Heading size="sm" color={THEME.textPrimary}>{category}</Heading>
<Badge bg={THEME.gold} color="gray.900">{people.length}</Badge>
</HStack>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
{people.map((person: any, idx: number) => (
<Card key={idx} bg={THEME.tableBg} border="1px solid" borderColor={THEME.border} size="sm">
<CardBody>
<HStack spacing={3} align="start">
<Avatar
name={person.name}
size="md"
bg={getCategoryColor(category)}
/>
<VStack align="start" spacing={1} flex={1}>
<HStack>
<Text fontWeight="bold" color={THEME.textPrimary}>{person.name}</Text>
{person.gender && (
<Icon
as={FaVenusMars}
color={person.gender === "男" ? "blue.400" : "pink.400"}
boxSize={3}
/>
)}
</HStack>
<Text fontSize="sm" color={THEME.goldLight}>
{person.position_name}
</Text>
<HStack spacing={2} flexWrap="wrap">
{person.education && (
<Tag size="sm" bg={THEME.tableHoverBg} color={THEME.textSecondary}>
<Icon as={FaGraduationCap} mr={1} boxSize={3} />
{person.education}
</Tag>
)}
{person.birth_year && (
<Tag size="sm" bg={THEME.tableHoverBg} color={THEME.textSecondary}>
{new Date().getFullYear() - parseInt(person.birth_year)}
</Tag>
)}
{person.nationality && person.nationality !== "中国" && (
<Tag size="sm" bg="orange.600" color="white">
<Icon as={FaPassport} mr={1} boxSize={3} />
{person.nationality}
</Tag>
)}
</HStack>
<Text fontSize="xs" color={THEME.textSecondary}>
{formatDate(person.start_date)}
</Text>
</VStack>
</HStack>
</CardBody>
</Card>
))}
</SimpleGrid>
</Box>
)
)}
</VStack>
);
};
export default ManagementPanel;

View File

@@ -3,7 +3,7 @@
export { default as LoadingState } from "./LoadingState";
export { default as ShareholderPanel } from "./ShareholderPanel";
export { default as ManagementPanel } from "./ManagementPanel";
export { ManagementPanel } from "./management";
export { default as AnnouncementsPanel } from "./AnnouncementsPanel";
export { default as BranchesPanel } from "./BranchesPanel";
export { default as BusinessInfoPanel } from "./BusinessInfoPanel";

View File

@@ -0,0 +1,63 @@
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/management/CategorySection.tsx
// 管理层分类区块组件
import React, { memo } from "react";
import {
Box,
HStack,
Heading,
Badge,
Icon,
SimpleGrid,
} from "@chakra-ui/react";
import type { IconType } from "react-icons";
import { THEME } from "../../config";
import ManagementCard from "./ManagementCard";
import type { ManagementPerson, ManagementCategory } from "./types";
interface CategorySectionProps {
category: ManagementCategory;
people: ManagementPerson[];
icon: IconType;
color: string;
}
const CategorySection: React.FC<CategorySectionProps> = ({
category,
people,
icon,
color,
}) => {
if (people.length === 0) {
return null;
}
return (
<Box>
{/* 分类标题 */}
<HStack mb={4}>
<Icon as={icon} color={color} boxSize={5} />
<Heading size="sm" color={THEME.textPrimary}>
{category}
</Heading>
<Badge bg={THEME.gold} color="gray.900">
{people.length}
</Badge>
</HStack>
{/* 人员卡片网格 */}
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
{people.map((person, idx) => (
<ManagementCard
key={`${person.name}-${idx}`}
person={person}
categoryColor={color}
/>
))}
</SimpleGrid>
</Box>
);
};
export default memo(CategorySection);

View File

@@ -0,0 +1,100 @@
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/management/ManagementCard.tsx
// 管理人员卡片组件
import React, { memo } from "react";
import {
HStack,
VStack,
Text,
Icon,
Card,
CardBody,
Avatar,
Tag,
} from "@chakra-ui/react";
import {
FaVenusMars,
FaGraduationCap,
FaPassport,
} from "react-icons/fa";
import { THEME } from "../../config";
import { formatDate } from "../../utils";
import type { ManagementPerson } from "./types";
interface ManagementCardProps {
person: ManagementPerson;
categoryColor: string;
}
const ManagementCard: React.FC<ManagementCardProps> = ({ person, categoryColor }) => {
const currentYear = new Date().getFullYear();
const age = person.birth_year ? currentYear - parseInt(person.birth_year, 10) : null;
return (
<Card
bg={THEME.tableBg}
border="1px solid"
borderColor={THEME.border}
size="sm"
>
<CardBody>
<HStack spacing={3} align="start">
<Avatar
name={person.name}
size="md"
bg={categoryColor}
/>
<VStack align="start" spacing={1} flex={1}>
{/* 姓名和性别 */}
<HStack>
<Text fontWeight="bold" color={THEME.textPrimary}>
{person.name}
</Text>
{person.gender && (
<Icon
as={FaVenusMars}
color={person.gender === "男" ? "blue.400" : "pink.400"}
boxSize={3}
/>
)}
</HStack>
{/* 职位 */}
<Text fontSize="sm" color={THEME.goldLight}>
{person.position_name}
</Text>
{/* 标签:学历、年龄、国籍 */}
<HStack spacing={2} flexWrap="wrap">
{person.education && (
<Tag size="sm" bg={THEME.tableHoverBg} color={THEME.textSecondary}>
<Icon as={FaGraduationCap} mr={1} boxSize={3} />
{person.education}
</Tag>
)}
{age && (
<Tag size="sm" bg={THEME.tableHoverBg} color={THEME.textSecondary}>
{age}
</Tag>
)}
{person.nationality && person.nationality !== "中国" && (
<Tag size="sm" bg="orange.600" color="white">
<Icon as={FaPassport} mr={1} boxSize={3} />
{person.nationality}
</Tag>
)}
</HStack>
{/* 任职日期 */}
<Text fontSize="xs" color={THEME.textSecondary}>
{formatDate(person.start_date)}
</Text>
</VStack>
</HStack>
</CardBody>
</Card>
);
};
export default memo(ManagementCard);

View File

@@ -0,0 +1,105 @@
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/management/ManagementPanel.tsx
// 管理团队 Tab Panel重构版
import React, { useMemo } from "react";
import { VStack } from "@chakra-ui/react";
import {
FaUserTie,
FaCrown,
FaEye,
FaUsers,
} from "react-icons/fa";
import { useManagementData } from "../../../hooks/useManagementData";
import { THEME } from "../../config";
import LoadingState from "../LoadingState";
import CategorySection from "./CategorySection";
import type {
ManagementPerson,
ManagementCategory,
CategorizedManagement,
CategoryConfig,
} from "./types";
interface ManagementPanelProps {
stockCode: string;
}
/**
* 分类配置映射
*/
const CATEGORY_CONFIG: Record<ManagementCategory, CategoryConfig> = {
: { icon: FaUserTie, color: THEME.gold },
: { icon: FaCrown, color: THEME.goldLight },
: { icon: FaEye, color: "green.400" },
: { icon: FaUsers, color: THEME.textSecondary },
};
/**
* 分类顺序
*/
const CATEGORY_ORDER: ManagementCategory[] = ["高管", "董事", "监事", "其他"];
/**
* 根据职位信息对管理人员进行分类
*/
const categorizeManagement = (management: ManagementPerson[]): CategorizedManagement => {
const categories: CategorizedManagement = {
: [],
: [],
: [],
: [],
};
management.forEach((person) => {
const positionCategory = person.position_category;
const positionName = person.position_name || "";
if (positionCategory === "高管" || positionName.includes("总")) {
categories["高管"].push(person);
} else if (positionCategory === "董事" || positionName.includes("董事")) {
categories["董事"].push(person);
} else if (positionCategory === "监事" || positionName.includes("监事")) {
categories["监事"].push(person);
} else {
categories["其他"].push(person);
}
});
return categories;
};
const ManagementPanel: React.FC<ManagementPanelProps> = ({ stockCode }) => {
const { management, loading } = useManagementData(stockCode);
// 使用 useMemo 缓存分类计算结果
const categorizedManagement = useMemo(
() => categorizeManagement(management as ManagementPerson[]),
[management]
);
if (loading) {
return <LoadingState message="加载管理团队数据..." />;
}
return (
<VStack spacing={6} align="stretch">
{CATEGORY_ORDER.map((category) => {
const config = CATEGORY_CONFIG[category];
const people = categorizedManagement[category];
return (
<CategorySection
key={category}
category={category}
people={people}
icon={config.icon}
color={config.color}
/>
);
})}
</VStack>
);
};
export default ManagementPanel;

View File

@@ -0,0 +1,7 @@
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/management/index.ts
// 管理团队组件导出
export { default as ManagementPanel } from "./ManagementPanel";
export { default as ManagementCard } from "./ManagementCard";
export { default as CategorySection } from "./CategorySection";
export * from "./types";

View File

@@ -0,0 +1,36 @@
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/management/types.ts
// 管理团队相关类型定义
import type { IconType } from "react-icons";
/**
* 管理人员信息
*/
export interface ManagementPerson {
name: string;
position_name?: string;
position_category?: string;
gender?: "男" | "女";
education?: string;
birth_year?: string;
nationality?: string;
start_date?: string;
}
/**
* 管理层分类
*/
export type ManagementCategory = "高管" | "董事" | "监事" | "其他";
/**
* 分类后的管理层数据
*/
export type CategorizedManagement = Record<ManagementCategory, ManagementPerson[]>;
/**
* 分类配置项
*/
export interface CategoryConfig {
icon: IconType;
color: string;
}