Compare commits
3 Commits
1cd8a2d7e9
...
3abee6b907
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3abee6b907 | ||
|
|
d86cef9f79 | ||
|
|
9aaf4400c1 |
@@ -42,27 +42,33 @@ export const PINGAN_BANK_DATA = {
|
|||||||
employees: 42099,
|
employees: 42099,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 实际控制人信息
|
// 实际控制人信息(数组格式)
|
||||||
actualControl: {
|
actualControl: [
|
||||||
|
{
|
||||||
|
actual_controller_name: '中国平安保险(集团)股份有限公司',
|
||||||
controller_name: '中国平安保险(集团)股份有限公司',
|
controller_name: '中国平安保险(集团)股份有限公司',
|
||||||
|
control_type: '企业法人',
|
||||||
controller_type: '企业',
|
controller_type: '企业',
|
||||||
shareholding_ratio: 52.38,
|
holding_ratio: 52.38,
|
||||||
|
holding_shares: 10168542300,
|
||||||
|
end_date: '2024-09-30',
|
||||||
control_chain: '中国平安保险(集团)股份有限公司 -> 平安银行股份有限公司',
|
control_chain: '中国平安保险(集团)股份有限公司 -> 平安银行股份有限公司',
|
||||||
is_listed: true,
|
is_listed: true,
|
||||||
change_date: '2023-12-31',
|
|
||||||
remark: '中国平安通过直接和间接方式控股平安银行',
|
remark: '中国平安通过直接和间接方式控股平安银行',
|
||||||
},
|
}
|
||||||
|
],
|
||||||
|
|
||||||
// 股权集中度
|
// 股权集中度(数组格式,按统计项分组)
|
||||||
concentration: {
|
concentration: [
|
||||||
top1_ratio: 52.38,
|
{ stat_item: '前1大股东', holding_ratio: 52.38, ratio_change: 0.00, end_date: '2024-09-30' },
|
||||||
top3_ratio: 58.42,
|
{ stat_item: '前3大股东', holding_ratio: 58.42, ratio_change: 0.15, end_date: '2024-09-30' },
|
||||||
top5_ratio: 60.15,
|
{ stat_item: '前5大股东', holding_ratio: 60.15, ratio_change: 0.22, end_date: '2024-09-30' },
|
||||||
top10_ratio: 63.28,
|
{ stat_item: '前10大股东', holding_ratio: 63.28, ratio_change: 0.35, end_date: '2024-09-30' },
|
||||||
update_date: '2024-09-30',
|
{ stat_item: '前1大股东', holding_ratio: 52.38, ratio_change: -0.12, end_date: '2024-06-30' },
|
||||||
concentration_level: '高度集中',
|
{ stat_item: '前3大股东', holding_ratio: 58.27, ratio_change: -0.08, end_date: '2024-06-30' },
|
||||||
herfindahl_index: 0.2845,
|
{ stat_item: '前5大股东', holding_ratio: 59.93, ratio_change: -0.15, end_date: '2024-06-30' },
|
||||||
},
|
{ stat_item: '前10大股东', holding_ratio: 62.93, ratio_change: -0.22, end_date: '2024-06-30' },
|
||||||
|
],
|
||||||
|
|
||||||
// 高管信息
|
// 高管信息
|
||||||
management: [
|
management: [
|
||||||
@@ -128,32 +134,32 @@ export const PINGAN_BANK_DATA = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
// 十大流通股东
|
// 十大流通股东(字段名与组件期望格式匹配)
|
||||||
topCirculationShareholders: [
|
topCirculationShareholders: [
|
||||||
{ shareholder_name: '中国平安保险(集团)股份有限公司', shares: 10168542300, ratio: 52.38, change: 0, shareholder_type: '企业' },
|
{ shareholder_rank: 1, shareholder_name: '中国平安保险(集团)股份有限公司', holding_shares: 10168542300, circulation_share_ratio: 52.38, shareholder_type: '法人', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '香港中央结算有限公司', shares: 542138600, ratio: 2.79, change: 12450000, shareholder_type: '境外法人' },
|
{ shareholder_rank: 2, shareholder_name: '香港中央结算有限公司', holding_shares: 542138600, circulation_share_ratio: 2.79, shareholder_type: 'QFII', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '深圳市投资控股有限公司', shares: 382456100, ratio: 1.97, change: 0, shareholder_type: '国有企业' },
|
{ shareholder_rank: 3, shareholder_name: '深圳市投资控股有限公司', holding_shares: 382456100, circulation_share_ratio: 1.97, shareholder_type: '法人', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '中国证券金融股份有限公司', shares: 298654200, ratio: 1.54, change: -5000000, shareholder_type: '证金公司' },
|
{ shareholder_rank: 4, shareholder_name: '中国证券金融股份有限公司', holding_shares: 298654200, circulation_share_ratio: 1.54, shareholder_type: '券商', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '中央汇金资产管理有限责任公司', shares: 267842100, ratio: 1.38, change: 0, shareholder_type: '中央汇金' },
|
{ shareholder_rank: 5, shareholder_name: '中央汇金资产管理有限责任公司', holding_shares: 267842100, circulation_share_ratio: 1.38, shareholder_type: '法人', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '全国社保基金一零三组合', shares: 156234500, ratio: 0.80, change: 23400000, shareholder_type: '社保基金' },
|
{ shareholder_rank: 6, shareholder_name: '全国社保基金一零三组合', holding_shares: 156234500, circulation_share_ratio: 0.80, shareholder_type: '社保', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '全国社保基金一零一组合', shares: 142356700, ratio: 0.73, change: 15600000, shareholder_type: '社保基金' },
|
{ shareholder_rank: 7, shareholder_name: '全国社保基金一零一组合', holding_shares: 142356700, circulation_share_ratio: 0.73, shareholder_type: '社保', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '中国人寿保险股份有限公司', shares: 128945600, ratio: 0.66, change: 0, shareholder_type: '保险公司' },
|
{ shareholder_rank: 8, shareholder_name: '中国人寿保险股份有限公司', holding_shares: 128945600, circulation_share_ratio: 0.66, shareholder_type: '保险', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: 'GIC PRIVATE LIMITED', shares: 98765400, ratio: 0.51, change: -8900000, shareholder_type: '境外法人' },
|
{ 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_name: '挪威中央银行', shares: 87654300, ratio: 0.45, change: 5600000, shareholder_type: '境外法人' }
|
{ shareholder_rank: 10, shareholder_name: '挪威中央银行', holding_shares: 87654300, circulation_share_ratio: 0.45, shareholder_type: 'QFII', end_date: '2024-09-30' }
|
||||||
],
|
],
|
||||||
|
|
||||||
// 十大股东
|
// 十大股东(字段名与组件期望格式匹配)
|
||||||
topShareholders: [
|
topShareholders: [
|
||||||
{ shareholder_name: '中国平安保险(集团)股份有限公司', shares: 10168542300, ratio: 52.38, change: 0, shareholder_type: '企业', is_restricted: false },
|
{ shareholder_rank: 1, shareholder_name: '中国平安保险(集团)股份有限公司', holding_shares: 10168542300, total_share_ratio: 52.38, shareholder_type: '法人', share_nature: '流通A股', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '香港中央结算有限公司', shares: 542138600, ratio: 2.79, change: 12450000, shareholder_type: '境外法人', is_restricted: false },
|
{ 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_name: '深圳市投资控股有限公司', shares: 382456100, ratio: 1.97, change: 0, shareholder_type: '国有企业', is_restricted: false },
|
{ shareholder_rank: 3, shareholder_name: '深圳市投资控股有限公司', holding_shares: 382456100, total_share_ratio: 1.97, shareholder_type: '法人', share_nature: '流通A股', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '中国证券金融股份有限公司', shares: 298654200, ratio: 1.54, change: -5000000, shareholder_type: '证金公司', is_restricted: false },
|
{ shareholder_rank: 4, shareholder_name: '中国证券金融股份有限公司', holding_shares: 298654200, total_share_ratio: 1.54, shareholder_type: '券商', share_nature: '流通A股', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '中央汇金资产管理有限责任公司', shares: 267842100, ratio: 1.38, change: 0, shareholder_type: '中央汇金', is_restricted: false },
|
{ shareholder_rank: 5, shareholder_name: '中央汇金资产管理有限责任公司', holding_shares: 267842100, total_share_ratio: 1.38, shareholder_type: '法人', share_nature: '流通A股', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '全国社保基金一零三组合', shares: 156234500, ratio: 0.80, change: 23400000, shareholder_type: '社保基金', is_restricted: false },
|
{ shareholder_rank: 6, shareholder_name: '全国社保基金一零三组合', holding_shares: 156234500, total_share_ratio: 0.80, shareholder_type: '社保', share_nature: '流通A股', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '全国社保基金一零一组合', shares: 142356700, ratio: 0.73, change: 15600000, shareholder_type: '社保基金', is_restricted: false },
|
{ shareholder_rank: 7, shareholder_name: '全国社保基金一零一组合', holding_shares: 142356700, total_share_ratio: 0.73, shareholder_type: '社保', share_nature: '流通A股', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: '中国人寿保险股份有限公司', shares: 128945600, ratio: 0.66, change: 0, shareholder_type: '保险公司', is_restricted: false },
|
{ shareholder_rank: 8, shareholder_name: '中国人寿保险股份有限公司', holding_shares: 128945600, total_share_ratio: 0.66, shareholder_type: '保险', share_nature: '流通A股', end_date: '2024-09-30' },
|
||||||
{ shareholder_name: 'GIC PRIVATE LIMITED', shares: 98765400, ratio: 0.51, change: -8900000, shareholder_type: '境外法人', is_restricted: false },
|
{ 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_name: '挪威中央银行', shares: 87654300, ratio: 0.45, change: 5600000, shareholder_type: '境外法人', is_restricted: false }
|
{ shareholder_rank: 10, shareholder_name: '挪威中央银行', holding_shares: 87654300, total_share_ratio: 0.45, shareholder_type: 'QFII', share_nature: '流通A股', end_date: '2024-09-30' }
|
||||||
],
|
],
|
||||||
|
|
||||||
// 分支机构
|
// 分支机构
|
||||||
@@ -998,23 +1004,29 @@ export const generateCompanyData = (stockCode, stockName = '示例公司') => {
|
|||||||
business_scope: '电子产品、通信设备、计算机软硬件的研发、生产、销售;技术咨询、技术服务;货物进出口、技术进出口。',
|
business_scope: '电子产品、通信设备、计算机软硬件的研发、生产、销售;技术咨询、技术服务;货物进出口、技术进出口。',
|
||||||
employees: employeeCount,
|
employees: employeeCount,
|
||||||
},
|
},
|
||||||
actualControl: {
|
actualControl: [
|
||||||
|
{
|
||||||
|
actual_controller_name: '某控股集团有限公司',
|
||||||
controller_name: '某控股集团有限公司',
|
controller_name: '某控股集团有限公司',
|
||||||
|
control_type: '企业法人',
|
||||||
controller_type: '企业',
|
controller_type: '企业',
|
||||||
shareholding_ratio: 35.5,
|
holding_ratio: 35.5,
|
||||||
|
holding_shares: 1560000000,
|
||||||
|
end_date: '2024-09-30',
|
||||||
control_chain: `某控股集团有限公司 -> ${stockName}股份有限公司`,
|
control_chain: `某控股集团有限公司 -> ${stockName}股份有限公司`,
|
||||||
is_listed: false,
|
is_listed: false,
|
||||||
change_date: '2023-12-31',
|
}
|
||||||
},
|
],
|
||||||
concentration: {
|
concentration: [
|
||||||
top1_ratio: 35.5,
|
{ stat_item: '前1大股东', holding_ratio: 35.5, ratio_change: 0.12, end_date: '2024-09-30' },
|
||||||
top3_ratio: 52.3,
|
{ stat_item: '前3大股东', holding_ratio: 52.3, ratio_change: 0.25, end_date: '2024-09-30' },
|
||||||
top5_ratio: 61.8,
|
{ stat_item: '前5大股东', holding_ratio: 61.8, ratio_change: 0.18, end_date: '2024-09-30' },
|
||||||
top10_ratio: 72.5,
|
{ stat_item: '前10大股东', holding_ratio: 72.5, ratio_change: 0.32, end_date: '2024-09-30' },
|
||||||
update_date: '2024-09-30',
|
{ stat_item: '前1大股东', holding_ratio: 35.38, ratio_change: -0.08, end_date: '2024-06-30' },
|
||||||
concentration_level: '适度集中',
|
{ stat_item: '前3大股东', holding_ratio: 52.05, ratio_change: -0.15, end_date: '2024-06-30' },
|
||||||
herfindahl_index: 0.1856,
|
{ stat_item: '前5大股东', holding_ratio: 61.62, ratio_change: -0.10, end_date: '2024-06-30' },
|
||||||
},
|
{ stat_item: '前10大股东', holding_ratio: 72.18, ratio_change: -0.20, end_date: '2024-06-30' },
|
||||||
|
],
|
||||||
management: [
|
management: [
|
||||||
{ name: '张三', position: '董事长', gender: '男', age: 55, education: '硕士', annual_compensation: 320.5, status: 'active' },
|
{ name: '张三', position: '董事长', gender: '男', age: 55, education: '硕士', annual_compensation: 320.5, status: 'active' },
|
||||||
{ name: '李四', position: '总经理', gender: '男', age: 50, education: '硕士', annual_compensation: 280.3, status: 'active' },
|
{ name: '李四', position: '总经理', gender: '男', age: 50, education: '硕士', annual_compensation: 280.3, status: 'active' },
|
||||||
@@ -1023,19 +1035,21 @@ export const generateCompanyData = (stockCode, stockName = '示例公司') => {
|
|||||||
{ name: '钱七', position: '技术总监', gender: '男', age: 42, education: '博士', annual_compensation: 250.8, status: 'active' },
|
{ name: '钱七', position: '技术总监', gender: '男', age: 42, education: '博士', annual_compensation: 250.8, status: 'active' },
|
||||||
],
|
],
|
||||||
topCirculationShareholders: Array(10).fill(null).map((_, i) => ({
|
topCirculationShareholders: Array(10).fill(null).map((_, i) => ({
|
||||||
shareholder_name: `股东${i + 1}`,
|
shareholder_rank: i + 1,
|
||||||
shares: Math.floor(Math.random() * 100000000),
|
shareholder_name: `流通股东${i + 1}`,
|
||||||
ratio: parseFloat(((10 - i) * 0.8 + Math.random() * 2).toFixed(2)),
|
holding_shares: Math.floor(Math.random() * 100000000) + 10000000,
|
||||||
change: Math.floor(Math.random() * 10000000) - 5000000,
|
circulation_share_ratio: parseFloat(((10 - i) * 0.8 + Math.random() * 2).toFixed(2)),
|
||||||
shareholder_type: i < 3 ? '企业' : (i < 6 ? '个人' : '机构')
|
shareholder_type: i < 3 ? '法人' : (i < 5 ? '基金' : (i < 7 ? '社保' : 'QFII')),
|
||||||
|
end_date: '2024-09-30'
|
||||||
})),
|
})),
|
||||||
topShareholders: Array(10).fill(null).map((_, i) => ({
|
topShareholders: Array(10).fill(null).map((_, i) => ({
|
||||||
|
shareholder_rank: i + 1,
|
||||||
shareholder_name: `股东${i + 1}`,
|
shareholder_name: `股东${i + 1}`,
|
||||||
shares: Math.floor(Math.random() * 100000000),
|
holding_shares: Math.floor(Math.random() * 100000000) + 10000000,
|
||||||
ratio: parseFloat(((10 - i) * 0.8 + Math.random() * 2).toFixed(2)),
|
total_share_ratio: parseFloat(((10 - i) * 0.8 + Math.random() * 2).toFixed(2)),
|
||||||
change: Math.floor(Math.random() * 10000000) - 5000000,
|
shareholder_type: i < 3 ? '法人' : (i < 5 ? '基金' : (i < 7 ? '社保' : 'QFII')),
|
||||||
shareholder_type: i < 3 ? '企业' : (i < 6 ? '个人' : '机构'),
|
share_nature: i < 2 ? '限售股' : '流通A股',
|
||||||
is_restricted: i < 2
|
end_date: '2024-09-30'
|
||||||
})),
|
})),
|
||||||
branches: [
|
branches: [
|
||||||
{ name: '北京分公司', address: '北京市朝阳区某路123号', phone: '010-12345678', type: '分公司', establish_date: '2012-05-01' },
|
{ name: '北京分公司', address: '北京市朝阳区某路123号', phone: '010-12345678', type: '分公司', establish_date: '2012-05-01' },
|
||||||
|
|||||||
@@ -21,11 +21,22 @@ src/views/Company/
|
|||||||
│ │
|
│ │
|
||||||
│ ├── CompanyOverview/ # Tab: 公司概览(TypeScript 拆分)
|
│ ├── CompanyOverview/ # Tab: 公司概览(TypeScript 拆分)
|
||||||
│ │ ├── index.tsx # 主组件(组合层,约 50 行)
|
│ │ ├── index.tsx # 主组件(组合层,约 50 行)
|
||||||
│ │ ├── CompanyHeaderCard.tsx # 头部卡片组件(约 130 行)
|
│ │ ├── CompanyHeaderCard.tsx # 头部卡片组件(黑金主题,约 200 行)
|
||||||
│ │ ├── BasicInfoTab.js # 基本信息 Tab(暂保持 JS)
|
│ │ ├── BasicInfoTab/ # 基本信息 Tab(TypeScript 可配置化重构)
|
||||||
|
│ │ │ ├── index.tsx # 主组件(可配置,约 120 行)
|
||||||
|
│ │ │ ├── config.ts # Tab 配置 + 黑金主题(约 90 行)
|
||||||
|
│ │ │ ├── utils.ts # 格式化工具函数(约 50 行)
|
||||||
|
│ │ │ └── components/ # 子组件
|
||||||
|
│ │ │ ├── index.ts # 组件统一导出
|
||||||
|
│ │ │ ├── LoadingState.tsx # 加载状态组件(黑金主题 Spinner)
|
||||||
|
│ │ │ ├── ShareholderPanel.tsx # 股权结构面板(实控人、十大股东、股权集中度)
|
||||||
|
│ │ │ ├── ManagementPanel.tsx # 管理团队面板(高管列表表格)
|
||||||
|
│ │ │ ├── AnnouncementsPanel.tsx # 公告信息面板(公告列表 + 披露日程)
|
||||||
|
│ │ │ ├── BranchesPanel.tsx # 分支机构面板(分支列表表格)
|
||||||
|
│ │ │ └── BusinessInfoPanel.tsx # 工商信息面板(注册资本、成立日期等)
|
||||||
│ │ ├── DeepAnalysisTab.js # 深度分析 Tab
|
│ │ ├── DeepAnalysisTab.js # 深度分析 Tab
|
||||||
│ │ ├── NewsEventsTab.js # 新闻事件 Tab
|
│ │ ├── NewsEventsTab.js # 新闻事件 Tab
|
||||||
│ │ ├── types.ts # 类型定义(约 50 行)
|
│ │ ├── types.ts # 类型定义(约 120 行)
|
||||||
│ │ ├── utils.ts # 格式化工具(约 20 行)
|
│ │ ├── utils.ts # 格式化工具(约 20 行)
|
||||||
│ │ └── hooks/
|
│ │ └── hooks/
|
||||||
│ │ └── useCompanyOverviewData.ts # 数据 Hook(约 100 行)
|
│ │ └── useCompanyOverviewData.ts # 数据 Hook(约 100 行)
|
||||||
|
|||||||
@@ -1,994 +0,0 @@
|
|||||||
// src/views/Company/components/CompanyOverview/BasicInfoTab.js
|
|
||||||
// 基本信息 Tab - 股权结构、管理团队、公司公告、分支机构、工商信息
|
|
||||||
// 懒加载优化:使用 isLazy + 独立 Hooks,点击 Tab 时才加载对应数据
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
VStack,
|
|
||||||
HStack,
|
|
||||||
Text,
|
|
||||||
Heading,
|
|
||||||
Badge,
|
|
||||||
Icon,
|
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
CardHeader,
|
|
||||||
SimpleGrid,
|
|
||||||
Avatar,
|
|
||||||
Table,
|
|
||||||
Thead,
|
|
||||||
Tbody,
|
|
||||||
Tr,
|
|
||||||
Th,
|
|
||||||
Td,
|
|
||||||
TableContainer,
|
|
||||||
Tag,
|
|
||||||
Tooltip,
|
|
||||||
Divider,
|
|
||||||
Center,
|
|
||||||
Code,
|
|
||||||
Tabs,
|
|
||||||
TabList,
|
|
||||||
TabPanels,
|
|
||||||
Tab,
|
|
||||||
TabPanel,
|
|
||||||
Stat,
|
|
||||||
StatLabel,
|
|
||||||
StatNumber,
|
|
||||||
StatHelpText,
|
|
||||||
IconButton,
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
useDisclosure,
|
|
||||||
Spinner,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
|
|
||||||
// 懒加载 Hooks
|
|
||||||
import { useShareholderData } from "./hooks/useShareholderData";
|
|
||||||
import { useManagementData } from "./hooks/useManagementData";
|
|
||||||
import { useAnnouncementsData } from "./hooks/useAnnouncementsData";
|
|
||||||
import { useBranchesData } from "./hooks/useBranchesData";
|
|
||||||
import { useDisclosureData } from "./hooks/useDisclosureData";
|
|
||||||
import { ExternalLinkIcon } from "@chakra-ui/icons";
|
|
||||||
import {
|
|
||||||
FaShareAlt,
|
|
||||||
FaUserTie,
|
|
||||||
FaBullhorn,
|
|
||||||
FaSitemap,
|
|
||||||
FaInfoCircle,
|
|
||||||
FaCrown,
|
|
||||||
FaChartPie,
|
|
||||||
FaUsers,
|
|
||||||
FaChartLine,
|
|
||||||
FaArrowUp,
|
|
||||||
FaArrowDown,
|
|
||||||
FaChartBar,
|
|
||||||
FaBuilding,
|
|
||||||
FaGlobe,
|
|
||||||
FaShieldAlt,
|
|
||||||
FaBriefcase,
|
|
||||||
FaCircle,
|
|
||||||
FaEye,
|
|
||||||
FaVenusMars,
|
|
||||||
FaGraduationCap,
|
|
||||||
FaPassport,
|
|
||||||
FaCalendarAlt,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
|
|
||||||
// 格式化工具函数
|
|
||||||
const formatUtils = {
|
|
||||||
formatPercentage: (value) => {
|
|
||||||
if (value === null || value === undefined) return "-";
|
|
||||||
return `${(value * 100).toFixed(2)}%`;
|
|
||||||
},
|
|
||||||
formatNumber: (value) => {
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
formatShares: (value) => {
|
|
||||||
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()}股`;
|
|
||||||
},
|
|
||||||
formatDate: (dateStr) => {
|
|
||||||
if (!dateStr) return "-";
|
|
||||||
return dateStr.split("T")[0];
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 股东类型标签组件
|
|
||||||
const ShareholderTypeBadge = ({ type }) => {
|
|
||||||
const typeConfig = {
|
|
||||||
基金: { 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 懒加载 TabPanel 子组件
|
|
||||||
// 每个子组件独立调用 Hook,配合 isLazy 实现真正的懒加载
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 股权结构 Tab Panel - 懒加载子组件
|
|
||||||
*/
|
|
||||||
const ShareholderTabPanel = ({ stockCode }) => {
|
|
||||||
const {
|
|
||||||
actualControl,
|
|
||||||
concentration,
|
|
||||||
topShareholders,
|
|
||||||
topCirculationShareholders,
|
|
||||||
loading,
|
|
||||||
} = useShareholderData(stockCode);
|
|
||||||
|
|
||||||
// 计算股权集中度变化
|
|
||||||
const getConcentrationTrend = () => {
|
|
||||||
const grouped = {};
|
|
||||||
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, 5);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Center h="200px">
|
|
||||||
<VStack>
|
|
||||||
<Spinner size="lg" color="blue.500" />
|
|
||||||
<Text fontSize="sm" color="gray.500">
|
|
||||||
加载股权结构数据...
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack spacing={6} align="stretch">
|
|
||||||
{actualControl.length > 0 && (
|
|
||||||
<Box>
|
|
||||||
<HStack mb={4}>
|
|
||||||
<Icon as={FaCrown} color="gold" boxSize={5} />
|
|
||||||
<Heading size="sm">实际控制人</Heading>
|
|
||||||
</HStack>
|
|
||||||
<Card variant="outline">
|
|
||||||
<CardBody>
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<VStack align="start">
|
|
||||||
<Text fontWeight="bold" fontSize="lg">
|
|
||||||
{actualControl[0].actual_controller_name}
|
|
||||||
</Text>
|
|
||||||
<HStack>
|
|
||||||
<Badge colorScheme="purple">
|
|
||||||
{actualControl[0].control_type}
|
|
||||||
</Badge>
|
|
||||||
<Text fontSize="sm" color="gray.500">
|
|
||||||
截至 {formatUtils.formatDate(actualControl[0].end_date)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
<Stat textAlign="right">
|
|
||||||
<StatLabel>控制比例</StatLabel>
|
|
||||||
<StatNumber color="purple.500">
|
|
||||||
{formatUtils.formatPercentage(actualControl[0].holding_ratio)}
|
|
||||||
</StatNumber>
|
|
||||||
<StatHelpText>
|
|
||||||
{formatUtils.formatShares(actualControl[0].holding_shares)}
|
|
||||||
</StatHelpText>
|
|
||||||
</Stat>
|
|
||||||
</HStack>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{concentration.length > 0 && (
|
|
||||||
<Box>
|
|
||||||
<HStack mb={4}>
|
|
||||||
<Icon as={FaChartPie} color="blue.500" boxSize={5} />
|
|
||||||
<Heading size="sm">股权集中度</Heading>
|
|
||||||
</HStack>
|
|
||||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
|
||||||
{getConcentrationTrend()
|
|
||||||
.slice(0, 1)
|
|
||||||
.map(([date, items]) => (
|
|
||||||
<Card key={date} variant="outline">
|
|
||||||
<CardHeader pb={2}>
|
|
||||||
<Text fontSize="sm" color="gray.500">
|
|
||||||
{formatUtils.formatDate(date)}
|
|
||||||
</Text>
|
|
||||||
</CardHeader>
|
|
||||||
<CardBody pt={2}>
|
|
||||||
<VStack spacing={3} align="stretch">
|
|
||||||
{Object.entries(items).map(([key, item]) => (
|
|
||||||
<HStack key={key} justify="space-between">
|
|
||||||
<Text fontSize="sm">{item.stat_item}</Text>
|
|
||||||
<HStack>
|
|
||||||
<Text fontWeight="bold" color="blue.500">
|
|
||||||
{formatUtils.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>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{topShareholders.length > 0 && (
|
|
||||||
<Box>
|
|
||||||
<HStack mb={4}>
|
|
||||||
<Icon as={FaUsers} color="green.500" boxSize={5} />
|
|
||||||
<Heading size="sm">十大股东</Heading>
|
|
||||||
<Badge>
|
|
||||||
{formatUtils.formatDate(topShareholders[0].end_date)}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
<TableContainer>
|
|
||||||
<Table size="sm" variant="striped">
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th>排名</Th>
|
|
||||||
<Th>股东名称</Th>
|
|
||||||
<Th>股东类型</Th>
|
|
||||||
<Th isNumeric>持股数量</Th>
|
|
||||||
<Th isNumeric>持股比例</Th>
|
|
||||||
<Th>股份性质</Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{topShareholders.slice(0, 10).map((shareholder, idx) => (
|
|
||||||
<Tr key={idx}>
|
|
||||||
<Td>
|
|
||||||
<Badge colorScheme={idx < 3 ? "red" : "gray"}>
|
|
||||||
{shareholder.shareholder_rank}
|
|
||||||
</Badge>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Tooltip label={shareholder.shareholder_name}>
|
|
||||||
<Text noOfLines={1} maxW="200px">
|
|
||||||
{shareholder.shareholder_name}
|
|
||||||
</Text>
|
|
||||||
</Tooltip>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<ShareholderTypeBadge type={shareholder.shareholder_type} />
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric fontWeight="medium">
|
|
||||||
{formatUtils.formatShares(shareholder.holding_shares)}
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric>
|
|
||||||
<Text color="blue.500" fontWeight="bold">
|
|
||||||
{formatUtils.formatPercentage(
|
|
||||||
shareholder.total_share_ratio
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Badge size="sm" variant="outline">
|
|
||||||
{shareholder.share_nature || "流通股"}
|
|
||||||
</Badge>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{topCirculationShareholders.length > 0 && (
|
|
||||||
<Box>
|
|
||||||
<HStack mb={4}>
|
|
||||||
<Icon as={FaChartLine} color="purple.500" boxSize={5} />
|
|
||||||
<Heading size="sm">十大流通股东</Heading>
|
|
||||||
<Badge>
|
|
||||||
{formatUtils.formatDate(topCirculationShareholders[0].end_date)}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
<TableContainer>
|
|
||||||
<Table size="sm" variant="striped">
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th>排名</Th>
|
|
||||||
<Th>股东名称</Th>
|
|
||||||
<Th>股东类型</Th>
|
|
||||||
<Th isNumeric>持股数量</Th>
|
|
||||||
<Th isNumeric>流通股比例</Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{topCirculationShareholders.slice(0, 10).map((shareholder, idx) => (
|
|
||||||
<Tr key={idx}>
|
|
||||||
<Td>
|
|
||||||
<Badge colorScheme={idx < 3 ? "orange" : "gray"}>
|
|
||||||
{shareholder.shareholder_rank}
|
|
||||||
</Badge>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Tooltip label={shareholder.shareholder_name}>
|
|
||||||
<Text noOfLines={1} maxW="250px">
|
|
||||||
{shareholder.shareholder_name}
|
|
||||||
</Text>
|
|
||||||
</Tooltip>
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<ShareholderTypeBadge type={shareholder.shareholder_type} />
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric fontWeight="medium">
|
|
||||||
{formatUtils.formatShares(shareholder.holding_shares)}
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric>
|
|
||||||
<Text color="purple.500" fontWeight="bold">
|
|
||||||
{formatUtils.formatPercentage(
|
|
||||||
shareholder.circulation_share_ratio
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理团队 Tab Panel - 懒加载子组件
|
|
||||||
*/
|
|
||||||
const ManagementTabPanel = ({ stockCode }) => {
|
|
||||||
const { management, loading } = useManagementData(stockCode);
|
|
||||||
|
|
||||||
// 管理层职位分类
|
|
||||||
const getManagementByCategory = () => {
|
|
||||||
const categories = {
|
|
||||||
高管: [],
|
|
||||||
董事: [],
|
|
||||||
监事: [],
|
|
||||||
其他: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
management.forEach((person) => {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Center h="200px">
|
|
||||||
<VStack>
|
|
||||||
<Spinner size="lg" color="blue.500" />
|
|
||||||
<Text fontSize="sm" color="gray.500">
|
|
||||||
加载管理团队数据...
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack spacing={6} align="stretch">
|
|
||||||
{Object.entries(getManagementByCategory()).map(
|
|
||||||
([category, people]) =>
|
|
||||||
people.length > 0 && (
|
|
||||||
<Box key={category}>
|
|
||||||
<HStack mb={4}>
|
|
||||||
<Icon
|
|
||||||
as={
|
|
||||||
category === "高管"
|
|
||||||
? FaUserTie
|
|
||||||
: category === "董事"
|
|
||||||
? FaCrown
|
|
||||||
: category === "监事"
|
|
||||||
? FaEye
|
|
||||||
: FaUsers
|
|
||||||
}
|
|
||||||
color={
|
|
||||||
category === "高管"
|
|
||||||
? "blue.500"
|
|
||||||
: category === "董事"
|
|
||||||
? "purple.500"
|
|
||||||
: category === "监事"
|
|
||||||
? "green.500"
|
|
||||||
: "gray.500"
|
|
||||||
}
|
|
||||||
boxSize={5}
|
|
||||||
/>
|
|
||||||
<Heading size="sm">{category}</Heading>
|
|
||||||
<Badge>{people.length}人</Badge>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
|
||||||
{people.map((person, idx) => (
|
|
||||||
<Card key={idx} variant="outline" size="sm">
|
|
||||||
<CardBody>
|
|
||||||
<HStack spacing={3} align="start">
|
|
||||||
<Avatar
|
|
||||||
name={person.name}
|
|
||||||
size="md"
|
|
||||||
bg={
|
|
||||||
category === "高管"
|
|
||||||
? "blue.500"
|
|
||||||
: category === "董事"
|
|
||||||
? "purple.500"
|
|
||||||
: category === "监事"
|
|
||||||
? "green.500"
|
|
||||||
: "gray.500"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<VStack align="start" spacing={1} flex={1}>
|
|
||||||
<HStack>
|
|
||||||
<Text fontWeight="bold">{person.name}</Text>
|
|
||||||
{person.gender && (
|
|
||||||
<Icon
|
|
||||||
as={FaVenusMars}
|
|
||||||
color={
|
|
||||||
person.gender === "男"
|
|
||||||
? "blue.400"
|
|
||||||
: "pink.400"
|
|
||||||
}
|
|
||||||
boxSize={3}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
<Text fontSize="sm" color="blue.600">
|
|
||||||
{person.position_name}
|
|
||||||
</Text>
|
|
||||||
<HStack spacing={2} flexWrap="wrap">
|
|
||||||
{person.education && (
|
|
||||||
<Tag size="sm" variant="subtle">
|
|
||||||
<Icon as={FaGraduationCap} mr={1} boxSize={3} />
|
|
||||||
{person.education}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{person.birth_year && (
|
|
||||||
<Tag size="sm" variant="subtle">
|
|
||||||
{new Date().getFullYear() -
|
|
||||||
parseInt(person.birth_year)}
|
|
||||||
岁
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
{person.nationality &&
|
|
||||||
person.nationality !== "中国" && (
|
|
||||||
<Tag size="sm" colorScheme="orange">
|
|
||||||
<Icon as={FaPassport} mr={1} boxSize={3} />
|
|
||||||
{person.nationality}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
<Text fontSize="xs" color="gray.500">
|
|
||||||
任职日期:{formatUtils.formatDate(person.start_date)}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</HStack>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 公司公告 Tab Panel - 懒加载子组件
|
|
||||||
*/
|
|
||||||
const AnnouncementsTabPanel = ({ stockCode }) => {
|
|
||||||
const { announcements, loading: announcementsLoading } =
|
|
||||||
useAnnouncementsData(stockCode);
|
|
||||||
const { disclosureSchedule, loading: disclosureLoading } =
|
|
||||||
useDisclosureData(stockCode);
|
|
||||||
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
|
||||||
const [selectedAnnouncement, setSelectedAnnouncement] = React.useState(null);
|
|
||||||
|
|
||||||
const handleAnnouncementClick = (announcement) => {
|
|
||||||
setSelectedAnnouncement(announcement);
|
|
||||||
onOpen();
|
|
||||||
};
|
|
||||||
|
|
||||||
const loading = announcementsLoading || disclosureLoading;
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Center h="200px">
|
|
||||||
<VStack>
|
|
||||||
<Spinner size="lg" color="blue.500" />
|
|
||||||
<Text fontSize="sm" color="gray.500">
|
|
||||||
加载公告数据...
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
{disclosureSchedule.length > 0 && (
|
|
||||||
<Box>
|
|
||||||
<HStack mb={3}>
|
|
||||||
<Icon as={FaCalendarAlt} color="orange.500" />
|
|
||||||
<Text fontWeight="bold">财报披露日程</Text>
|
|
||||||
</HStack>
|
|
||||||
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={3}>
|
|
||||||
{disclosureSchedule.slice(0, 4).map((schedule, idx) => (
|
|
||||||
<Card
|
|
||||||
key={idx}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
bg={schedule.is_disclosed ? "green.50" : "orange.50"}
|
|
||||||
>
|
|
||||||
<CardBody p={3}>
|
|
||||||
<VStack spacing={1}>
|
|
||||||
<Badge
|
|
||||||
colorScheme={schedule.is_disclosed ? "green" : "orange"}
|
|
||||||
>
|
|
||||||
{schedule.report_name}
|
|
||||||
</Badge>
|
|
||||||
<Text fontSize="sm" fontWeight="bold">
|
|
||||||
{schedule.is_disclosed ? "已披露" : "预计"}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="xs">
|
|
||||||
{formatUtils.formatDate(
|
|
||||||
schedule.is_disclosed
|
|
||||||
? schedule.actual_date
|
|
||||||
: schedule.latest_scheduled_date
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<HStack mb={3}>
|
|
||||||
<Icon as={FaBullhorn} color="blue.500" />
|
|
||||||
<Text fontWeight="bold">最新公告</Text>
|
|
||||||
</HStack>
|
|
||||||
<VStack spacing={2} align="stretch">
|
|
||||||
{announcements.map((announcement, idx) => (
|
|
||||||
<Card
|
|
||||||
key={idx}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
cursor="pointer"
|
|
||||||
onClick={() => handleAnnouncementClick(announcement)}
|
|
||||||
_hover={{ bg: "gray.50" }}
|
|
||||||
>
|
|
||||||
<CardBody p={3}>
|
|
||||||
<HStack justify="space-between">
|
|
||||||
<VStack align="start" spacing={1} flex={1}>
|
|
||||||
<HStack>
|
|
||||||
<Badge size="sm" colorScheme="blue">
|
|
||||||
{announcement.info_type || "公告"}
|
|
||||||
</Badge>
|
|
||||||
<Text fontSize="xs" color="gray.500">
|
|
||||||
{formatUtils.formatDate(announcement.announce_date)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<Text fontSize="sm" fontWeight="medium" noOfLines={1}>
|
|
||||||
{announcement.title}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
<HStack>
|
|
||||||
{announcement.format && (
|
|
||||||
<Tag size="sm" variant="subtle">
|
|
||||||
{announcement.format}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
icon={<ExternalLinkIcon />}
|
|
||||||
variant="ghost"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
window.open(announcement.url, "_blank");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</HStack>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
</VStack>
|
|
||||||
|
|
||||||
{/* 公告详情模态框 */}
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text>{selectedAnnouncement?.title}</Text>
|
|
||||||
<HStack>
|
|
||||||
<Badge colorScheme="blue">
|
|
||||||
{selectedAnnouncement?.info_type || "公告"}
|
|
||||||
</Badge>
|
|
||||||
<Text fontSize="sm" color="gray.500">
|
|
||||||
{formatUtils.formatDate(selectedAnnouncement?.announce_date)}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody>
|
|
||||||
<VStack align="start" spacing={3}>
|
|
||||||
<Text fontSize="sm">
|
|
||||||
文件格式:{selectedAnnouncement?.format || "-"}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm">
|
|
||||||
文件大小:{selectedAnnouncement?.file_size || "-"} KB
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
mr={3}
|
|
||||||
onClick={() => window.open(selectedAnnouncement?.url, "_blank")}
|
|
||||||
>
|
|
||||||
查看原文
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" onClick={onClose}>
|
|
||||||
关闭
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 分支机构 Tab Panel - 懒加载子组件
|
|
||||||
*/
|
|
||||||
const BranchesTabPanel = ({ stockCode }) => {
|
|
||||||
const { branches, loading } = useBranchesData(stockCode);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Center h="200px">
|
|
||||||
<VStack>
|
|
||||||
<Spinner size="lg" color="blue.500" />
|
|
||||||
<Text fontSize="sm" color="gray.500">
|
|
||||||
加载分支机构数据...
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (branches.length === 0) {
|
|
||||||
return (
|
|
||||||
<Center h="200px">
|
|
||||||
<VStack>
|
|
||||||
<Icon as={FaSitemap} boxSize={12} color="gray.300" />
|
|
||||||
<Text color="gray.500">暂无分支机构信息</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
|
||||||
{branches.map((branch, idx) => (
|
|
||||||
<Card key={idx} variant="outline">
|
|
||||||
<CardBody>
|
|
||||||
<VStack align="start" spacing={3}>
|
|
||||||
<HStack justify="space-between" w="full">
|
|
||||||
<Text fontWeight="bold">{branch.branch_name}</Text>
|
|
||||||
<Badge
|
|
||||||
colorScheme={
|
|
||||||
branch.business_status === "存续" ? "green" : "red"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{branch.business_status}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<SimpleGrid columns={2} spacing={2} w="full">
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontSize="xs" color="gray.500">
|
|
||||||
注册资本
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" fontWeight="medium">
|
|
||||||
{branch.register_capital || "-"}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontSize="xs" color="gray.500">
|
|
||||||
法人代表
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" fontWeight="medium">
|
|
||||||
{branch.legal_person || "-"}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontSize="xs" color="gray.500">
|
|
||||||
成立日期
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" fontWeight="medium">
|
|
||||||
{formatUtils.formatDate(branch.register_date)}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<Text fontSize="xs" color="gray.500">
|
|
||||||
关联企业
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" fontWeight="medium">
|
|
||||||
{branch.related_company_count || 0} 家
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</SimpleGrid>
|
|
||||||
</VStack>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 工商信息 Tab Panel - 使用父组件传入的 basicInfo
|
|
||||||
*/
|
|
||||||
const BusinessInfoTabPanel = ({ basicInfo }) => {
|
|
||||||
if (!basicInfo) {
|
|
||||||
return (
|
|
||||||
<Center h="200px">
|
|
||||||
<Text color="gray.500">暂无工商信息</Text>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack spacing={4} align="stretch">
|
|
||||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={6}>
|
|
||||||
<Box>
|
|
||||||
<Heading size="sm" mb={3}>
|
|
||||||
工商信息
|
|
||||||
</Heading>
|
|
||||||
<VStack align="start" spacing={2}>
|
|
||||||
<HStack w="full">
|
|
||||||
<Text fontSize="sm" color="gray.600" minW="80px">
|
|
||||||
统一信用代码
|
|
||||||
</Text>
|
|
||||||
<Code fontSize="xs">{basicInfo.credit_code}</Code>
|
|
||||||
</HStack>
|
|
||||||
<HStack w="full">
|
|
||||||
<Text fontSize="sm" color="gray.600" minW="80px">
|
|
||||||
公司规模
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm">{basicInfo.company_size}</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack w="full">
|
|
||||||
<Text fontSize="sm" color="gray.600" minW="80px">
|
|
||||||
注册地址
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" noOfLines={2}>
|
|
||||||
{basicInfo.reg_address}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack w="full">
|
|
||||||
<Text fontSize="sm" color="gray.600" minW="80px">
|
|
||||||
办公地址
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" noOfLines={2}>
|
|
||||||
{basicInfo.office_address}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Heading size="sm" mb={3}>
|
|
||||||
服务机构
|
|
||||||
</Heading>
|
|
||||||
<VStack align="start" spacing={2}>
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="sm" color="gray.600">
|
|
||||||
会计师事务所
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" fontWeight="medium">
|
|
||||||
{basicInfo.accounting_firm}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="sm" color="gray.600">
|
|
||||||
律师事务所
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" fontWeight="medium">
|
|
||||||
{basicInfo.law_firm}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Heading size="sm" mb={3}>
|
|
||||||
主营业务
|
|
||||||
</Heading>
|
|
||||||
<Text fontSize="sm" lineHeight="tall">
|
|
||||||
{basicInfo.main_business}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Heading size="sm" mb={3}>
|
|
||||||
经营范围
|
|
||||||
</Heading>
|
|
||||||
<Text fontSize="sm" lineHeight="tall" color="gray.700">
|
|
||||||
{basicInfo.business_scope}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 主组件
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基本信息 Tab 组件(懒加载版本)
|
|
||||||
*
|
|
||||||
* Props:
|
|
||||||
* - stockCode: 股票代码(用于懒加载数据)
|
|
||||||
* - basicInfo: 公司基本信息(从父组件传入,用于工商信息 Tab)
|
|
||||||
* - cardBg: 卡片背景色
|
|
||||||
*
|
|
||||||
* 懒加载策略:
|
|
||||||
* - 使用 Chakra UI Tabs 的 isLazy 属性
|
|
||||||
* - 每个 TabPanel 使用独立子组件,在首次激活时才渲染并加载数据
|
|
||||||
*/
|
|
||||||
const BasicInfoTab = ({ stockCode, basicInfo, cardBg }) => {
|
|
||||||
return (
|
|
||||||
<Card bg={cardBg} shadow="md">
|
|
||||||
<CardBody>
|
|
||||||
<Tabs isLazy variant="enclosed" colorScheme="blue">
|
|
||||||
<TabList flexWrap="wrap">
|
|
||||||
<Tab>
|
|
||||||
<Icon as={FaShareAlt} mr={2} />
|
|
||||||
股权结构
|
|
||||||
</Tab>
|
|
||||||
<Tab>
|
|
||||||
<Icon as={FaUserTie} mr={2} />
|
|
||||||
管理团队
|
|
||||||
</Tab>
|
|
||||||
<Tab>
|
|
||||||
<Icon as={FaBullhorn} mr={2} />
|
|
||||||
公司公告
|
|
||||||
</Tab>
|
|
||||||
<Tab>
|
|
||||||
<Icon as={FaSitemap} mr={2} />
|
|
||||||
分支机构
|
|
||||||
</Tab>
|
|
||||||
<Tab>
|
|
||||||
<Icon as={FaInfoCircle} mr={2} />
|
|
||||||
工商信息
|
|
||||||
</Tab>
|
|
||||||
</TabList>
|
|
||||||
|
|
||||||
<TabPanels>
|
|
||||||
{/* 股权结构 - 懒加载 */}
|
|
||||||
<TabPanel>
|
|
||||||
<ShareholderTabPanel stockCode={stockCode} />
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
{/* 管理团队 - 懒加载 */}
|
|
||||||
<TabPanel>
|
|
||||||
<ManagementTabPanel stockCode={stockCode} />
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
{/* 公司公告 - 懒加载 */}
|
|
||||||
<TabPanel>
|
|
||||||
<AnnouncementsTabPanel stockCode={stockCode} />
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
{/* 分支机构 - 懒加载 */}
|
|
||||||
<TabPanel>
|
|
||||||
<BranchesTabPanel stockCode={stockCode} />
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
{/* 工商信息 - 使用父组件传入的 basicInfo */}
|
|
||||||
<TabPanel>
|
|
||||||
<BusinessInfoTabPanel basicInfo={basicInfo} />
|
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
|
||||||
</Tabs>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BasicInfoTab;
|
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/AnnouncementsPanel.tsx
|
||||||
|
// 公司公告 Tab Panel
|
||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Icon,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
SimpleGrid,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
Button,
|
||||||
|
Tag,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
useDisclosure,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { FaCalendarAlt, FaBullhorn } from "react-icons/fa";
|
||||||
|
import { ExternalLinkIcon } from "@chakra-ui/icons";
|
||||||
|
|
||||||
|
import { useAnnouncementsData } from "../../hooks/useAnnouncementsData";
|
||||||
|
import { useDisclosureData } from "../../hooks/useDisclosureData";
|
||||||
|
import { THEME } from "../config";
|
||||||
|
import { formatDate } from "../utils";
|
||||||
|
import LoadingState from "./LoadingState";
|
||||||
|
|
||||||
|
interface AnnouncementsPanelProps {
|
||||||
|
stockCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnnouncementsPanel: React.FC<AnnouncementsPanelProps> = ({ stockCode }) => {
|
||||||
|
const { announcements, loading: announcementsLoading } = useAnnouncementsData(stockCode);
|
||||||
|
const { disclosureSchedule, loading: disclosureLoading } = useDisclosureData(stockCode);
|
||||||
|
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const [selectedAnnouncement, setSelectedAnnouncement] = useState<any>(null);
|
||||||
|
|
||||||
|
const handleAnnouncementClick = (announcement: any) => {
|
||||||
|
setSelectedAnnouncement(announcement);
|
||||||
|
onOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loading = announcementsLoading || disclosureLoading;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingState message="加载公告数据..." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
{/* 财报披露日程 */}
|
||||||
|
{disclosureSchedule.length > 0 && (
|
||||||
|
<Box>
|
||||||
|
<HStack mb={3}>
|
||||||
|
<Icon as={FaCalendarAlt} color={THEME.gold} />
|
||||||
|
<Text fontWeight="bold" color={THEME.textPrimary}>财报披露日程</Text>
|
||||||
|
</HStack>
|
||||||
|
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={3}>
|
||||||
|
{disclosureSchedule.slice(0, 4).map((schedule: any, idx: number) => (
|
||||||
|
<Card
|
||||||
|
key={idx}
|
||||||
|
bg={schedule.is_disclosed ? "green.900" : "orange.900"}
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={schedule.is_disclosed ? "green.600" : "orange.600"}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<CardBody p={3}>
|
||||||
|
<VStack spacing={1}>
|
||||||
|
<Badge colorScheme={schedule.is_disclosed ? "green" : "orange"}>
|
||||||
|
{schedule.report_name}
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="sm" fontWeight="bold" color={THEME.textPrimary}>
|
||||||
|
{schedule.is_disclosed ? "已披露" : "预计"}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color={THEME.textSecondary}>
|
||||||
|
{formatDate(
|
||||||
|
schedule.is_disclosed
|
||||||
|
? schedule.actual_date
|
||||||
|
: schedule.latest_scheduled_date
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider borderColor={THEME.border} />
|
||||||
|
|
||||||
|
{/* 最新公告 */}
|
||||||
|
<Box>
|
||||||
|
<HStack mb={3}>
|
||||||
|
<Icon as={FaBullhorn} color={THEME.gold} />
|
||||||
|
<Text fontWeight="bold" color={THEME.textPrimary}>最新公告</Text>
|
||||||
|
</HStack>
|
||||||
|
<VStack spacing={2} align="stretch">
|
||||||
|
{announcements.map((announcement: any, idx: number) => (
|
||||||
|
<Card
|
||||||
|
key={idx}
|
||||||
|
bg={THEME.tableBg}
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={THEME.border}
|
||||||
|
size="sm"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => handleAnnouncementClick(announcement)}
|
||||||
|
_hover={{ bg: THEME.tableHoverBg }}
|
||||||
|
>
|
||||||
|
<CardBody p={3}>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<VStack align="start" spacing={1} flex={1}>
|
||||||
|
<HStack>
|
||||||
|
<Badge size="sm" bg={THEME.gold} color="gray.900">
|
||||||
|
{announcement.info_type || "公告"}
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="xs" color={THEME.textSecondary}>
|
||||||
|
{formatDate(announcement.announce_date)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<Text fontSize="sm" fontWeight="medium" noOfLines={1} color={THEME.textPrimary}>
|
||||||
|
{announcement.title}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
<HStack>
|
||||||
|
{announcement.format && (
|
||||||
|
<Tag size="sm" bg={THEME.tableHoverBg} color={THEME.textSecondary}>
|
||||||
|
{announcement.format}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
icon={<ExternalLinkIcon />}
|
||||||
|
variant="ghost"
|
||||||
|
color={THEME.goldLight}
|
||||||
|
aria-label="查看原文"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
window.open(announcement.url, "_blank");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* 公告详情模态框 */}
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent bg={THEME.cardBg}>
|
||||||
|
<ModalHeader color={THEME.textPrimary}>
|
||||||
|
<VStack align="start" spacing={1}>
|
||||||
|
<Text>{selectedAnnouncement?.title}</Text>
|
||||||
|
<HStack>
|
||||||
|
<Badge bg={THEME.gold} color="gray.900">
|
||||||
|
{selectedAnnouncement?.info_type || "公告"}
|
||||||
|
</Badge>
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary}>
|
||||||
|
{formatDate(selectedAnnouncement?.announce_date)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton color={THEME.textPrimary} />
|
||||||
|
<ModalBody>
|
||||||
|
<VStack align="start" spacing={3}>
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary}>
|
||||||
|
文件格式:{selectedAnnouncement?.format || "-"}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary}>
|
||||||
|
文件大小:{selectedAnnouncement?.file_size || "-"} KB
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
bg={THEME.gold}
|
||||||
|
color="gray.900"
|
||||||
|
mr={3}
|
||||||
|
_hover={{ bg: THEME.goldLight }}
|
||||||
|
onClick={() => window.open(selectedAnnouncement?.url, "_blank")}
|
||||||
|
>
|
||||||
|
查看原文
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" color={THEME.textSecondary} onClick={onClose}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnnouncementsPanel;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/BranchesPanel.tsx
|
||||||
|
// 分支机构 Tab Panel
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Icon,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
SimpleGrid,
|
||||||
|
Center,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { FaSitemap } from "react-icons/fa";
|
||||||
|
|
||||||
|
import { useBranchesData } from "../../hooks/useBranchesData";
|
||||||
|
import { THEME } from "../config";
|
||||||
|
import { formatDate } from "../utils";
|
||||||
|
import LoadingState from "./LoadingState";
|
||||||
|
|
||||||
|
interface BranchesPanelProps {
|
||||||
|
stockCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BranchesPanel: React.FC<BranchesPanelProps> = ({ stockCode }) => {
|
||||||
|
const { branches, loading } = useBranchesData(stockCode);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingState message="加载分支机构数据..." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (branches.length === 0) {
|
||||||
|
return (
|
||||||
|
<Center h="200px">
|
||||||
|
<VStack>
|
||||||
|
<Icon as={FaSitemap} boxSize={12} color={THEME.textSecondary} />
|
||||||
|
<Text color={THEME.textSecondary}>暂无分支机构信息</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
|
||||||
|
{branches.map((branch: any, idx: number) => (
|
||||||
|
<Card key={idx} bg={THEME.tableBg} border="1px solid" borderColor={THEME.border}>
|
||||||
|
<CardBody>
|
||||||
|
<VStack align="start" spacing={3}>
|
||||||
|
<HStack justify="space-between" w="full">
|
||||||
|
<Text fontWeight="bold" color={THEME.textPrimary}>{branch.branch_name}</Text>
|
||||||
|
<Badge
|
||||||
|
colorScheme={branch.business_status === "存续" ? "green" : "red"}
|
||||||
|
>
|
||||||
|
{branch.business_status}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<SimpleGrid columns={2} spacing={2} w="full">
|
||||||
|
<VStack align="start" spacing={1}>
|
||||||
|
<Text fontSize="xs" color={THEME.textSecondary}>注册资本</Text>
|
||||||
|
<Text fontSize="sm" fontWeight="medium" color={THEME.textPrimary}>
|
||||||
|
{branch.register_capital || "-"}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
<VStack align="start" spacing={1}>
|
||||||
|
<Text fontSize="xs" color={THEME.textSecondary}>法人代表</Text>
|
||||||
|
<Text fontSize="sm" fontWeight="medium" color={THEME.textPrimary}>
|
||||||
|
{branch.legal_person || "-"}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
<VStack align="start" spacing={1}>
|
||||||
|
<Text fontSize="xs" color={THEME.textSecondary}>成立日期</Text>
|
||||||
|
<Text fontSize="sm" fontWeight="medium" color={THEME.textPrimary}>
|
||||||
|
{formatDate(branch.register_date)}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
<VStack align="start" spacing={1}>
|
||||||
|
<Text fontSize="xs" color={THEME.textSecondary}>关联企业</Text>
|
||||||
|
<Text fontSize="sm" fontWeight="medium" color={THEME.textPrimary}>
|
||||||
|
{branch.related_company_count || 0} 家
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</SimpleGrid>
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BranchesPanel;
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/BusinessInfoPanel.tsx
|
||||||
|
// 工商信息 Tab Panel
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Heading,
|
||||||
|
SimpleGrid,
|
||||||
|
Divider,
|
||||||
|
Center,
|
||||||
|
Code,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { THEME } from "../config";
|
||||||
|
|
||||||
|
interface BusinessInfoPanelProps {
|
||||||
|
basicInfo: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BusinessInfoPanel: React.FC<BusinessInfoPanelProps> = ({ basicInfo }) => {
|
||||||
|
if (!basicInfo) {
|
||||||
|
return (
|
||||||
|
<Center h="200px">
|
||||||
|
<Text color={THEME.textSecondary}>暂无工商信息</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={6}>
|
||||||
|
<Box>
|
||||||
|
<Heading size="sm" mb={3} color={THEME.textPrimary}>工商信息</Heading>
|
||||||
|
<VStack align="start" spacing={2}>
|
||||||
|
<HStack w="full">
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary} minW="80px">
|
||||||
|
统一信用代码
|
||||||
|
</Text>
|
||||||
|
<Code fontSize="xs" bg={THEME.tableHoverBg} color={THEME.goldLight}>
|
||||||
|
{basicInfo.credit_code}
|
||||||
|
</Code>
|
||||||
|
</HStack>
|
||||||
|
<HStack w="full">
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary} minW="80px">
|
||||||
|
公司规模
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color={THEME.textPrimary}>{basicInfo.company_size}</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack w="full" align="start">
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary} minW="80px">
|
||||||
|
注册地址
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" noOfLines={2} color={THEME.textPrimary}>
|
||||||
|
{basicInfo.reg_address}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack w="full" align="start">
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary} minW="80px">
|
||||||
|
办公地址
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" noOfLines={2} color={THEME.textPrimary}>
|
||||||
|
{basicInfo.office_address}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Heading size="sm" mb={3} color={THEME.textPrimary}>服务机构</Heading>
|
||||||
|
<VStack align="start" spacing={2}>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary}>会计师事务所</Text>
|
||||||
|
<Text fontSize="sm" fontWeight="medium" color={THEME.textPrimary}>
|
||||||
|
{basicInfo.accounting_firm}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary}>律师事务所</Text>
|
||||||
|
<Text fontSize="sm" fontWeight="medium" color={THEME.textPrimary}>
|
||||||
|
{basicInfo.law_firm}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<Divider borderColor={THEME.border} />
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Heading size="sm" mb={3} color={THEME.textPrimary}>主营业务</Heading>
|
||||||
|
<Text fontSize="sm" lineHeight="tall" color={THEME.textSecondary}>
|
||||||
|
{basicInfo.main_business}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Heading size="sm" mb={3} color={THEME.textPrimary}>经营范围</Heading>
|
||||||
|
<Text fontSize="sm" lineHeight="tall" color={THEME.textSecondary}>
|
||||||
|
{basicInfo.business_scope}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BusinessInfoPanel;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/LoadingState.tsx
|
||||||
|
// 复用的加载状态组件
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Center, VStack, Spinner, Text } from "@chakra-ui/react";
|
||||||
|
import { THEME } from "../config";
|
||||||
|
|
||||||
|
interface LoadingStateProps {
|
||||||
|
message?: string;
|
||||||
|
height?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载状态组件(黑金主题)
|
||||||
|
*/
|
||||||
|
const LoadingState: React.FC<LoadingStateProps> = ({
|
||||||
|
message = "加载中...",
|
||||||
|
height = "200px",
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Center h={height}>
|
||||||
|
<VStack>
|
||||||
|
<Spinner size="lg" color={THEME.gold} thickness="3px" />
|
||||||
|
<Text fontSize="sm" color={THEME.textSecondary}>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingState;
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// 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;
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/ShareholderPanel.tsx
|
||||||
|
// 股权结构 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 { useShareholderData } from "../../hooks/useShareholderData";
|
||||||
|
import { THEME } from "../config";
|
||||||
|
import { formatPercentage, formatShares, formatDate } from "../utils";
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShareholderPanel: React.FC<ShareholderPanelProps> = ({ stockCode }) => {
|
||||||
|
const {
|
||||||
|
actualControl,
|
||||||
|
concentration,
|
||||||
|
topShareholders,
|
||||||
|
topCirculationShareholders,
|
||||||
|
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 && (
|
||||||
|
<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>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 股权集中度 */}
|
||||||
|
{concentration.length > 0 && (
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShareholderPanel;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/components/index.ts
|
||||||
|
// 组件导出
|
||||||
|
|
||||||
|
export { default as LoadingState } from "./LoadingState";
|
||||||
|
export { default as ShareholderPanel } from "./ShareholderPanel";
|
||||||
|
export { default as ManagementPanel } from "./ManagementPanel";
|
||||||
|
export { default as AnnouncementsPanel } from "./AnnouncementsPanel";
|
||||||
|
export { default as BranchesPanel } from "./BranchesPanel";
|
||||||
|
export { default as BusinessInfoPanel } from "./BusinessInfoPanel";
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/config.ts
|
||||||
|
// Tab 配置 + 黑金主题配置
|
||||||
|
|
||||||
|
import { IconType } from "react-icons";
|
||||||
|
import {
|
||||||
|
FaShareAlt,
|
||||||
|
FaUserTie,
|
||||||
|
FaBullhorn,
|
||||||
|
FaSitemap,
|
||||||
|
FaInfoCircle,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
|
||||||
|
// 主题类型定义
|
||||||
|
export interface Theme {
|
||||||
|
bg: string;
|
||||||
|
cardBg: string;
|
||||||
|
tableBg: string;
|
||||||
|
tableHoverBg: string;
|
||||||
|
gold: string;
|
||||||
|
goldLight: string;
|
||||||
|
textPrimary: string;
|
||||||
|
textSecondary: string;
|
||||||
|
border: string;
|
||||||
|
tabSelected: {
|
||||||
|
bg: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
tabUnselected: {
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 黑金主题配置
|
||||||
|
export const THEME: Theme = {
|
||||||
|
bg: "gray.900",
|
||||||
|
cardBg: "gray.800",
|
||||||
|
tableBg: "gray.700",
|
||||||
|
tableHoverBg: "gray.600",
|
||||||
|
gold: "#D4AF37",
|
||||||
|
goldLight: "#F0D78C",
|
||||||
|
textPrimary: "white",
|
||||||
|
textSecondary: "gray.400",
|
||||||
|
border: "rgba(212, 175, 55, 0.3)",
|
||||||
|
tabSelected: {
|
||||||
|
bg: "#D4AF37",
|
||||||
|
color: "gray.900",
|
||||||
|
},
|
||||||
|
tabUnselected: {
|
||||||
|
color: "#D4AF37",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tab 配置类型
|
||||||
|
export interface TabConfig {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
icon: IconType;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab 配置
|
||||||
|
export const TAB_CONFIG: TabConfig[] = [
|
||||||
|
{
|
||||||
|
key: "shareholder",
|
||||||
|
name: "股权结构",
|
||||||
|
icon: FaShareAlt,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "management",
|
||||||
|
name: "管理团队",
|
||||||
|
icon: FaUserTie,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "announcements",
|
||||||
|
name: "公司公告",
|
||||||
|
icon: FaBullhorn,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "branches",
|
||||||
|
name: "分支机构",
|
||||||
|
icon: FaSitemap,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "business",
|
||||||
|
name: "工商信息",
|
||||||
|
icon: FaInfoCircle,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取启用的 Tab 列表
|
||||||
|
export const getEnabledTabs = (enabledKeys?: string[]): TabConfig[] => {
|
||||||
|
if (!enabledKeys || enabledKeys.length === 0) {
|
||||||
|
return TAB_CONFIG.filter((tab) => tab.enabled);
|
||||||
|
}
|
||||||
|
return TAB_CONFIG.filter(
|
||||||
|
(tab) => tab.enabled && enabledKeys.includes(tab.key)
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/index.tsx
|
||||||
|
// 基本信息 Tab 组件 - 可配置版本(黑金主题)
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
Tabs,
|
||||||
|
TabList,
|
||||||
|
TabPanels,
|
||||||
|
Tab,
|
||||||
|
TabPanel,
|
||||||
|
Icon,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { THEME, TAB_CONFIG, getEnabledTabs, type TabConfig } from "./config";
|
||||||
|
import {
|
||||||
|
ShareholderPanel,
|
||||||
|
ManagementPanel,
|
||||||
|
AnnouncementsPanel,
|
||||||
|
BranchesPanel,
|
||||||
|
BusinessInfoPanel,
|
||||||
|
} from "./components";
|
||||||
|
|
||||||
|
// Props 类型定义
|
||||||
|
export interface BasicInfoTabProps {
|
||||||
|
stockCode: string;
|
||||||
|
basicInfo?: any;
|
||||||
|
|
||||||
|
// 可配置项
|
||||||
|
enabledTabs?: string[]; // 指定显示哪些 Tab(通过 key)
|
||||||
|
defaultTabIndex?: number; // 默认选中 Tab
|
||||||
|
onTabChange?: (index: number, tabKey: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab 组件映射
|
||||||
|
const TAB_COMPONENTS: Record<string, React.FC<any>> = {
|
||||||
|
shareholder: ShareholderPanel,
|
||||||
|
management: ManagementPanel,
|
||||||
|
announcements: AnnouncementsPanel,
|
||||||
|
branches: BranchesPanel,
|
||||||
|
business: BusinessInfoPanel,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基本信息 Tab 组件
|
||||||
|
*
|
||||||
|
* 特性:
|
||||||
|
* - 可配置显示哪些 Tab(enabledTabs)
|
||||||
|
* - 黑金主题
|
||||||
|
* - 懒加载(isLazy)
|
||||||
|
* - 支持 Tab 变更回调
|
||||||
|
*/
|
||||||
|
const BasicInfoTab: React.FC<BasicInfoTabProps> = ({
|
||||||
|
stockCode,
|
||||||
|
basicInfo,
|
||||||
|
enabledTabs,
|
||||||
|
defaultTabIndex = 0,
|
||||||
|
onTabChange,
|
||||||
|
}) => {
|
||||||
|
// 获取启用的 Tab 配置
|
||||||
|
const tabs = getEnabledTabs(enabledTabs);
|
||||||
|
|
||||||
|
// 处理 Tab 变更
|
||||||
|
const handleTabChange = (index: number) => {
|
||||||
|
if (onTabChange && tabs[index]) {
|
||||||
|
onTabChange(index, tabs[index].key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染单个 Tab 内容
|
||||||
|
const renderTabContent = (tab: TabConfig) => {
|
||||||
|
const Component = TAB_COMPONENTS[tab.key];
|
||||||
|
if (!Component) return null;
|
||||||
|
|
||||||
|
// business Tab 需要 basicInfo,其他需要 stockCode
|
||||||
|
if (tab.key === "business") {
|
||||||
|
return <Component basicInfo={basicInfo} />;
|
||||||
|
}
|
||||||
|
return <Component stockCode={stockCode} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card bg={THEME.cardBg} shadow="md" border="1px solid" borderColor={THEME.border}>
|
||||||
|
<CardBody p={0}>
|
||||||
|
<Tabs
|
||||||
|
isLazy
|
||||||
|
variant="unstyled"
|
||||||
|
defaultIndex={defaultTabIndex}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
>
|
||||||
|
<TabList
|
||||||
|
bg={THEME.bg}
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor={THEME.border}
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
flexWrap="wrap"
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Tab
|
||||||
|
key={tab.key}
|
||||||
|
color={THEME.tabUnselected.color}
|
||||||
|
borderRadius="full"
|
||||||
|
px={4}
|
||||||
|
py={2}
|
||||||
|
fontSize="sm"
|
||||||
|
_selected={{
|
||||||
|
bg: THEME.tabSelected.bg,
|
||||||
|
color: THEME.tabSelected.color,
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
_hover={{
|
||||||
|
bg: THEME.tableHoverBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Icon as={tab.icon} boxSize={4} />
|
||||||
|
<Text>{tab.name}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanels p={4}>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabPanel key={tab.key} p={0}>
|
||||||
|
{renderTabContent(tab)}
|
||||||
|
</TabPanel>
|
||||||
|
))}
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BasicInfoTab;
|
||||||
|
|
||||||
|
// 导出配置和工具,供外部使用
|
||||||
|
export { THEME, TAB_CONFIG, getEnabledTabs } from "./config";
|
||||||
|
export * from "./utils";
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/BasicInfoTab/utils.ts
|
||||||
|
// 格式化工具函数
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化百分比
|
||||||
|
*/
|
||||||
|
export const formatPercentage = (value: number | null | undefined): string => {
|
||||||
|
if (value === null || value === undefined) return "-";
|
||||||
|
return `${(value * 100).toFixed(2)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化数字(自动转换亿/万)
|
||||||
|
*/
|
||||||
|
export const formatNumber = (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();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化股数(自动转换亿股/万股)
|
||||||
|
*/
|
||||||
|
export 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()}股`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期(去掉时间部分)
|
||||||
|
*/
|
||||||
|
export const formatDate = (dateStr: string | null | undefined): string => {
|
||||||
|
if (!dateStr) return "-";
|
||||||
|
return dateStr.split("T")[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出工具对象(兼容旧代码)
|
||||||
|
export const formatUtils = {
|
||||||
|
formatPercentage,
|
||||||
|
formatNumber,
|
||||||
|
formatShares,
|
||||||
|
formatDate,
|
||||||
|
};
|
||||||
@@ -56,7 +56,6 @@ const CompanyOverview: React.FC<CompanyOverviewProps> = ({ stockCode }) => {
|
|||||||
<BasicInfoTab
|
<BasicInfoTab
|
||||||
stockCode={stockCode}
|
stockCode={stockCode}
|
||||||
basicInfo={basicInfo}
|
basicInfo={basicInfo}
|
||||||
cardBg="white"
|
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,9 +28,13 @@ export interface BasicInfo {
|
|||||||
* 实际控制人
|
* 实际控制人
|
||||||
*/
|
*/
|
||||||
export interface ActualControl {
|
export interface ActualControl {
|
||||||
|
actual_controller_name?: string;
|
||||||
controller_name?: string;
|
controller_name?: string;
|
||||||
|
control_type?: string;
|
||||||
controller_type?: string;
|
controller_type?: string;
|
||||||
holding_ratio?: number;
|
holding_ratio?: number;
|
||||||
|
holding_shares?: number;
|
||||||
|
end_date?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +44,10 @@ export interface Concentration {
|
|||||||
top1_ratio?: number;
|
top1_ratio?: number;
|
||||||
top5_ratio?: number;
|
top5_ratio?: number;
|
||||||
top10_ratio?: number;
|
top10_ratio?: number;
|
||||||
|
stat_item?: string;
|
||||||
|
holding_ratio?: number;
|
||||||
|
ratio_change?: number;
|
||||||
|
end_date?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,8 +56,14 @@ export interface Concentration {
|
|||||||
export interface Management {
|
export interface Management {
|
||||||
name?: string;
|
name?: string;
|
||||||
position?: string;
|
position?: string;
|
||||||
|
position_name?: string;
|
||||||
|
position_category?: string;
|
||||||
start_date?: string;
|
start_date?: string;
|
||||||
end_date?: string;
|
end_date?: string;
|
||||||
|
gender?: string;
|
||||||
|
education?: string;
|
||||||
|
birth_year?: string;
|
||||||
|
nationality?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,8 +71,15 @@ export interface Management {
|
|||||||
*/
|
*/
|
||||||
export interface Shareholder {
|
export interface Shareholder {
|
||||||
shareholder_name?: string;
|
shareholder_name?: string;
|
||||||
|
shareholder_type?: string;
|
||||||
|
shareholder_rank?: number;
|
||||||
holding_ratio?: number;
|
holding_ratio?: number;
|
||||||
holding_amount?: number;
|
holding_amount?: number;
|
||||||
|
holding_shares?: number;
|
||||||
|
total_share_ratio?: number;
|
||||||
|
circulation_share_ratio?: number;
|
||||||
|
share_nature?: string;
|
||||||
|
end_date?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user