- DeepAnalysisTab: 移除 useColorModeValue,使用固定颜色值 - NewsEventsTab: 移除 useColorModeValue,简化 hover 颜色 - FinancialPanorama: 移除 useColorMode/useColorModeValue - MarketDataView: 移除 dark 主题配置,简化颜色逻辑 - StockQuoteCard: 移除 useColorModeValue,使用固定颜色 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
538 lines
20 KiB
JavaScript
538 lines
20 KiB
JavaScript
// 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,
|
||
} 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 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: "gray.50",
|
||
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;
|