refactor: ShareholderPanel 拆分为子组件 + 黑金主题优化

- 新增 ActualControlCard 实际控制人卡片组件
- 新增 ConcentrationCard 股权集中度卡片(含 ECharts 饼图)
- 新增 ShareholdersTable 合并表格(支持十大股东/十大流通股东)
- Mock 数据优化:股东名称改为真实格式
- Handler 修复:数组格式处理 + holding_ratio 百分比转换
- UI: 黑金主题统一、表格 hover 金色半透明

🤖 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 18:00:00 +08:00
parent 3abee6b907
commit 395dc27fe2
7 changed files with 642 additions and 306 deletions

View File

@@ -1,82 +1,28 @@
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx
// 股权结构 Tab Panel
// 股权结构 Tab Panel - 使用拆分后的子组件
import React from "react";
import {
Box,
VStack,
HStack,
Text,
Heading,
Badge,
Icon,
Card,
CardBody,
CardHeader,
SimpleGrid,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Tooltip,
Stat,
StatLabel,
StatNumber,
StatHelpText,
} from "@chakra-ui/react";
import {
FaCrown,
FaChartPie,
FaUsers,
FaChartLine,
FaArrowUp,
FaArrowDown,
FaChartBar,
FaBuilding,
FaGlobe,
FaShieldAlt,
FaBriefcase,
FaCircle,
FaUserTie,
} from "react-icons/fa";
import { VStack, SimpleGrid, Box } from "@chakra-ui/react";
import { useShareholderData } from "../../hooks/useShareholderData";
import { THEME } from "../config";
import { formatPercentage, formatShares, formatDate } from "../utils";
import {
ActualControlCard,
ConcentrationCard,
ShareholdersTable,
} from "../../components/shareholder";
import LoadingState from "./LoadingState";
interface ShareholderPanelProps {
stockCode: string;
}
// 股东类型标签组件
const ShareholderTypeBadge: React.FC<{ type: string }> = ({ type }) => {
const typeConfig: Record<string, { color: string; icon: React.ElementType }> = {
: { color: "blue", icon: FaChartBar },
: { color: "green", icon: FaUserTie },
: { color: "purple", icon: FaBuilding },
QFII: { color: "orange", icon: FaGlobe },
: { color: "red", icon: FaShieldAlt },
: { color: "teal", icon: FaShieldAlt },
: { color: "cyan", icon: FaBriefcase },
: { color: "pink", icon: FaChartLine },
};
const config = Object.entries(typeConfig).find(([key]) =>
type?.includes(key)
)?.[1] || { color: "gray", icon: FaCircle };
return (
<Badge colorScheme={config.color} size="sm">
<Icon as={config.icon} mr={1} boxSize={3} />
{type}
</Badge>
);
};
/**
* 股权结构面板
* 使用拆分后的子组件:
* - ActualControlCard: 实际控制人卡片
* - ConcentrationCard: 股权集中度卡片
* - ShareholdersTable: 股东表格(合并版,支持十大股东和十大流通股东)
*/
const ShareholderPanel: React.FC<ShareholderPanelProps> = ({ stockCode }) => {
const {
actualControl,
@@ -86,226 +32,31 @@ const ShareholderPanel: React.FC<ShareholderPanelProps> = ({ stockCode }) => {
loading,
} = useShareholderData(stockCode);
// 计算股权集中度变化
const getConcentrationTrend = () => {
const grouped: Record<string, Record<string, any>> = {};
concentration.forEach((item: any) => {
if (!grouped[item.end_date]) {
grouped[item.end_date] = {};
}
grouped[item.end_date][item.stat_item] = item;
});
return Object.entries(grouped)
.sort((a, b) => b[0].localeCompare(a[0]))
.slice(0, 5);
};
if (loading) {
return <LoadingState message="加载股权结构数据..." />;
}
return (
<VStack spacing={6} align="stretch">
{/* 实际控制人 */}
{actualControl.length > 0 && (
{/* 实际控制人 + 股权集中度 左右分布 */}
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
<Box>
<HStack mb={4}>
<Icon as={FaCrown} color={THEME.gold} boxSize={5} />
<Heading size="sm" color={THEME.textPrimary}></Heading>
</HStack>
<Card bg={THEME.tableBg} border="1px solid" borderColor={THEME.border}>
<CardBody>
<HStack justify="space-between">
<VStack align="start">
<Text fontWeight="bold" fontSize="lg" color={THEME.textPrimary}>
{actualControl[0].actual_controller_name}
</Text>
<HStack>
<Badge bg={THEME.gold} color="gray.900">
{actualControl[0].control_type}
</Badge>
<Text fontSize="sm" color={THEME.textSecondary}>
{formatDate(actualControl[0].end_date)}
</Text>
</HStack>
</VStack>
<Stat textAlign="right">
<StatLabel color={THEME.textSecondary}></StatLabel>
<StatNumber color={THEME.goldLight}>
{formatPercentage(actualControl[0].holding_ratio)}
</StatNumber>
<StatHelpText color={THEME.textSecondary}>
{formatShares(actualControl[0].holding_shares)}
</StatHelpText>
</Stat>
</HStack>
</CardBody>
</Card>
<ActualControlCard actualControl={actualControl} />
</Box>
)}
<Box>
<ConcentrationCard concentration={concentration} />
</Box>
</SimpleGrid>
{/* 股权集中度 */}
{concentration.length > 0 && (
{/* 十大股东 + 十大流通股东 左右分布 */}
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={6}>
<Box>
<HStack mb={4}>
<Icon as={FaChartPie} color={THEME.gold} boxSize={5} />
<Heading size="sm" color={THEME.textPrimary}></Heading>
</HStack>
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
{getConcentrationTrend()
.slice(0, 1)
.map(([date, items]) => (
<Card key={date} bg={THEME.tableBg} border="1px solid" borderColor={THEME.border}>
<CardHeader pb={2}>
<Text fontSize="sm" color={THEME.textSecondary}>
{formatDate(date)}
</Text>
</CardHeader>
<CardBody pt={2}>
<VStack spacing={3} align="stretch">
{Object.entries(items).map(([key, item]: [string, any]) => (
<HStack key={key} justify="space-between">
<Text fontSize="sm" color={THEME.textPrimary}>{item.stat_item}</Text>
<HStack>
<Text fontWeight="bold" color={THEME.goldLight}>
{formatPercentage(item.holding_ratio)}
</Text>
{item.ratio_change && (
<Badge colorScheme={item.ratio_change > 0 ? "red" : "green"}>
<Icon
as={item.ratio_change > 0 ? FaArrowUp : FaArrowDown}
mr={1}
boxSize={3}
/>
{Math.abs(item.ratio_change).toFixed(2)}%
</Badge>
)}
</HStack>
</HStack>
))}
</VStack>
</CardBody>
</Card>
))}
</SimpleGrid>
<ShareholdersTable type="top" shareholders={topShareholders} />
</Box>
)}
{/* 十大股东 */}
{topShareholders.length > 0 && (
<Box>
<HStack mb={4}>
<Icon as={FaUsers} color={THEME.gold} boxSize={5} />
<Heading size="sm" color={THEME.textPrimary}></Heading>
<Badge bg={THEME.gold} color="gray.900">
{formatDate(topShareholders[0].end_date)}
</Badge>
</HStack>
<TableContainer>
<Table size="sm" variant="unstyled">
<Thead>
<Tr borderBottom="1px solid" borderColor={THEME.border}>
<Th color={THEME.textSecondary}></Th>
<Th color={THEME.textSecondary}></Th>
<Th color={THEME.textSecondary}></Th>
<Th isNumeric color={THEME.textSecondary}></Th>
<Th isNumeric color={THEME.textSecondary}></Th>
<Th color={THEME.textSecondary}></Th>
</Tr>
</Thead>
<Tbody>
{topShareholders.slice(0, 10).map((shareholder: any, idx: number) => (
<Tr key={idx} borderBottom="1px solid" borderColor={THEME.border} _hover={{ bg: THEME.tableHoverBg }}>
<Td>
<Badge bg={idx < 3 ? THEME.gold : "gray.600"} color={idx < 3 ? "gray.900" : THEME.textPrimary}>
{shareholder.shareholder_rank}
</Badge>
</Td>
<Td>
<Tooltip label={shareholder.shareholder_name}>
<Text noOfLines={1} maxW="200px" color={THEME.textPrimary}>
{shareholder.shareholder_name}
</Text>
</Tooltip>
</Td>
<Td>
<ShareholderTypeBadge type={shareholder.shareholder_type} />
</Td>
<Td isNumeric fontWeight="medium" color={THEME.textPrimary}>
{formatShares(shareholder.holding_shares)}
</Td>
<Td isNumeric>
<Text color={THEME.goldLight} fontWeight="bold">
{formatPercentage(shareholder.total_share_ratio)}
</Text>
</Td>
<Td>
<Badge size="sm" bg="transparent" border="1px solid" borderColor={THEME.border} color={THEME.textSecondary}>
{shareholder.share_nature || "流通股"}
</Badge>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
<ShareholdersTable type="circulation" shareholders={topCirculationShareholders} />
</Box>
)}
{/* 十大流通股东 */}
{topCirculationShareholders.length > 0 && (
<Box>
<HStack mb={4}>
<Icon as={FaChartLine} color={THEME.gold} boxSize={5} />
<Heading size="sm" color={THEME.textPrimary}></Heading>
<Badge bg={THEME.gold} color="gray.900">
{formatDate(topCirculationShareholders[0].end_date)}
</Badge>
</HStack>
<TableContainer>
<Table size="sm" variant="unstyled">
<Thead>
<Tr borderBottom="1px solid" borderColor={THEME.border}>
<Th color={THEME.textSecondary}></Th>
<Th color={THEME.textSecondary}></Th>
<Th color={THEME.textSecondary}></Th>
<Th isNumeric color={THEME.textSecondary}></Th>
<Th isNumeric color={THEME.textSecondary}></Th>
</Tr>
</Thead>
<Tbody>
{topCirculationShareholders.slice(0, 10).map((shareholder: any, idx: number) => (
<Tr key={idx} borderBottom="1px solid" borderColor={THEME.border} _hover={{ bg: THEME.tableHoverBg }}>
<Td>
<Badge bg={idx < 3 ? THEME.gold : "gray.600"} color={idx < 3 ? "gray.900" : THEME.textPrimary}>
{shareholder.shareholder_rank}
</Badge>
</Td>
<Td>
<Tooltip label={shareholder.shareholder_name}>
<Text noOfLines={1} maxW="250px" color={THEME.textPrimary}>
{shareholder.shareholder_name}
</Text>
</Tooltip>
</Td>
<Td>
<ShareholderTypeBadge type={shareholder.shareholder_type} />
</Td>
<Td isNumeric fontWeight="medium" color={THEME.textPrimary}>
{formatShares(shareholder.holding_shares)}
</Td>
<Td isNumeric>
<Text color={THEME.goldLight} fontWeight="bold">
{formatPercentage(shareholder.circulation_share_ratio)}
</Text>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
)}
</SimpleGrid>
</VStack>
);
};