diff --git a/src/mocks/data/company.js b/src/mocks/data/company.js index 8aa396ef..92b724ce 100644 --- a/src/mocks/data/company.js +++ b/src/mocks/data/company.js @@ -137,29 +137,29 @@ export const PINGAN_BANK_DATA = { // 十大流通股东(字段名与组件期望格式匹配) topCirculationShareholders: [ { shareholder_rank: 1, shareholder_name: '中国平安保险(集团)股份有限公司', holding_shares: 10168542300, circulation_share_ratio: 52.38, shareholder_type: '法人', end_date: '2024-09-30' }, - { shareholder_rank: 2, shareholder_name: '香港中央结算有限公司', holding_shares: 542138600, circulation_share_ratio: 2.79, shareholder_type: 'QFII', end_date: '2024-09-30' }, - { shareholder_rank: 3, shareholder_name: '深圳市投资控股有限公司', holding_shares: 382456100, circulation_share_ratio: 1.97, shareholder_type: '法人', end_date: '2024-09-30' }, + { shareholder_rank: 2, shareholder_name: '香港中央结算有限公司(陆股通)', holding_shares: 542138600, circulation_share_ratio: 2.79, shareholder_type: 'QFII', end_date: '2024-09-30' }, + { shareholder_rank: 3, shareholder_name: '中国平安人寿保险股份有限公司-传统-普通保险产品', holding_shares: 382456100, circulation_share_ratio: 1.97, shareholder_type: '保险', end_date: '2024-09-30' }, { shareholder_rank: 4, shareholder_name: '中国证券金融股份有限公司', holding_shares: 298654200, circulation_share_ratio: 1.54, shareholder_type: '券商', end_date: '2024-09-30' }, { shareholder_rank: 5, shareholder_name: '中央汇金资产管理有限责任公司', holding_shares: 267842100, circulation_share_ratio: 1.38, shareholder_type: '法人', end_date: '2024-09-30' }, { shareholder_rank: 6, shareholder_name: '全国社保基金一零三组合', holding_shares: 156234500, circulation_share_ratio: 0.80, shareholder_type: '社保', end_date: '2024-09-30' }, - { shareholder_rank: 7, shareholder_name: '全国社保基金一零一组合', holding_shares: 142356700, circulation_share_ratio: 0.73, shareholder_type: '社保', end_date: '2024-09-30' }, - { shareholder_rank: 8, shareholder_name: '中国人寿保险股份有限公司', holding_shares: 128945600, circulation_share_ratio: 0.66, shareholder_type: '保险', end_date: '2024-09-30' }, - { shareholder_rank: 9, shareholder_name: 'GIC PRIVATE LIMITED', holding_shares: 98765400, circulation_share_ratio: 0.51, shareholder_type: 'QFII', end_date: '2024-09-30' }, - { shareholder_rank: 10, shareholder_name: '挪威中央银行', holding_shares: 87654300, circulation_share_ratio: 0.45, shareholder_type: 'QFII', end_date: '2024-09-30' } + { shareholder_rank: 7, shareholder_name: '华夏上证50交易型开放式指数证券投资基金', holding_shares: 142356700, circulation_share_ratio: 0.73, shareholder_type: '基金', end_date: '2024-09-30' }, + { shareholder_rank: 8, shareholder_name: '中国人寿保险股份有限公司-分红-个人分红-005L-FH002深', holding_shares: 128945600, circulation_share_ratio: 0.66, shareholder_type: '保险', end_date: '2024-09-30' }, + { shareholder_rank: 9, shareholder_name: '易方达沪深300交易型开放式指数发起式证券投资基金', holding_shares: 98765400, circulation_share_ratio: 0.51, shareholder_type: '基金', end_date: '2024-09-30' }, + { shareholder_rank: 10, shareholder_name: '嘉实沪深300交易型开放式指数证券投资基金', holding_shares: 87654300, circulation_share_ratio: 0.45, shareholder_type: '基金', end_date: '2024-09-30' } ], // 十大股东(字段名与组件期望格式匹配) topShareholders: [ { shareholder_rank: 1, shareholder_name: '中国平安保险(集团)股份有限公司', holding_shares: 10168542300, total_share_ratio: 52.38, shareholder_type: '法人', share_nature: '流通A股', end_date: '2024-09-30' }, - { shareholder_rank: 2, shareholder_name: '香港中央结算有限公司', holding_shares: 542138600, total_share_ratio: 2.79, shareholder_type: 'QFII', share_nature: '流通A股', end_date: '2024-09-30' }, - { shareholder_rank: 3, shareholder_name: '深圳市投资控股有限公司', holding_shares: 382456100, total_share_ratio: 1.97, shareholder_type: '法人', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 2, shareholder_name: '香港中央结算有限公司(陆股通)', holding_shares: 542138600, total_share_ratio: 2.79, shareholder_type: 'QFII', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 3, shareholder_name: '中国平安人寿保险股份有限公司-传统-普通保险产品', holding_shares: 382456100, total_share_ratio: 1.97, shareholder_type: '保险', share_nature: '流通A股', end_date: '2024-09-30' }, { shareholder_rank: 4, shareholder_name: '中国证券金融股份有限公司', holding_shares: 298654200, total_share_ratio: 1.54, shareholder_type: '券商', share_nature: '流通A股', end_date: '2024-09-30' }, { shareholder_rank: 5, shareholder_name: '中央汇金资产管理有限责任公司', holding_shares: 267842100, total_share_ratio: 1.38, shareholder_type: '法人', share_nature: '流通A股', end_date: '2024-09-30' }, { shareholder_rank: 6, shareholder_name: '全国社保基金一零三组合', holding_shares: 156234500, total_share_ratio: 0.80, shareholder_type: '社保', share_nature: '流通A股', end_date: '2024-09-30' }, - { shareholder_rank: 7, shareholder_name: '全国社保基金一零一组合', holding_shares: 142356700, total_share_ratio: 0.73, shareholder_type: '社保', share_nature: '流通A股', end_date: '2024-09-30' }, - { shareholder_rank: 8, shareholder_name: '中国人寿保险股份有限公司', holding_shares: 128945600, total_share_ratio: 0.66, shareholder_type: '保险', share_nature: '流通A股', end_date: '2024-09-30' }, - { shareholder_rank: 9, shareholder_name: 'GIC PRIVATE LIMITED', holding_shares: 98765400, total_share_ratio: 0.51, shareholder_type: 'QFII', share_nature: '流通A股', end_date: '2024-09-30' }, - { shareholder_rank: 10, shareholder_name: '挪威中央银行', holding_shares: 87654300, total_share_ratio: 0.45, shareholder_type: 'QFII', share_nature: '流通A股', end_date: '2024-09-30' } + { shareholder_rank: 7, shareholder_name: '华夏上证50交易型开放式指数证券投资基金', holding_shares: 142356700, total_share_ratio: 0.73, shareholder_type: '基金', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 8, shareholder_name: '中国人寿保险股份有限公司-分红-个人分红-005L-FH002深', holding_shares: 128945600, total_share_ratio: 0.66, shareholder_type: '保险', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 9, shareholder_name: '易方达沪深300交易型开放式指数发起式证券投资基金', holding_shares: 98765400, total_share_ratio: 0.51, shareholder_type: '基金', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 10, shareholder_name: '嘉实沪深300交易型开放式指数证券投资基金', holding_shares: 87654300, total_share_ratio: 0.45, shareholder_type: '基金', share_nature: '流通A股', end_date: '2024-09-30' } ], // 分支机构 @@ -1034,23 +1034,30 @@ export const generateCompanyData = (stockCode, stockName = '示例公司') => { { name: '赵六', position: '财务总监', gender: '男', age: 48, education: '硕士', annual_compensation: 200.5, status: 'active' }, { name: '钱七', position: '技术总监', gender: '男', age: 42, education: '博士', annual_compensation: 250.8, status: 'active' }, ], - topCirculationShareholders: Array(10).fill(null).map((_, i) => ({ - shareholder_rank: i + 1, - shareholder_name: `流通股东${i + 1}`, - holding_shares: Math.floor(Math.random() * 100000000) + 10000000, - circulation_share_ratio: parseFloat(((10 - i) * 0.8 + Math.random() * 2).toFixed(2)), - shareholder_type: i < 3 ? '法人' : (i < 5 ? '基金' : (i < 7 ? '社保' : 'QFII')), - end_date: '2024-09-30' - })), - topShareholders: Array(10).fill(null).map((_, i) => ({ - shareholder_rank: i + 1, - shareholder_name: `股东${i + 1}`, - holding_shares: Math.floor(Math.random() * 100000000) + 10000000, - total_share_ratio: parseFloat(((10 - i) * 0.8 + Math.random() * 2).toFixed(2)), - shareholder_type: i < 3 ? '法人' : (i < 5 ? '基金' : (i < 7 ? '社保' : 'QFII')), - share_nature: i < 2 ? '限售股' : '流通A股', - end_date: '2024-09-30' - })), + topCirculationShareholders: [ + { shareholder_rank: 1, shareholder_name: '某控股集团有限公司', holding_shares: 560000000, circulation_share_ratio: 35.50, shareholder_type: '法人', end_date: '2024-09-30' }, + { shareholder_rank: 2, shareholder_name: '香港中央结算有限公司(陆股通)', holding_shares: 156000000, circulation_share_ratio: 9.88, shareholder_type: 'QFII', end_date: '2024-09-30' }, + { shareholder_rank: 3, shareholder_name: '中国平安人寿保险股份有限公司-传统-普通保险产品', holding_shares: 89000000, circulation_share_ratio: 5.64, shareholder_type: '保险', end_date: '2024-09-30' }, + { shareholder_rank: 4, shareholder_name: '中国证券金融股份有限公司', holding_shares: 67000000, circulation_share_ratio: 4.24, shareholder_type: '券商', end_date: '2024-09-30' }, + { shareholder_rank: 5, shareholder_name: '中央汇金资产管理有限责任公司', holding_shares: 45000000, circulation_share_ratio: 2.85, shareholder_type: '法人', end_date: '2024-09-30' }, + { shareholder_rank: 6, shareholder_name: '全国社保基金一零三组合', holding_shares: 34000000, circulation_share_ratio: 2.15, shareholder_type: '社保', end_date: '2024-09-30' }, + { shareholder_rank: 7, shareholder_name: '华夏上证50交易型开放式指数证券投资基金', holding_shares: 28000000, circulation_share_ratio: 1.77, shareholder_type: '基金', end_date: '2024-09-30' }, + { shareholder_rank: 8, shareholder_name: '中国人寿保险股份有限公司-分红-个人分红-005L-FH002深', holding_shares: 23000000, circulation_share_ratio: 1.46, shareholder_type: '保险', end_date: '2024-09-30' }, + { shareholder_rank: 9, shareholder_name: '易方达沪深300交易型开放式指数发起式证券投资基金', holding_shares: 19000000, circulation_share_ratio: 1.20, shareholder_type: '基金', end_date: '2024-09-30' }, + { shareholder_rank: 10, shareholder_name: '嘉实沪深300交易型开放式指数证券投资基金', holding_shares: 15000000, circulation_share_ratio: 0.95, shareholder_type: '基金', end_date: '2024-09-30' } + ], + topShareholders: [ + { shareholder_rank: 1, shareholder_name: '某控股集团有限公司', holding_shares: 560000000, total_share_ratio: 35.50, shareholder_type: '法人', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 2, shareholder_name: '香港中央结算有限公司(陆股通)', holding_shares: 156000000, total_share_ratio: 9.88, shareholder_type: 'QFII', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 3, shareholder_name: '中国平安人寿保险股份有限公司-传统-普通保险产品', holding_shares: 89000000, total_share_ratio: 5.64, shareholder_type: '保险', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 4, shareholder_name: '中国证券金融股份有限公司', holding_shares: 67000000, total_share_ratio: 4.24, shareholder_type: '券商', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 5, shareholder_name: '中央汇金资产管理有限责任公司', holding_shares: 45000000, total_share_ratio: 2.85, shareholder_type: '法人', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 6, shareholder_name: '全国社保基金一零三组合', holding_shares: 34000000, total_share_ratio: 2.15, shareholder_type: '社保', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 7, shareholder_name: '华夏上证50交易型开放式指数证券投资基金', holding_shares: 28000000, total_share_ratio: 1.77, shareholder_type: '基金', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 8, shareholder_name: '中国人寿保险股份有限公司-分红-个人分红-005L-FH002深', holding_shares: 23000000, total_share_ratio: 1.46, shareholder_type: '保险', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 9, shareholder_name: '易方达沪深300交易型开放式指数发起式证券投资基金', holding_shares: 19000000, total_share_ratio: 1.20, shareholder_type: '基金', share_nature: '流通A股', end_date: '2024-09-30' }, + { shareholder_rank: 10, shareholder_name: '嘉实沪深300交易型开放式指数证券投资基金', holding_shares: 15000000, total_share_ratio: 0.95, shareholder_type: '基金', share_nature: '流通A股', end_date: '2024-09-30' } + ], branches: [ { name: '北京分公司', address: '北京市朝阳区某路123号', phone: '010-12345678', type: '分公司', establish_date: '2012-05-01' }, { name: '上海分公司', address: '上海市浦东新区某路456号', phone: '021-12345678', type: '分公司', establish_date: '2013-08-15' }, diff --git a/src/mocks/handlers/company.js b/src/mocks/handlers/company.js index 7e45e472..20677aa7 100644 --- a/src/mocks/handlers/company.js +++ b/src/mocks/handlers/company.js @@ -67,10 +67,19 @@ export const companyHandlers = [ await delay(150); const { stockCode } = params; const data = getCompanyData(stockCode); + const raw = data.actualControl; + + // 数据已经是数组格式,只做数值转换(holding_ratio 从 0-100 转为 0-1) + const formatted = Array.isArray(raw) + ? raw.map(item => ({ + ...item, + holding_ratio: item.holding_ratio > 1 ? item.holding_ratio / 100 : item.holding_ratio, + })) + : []; return HttpResponse.json({ success: true, - data: data.actualControl + data: formatted }); }), @@ -79,10 +88,19 @@ export const companyHandlers = [ await delay(150); const { stockCode } = params; const data = getCompanyData(stockCode); + const raw = data.concentration; + + // 数据已经是数组格式,只做数值转换(holding_ratio 从 0-100 转为 0-1) + const formatted = Array.isArray(raw) + ? raw.map(item => ({ + ...item, + holding_ratio: item.holding_ratio > 1 ? item.holding_ratio / 100 : item.holding_ratio, + })) + : []; return HttpResponse.json({ success: true, - data: data.concentration + data: formatted }); }), diff --git a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx index e87ec1b4..23c903ba 100644 --- a/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx +++ b/src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx @@ -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 = { - 基金: { 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 ( - - - {type} - - ); -}; - +/** + * 股权结构面板 + * 使用拆分后的子组件: + * - ActualControlCard: 实际控制人卡片 + * - ConcentrationCard: 股权集中度卡片 + * - ShareholdersTable: 股东表格(合并版,支持十大股东和十大流通股东) + */ const ShareholderPanel: React.FC = ({ stockCode }) => { const { actualControl, @@ -86,226 +32,31 @@ const ShareholderPanel: React.FC = ({ stockCode }) => { loading, } = useShareholderData(stockCode); - // 计算股权集中度变化 - const getConcentrationTrend = () => { - const grouped: Record> = {}; - 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 ; } return ( - {/* 实际控制人 */} - {actualControl.length > 0 && ( + {/* 实际控制人 + 股权集中度 左右分布 */} + - - - 实际控制人 - - - - - - - {actualControl[0].actual_controller_name} - - - - {actualControl[0].control_type} - - - 截至 {formatDate(actualControl[0].end_date)} - - - - - 控制比例 - - {formatPercentage(actualControl[0].holding_ratio)} - - - {formatShares(actualControl[0].holding_shares)} - - - - - + - )} + + + + - {/* 股权集中度 */} - {concentration.length > 0 && ( + {/* 十大股东 + 十大流通股东 左右分布 */} + - - - 股权集中度 - - - {getConcentrationTrend() - .slice(0, 1) - .map(([date, items]) => ( - - - - {formatDate(date)} - - - - - {Object.entries(items).map(([key, item]: [string, any]) => ( - - {item.stat_item} - - - {formatPercentage(item.holding_ratio)} - - {item.ratio_change && ( - 0 ? "red" : "green"}> - 0 ? FaArrowUp : FaArrowDown} - mr={1} - boxSize={3} - /> - {Math.abs(item.ratio_change).toFixed(2)}% - - )} - - - ))} - - - - ))} - + - )} - - {/* 十大股东 */} - {topShareholders.length > 0 && ( - - - 十大股东 - - {formatDate(topShareholders[0].end_date)} - - - - - - - - - - - - - - - - {topShareholders.slice(0, 10).map((shareholder: any, idx: number) => ( - - - - - - - - - ))} - -
排名股东名称股东类型持股数量持股比例股份性质
- - {shareholder.shareholder_rank} - - - - - {shareholder.shareholder_name} - - - - - - {formatShares(shareholder.holding_shares)} - - - {formatPercentage(shareholder.total_share_ratio)} - - - - {shareholder.share_nature || "流通股"} - -
-
+
- )} - - {/* 十大流通股东 */} - {topCirculationShareholders.length > 0 && ( - - - - 十大流通股东 - - {formatDate(topCirculationShareholders[0].end_date)} - - - - - - - - - - - - - - - {topCirculationShareholders.slice(0, 10).map((shareholder: any, idx: number) => ( - - - - - - - - ))} - -
排名股东名称股东类型持股数量流通股比例
- - {shareholder.shareholder_rank} - - - - - {shareholder.shareholder_name} - - - - - - {formatShares(shareholder.holding_shares)} - - - {formatPercentage(shareholder.circulation_share_ratio)} - -
-
-
- )} +
); }; diff --git a/src/views/Company/components/CompanyOverview/components/shareholder/ActualControlCard.tsx b/src/views/Company/components/CompanyOverview/components/shareholder/ActualControlCard.tsx new file mode 100644 index 00000000..d29d6c6a --- /dev/null +++ b/src/views/Company/components/CompanyOverview/components/shareholder/ActualControlCard.tsx @@ -0,0 +1,96 @@ +// src/views/Company/components/CompanyOverview/components/shareholder/ActualControlCard.tsx +// 实际控制人卡片组件 + +import React from "react"; +import { + Box, + VStack, + HStack, + Text, + Heading, + Badge, + Icon, + Card, + CardBody, + Stat, + StatLabel, + StatNumber, + StatHelpText, +} from "@chakra-ui/react"; +import { FaCrown } from "react-icons/fa"; +import type { ActualControl } from "../../types"; +import { THEME } from "../../BasicInfoTab/config"; + +// 格式化工具函数 +const formatPercentage = (value: number | null | undefined): string => { + if (value === null || value === undefined) return "-"; + return `${(value * 100).toFixed(2)}%`; +}; + +const formatShares = (value: number | null | undefined): string => { + if (value === null || value === undefined) return "-"; + if (value >= 100000000) { + return `${(value / 100000000).toFixed(2)}亿股`; + } else if (value >= 10000) { + return `${(value / 10000).toFixed(2)}万股`; + } + return `${value.toLocaleString()}股`; +}; + +const formatDate = (dateStr: string | null | undefined): string => { + if (!dateStr) return "-"; + return dateStr.split("T")[0]; +}; + +interface ActualControlCardProps { + actualControl: ActualControl[]; +} + +/** + * 实际控制人卡片 + */ +const ActualControlCard: React.FC = ({ actualControl = [] }) => { + if (!actualControl.length) return null; + + const data = actualControl[0]; + + return ( + + + + 实际控制人 + + + + + + + {data.actual_controller_name} + + + {data.control_type} + + 截至 {formatDate(data.end_date)} + + + + + 控制比例 + + {formatPercentage(data.holding_ratio)} + + {formatShares(data.holding_shares)} + + + + + + ); +}; + +export default ActualControlCard; diff --git a/src/views/Company/components/CompanyOverview/components/shareholder/ConcentrationCard.tsx b/src/views/Company/components/CompanyOverview/components/shareholder/ConcentrationCard.tsx new file mode 100644 index 00000000..dc82c45c --- /dev/null +++ b/src/views/Company/components/CompanyOverview/components/shareholder/ConcentrationCard.tsx @@ -0,0 +1,234 @@ +// src/views/Company/components/CompanyOverview/components/shareholder/ConcentrationCard.tsx +// 股权集中度卡片组件 + +import React, { useMemo, useRef, useEffect } from "react"; +import { + Box, + VStack, + HStack, + Text, + Heading, + Badge, + Icon, + Card, + CardBody, + CardHeader, + SimpleGrid, +} from "@chakra-ui/react"; +import { FaChartPie, FaArrowUp, FaArrowDown } from "react-icons/fa"; +import * as echarts from "echarts"; +import type { Concentration } from "../../types"; +import { THEME } from "../../BasicInfoTab/config"; + +// 格式化工具函数 +const formatPercentage = (value: number | null | undefined): string => { + if (value === null || value === undefined) return "-"; + return `${(value * 100).toFixed(2)}%`; +}; + +const formatDate = (dateStr: string | null | undefined): string => { + if (!dateStr) return "-"; + return dateStr.split("T")[0]; +}; + +interface ConcentrationCardProps { + concentration: Concentration[]; +} + +// 饼图颜色配置(黑金主题) +const PIE_COLORS = [ + "#D4AF37", // 金色 - 前1大股东 + "#F0D78C", // 浅金色 - 第2-3大股东 + "#B8860B", // 暗金色 - 第4-5大股东 + "#DAA520", // 金麒麟色 - 第6-10大股东 + "#4A5568", // 灰色 - 其他股东 +]; + +/** + * 股权集中度卡片 + */ +const ConcentrationCard: React.FC = ({ concentration = [] }) => { + const chartRef = useRef(null); + const chartInstance = useRef(null); + + // 按日期分组 + const groupedData = useMemo(() => { + const grouped: Record> = {}; + concentration.forEach((item) => { + 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, 1); // 只取最新一期 + }, [concentration]); + + // 计算饼图数据 + const pieData = useMemo(() => { + if (groupedData.length === 0) return []; + + const [, items] = groupedData[0]; + const top1 = items["前1大股东"]?.holding_ratio || 0; + const top3 = items["前3大股东"]?.holding_ratio || 0; + const top5 = items["前5大股东"]?.holding_ratio || 0; + const top10 = items["前10大股东"]?.holding_ratio || 0; + + return [ + { name: "前1大股东", value: Number((top1 * 100).toFixed(2)) }, + { name: "第2-3大股东", value: Number(((top3 - top1) * 100).toFixed(2)) }, + { name: "第4-5大股东", value: Number(((top5 - top3) * 100).toFixed(2)) }, + { name: "第6-10大股东", value: Number(((top10 - top5) * 100).toFixed(2)) }, + { name: "其他股东", value: Number(((1 - top10) * 100).toFixed(2)) }, + ].filter(item => item.value > 0); + }, [groupedData]); + + // 初始化和更新图表 + useEffect(() => { + if (!chartRef.current || pieData.length === 0) return; + + // 使用 requestAnimationFrame 确保 DOM 渲染完成后再初始化 + const initChart = () => { + if (!chartRef.current) return; + + // 初始化图表 + if (!chartInstance.current) { + chartInstance.current = echarts.init(chartRef.current); + } + + const option: echarts.EChartsOption = { + backgroundColor: "transparent", + tooltip: { + trigger: "item", + formatter: "{b}: {c}%", + backgroundColor: "rgba(0,0,0,0.8)", + borderColor: THEME.gold, + textStyle: { color: "#fff" }, + }, + legend: { + orient: "vertical", + right: 10, + top: "center", + textStyle: { color: THEME.textSecondary, fontSize: 11 }, + itemWidth: 12, + itemHeight: 12, + }, + series: [ + { + name: "股权集中度", + type: "pie", + radius: ["40%", "70%"], + center: ["35%", "50%"], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 4, + borderColor: THEME.cardBg, + borderWidth: 2, + }, + label: { + show: false, + }, + emphasis: { + label: { + show: true, + fontSize: 12, + fontWeight: "bold", + color: THEME.textPrimary, + formatter: "{b}\n{c}%", + }, + }, + labelLine: { show: false }, + data: pieData.map((item, index) => ({ + ...item, + itemStyle: { color: PIE_COLORS[index] }, + })), + }, + ], + }; + + chartInstance.current.setOption(option); + + // 延迟 resize 确保容器尺寸已计算完成 + setTimeout(() => { + chartInstance.current?.resize(); + }, 100); + }; + + // 延迟初始化,确保布局完成 + const rafId = requestAnimationFrame(initChart); + + // 响应式 + const handleResize = () => chartInstance.current?.resize(); + window.addEventListener("resize", handleResize); + + return () => { + cancelAnimationFrame(rafId); + window.removeEventListener("resize", handleResize); + }; + }, [pieData]); + + // 组件卸载时销毁图表 + useEffect(() => { + return () => { + chartInstance.current?.dispose(); + }; + }, []); + + if (!concentration.length) return null; + + return ( + + + + 股权集中度 + + + {/* 数据卡片 */} + {groupedData.map(([date, items]) => ( + + + + {formatDate(date)} + + + + + {Object.entries(items).map(([key, item]) => ( + + {item.stat_item} + + + {formatPercentage(item.holding_ratio)} + + {item.ratio_change && ( + 0 ? "red" : "green"} + > + 0 ? FaArrowUp : FaArrowDown} + mr={1} + boxSize={3} + /> + {Math.abs(item.ratio_change).toFixed(2)}% + + )} + + + ))} + + + + ))} + {/* 饼图 */} + + + + + + + + ); +}; + +export default ConcentrationCard; diff --git a/src/views/Company/components/CompanyOverview/components/shareholder/ShareholdersTable.tsx b/src/views/Company/components/CompanyOverview/components/shareholder/ShareholdersTable.tsx new file mode 100644 index 00000000..41de8db1 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/components/shareholder/ShareholdersTable.tsx @@ -0,0 +1,224 @@ +// src/views/Company/components/CompanyOverview/components/shareholder/ShareholdersTable.tsx +// 股东表格组件(合并版)- 支持十大股东和十大流通股东 + +import React, { useMemo } from "react"; +import { Box, HStack, Heading, Badge, Icon, useBreakpointValue } from "@chakra-ui/react"; +import { Table, Tag, Tooltip, ConfigProvider } from "antd"; +import type { ColumnsType } from "antd/es/table"; +import { FaUsers, FaChartLine } from "react-icons/fa"; +import type { Shareholder } from "../../types"; +import { THEME } from "../../BasicInfoTab/config"; + +// antd 表格黑金主题配置 +const TABLE_THEME = { + token: { + colorBgContainer: "#2D3748", // gray.700 + colorText: "white", + colorTextHeading: "#D4AF37", // 金色 + colorBorderSecondary: "rgba(212, 175, 55, 0.3)", + }, + components: { + Table: { + headerBg: "#1A202C", // gray.900 + headerColor: "#D4AF37", // 金色 + rowHoverBg: "rgba(212, 175, 55, 0.15)", // 金色半透明,文字更清晰 + borderColor: "rgba(212, 175, 55, 0.2)", + }, + }, +}; + +// 格式化工具函数 +const formatPercentage = (value: number | null | undefined): string => { + if (value === null || value === undefined) return "-"; + return `${(value * 100).toFixed(2)}%`; +}; + +const formatShares = (value: number | null | undefined): string => { + if (value === null || value === undefined) return "-"; + if (value >= 100000000) { + return `${(value / 100000000).toFixed(2)}亿股`; + } else if (value >= 10000) { + return `${(value / 10000).toFixed(2)}万股`; + } + return `${value.toLocaleString()}股`; +}; + +const formatDate = (dateStr: string | null | undefined): string => { + if (!dateStr) return "-"; + return dateStr.split("T")[0]; +}; + +// 股东类型颜色映射 +const shareholderTypeColors: Record = { + 基金: "blue", + 个人: "green", + 法人: "purple", + QFII: "orange", + 社保: "red", + 保险: "cyan", + 信托: "geekblue", + 券商: "magenta", + 企业: "purple", + 机构: "blue", +}; + +const getShareholderTypeColor = (type: string | undefined): string => { + if (!type) return "default"; + for (const [key, color] of Object.entries(shareholderTypeColors)) { + if (type.includes(key)) return color; + } + return "default"; +}; + +interface ShareholdersTableProps { + type?: "top" | "circulation"; + shareholders: Shareholder[]; + title?: string; +} + +/** + * 股东表格组件 + * @param type - 表格类型: "top" 十大股东 | "circulation" 十大流通股东 + * @param shareholders - 股东数据数组 + * @param title - 自定义标题 + */ +const ShareholdersTable: React.FC = ({ + type = "top", + shareholders = [], + title, +}) => { + const isMobile = useBreakpointValue({ base: true, md: false }); + + // 配置 + const config = useMemo(() => { + if (type === "circulation") { + return { + title: title || "十大流通股东", + icon: FaChartLine, + iconColor: "purple.500", + ratioField: "circulation_share_ratio" as keyof Shareholder, + ratioLabel: "流通股比例", + rankColor: "orange", + showNature: true, // 与十大股东保持一致 + }; + } + return { + title: title || "十大股东", + icon: FaUsers, + iconColor: "green.500", + ratioField: "total_share_ratio" as keyof Shareholder, + ratioLabel: "持股比例", + rankColor: "red", + showNature: true, + }; + }, [type, title]); + + // 表格列定义 + const columns: ColumnsType = useMemo(() => { + const baseColumns: ColumnsType = [ + { + title: "排名", + dataIndex: "shareholder_rank", + key: "rank", + width: 45, + render: (rank: number, _: Shareholder, index: number) => ( + + {rank || index + 1} + + ), + }, + { + title: "股东名称", + dataIndex: "shareholder_name", + key: "name", + ellipsis: true, + render: (name: string) => ( + + {name} + + ), + }, + { + title: "股东类型", + dataIndex: "shareholder_type", + key: "type", + width: 90, + responsive: ["md"], + render: (shareholderType: string) => ( + {shareholderType || "-"} + ), + }, + { + title: "持股数量", + dataIndex: "holding_shares", + key: "shares", + width: 100, + align: "right", + responsive: ["md"], + sorter: (a: Shareholder, b: Shareholder) => (a.holding_shares || 0) - (b.holding_shares || 0), + render: (shares: number) => formatShares(shares), + }, + { + title: {config.ratioLabel}, + dataIndex: config.ratioField as string, + key: "ratio", + width: 110, + align: "right", + sorter: (a: Shareholder, b: Shareholder) => { + const aVal = (a[config.ratioField] as number) || 0; + const bVal = (b[config.ratioField] as number) || 0; + return aVal - bVal; + }, + defaultSortOrder: "descend", + render: (ratio: number) => ( + + {formatPercentage(ratio)} + + ), + }, + ]; + + // 十大股东显示股份性质 + if (config.showNature) { + baseColumns.push({ + title: "股份性质", + dataIndex: "share_nature", + key: "nature", + width: 80, + responsive: ["lg"], + render: (nature: string) => ( + {nature || "流通股"} + ), + }); + } + + return baseColumns; + }, [config, type]); + + if (!shareholders.length) return null; + + // 获取数据日期 + const reportDate = shareholders[0]?.end_date; + + return ( + + + + {config.title} + {reportDate && {formatDate(reportDate)}} + + + `${record.shareholder_name}-${index}`} + pagination={false} + size={isMobile ? "small" : "middle"} + scroll={{ x: isMobile ? 400 : undefined }} + /> + + + ); +}; + +export default ShareholdersTable; diff --git a/src/views/Company/components/CompanyOverview/components/shareholder/index.ts b/src/views/Company/components/CompanyOverview/components/shareholder/index.ts new file mode 100644 index 00000000..13192679 --- /dev/null +++ b/src/views/Company/components/CompanyOverview/components/shareholder/index.ts @@ -0,0 +1,6 @@ +// src/views/Company/components/CompanyOverview/components/shareholder/index.ts +// 股权结构子组件汇总导出 + +export { default as ActualControlCard } from "./ActualControlCard"; +export { default as ConcentrationCard } from "./ConcentrationCard"; +export { default as ShareholdersTable } from "./ShareholdersTable";