Compare commits
2 Commits
bf8847698b
...
a7ab87f7c4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7ab87f7c4 | ||
|
|
9a77bb6f0b |
@@ -368,6 +368,25 @@ export const stockHandlers = [
|
|||||||
stockMap[s.code] = s.name;
|
stockMap[s.code] = s.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 行业和指数映射表
|
||||||
|
const stockIndustryMap = {
|
||||||
|
'000001': { industry_l1: '金融', industry: '银行', index_tags: ['沪深300', '上证180'] },
|
||||||
|
'600519': { industry_l1: '消费', industry: '白酒', index_tags: ['沪深300', '上证50'] },
|
||||||
|
'300750': { industry_l1: '工业', industry: '电池', index_tags: ['创业板50'] },
|
||||||
|
'601318': { industry_l1: '金融', industry: '保险', index_tags: ['沪深300', '上证50'] },
|
||||||
|
'600036': { industry_l1: '金融', industry: '银行', index_tags: ['沪深300', '上证50'] },
|
||||||
|
'000858': { industry_l1: '消费', industry: '白酒', index_tags: ['沪深300'] },
|
||||||
|
'002594': { industry_l1: '汽车', industry: '乘用车', index_tags: ['沪深300', '创业板指'] },
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultIndustries = [
|
||||||
|
{ industry_l1: '科技', industry: '软件' },
|
||||||
|
{ industry_l1: '医药', industry: '化学制药' },
|
||||||
|
{ industry_l1: '消费', industry: '食品' },
|
||||||
|
{ industry_l1: '金融', industry: '证券' },
|
||||||
|
{ industry_l1: '工业', industry: '机械' },
|
||||||
|
];
|
||||||
|
|
||||||
// 为每只股票生成报价数据
|
// 为每只股票生成报价数据
|
||||||
const quotesData = {};
|
const quotesData = {};
|
||||||
codes.forEach(stockCode => {
|
codes.forEach(stockCode => {
|
||||||
@@ -380,6 +399,11 @@ export const stockHandlers = [
|
|||||||
// 昨收
|
// 昨收
|
||||||
const prevClose = parseFloat((basePrice - change).toFixed(2));
|
const prevClose = parseFloat((basePrice - change).toFixed(2));
|
||||||
|
|
||||||
|
// 获取行业和指数信息
|
||||||
|
const codeWithoutSuffix = stockCode.replace(/\.(SH|SZ)$/i, '');
|
||||||
|
const industryInfo = stockIndustryMap[codeWithoutSuffix] ||
|
||||||
|
defaultIndustries[Math.floor(Math.random() * defaultIndustries.length)];
|
||||||
|
|
||||||
quotesData[stockCode] = {
|
quotesData[stockCode] = {
|
||||||
code: stockCode,
|
code: stockCode,
|
||||||
name: stockMap[stockCode] || `股票${stockCode}`,
|
name: stockMap[stockCode] || `股票${stockCode}`,
|
||||||
@@ -393,7 +417,11 @@ export const stockHandlers = [
|
|||||||
volume: Math.floor(Math.random() * 100000000),
|
volume: Math.floor(Math.random() * 100000000),
|
||||||
amount: parseFloat((Math.random() * 10000000000).toFixed(2)),
|
amount: parseFloat((Math.random() * 10000000000).toFixed(2)),
|
||||||
market: stockCode.startsWith('6') ? 'SH' : 'SZ',
|
market: stockCode.startsWith('6') ? 'SH' : 'SZ',
|
||||||
update_time: new Date().toISOString()
|
update_time: new Date().toISOString(),
|
||||||
|
// 行业和指数标签
|
||||||
|
industry_l1: industryInfo.industry_l1,
|
||||||
|
industry: industryInfo.industry,
|
||||||
|
index_tags: industryInfo.index_tags || []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/CompanyHeaderCard.tsx
|
||||||
|
// 公司头部信息卡片组件
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
Heading,
|
||||||
|
SimpleGrid,
|
||||||
|
Divider,
|
||||||
|
Icon,
|
||||||
|
Grid,
|
||||||
|
GridItem,
|
||||||
|
Stat,
|
||||||
|
StatLabel,
|
||||||
|
StatNumber,
|
||||||
|
Circle,
|
||||||
|
Link,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import {
|
||||||
|
FaBuilding,
|
||||||
|
FaMapMarkerAlt,
|
||||||
|
FaUserShield,
|
||||||
|
FaBriefcase,
|
||||||
|
FaCalendarAlt,
|
||||||
|
FaGlobe,
|
||||||
|
FaEnvelope,
|
||||||
|
FaPhone,
|
||||||
|
FaCrown,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import { ExternalLinkIcon } from "@chakra-ui/icons";
|
||||||
|
|
||||||
|
import type { CompanyHeaderCardProps } from "./types";
|
||||||
|
import { formatRegisteredCapital, formatDate } from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司头部信息卡片组件
|
||||||
|
*/
|
||||||
|
const CompanyHeaderCard: React.FC<CompanyHeaderCardProps> = ({ basicInfo }) => {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
bg="white"
|
||||||
|
shadow="lg"
|
||||||
|
borderTop="4px solid"
|
||||||
|
borderTopColor="blue.500"
|
||||||
|
>
|
||||||
|
<CardBody>
|
||||||
|
<Grid templateColumns="repeat(12, 1fr)" gap={6}>
|
||||||
|
{/* 左侧:公司基本信息 */}
|
||||||
|
<GridItem colSpan={{ base: 12, lg: 8 }}>
|
||||||
|
<VStack align="start" spacing={4}>
|
||||||
|
{/* 公司名称和代码 */}
|
||||||
|
<HStack spacing={4}>
|
||||||
|
<Circle size="60px" bg="blue.500">
|
||||||
|
<Icon as={FaBuilding} color="white" boxSize={8} />
|
||||||
|
</Circle>
|
||||||
|
<VStack align="start" spacing={1}>
|
||||||
|
<HStack>
|
||||||
|
<Heading size="lg" color="blue.600">
|
||||||
|
{basicInfo.ORGNAME || basicInfo.SECNAME}
|
||||||
|
</Heading>
|
||||||
|
<Badge colorScheme="blue" fontSize="md" px={2} py={1}>
|
||||||
|
{basicInfo.SECCODE}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Badge colorScheme="purple" fontSize="xs">
|
||||||
|
{basicInfo.sw_industry_l1}
|
||||||
|
</Badge>
|
||||||
|
<Badge colorScheme="orange" fontSize="xs">
|
||||||
|
{basicInfo.sw_industry_l2}
|
||||||
|
</Badge>
|
||||||
|
{basicInfo.sw_industry_l3 && (
|
||||||
|
<Badge colorScheme="green" fontSize="xs">
|
||||||
|
{basicInfo.sw_industry_l3}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* 管理层信息 */}
|
||||||
|
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={3} w="full">
|
||||||
|
<HStack>
|
||||||
|
<Icon as={FaUserShield} color="gray.500" boxSize={4} />
|
||||||
|
<Text fontSize="sm">
|
||||||
|
<Text as="span" color="gray.500">法定代表人:</Text>
|
||||||
|
<Text as="span" fontWeight="bold">{basicInfo.legal_representative}</Text>
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack>
|
||||||
|
<Icon as={FaCrown} color="gray.500" boxSize={4} />
|
||||||
|
<Text fontSize="sm">
|
||||||
|
<Text as="span" color="gray.500">董事长:</Text>
|
||||||
|
<Text as="span" fontWeight="bold">{basicInfo.chairman}</Text>
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack>
|
||||||
|
<Icon as={FaBriefcase} color="gray.500" boxSize={4} />
|
||||||
|
<Text fontSize="sm">
|
||||||
|
<Text as="span" color="gray.500">总经理:</Text>
|
||||||
|
<Text as="span" fontWeight="bold">{basicInfo.general_manager}</Text>
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack>
|
||||||
|
<Icon as={FaCalendarAlt} color="gray.500" boxSize={4} />
|
||||||
|
<Text fontSize="sm">
|
||||||
|
<Text as="span" color="gray.500">成立日期:</Text>
|
||||||
|
<Text as="span" fontWeight="bold">{formatDate(basicInfo.establish_date)}</Text>
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{/* 公司简介 */}
|
||||||
|
<Text fontSize="sm" color="gray.600" noOfLines={2}>
|
||||||
|
{basicInfo.company_intro}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
|
{/* 右侧:注册资本和联系方式 */}
|
||||||
|
<GridItem colSpan={{ base: 12, lg: 4 }}>
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
<Stat>
|
||||||
|
<StatLabel>注册资本</StatLabel>
|
||||||
|
<StatNumber fontSize="2xl" color="blue.500">
|
||||||
|
{formatRegisteredCapital(basicInfo.reg_capital)}
|
||||||
|
</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<VStack align="stretch" spacing={1}>
|
||||||
|
<HStack fontSize="sm">
|
||||||
|
<Icon as={FaMapMarkerAlt} color="gray.500" boxSize={3} />
|
||||||
|
<Text noOfLines={1}>{basicInfo.province} {basicInfo.city}</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack fontSize="sm">
|
||||||
|
<Icon as={FaGlobe} color="gray.500" boxSize={3} />
|
||||||
|
<Link href={basicInfo.website} isExternal color="blue.500" noOfLines={1}>
|
||||||
|
{basicInfo.website} <ExternalLinkIcon mx="2px" />
|
||||||
|
</Link>
|
||||||
|
</HStack>
|
||||||
|
<HStack fontSize="sm">
|
||||||
|
<Icon as={FaEnvelope} color="gray.500" boxSize={3} />
|
||||||
|
<Text noOfLines={1}>{basicInfo.email}</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack fontSize="sm">
|
||||||
|
<Icon as={FaPhone} color="gray.500" boxSize={3} />
|
||||||
|
<Text>{basicInfo.tel}</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompanyHeaderCard;
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/hooks/useCompanyOverviewData.ts
|
||||||
|
// 公司概览数据加载 Hook
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { logger } from "@utils/logger";
|
||||||
|
import { getApiBase } from "@utils/apiConfig";
|
||||||
|
import type {
|
||||||
|
BasicInfo,
|
||||||
|
ActualControl,
|
||||||
|
Concentration,
|
||||||
|
Management,
|
||||||
|
Shareholder,
|
||||||
|
Branch,
|
||||||
|
Announcement,
|
||||||
|
DisclosureSchedule,
|
||||||
|
CompanyOverviewData,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
const API_BASE_URL = getApiBase();
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司概览数据加载 Hook
|
||||||
|
* @param propStockCode - 股票代码
|
||||||
|
* @returns 公司概览数据
|
||||||
|
*/
|
||||||
|
export const useCompanyOverviewData = (propStockCode?: string): CompanyOverviewData => {
|
||||||
|
const [stockCode, setStockCode] = useState(propStockCode || "000001");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [dataLoaded, setDataLoaded] = useState(false);
|
||||||
|
|
||||||
|
// 基本信息数据
|
||||||
|
const [basicInfo, setBasicInfo] = useState<BasicInfo | null>(null);
|
||||||
|
const [actualControl, setActualControl] = useState<ActualControl[]>([]);
|
||||||
|
const [concentration, setConcentration] = useState<Concentration[]>([]);
|
||||||
|
const [management, setManagement] = useState<Management[]>([]);
|
||||||
|
const [topCirculationShareholders, setTopCirculationShareholders] = useState<Shareholder[]>([]);
|
||||||
|
const [topShareholders, setTopShareholders] = useState<Shareholder[]>([]);
|
||||||
|
const [branches, setBranches] = useState<Branch[]>([]);
|
||||||
|
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
||||||
|
const [disclosureSchedule, setDisclosureSchedule] = useState<DisclosureSchedule[]>([]);
|
||||||
|
|
||||||
|
// 监听 props 中的 stockCode 变化
|
||||||
|
useEffect(() => {
|
||||||
|
if (propStockCode && propStockCode !== stockCode) {
|
||||||
|
setStockCode(propStockCode);
|
||||||
|
setDataLoaded(false);
|
||||||
|
}
|
||||||
|
}, [propStockCode, stockCode]);
|
||||||
|
|
||||||
|
// 加载基本信息数据(9个接口)
|
||||||
|
const loadBasicInfoData = useCallback(async () => {
|
||||||
|
if (dataLoaded) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [
|
||||||
|
basicRes,
|
||||||
|
actualRes,
|
||||||
|
concentrationRes,
|
||||||
|
managementRes,
|
||||||
|
circulationRes,
|
||||||
|
shareholdersRes,
|
||||||
|
branchesRes,
|
||||||
|
announcementsRes,
|
||||||
|
disclosureRes,
|
||||||
|
] = await Promise.all([
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/basic-info`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<BasicInfo>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/actual-control`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<ActualControl[]>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/concentration`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<Concentration[]>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/management?active_only=true`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<Management[]>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/top-circulation-shareholders?limit=10`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<Shareholder[]>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/top-shareholders?limit=10`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<Shareholder[]>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/branches`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<Branch[]>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/announcements?limit=20`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<Announcement[]>>,
|
||||||
|
fetch(`${API_BASE_URL}/api/stock/${stockCode}/disclosure-schedule`).then((r) =>
|
||||||
|
r.json()
|
||||||
|
) as Promise<ApiResponse<DisclosureSchedule[]>>,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (basicRes.success) setBasicInfo(basicRes.data);
|
||||||
|
if (actualRes.success) setActualControl(actualRes.data);
|
||||||
|
if (concentrationRes.success) setConcentration(concentrationRes.data);
|
||||||
|
if (managementRes.success) setManagement(managementRes.data);
|
||||||
|
if (circulationRes.success) setTopCirculationShareholders(circulationRes.data);
|
||||||
|
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
|
||||||
|
if (branchesRes.success) setBranches(branchesRes.data);
|
||||||
|
if (announcementsRes.success) setAnnouncements(announcementsRes.data);
|
||||||
|
if (disclosureRes.success) setDisclosureSchedule(disclosureRes.data);
|
||||||
|
|
||||||
|
setDataLoaded(true);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("useCompanyOverviewData", "loadBasicInfoData", err, { stockCode });
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [stockCode, dataLoaded]);
|
||||||
|
|
||||||
|
// 首次加载
|
||||||
|
useEffect(() => {
|
||||||
|
if (stockCode) {
|
||||||
|
loadBasicInfoData();
|
||||||
|
}
|
||||||
|
}, [stockCode, loadBasicInfoData]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
basicInfo,
|
||||||
|
actualControl,
|
||||||
|
concentration,
|
||||||
|
management,
|
||||||
|
topCirculationShareholders,
|
||||||
|
topShareholders,
|
||||||
|
branches,
|
||||||
|
announcements,
|
||||||
|
disclosureSchedule,
|
||||||
|
loading,
|
||||||
|
dataLoaded,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
// src/views/Company/components/CompanyOverview/index.js
|
|
||||||
// 公司概览 - 头部卡片 + 基本信息
|
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
VStack,
|
|
||||||
HStack,
|
|
||||||
Text,
|
|
||||||
Badge,
|
|
||||||
Card,
|
|
||||||
CardBody,
|
|
||||||
Heading,
|
|
||||||
SimpleGrid,
|
|
||||||
Divider,
|
|
||||||
Spinner,
|
|
||||||
Center,
|
|
||||||
Icon,
|
|
||||||
Grid,
|
|
||||||
GridItem,
|
|
||||||
Stat,
|
|
||||||
StatLabel,
|
|
||||||
StatNumber,
|
|
||||||
Circle,
|
|
||||||
Link,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
|
|
||||||
import {
|
|
||||||
FaBuilding,
|
|
||||||
FaMapMarkerAlt,
|
|
||||||
FaUserShield,
|
|
||||||
FaBriefcase,
|
|
||||||
FaCalendarAlt,
|
|
||||||
FaGlobe,
|
|
||||||
FaEnvelope,
|
|
||||||
FaPhone,
|
|
||||||
FaCrown,
|
|
||||||
} from "react-icons/fa";
|
|
||||||
|
|
||||||
import { ExternalLinkIcon } from "@chakra-ui/icons";
|
|
||||||
|
|
||||||
import { logger } from "@utils/logger";
|
|
||||||
import { getApiBase } from "@utils/apiConfig";
|
|
||||||
|
|
||||||
// 子组件
|
|
||||||
import BasicInfoTab from "./BasicInfoTab";
|
|
||||||
|
|
||||||
// API配置
|
|
||||||
const API_BASE_URL = getApiBase();
|
|
||||||
|
|
||||||
// 格式化工具
|
|
||||||
const formatUtils = {
|
|
||||||
formatRegisteredCapital: (value) => {
|
|
||||||
if (!value && value !== 0) return "-";
|
|
||||||
const absValue = Math.abs(value);
|
|
||||||
if (absValue >= 100000) {
|
|
||||||
return (value / 10000).toFixed(2) + "亿元";
|
|
||||||
}
|
|
||||||
return value.toFixed(2) + "万元";
|
|
||||||
},
|
|
||||||
formatDate: (dateString) => {
|
|
||||||
if (!dateString) return "-";
|
|
||||||
return new Date(dateString).toLocaleDateString("zh-CN");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 公司概览组件
|
|
||||||
*
|
|
||||||
* 功能:
|
|
||||||
* - 显示公司头部信息卡片
|
|
||||||
* - 显示基本信息(股权结构、管理层、公告等)
|
|
||||||
*
|
|
||||||
* @param {Object} props
|
|
||||||
* @param {string} props.stockCode - 股票代码
|
|
||||||
*/
|
|
||||||
const CompanyOverview = ({ stockCode: propStockCode }) => {
|
|
||||||
const [stockCode, setStockCode] = useState(propStockCode || "000001");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [dataLoaded, setDataLoaded] = useState(false);
|
|
||||||
|
|
||||||
// 监听 props 中的 stockCode 变化
|
|
||||||
useEffect(() => {
|
|
||||||
if (propStockCode && propStockCode !== stockCode) {
|
|
||||||
setStockCode(propStockCode);
|
|
||||||
setDataLoaded(false);
|
|
||||||
}
|
|
||||||
}, [propStockCode, stockCode]);
|
|
||||||
|
|
||||||
// 基本信息数据
|
|
||||||
const [basicInfo, setBasicInfo] = useState(null);
|
|
||||||
const [actualControl, setActualControl] = useState([]);
|
|
||||||
const [concentration, setConcentration] = useState([]);
|
|
||||||
const [management, setManagement] = useState([]);
|
|
||||||
const [topCirculationShareholders, setTopCirculationShareholders] = useState([]);
|
|
||||||
const [topShareholders, setTopShareholders] = useState([]);
|
|
||||||
const [branches, setBranches] = useState([]);
|
|
||||||
const [announcements, setAnnouncements] = useState([]);
|
|
||||||
const [disclosureSchedule, setDisclosureSchedule] = useState([]);
|
|
||||||
|
|
||||||
const [_error, setError] = useState(null);
|
|
||||||
|
|
||||||
// 加载基本信息数据(9个接口)
|
|
||||||
const loadBasicInfoData = async () => {
|
|
||||||
if (dataLoaded) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const requests = [
|
|
||||||
fetch(`${API_BASE_URL}/api/stock/${stockCode}/basic-info`).then((r) =>
|
|
||||||
r.json()
|
|
||||||
),
|
|
||||||
fetch(
|
|
||||||
`${API_BASE_URL}/api/stock/${stockCode}/actual-control`
|
|
||||||
).then((r) => r.json()),
|
|
||||||
fetch(
|
|
||||||
`${API_BASE_URL}/api/stock/${stockCode}/concentration`
|
|
||||||
).then((r) => r.json()),
|
|
||||||
fetch(
|
|
||||||
`${API_BASE_URL}/api/stock/${stockCode}/management?active_only=true`
|
|
||||||
).then((r) => r.json()),
|
|
||||||
fetch(
|
|
||||||
`${API_BASE_URL}/api/stock/${stockCode}/top-circulation-shareholders?limit=10`
|
|
||||||
).then((r) => r.json()),
|
|
||||||
fetch(
|
|
||||||
`${API_BASE_URL}/api/stock/${stockCode}/top-shareholders?limit=10`
|
|
||||||
).then((r) => r.json()),
|
|
||||||
fetch(`${API_BASE_URL}/api/stock/${stockCode}/branches`).then((r) =>
|
|
||||||
r.json()
|
|
||||||
),
|
|
||||||
fetch(
|
|
||||||
`${API_BASE_URL}/api/stock/${stockCode}/announcements?limit=20`
|
|
||||||
).then((r) => r.json()),
|
|
||||||
fetch(
|
|
||||||
`${API_BASE_URL}/api/stock/${stockCode}/disclosure-schedule`
|
|
||||||
).then((r) => r.json()),
|
|
||||||
];
|
|
||||||
|
|
||||||
const [
|
|
||||||
basicRes,
|
|
||||||
actualRes,
|
|
||||||
concentrationRes,
|
|
||||||
managementRes,
|
|
||||||
circulationRes,
|
|
||||||
shareholdersRes,
|
|
||||||
branchesRes,
|
|
||||||
announcementsRes,
|
|
||||||
disclosureRes,
|
|
||||||
] = await Promise.all(requests);
|
|
||||||
|
|
||||||
if (basicRes.success) setBasicInfo(basicRes.data);
|
|
||||||
if (actualRes.success) setActualControl(actualRes.data);
|
|
||||||
if (concentrationRes.success) setConcentration(concentrationRes.data);
|
|
||||||
if (managementRes.success) setManagement(managementRes.data);
|
|
||||||
if (circulationRes.success)
|
|
||||||
setTopCirculationShareholders(circulationRes.data);
|
|
||||||
if (shareholdersRes.success) setTopShareholders(shareholdersRes.data);
|
|
||||||
if (branchesRes.success) setBranches(branchesRes.data);
|
|
||||||
if (announcementsRes.success) setAnnouncements(announcementsRes.data);
|
|
||||||
if (disclosureRes.success) setDisclosureSchedule(disclosureRes.data);
|
|
||||||
|
|
||||||
setDataLoaded(true);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message);
|
|
||||||
logger.error("CompanyOverview", "loadBasicInfoData", err, { stockCode });
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 首次加载
|
|
||||||
useEffect(() => {
|
|
||||||
if (stockCode) {
|
|
||||||
loadBasicInfoData();
|
|
||||||
}
|
|
||||||
}, [stockCode]);
|
|
||||||
|
|
||||||
if (loading && !basicInfo) {
|
|
||||||
return (
|
|
||||||
<Center h="300px">
|
|
||||||
<VStack spacing={4}>
|
|
||||||
<Spinner size="xl" color="blue.500" thickness="4px" />
|
|
||||||
<Text>正在加载公司概览数据...</Text>
|
|
||||||
</VStack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack spacing={6} align="stretch">
|
|
||||||
{/* 公司头部信息卡片 */}
|
|
||||||
{basicInfo && (
|
|
||||||
<Card
|
|
||||||
bg="white"
|
|
||||||
shadow="lg"
|
|
||||||
borderTop="4px solid"
|
|
||||||
borderTopColor="blue.500"
|
|
||||||
>
|
|
||||||
<CardBody>
|
|
||||||
<Grid templateColumns="repeat(12, 1fr)" gap={6}>
|
|
||||||
<GridItem colSpan={{ base: 12, lg: 8 }}>
|
|
||||||
<VStack align="start" spacing={4}>
|
|
||||||
<HStack spacing={4}>
|
|
||||||
<Circle size="60px" bg="blue.500">
|
|
||||||
<Icon as={FaBuilding} color="white" boxSize={8} />
|
|
||||||
</Circle>
|
|
||||||
<VStack align="start" spacing={1}>
|
|
||||||
<HStack>
|
|
||||||
<Heading size="lg" color="blue.600">
|
|
||||||
{basicInfo.ORGNAME || basicInfo.SECNAME}
|
|
||||||
</Heading>
|
|
||||||
<Badge colorScheme="blue" fontSize="md" px={2} py={1}>
|
|
||||||
{basicInfo.SECCODE}
|
|
||||||
</Badge>
|
|
||||||
</HStack>
|
|
||||||
<HStack spacing={2}>
|
|
||||||
<Badge colorScheme="purple" fontSize="xs">
|
|
||||||
{basicInfo.sw_industry_l1}
|
|
||||||
</Badge>
|
|
||||||
<Badge colorScheme="orange" fontSize="xs">
|
|
||||||
{basicInfo.sw_industry_l2}
|
|
||||||
</Badge>
|
|
||||||
{basicInfo.sw_industry_l3 && (
|
|
||||||
<Badge colorScheme="green" fontSize="xs">
|
|
||||||
{basicInfo.sw_industry_l3}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={3} w="full">
|
|
||||||
<HStack>
|
|
||||||
<Icon as={FaUserShield} color="gray.500" boxSize={4} />
|
|
||||||
<Text fontSize="sm">
|
|
||||||
<Text as="span" color="gray.500">法定代表人:</Text>
|
|
||||||
<Text as="span" fontWeight="bold">{basicInfo.legal_representative}</Text>
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack>
|
|
||||||
<Icon as={FaCrown} color="gray.500" boxSize={4} />
|
|
||||||
<Text fontSize="sm">
|
|
||||||
<Text as="span" color="gray.500">董事长:</Text>
|
|
||||||
<Text as="span" fontWeight="bold">{basicInfo.chairman}</Text>
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack>
|
|
||||||
<Icon as={FaBriefcase} color="gray.500" boxSize={4} />
|
|
||||||
<Text fontSize="sm">
|
|
||||||
<Text as="span" color="gray.500">总经理:</Text>
|
|
||||||
<Text as="span" fontWeight="bold">{basicInfo.general_manager}</Text>
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack>
|
|
||||||
<Icon as={FaCalendarAlt} color="gray.500" boxSize={4} />
|
|
||||||
<Text fontSize="sm">
|
|
||||||
<Text as="span" color="gray.500">成立日期:</Text>
|
|
||||||
<Text as="span" fontWeight="bold">{formatUtils.formatDate(basicInfo.establish_date)}</Text>
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
<Text fontSize="sm" color="gray.600" noOfLines={2}>
|
|
||||||
{basicInfo.company_intro}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</GridItem>
|
|
||||||
|
|
||||||
<GridItem colSpan={{ base: 12, lg: 4 }}>
|
|
||||||
<VStack spacing={3} align="stretch">
|
|
||||||
<Stat>
|
|
||||||
<StatLabel>注册资本</StatLabel>
|
|
||||||
<StatNumber fontSize="2xl" color="blue.500">
|
|
||||||
{formatUtils.formatRegisteredCapital(basicInfo.reg_capital)}
|
|
||||||
</StatNumber>
|
|
||||||
</Stat>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<VStack align="stretch" spacing={1}>
|
|
||||||
<HStack fontSize="sm">
|
|
||||||
<Icon as={FaMapMarkerAlt} color="gray.500" boxSize={3} />
|
|
||||||
<Text noOfLines={1}>{basicInfo.province} {basicInfo.city}</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack fontSize="sm">
|
|
||||||
<Icon as={FaGlobe} color="gray.500" boxSize={3} />
|
|
||||||
<Link href={basicInfo.website} isExternal color="blue.500" noOfLines={1}>
|
|
||||||
{basicInfo.website} <ExternalLinkIcon mx="2px" />
|
|
||||||
</Link>
|
|
||||||
</HStack>
|
|
||||||
<HStack fontSize="sm">
|
|
||||||
<Icon as={FaEnvelope} color="gray.500" boxSize={3} />
|
|
||||||
<Text noOfLines={1}>{basicInfo.email}</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack fontSize="sm">
|
|
||||||
<Icon as={FaPhone} color="gray.500" boxSize={3} />
|
|
||||||
<Text>{basicInfo.tel}</Text>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
</GridItem>
|
|
||||||
</Grid>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 基本信息内容 */}
|
|
||||||
<BasicInfoTab
|
|
||||||
basicInfo={basicInfo}
|
|
||||||
actualControl={actualControl}
|
|
||||||
concentration={concentration}
|
|
||||||
topShareholders={topShareholders}
|
|
||||||
topCirculationShareholders={topCirculationShareholders}
|
|
||||||
management={management}
|
|
||||||
announcements={announcements}
|
|
||||||
branches={branches}
|
|
||||||
disclosureSchedule={disclosureSchedule}
|
|
||||||
cardBg="white"
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CompanyOverview;
|
|
||||||
70
src/views/Company/components/CompanyOverview/index.tsx
Normal file
70
src/views/Company/components/CompanyOverview/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/index.tsx
|
||||||
|
// 公司概览 - 主组件(组合层)
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { VStack, Spinner, Center, Text } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { useCompanyOverviewData } from "./hooks/useCompanyOverviewData";
|
||||||
|
import CompanyHeaderCard from "./CompanyHeaderCard";
|
||||||
|
import type { CompanyOverviewProps } from "./types";
|
||||||
|
|
||||||
|
// 子组件(暂保持 JS)
|
||||||
|
import BasicInfoTab from "./BasicInfoTab";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公司概览组件
|
||||||
|
*
|
||||||
|
* 功能:
|
||||||
|
* - 显示公司头部信息卡片
|
||||||
|
* - 显示基本信息(股权结构、管理层、公告等)
|
||||||
|
*/
|
||||||
|
const CompanyOverview: React.FC<CompanyOverviewProps> = ({ stockCode }) => {
|
||||||
|
const {
|
||||||
|
basicInfo,
|
||||||
|
actualControl,
|
||||||
|
concentration,
|
||||||
|
management,
|
||||||
|
topCirculationShareholders,
|
||||||
|
topShareholders,
|
||||||
|
branches,
|
||||||
|
announcements,
|
||||||
|
disclosureSchedule,
|
||||||
|
loading,
|
||||||
|
} = useCompanyOverviewData(stockCode);
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
if (loading && !basicInfo) {
|
||||||
|
return (
|
||||||
|
<Center h="300px">
|
||||||
|
<VStack spacing={4}>
|
||||||
|
<Spinner size="xl" color="blue.500" thickness="4px" />
|
||||||
|
<Text>正在加载公司概览数据...</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack spacing={6} align="stretch">
|
||||||
|
{/* 公司头部信息卡片 */}
|
||||||
|
{basicInfo && <CompanyHeaderCard basicInfo={basicInfo} />}
|
||||||
|
|
||||||
|
{/* 基本信息内容 */}
|
||||||
|
<BasicInfoTab
|
||||||
|
basicInfo={basicInfo}
|
||||||
|
actualControl={actualControl}
|
||||||
|
concentration={concentration}
|
||||||
|
topShareholders={topShareholders}
|
||||||
|
topCirculationShareholders={topCirculationShareholders}
|
||||||
|
management={management}
|
||||||
|
announcements={announcements}
|
||||||
|
branches={branches}
|
||||||
|
disclosureSchedule={disclosureSchedule}
|
||||||
|
cardBg="white"
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompanyOverview;
|
||||||
@@ -97,18 +97,40 @@ const StockQuoteCard: React.FC<StockQuoteCardProps> = ({
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
{/* 顶部:股票名称 + 关注/分享按钮 + 更新时间 */}
|
{/* 顶部:股票名称 + 关注/分享按钮 + 更新时间 */}
|
||||||
<Flex justify="space-between" align="center" mb={4}>
|
<Flex justify="space-between" align="center" mb={4}>
|
||||||
{/* 左侧:名称(代码) | 指数标签 */}
|
{/* 左侧:股票名称 + 行业标签 + 指数标签 */}
|
||||||
<HStack spacing={2}>
|
<HStack spacing={3} align="center">
|
||||||
<Text fontSize="22px" fontWeight="bold" color={valueColor}>
|
{/* 股票名称 - 突出显示 */}
|
||||||
{data.name}({data.code})
|
<Text fontSize="26px" fontWeight="800" color={valueColor}>
|
||||||
|
{data.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text fontSize="18px" fontWeight="normal" color={labelColor}>
|
||||||
|
({data.code})
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 行业标签 */}
|
||||||
|
{(data.industryL1 || data.industry) && (
|
||||||
|
<Badge
|
||||||
|
bg="transparent"
|
||||||
|
color={labelColor}
|
||||||
|
fontSize="14px"
|
||||||
|
fontWeight="medium"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={borderColor}
|
||||||
|
px={2}
|
||||||
|
py={0.5}
|
||||||
|
borderRadius="md"
|
||||||
|
>
|
||||||
|
{data.industryL1 && data.industry
|
||||||
|
? `${data.industryL1} · ${data.industry}`
|
||||||
|
: data.industry || data.industryL1}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 指数标签 */}
|
||||||
{data.indexTags?.length > 0 && (
|
{data.indexTags?.length > 0 && (
|
||||||
<>
|
<Text fontSize="14px" color={labelColor}>
|
||||||
<Text color={labelColor} fontSize="22px" fontWeight="light">|</Text>
|
|
||||||
<Text fontSize="16px" color={labelColor}>
|
|
||||||
{data.indexTags.join('、')}
|
{data.indexTags.join('、')}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface StockQuoteCardData {
|
|||||||
name: string; // 股票名称
|
name: string; // 股票名称
|
||||||
code: string; // 股票代码
|
code: string; // 股票代码
|
||||||
indexTags: string[]; // 指数标签(如 沪深300、上证50)
|
indexTags: string[]; // 指数标签(如 沪深300、上证50)
|
||||||
|
industry?: string; // 所属行业(二级),如 "银行"
|
||||||
|
industryL1?: string; // 一级行业,如 "金融"
|
||||||
|
|
||||||
// 价格信息
|
// 价格信息
|
||||||
currentPrice: number; // 当前价格
|
currentPrice: number; // 当前价格
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const transformQuoteData = (apiData, stockCode) => {
|
|||||||
name: apiData.name || apiData.stock_name || '未知',
|
name: apiData.name || apiData.stock_name || '未知',
|
||||||
code: apiData.code || apiData.stock_code || stockCode,
|
code: apiData.code || apiData.stock_code || stockCode,
|
||||||
indexTags: apiData.index_tags || apiData.indexTags || [],
|
indexTags: apiData.index_tags || apiData.indexTags || [],
|
||||||
|
industry: apiData.industry || apiData.sw_industry_l2 || '',
|
||||||
|
industryL1: apiData.industry_l1 || apiData.sw_industry_l1 || '',
|
||||||
|
|
||||||
// 价格信息
|
// 价格信息
|
||||||
currentPrice: apiData.current_price || apiData.currentPrice || apiData.close || 0,
|
currentPrice: apiData.current_price || apiData.currentPrice || apiData.close || 0,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const CompanyIndex = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Tab 切换区域:概览、行情、财务、预测 */}
|
{/* Tab 切换区域:概览、行情、财务、预测 */}
|
||||||
{/* <CompanyTabs stockCode={stockCode} onTabChange={trackTabChanged} bgColor="#1A202C"/> */}
|
<CompanyTabs stockCode={stockCode} onTabChange={trackTabChanged} bgColor="#1A202C"/>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user