Files
vf_react/src/views/Company/components/CompanyOverview/NewsEventsTab.js
zdl 40f6eaced6 refactor: 移除暗色模式相关代码,使用固定浅色主题
- 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>
2025-12-09 18:46:30 +08:00

538 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;