refactor: CompanyOverview 组件按 Tab 拆分为独立子组件
将 2682 行的大型组件拆分为 4 个模块化文件: - index.js (~550行): 状态管理 + 数据加载 + Tab 容器 - DeepAnalysisTab.js (~1800行): 深度分析 Tab(核心定位、竞争力、产业链) - BasicInfoTab.js (~940行): 基本信息 Tab(股权结构、管理团队、公告) - NewsEventsTab.js (~540行): 新闻动态 Tab(事件列表 + 分页) 重构内容: - 提取 8 个内部子组件到对应 Tab 文件 - 修复 useColorModeValue 在 map 回调中调用的 hooks 规则违规 - 清理未使用的 imports - 完善公告详情模态框(补全 ModalFooter) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
941
src/views/Company/components/CompanyOverview/BasicInfoTab.js
Normal file
941
src/views/Company/components/CompanyOverview/BasicInfoTab.js
Normal file
@@ -0,0 +1,941 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/BasicInfoTab.js
|
||||||
|
// 基本信息 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,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基本信息 Tab 组件
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - basicInfo: 公司基本信息
|
||||||
|
* - actualControl: 实际控制人数组
|
||||||
|
* - concentration: 股权集中度数组
|
||||||
|
* - topShareholders: 前十大股东数组
|
||||||
|
* - topCirculationShareholders: 前十大流通股东数组
|
||||||
|
* - management: 管理层数组
|
||||||
|
* - announcements: 公告列表数组
|
||||||
|
* - branches: 分支机构数组
|
||||||
|
* - disclosureSchedule: 披露日程数组
|
||||||
|
* - cardBg: 卡片背景色
|
||||||
|
* - onAnnouncementClick: 公告点击回调 (announcement) => void
|
||||||
|
*/
|
||||||
|
const BasicInfoTab = ({
|
||||||
|
basicInfo,
|
||||||
|
actualControl = [],
|
||||||
|
concentration = [],
|
||||||
|
topShareholders = [],
|
||||||
|
topCirculationShareholders = [],
|
||||||
|
management = [],
|
||||||
|
announcements = [],
|
||||||
|
branches = [],
|
||||||
|
disclosureSchedule = [],
|
||||||
|
cardBg,
|
||||||
|
}) => {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const [selectedAnnouncement, setSelectedAnnouncement] = React.useState(null);
|
||||||
|
|
||||||
|
// 管理层职位分类
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算股权集中度变化
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理公告点击
|
||||||
|
const handleAnnouncementClick = (announcement) => {
|
||||||
|
setSelectedAnnouncement(announcement);
|
||||||
|
onOpen();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card bg={cardBg} shadow="md">
|
||||||
|
<CardBody>
|
||||||
|
<Tabs 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>
|
||||||
|
<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>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 管理团队标签页 */}
|
||||||
|
<TabPanel>
|
||||||
|
<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>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 公司公告标签页 */}
|
||||||
|
<TabPanel>
|
||||||
|
<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>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 分支机构标签页 */}
|
||||||
|
<TabPanel>
|
||||||
|
{branches.length > 0 ? (
|
||||||
|
<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>
|
||||||
|
) : (
|
||||||
|
<Center h="200px">
|
||||||
|
<VStack>
|
||||||
|
<Icon as={FaSitemap} boxSize={12} color="gray.300" />
|
||||||
|
<Text color="gray.500">暂无分支机构信息</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* 工商信息标签页 */}
|
||||||
|
<TabPanel>
|
||||||
|
{basicInfo && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 公告详情模态框 */}
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BasicInfoTab;
|
||||||
1799
src/views/Company/components/CompanyOverview/DeepAnalysisTab.js
Normal file
1799
src/views/Company/components/CompanyOverview/DeepAnalysisTab.js
Normal file
File diff suppressed because it is too large
Load Diff
541
src/views/Company/components/CompanyOverview/NewsEventsTab.js
Normal file
541
src/views/Company/components/CompanyOverview/NewsEventsTab.js
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
// src/views/Company/components/CompanyOverview/NewsEventsTab.js
|
||||||
|
// 新闻动态 Tab - 相关新闻事件列表 + 分页
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Icon,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputLeftElement,
|
||||||
|
Tag,
|
||||||
|
Center,
|
||||||
|
Spinner,
|
||||||
|
useColorModeValue,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { SearchIcon } from "@chakra-ui/icons";
|
||||||
|
import {
|
||||||
|
FaNewspaper,
|
||||||
|
FaBullhorn,
|
||||||
|
FaGavel,
|
||||||
|
FaFlask,
|
||||||
|
FaDollarSign,
|
||||||
|
FaShieldAlt,
|
||||||
|
FaFileAlt,
|
||||||
|
FaIndustry,
|
||||||
|
FaEye,
|
||||||
|
FaFire,
|
||||||
|
FaChartLine,
|
||||||
|
FaChevronLeft,
|
||||||
|
FaChevronRight,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新闻动态 Tab 组件
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - newsEvents: 新闻事件列表数组
|
||||||
|
* - newsLoading: 加载状态
|
||||||
|
* - newsPagination: 分页信息 { page, per_page, total, pages, has_next, has_prev }
|
||||||
|
* - searchQuery: 搜索关键词
|
||||||
|
* - onSearchChange: 搜索输入回调 (value) => void
|
||||||
|
* - onSearch: 搜索提交回调 () => void
|
||||||
|
* - onPageChange: 分页回调 (page) => void
|
||||||
|
* - cardBg: 卡片背景色
|
||||||
|
*/
|
||||||
|
const NewsEventsTab = ({
|
||||||
|
newsEvents = [],
|
||||||
|
newsLoading = false,
|
||||||
|
newsPagination = {
|
||||||
|
page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
total: 0,
|
||||||
|
pages: 0,
|
||||||
|
has_next: false,
|
||||||
|
has_prev: false,
|
||||||
|
},
|
||||||
|
searchQuery = "",
|
||||||
|
onSearchChange,
|
||||||
|
onSearch,
|
||||||
|
onPageChange,
|
||||||
|
cardBg,
|
||||||
|
}) => {
|
||||||
|
// 颜色模式值需要在组件顶层调用
|
||||||
|
const hoverBg = useColorModeValue("gray.50", "gray.700");
|
||||||
|
|
||||||
|
// 事件类型图标映射
|
||||||
|
const getEventTypeIcon = (eventType) => {
|
||||||
|
const iconMap = {
|
||||||
|
企业公告: FaBullhorn,
|
||||||
|
政策: FaGavel,
|
||||||
|
技术突破: FaFlask,
|
||||||
|
企业融资: FaDollarSign,
|
||||||
|
政策监管: FaShieldAlt,
|
||||||
|
政策动态: FaFileAlt,
|
||||||
|
行业事件: FaIndustry,
|
||||||
|
};
|
||||||
|
return iconMap[eventType] || FaNewspaper;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重要性颜色映射
|
||||||
|
const getImportanceColor = (importance) => {
|
||||||
|
const colorMap = {
|
||||||
|
S: "red",
|
||||||
|
A: "orange",
|
||||||
|
B: "yellow",
|
||||||
|
C: "green",
|
||||||
|
};
|
||||||
|
return colorMap[importance] || "gray";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理搜索输入
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
onSearchChange?.(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理搜索提交
|
||||||
|
const handleSearchSubmit = () => {
|
||||||
|
onSearch?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理键盘事件
|
||||||
|
const handleKeyPress = (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleSearchSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理分页
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
onPageChange?.(page);
|
||||||
|
// 滚动到列表顶部
|
||||||
|
document
|
||||||
|
.getElementById("news-list-top")
|
||||||
|
?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染分页按钮
|
||||||
|
const renderPaginationButtons = () => {
|
||||||
|
const { page: currentPage, pages: totalPages } = newsPagination;
|
||||||
|
const pageButtons = [];
|
||||||
|
|
||||||
|
// 显示当前页及前后各2页
|
||||||
|
let startPage = Math.max(1, currentPage - 2);
|
||||||
|
let endPage = Math.min(totalPages, currentPage + 2);
|
||||||
|
|
||||||
|
// 如果开始页大于1,显示省略号
|
||||||
|
if (startPage > 1) {
|
||||||
|
pageButtons.push(
|
||||||
|
<Text key="start-ellipsis" fontSize="sm" color="gray.400">
|
||||||
|
...
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pageButtons.push(
|
||||||
|
<Button
|
||||||
|
key={i}
|
||||||
|
size="sm"
|
||||||
|
variant={i === currentPage ? "solid" : "outline"}
|
||||||
|
colorScheme={i === currentPage ? "blue" : "gray"}
|
||||||
|
onClick={() => handlePageChange(i)}
|
||||||
|
isDisabled={newsLoading}
|
||||||
|
>
|
||||||
|
{i}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果结束页小于总页数,显示省略号
|
||||||
|
if (endPage < totalPages) {
|
||||||
|
pageButtons.push(
|
||||||
|
<Text key="end-ellipsis" fontSize="sm" color="gray.400">
|
||||||
|
...
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageButtons;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
<Card bg={cardBg} shadow="md">
|
||||||
|
<CardBody>
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
{/* 搜索框和统计信息 */}
|
||||||
|
<HStack justify="space-between" flexWrap="wrap">
|
||||||
|
<HStack flex={1} minW="300px">
|
||||||
|
<InputGroup>
|
||||||
|
<InputLeftElement pointerEvents="none">
|
||||||
|
<SearchIcon color="gray.400" />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索相关新闻..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={handleSearchSubmit}
|
||||||
|
isLoading={newsLoading}
|
||||||
|
minW="80px"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{newsPagination.total > 0 && (
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Icon as={FaNewspaper} color="blue.500" />
|
||||||
|
<Text fontSize="sm" color="gray.600">
|
||||||
|
共找到{" "}
|
||||||
|
<Text as="span" fontWeight="bold" color="blue.600">
|
||||||
|
{newsPagination.total}
|
||||||
|
</Text>{" "}
|
||||||
|
条新闻
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<div id="news-list-top" />
|
||||||
|
|
||||||
|
{/* 新闻列表 */}
|
||||||
|
{newsLoading ? (
|
||||||
|
<Center h="400px">
|
||||||
|
<VStack spacing={3}>
|
||||||
|
<Spinner size="xl" color="blue.500" thickness="4px" />
|
||||||
|
<Text color="gray.600">正在加载新闻...</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
) : newsEvents.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<VStack spacing={3} align="stretch">
|
||||||
|
{newsEvents.map((event, idx) => {
|
||||||
|
const importanceColor = getImportanceColor(
|
||||||
|
event.importance
|
||||||
|
);
|
||||||
|
const eventTypeIcon = getEventTypeIcon(event.event_type);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={event.id || idx}
|
||||||
|
variant="outline"
|
||||||
|
_hover={{
|
||||||
|
bg: hoverBg,
|
||||||
|
shadow: "md",
|
||||||
|
borderColor: "blue.300",
|
||||||
|
}}
|
||||||
|
transition="all 0.2s"
|
||||||
|
>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<VStack align="stretch" spacing={3}>
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<HStack justify="space-between" align="start">
|
||||||
|
<VStack align="start" spacing={2} flex={1}>
|
||||||
|
<HStack>
|
||||||
|
<Icon
|
||||||
|
as={eventTypeIcon}
|
||||||
|
color="blue.500"
|
||||||
|
boxSize={5}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize="lg"
|
||||||
|
lineHeight="1.3"
|
||||||
|
>
|
||||||
|
{event.title}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 标签栏 */}
|
||||||
|
<HStack spacing={2} flexWrap="wrap">
|
||||||
|
{event.importance && (
|
||||||
|
<Badge
|
||||||
|
colorScheme={importanceColor}
|
||||||
|
variant="solid"
|
||||||
|
px={2}
|
||||||
|
>
|
||||||
|
{event.importance}级
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{event.event_type && (
|
||||||
|
<Badge colorScheme="blue" variant="outline">
|
||||||
|
{event.event_type}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{event.invest_score && (
|
||||||
|
<Badge
|
||||||
|
colorScheme="purple"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
投资分: {event.invest_score}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{event.keywords && event.keywords.length > 0 && (
|
||||||
|
<>
|
||||||
|
{event.keywords
|
||||||
|
.slice(0, 4)
|
||||||
|
.map((keyword, kidx) => (
|
||||||
|
<Tag
|
||||||
|
key={kidx}
|
||||||
|
size="sm"
|
||||||
|
colorScheme="cyan"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{typeof keyword === "string"
|
||||||
|
? keyword
|
||||||
|
: keyword?.concept ||
|
||||||
|
keyword?.name ||
|
||||||
|
"未知"}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* 右侧信息栏 */}
|
||||||
|
<VStack align="end" spacing={1} minW="100px">
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
{event.created_at
|
||||||
|
? new Date(
|
||||||
|
event.created_at
|
||||||
|
).toLocaleDateString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
})
|
||||||
|
: ""}
|
||||||
|
</Text>
|
||||||
|
<HStack spacing={3}>
|
||||||
|
{event.view_count !== undefined && (
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon
|
||||||
|
as={FaEye}
|
||||||
|
boxSize={3}
|
||||||
|
color="gray.400"
|
||||||
|
/>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
{event.view_count}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
{event.hot_score !== undefined && (
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon
|
||||||
|
as={FaFire}
|
||||||
|
boxSize={3}
|
||||||
|
color="orange.400"
|
||||||
|
/>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
{event.hot_score.toFixed(1)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
{event.creator && (
|
||||||
|
<Text fontSize="xs" color="gray.400">
|
||||||
|
@{event.creator.username}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 描述 */}
|
||||||
|
{event.description && (
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
color="gray.700"
|
||||||
|
lineHeight="1.6"
|
||||||
|
>
|
||||||
|
{event.description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 收益率数据 */}
|
||||||
|
{(event.related_avg_chg !== null ||
|
||||||
|
event.related_max_chg !== null ||
|
||||||
|
event.related_week_chg !== null) && (
|
||||||
|
<Box
|
||||||
|
pt={2}
|
||||||
|
borderTop="1px"
|
||||||
|
borderColor="gray.200"
|
||||||
|
>
|
||||||
|
<HStack spacing={6} flexWrap="wrap">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Icon
|
||||||
|
as={FaChartLine}
|
||||||
|
boxSize={3}
|
||||||
|
color="gray.500"
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
fontSize="xs"
|
||||||
|
color="gray.500"
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
相关涨跌:
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
{event.related_avg_chg !== null &&
|
||||||
|
event.related_avg_chg !== undefined && (
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
平均
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={
|
||||||
|
event.related_avg_chg > 0
|
||||||
|
? "red.500"
|
||||||
|
: "green.500"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{event.related_avg_chg > 0 ? "+" : ""}
|
||||||
|
{event.related_avg_chg.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
{event.related_max_chg !== null &&
|
||||||
|
event.related_max_chg !== undefined && (
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
最大
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={
|
||||||
|
event.related_max_chg > 0
|
||||||
|
? "red.500"
|
||||||
|
: "green.500"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{event.related_max_chg > 0 ? "+" : ""}
|
||||||
|
{event.related_max_chg.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
{event.related_week_chg !== null &&
|
||||||
|
event.related_week_chg !== undefined && (
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Text fontSize="xs" color="gray.500">
|
||||||
|
周
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="bold"
|
||||||
|
color={
|
||||||
|
event.related_week_chg > 0
|
||||||
|
? "red.500"
|
||||||
|
: "green.500"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{event.related_week_chg > 0
|
||||||
|
? "+"
|
||||||
|
: ""}
|
||||||
|
{event.related_week_chg.toFixed(2)}%
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* 分页控件 */}
|
||||||
|
{newsPagination.pages > 1 && (
|
||||||
|
<Box pt={4}>
|
||||||
|
<HStack
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
flexWrap="wrap"
|
||||||
|
>
|
||||||
|
{/* 分页信息 */}
|
||||||
|
<Text fontSize="sm" color="gray.600">
|
||||||
|
第 {newsPagination.page} / {newsPagination.pages} 页
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 分页按钮 */}
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(1)}
|
||||||
|
isDisabled={!newsPagination.has_prev || newsLoading}
|
||||||
|
leftIcon={<Icon as={FaChevronLeft} />}
|
||||||
|
>
|
||||||
|
首页
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handlePageChange(newsPagination.page - 1)
|
||||||
|
}
|
||||||
|
isDisabled={!newsPagination.has_prev || newsLoading}
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 页码按钮 */}
|
||||||
|
{renderPaginationButtons()}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handlePageChange(newsPagination.page + 1)
|
||||||
|
}
|
||||||
|
isDisabled={!newsPagination.has_next || newsLoading}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(newsPagination.pages)}
|
||||||
|
isDisabled={!newsPagination.has_next || newsLoading}
|
||||||
|
rightIcon={<Icon as={FaChevronRight} />}
|
||||||
|
>
|
||||||
|
末页
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Center h="400px">
|
||||||
|
<VStack spacing={3}>
|
||||||
|
<Icon as={FaNewspaper} boxSize={16} color="gray.300" />
|
||||||
|
<Text color="gray.500" fontSize="lg" fontWeight="medium">
|
||||||
|
暂无相关新闻
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color="gray.400">
|
||||||
|
{searchQuery ? "尝试修改搜索关键词" : "该公司暂无新闻动态"}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsEventsTab;
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user