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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user