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:
@@ -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` 可独立使用
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user