refactor(DynamicTracking): 将 NewsEventsTab 移至正确目录并重构
- 从 CompanyOverview/ 移动到 DynamicTracking/(修复跨目录引用) - 拆分为目录结构:constants.ts, types.ts, utils.ts - 提取 5 个子组件:NewsSearchBar, NewsEventCard, NewsPagination, NewsEmptyState, NewsLoadingState - 转换为 TypeScript,添加完整类型定义(ThemeConfig, NewsEvent 等) - 所有子组件使用 React.memo 优化渲染 - 更新 NewsPanel.js 引用路径 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,663 +0,0 @@
|
||||
// src/views/Company/components/CompanyOverview/NewsEventsTab.js
|
||||
// 新闻动态 Tab - 相关新闻事件列表 + 分页
|
||||
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
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";
|
||||
import { getEventDetailUrl } from "@/utils/idEncoder";
|
||||
|
||||
// 黑金主题配色(文字使用更亮的金色提高对比度)
|
||||
const THEME_PRESETS = {
|
||||
blackGold: {
|
||||
bg: "#0A0E17",
|
||||
cardBg: "#1A1F2E",
|
||||
cardHoverBg: "#212633",
|
||||
cardBorder: "rgba(212, 175, 55, 0.2)",
|
||||
cardHoverBorder: "#F4D03F", // 亮金色
|
||||
textPrimary: "#E8E9ED",
|
||||
textSecondary: "#A0A4B8",
|
||||
textMuted: "#6B7280",
|
||||
gold: "#F4D03F", // 亮金色(用于文字)
|
||||
goldLight: "#FFD54F",
|
||||
inputBg: "#151922",
|
||||
inputBorder: "#2D3748",
|
||||
buttonBg: "#D4AF37", // 按钮背景保持深金色
|
||||
buttonText: "#0A0E17",
|
||||
buttonHoverBg: "#FFD54F",
|
||||
badgeS: { bg: "rgba(255, 195, 0, 0.2)", color: "#FFD54F" },
|
||||
badgeA: { bg: "rgba(249, 115, 22, 0.2)", color: "#FB923C" },
|
||||
badgeB: { bg: "rgba(59, 130, 246, 0.2)", color: "#60A5FA" },
|
||||
badgeC: { bg: "rgba(107, 114, 128, 0.2)", color: "#9CA3AF" },
|
||||
tagBg: "rgba(212, 175, 55, 0.15)",
|
||||
tagColor: "#F4D03F", // 亮金色
|
||||
spinnerColor: "#F4D03F", // 亮金色
|
||||
},
|
||||
default: {
|
||||
bg: "white",
|
||||
cardBg: "white",
|
||||
cardHoverBg: "gray.50",
|
||||
cardBorder: "gray.200",
|
||||
cardHoverBorder: "blue.300",
|
||||
textPrimary: "gray.800",
|
||||
textSecondary: "gray.600",
|
||||
textMuted: "gray.500",
|
||||
gold: "blue.500",
|
||||
goldLight: "blue.400",
|
||||
inputBg: "white",
|
||||
inputBorder: "gray.200",
|
||||
buttonBg: "blue.500",
|
||||
buttonText: "white",
|
||||
buttonHoverBg: "blue.600",
|
||||
badgeS: { bg: "red.100", color: "red.600" },
|
||||
badgeA: { bg: "orange.100", color: "orange.600" },
|
||||
badgeB: { bg: "yellow.100", color: "yellow.600" },
|
||||
badgeC: { bg: "green.100", color: "green.600" },
|
||||
tagBg: "cyan.50",
|
||||
tagColor: "cyan.600",
|
||||
spinnerColor: "blue.500",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 新闻动态 Tab 组件
|
||||
*
|
||||
* Props:
|
||||
* - newsEvents: 新闻事件列表数组
|
||||
* - newsLoading: 加载状态
|
||||
* - newsPagination: 分页信息 { page, per_page, total, pages, has_next, has_prev }
|
||||
* - searchQuery: 搜索关键词
|
||||
* - onSearchChange: 搜索输入回调 (value) => void
|
||||
* - onSearch: 搜索提交回调 () => void
|
||||
* - onPageChange: 分页回调 (page) => void
|
||||
* - cardBg: 卡片背景色
|
||||
* - themePreset: 主题预设 'blackGold' | 'default'
|
||||
*/
|
||||
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,
|
||||
themePreset = "default",
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 获取主题配色
|
||||
const theme = THEME_PRESETS[themePreset] || THEME_PRESETS.default;
|
||||
const isBlackGold = themePreset === "blackGold";
|
||||
|
||||
// 点击事件卡片,跳转到详情页
|
||||
const handleEventClick = (eventId) => {
|
||||
if (eventId) {
|
||||
navigate(getEventDetailUrl(eventId));
|
||||
}
|
||||
};
|
||||
// 事件类型图标映射
|
||||
const getEventTypeIcon = (eventType) => {
|
||||
const iconMap = {
|
||||
企业公告: FaBullhorn,
|
||||
政策: FaGavel,
|
||||
技术突破: FaFlask,
|
||||
企业融资: FaDollarSign,
|
||||
政策监管: FaShieldAlt,
|
||||
政策动态: FaFileAlt,
|
||||
行业事件: FaIndustry,
|
||||
};
|
||||
return iconMap[eventType] || FaNewspaper;
|
||||
};
|
||||
|
||||
// 重要性颜色映射 - 根据主题返回不同配色
|
||||
const getImportanceBadgeStyle = (importance) => {
|
||||
if (isBlackGold) {
|
||||
const styles = {
|
||||
S: theme.badgeS,
|
||||
A: theme.badgeA,
|
||||
B: theme.badgeB,
|
||||
C: theme.badgeC,
|
||||
};
|
||||
return styles[importance] || { bg: "rgba(107, 114, 128, 0.2)", color: "#9CA3AF" };
|
||||
}
|
||||
// 默认主题使用 colorScheme
|
||||
const colorMap = {
|
||||
S: "red",
|
||||
A: "orange",
|
||||
B: "yellow",
|
||||
C: "green",
|
||||
};
|
||||
return { colorScheme: 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={theme.textMuted}>
|
||||
...
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const isActive = i === currentPage;
|
||||
pageButtons.push(
|
||||
<Button
|
||||
key={i}
|
||||
size="sm"
|
||||
bg={isActive ? theme.buttonBg : (isBlackGold ? theme.inputBg : undefined)}
|
||||
color={isActive ? theme.buttonText : theme.textSecondary}
|
||||
borderColor={isActive ? theme.gold : theme.cardBorder}
|
||||
borderWidth="1px"
|
||||
_hover={{
|
||||
bg: isActive ? theme.buttonHoverBg : theme.cardHoverBg,
|
||||
borderColor: theme.gold
|
||||
}}
|
||||
onClick={() => handlePageChange(i)}
|
||||
isDisabled={newsLoading}
|
||||
>
|
||||
{i}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果结束页小于总页数,显示省略号
|
||||
if (endPage < totalPages) {
|
||||
pageButtons.push(
|
||||
<Text key="end-ellipsis" fontSize="sm" color={theme.textMuted}>
|
||||
...
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return pageButtons;
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Card bg={cardBg || theme.cardBg} shadow="md" borderColor={theme.cardBorder} borderWidth={isBlackGold ? "1px" : "0"}>
|
||||
<CardBody>
|
||||
<VStack spacing={4} align="stretch">
|
||||
{/* 搜索框和统计信息 */}
|
||||
<HStack justify="space-between" flexWrap="wrap">
|
||||
<HStack flex={1} minW="300px">
|
||||
<InputGroup>
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<SearchIcon color={theme.textMuted} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder="搜索相关新闻..."
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
onKeyPress={handleKeyPress}
|
||||
bg={theme.inputBg}
|
||||
borderColor={theme.inputBorder}
|
||||
color={theme.textPrimary}
|
||||
_placeholder={{ color: theme.textMuted }}
|
||||
_hover={{ borderColor: theme.gold }}
|
||||
_focus={{ borderColor: theme.gold, boxShadow: `0 0 0 1px ${theme.gold}` }}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Button
|
||||
bg={theme.buttonBg}
|
||||
color={theme.buttonText}
|
||||
_hover={{ bg: theme.buttonHoverBg }}
|
||||
onClick={handleSearchSubmit}
|
||||
isLoading={newsLoading}
|
||||
minW="80px"
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
{newsPagination.total > 0 && (
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FaNewspaper} color={theme.gold} />
|
||||
<Text fontSize="sm" color={theme.textSecondary}>
|
||||
共找到{" "}
|
||||
<Text as="span" fontWeight="bold" color={theme.gold}>
|
||||
{newsPagination.total}
|
||||
</Text>{" "}
|
||||
条新闻
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<div id="news-list-top" />
|
||||
|
||||
{/* 新闻列表 */}
|
||||
{newsLoading ? (
|
||||
<Center h="400px">
|
||||
<VStack spacing={3}>
|
||||
<Spinner size="xl" color={theme.spinnerColor} thickness="4px" />
|
||||
<Text color={theme.textSecondary}>正在加载新闻...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : newsEvents.length > 0 ? (
|
||||
<>
|
||||
<VStack spacing={3} align="stretch">
|
||||
{newsEvents.map((event, idx) => {
|
||||
const importanceBadgeStyle = getImportanceBadgeStyle(
|
||||
event.importance
|
||||
);
|
||||
const eventTypeIcon = getEventTypeIcon(event.event_type);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={event.id || idx}
|
||||
variant="outline"
|
||||
bg={theme.cardBg}
|
||||
borderColor={theme.cardBorder}
|
||||
cursor="pointer"
|
||||
onClick={() => handleEventClick(event.id)}
|
||||
_hover={{
|
||||
bg: theme.cardHoverBg,
|
||||
shadow: "md",
|
||||
borderColor: theme.cardHoverBorder,
|
||||
}}
|
||||
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={theme.gold}
|
||||
boxSize={5}
|
||||
/>
|
||||
<Text
|
||||
fontWeight="bold"
|
||||
fontSize="lg"
|
||||
lineHeight="1.3"
|
||||
color={theme.textPrimary}
|
||||
>
|
||||
{event.title}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 标签栏 */}
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{event.importance && (
|
||||
<Badge
|
||||
{...(isBlackGold ? {} : { colorScheme: importanceBadgeStyle.colorScheme, variant: "solid" })}
|
||||
bg={isBlackGold ? importanceBadgeStyle.bg : undefined}
|
||||
color={isBlackGold ? importanceBadgeStyle.color : undefined}
|
||||
px={2}
|
||||
>
|
||||
{event.importance}级
|
||||
</Badge>
|
||||
)}
|
||||
{event.event_type && (
|
||||
<Badge
|
||||
{...(isBlackGold ? {} : { colorScheme: "blue", variant: "outline" })}
|
||||
bg={isBlackGold ? "rgba(59, 130, 246, 0.2)" : undefined}
|
||||
color={isBlackGold ? "#60A5FA" : undefined}
|
||||
borderColor={isBlackGold ? "rgba(59, 130, 246, 0.3)" : undefined}
|
||||
>
|
||||
{event.event_type}
|
||||
</Badge>
|
||||
)}
|
||||
{event.invest_score && (
|
||||
<Badge
|
||||
{...(isBlackGold ? {} : { colorScheme: "purple", variant: "subtle" })}
|
||||
bg={isBlackGold ? "rgba(139, 92, 246, 0.2)" : undefined}
|
||||
color={isBlackGold ? "#A78BFA" : undefined}
|
||||
>
|
||||
投资分: {event.invest_score}
|
||||
</Badge>
|
||||
)}
|
||||
{event.keywords && event.keywords.length > 0 && (
|
||||
<>
|
||||
{event.keywords
|
||||
.slice(0, 4)
|
||||
.map((keyword, kidx) => (
|
||||
<Tag
|
||||
key={kidx}
|
||||
size="sm"
|
||||
{...(isBlackGold ? {} : { colorScheme: "cyan", variant: "subtle" })}
|
||||
bg={isBlackGold ? theme.tagBg : undefined}
|
||||
color={isBlackGold ? theme.tagColor : undefined}
|
||||
>
|
||||
{typeof keyword === "string"
|
||||
? keyword
|
||||
: keyword?.concept ||
|
||||
keyword?.name ||
|
||||
"未知"}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
{/* 右侧信息栏 */}
|
||||
<VStack align="end" spacing={1} minW="100px">
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
{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={theme.textMuted}
|
||||
/>
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
{event.view_count}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
{event.hot_score !== undefined && (
|
||||
<HStack spacing={1}>
|
||||
<Icon
|
||||
as={FaFire}
|
||||
boxSize={3}
|
||||
color={theme.goldLight}
|
||||
/>
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
{event.hot_score.toFixed(1)}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
{event.creator && (
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
@{event.creator.username}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
{/* 描述 */}
|
||||
{event.description && (
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color={theme.textSecondary}
|
||||
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={theme.cardBorder}
|
||||
>
|
||||
<HStack spacing={6} flexWrap="wrap">
|
||||
<HStack spacing={1}>
|
||||
<Icon
|
||||
as={FaChartLine}
|
||||
boxSize={3}
|
||||
color={theme.textMuted}
|
||||
/>
|
||||
<Text
|
||||
fontSize="xs"
|
||||
color={theme.textMuted}
|
||||
fontWeight="medium"
|
||||
>
|
||||
相关涨跌:
|
||||
</Text>
|
||||
</HStack>
|
||||
{event.related_avg_chg !== null &&
|
||||
event.related_avg_chg !== undefined && (
|
||||
<HStack spacing={1}>
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
平均
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="bold"
|
||||
color={
|
||||
event.related_avg_chg > 0
|
||||
? "#EF4444"
|
||||
: "#10B981"
|
||||
}
|
||||
>
|
||||
{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={theme.textMuted}>
|
||||
最大
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="bold"
|
||||
color={
|
||||
event.related_max_chg > 0
|
||||
? "#EF4444"
|
||||
: "#10B981"
|
||||
}
|
||||
>
|
||||
{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={theme.textMuted}>
|
||||
周
|
||||
</Text>
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="bold"
|
||||
color={
|
||||
event.related_week_chg > 0
|
||||
? "#EF4444"
|
||||
: "#10B981"
|
||||
}
|
||||
>
|
||||
{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={theme.textSecondary}>
|
||||
第 {newsPagination.page} / {newsPagination.pages} 页
|
||||
</Text>
|
||||
|
||||
{/* 分页按钮 */}
|
||||
<HStack spacing={2}>
|
||||
<Button
|
||||
size="sm"
|
||||
bg={isBlackGold ? theme.inputBg : undefined}
|
||||
color={theme.textSecondary}
|
||||
borderColor={theme.cardBorder}
|
||||
borderWidth="1px"
|
||||
_hover={{ bg: theme.cardHoverBg, borderColor: theme.gold }}
|
||||
onClick={() => handlePageChange(1)}
|
||||
isDisabled={!newsPagination.has_prev || newsLoading}
|
||||
leftIcon={<Icon as={FaChevronLeft} />}
|
||||
>
|
||||
首页
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
bg={isBlackGold ? theme.inputBg : undefined}
|
||||
color={theme.textSecondary}
|
||||
borderColor={theme.cardBorder}
|
||||
borderWidth="1px"
|
||||
_hover={{ bg: theme.cardHoverBg, borderColor: theme.gold }}
|
||||
onClick={() =>
|
||||
handlePageChange(newsPagination.page - 1)
|
||||
}
|
||||
isDisabled={!newsPagination.has_prev || newsLoading}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
|
||||
{/* 页码按钮 */}
|
||||
{renderPaginationButtons()}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
bg={isBlackGold ? theme.inputBg : undefined}
|
||||
color={theme.textSecondary}
|
||||
borderColor={theme.cardBorder}
|
||||
borderWidth="1px"
|
||||
_hover={{ bg: theme.cardHoverBg, borderColor: theme.gold }}
|
||||
onClick={() =>
|
||||
handlePageChange(newsPagination.page + 1)
|
||||
}
|
||||
isDisabled={!newsPagination.has_next || newsLoading}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
bg={isBlackGold ? theme.inputBg : undefined}
|
||||
color={theme.textSecondary}
|
||||
borderColor={theme.cardBorder}
|
||||
borderWidth="1px"
|
||||
_hover={{ bg: theme.cardHoverBg, borderColor: theme.gold }}
|
||||
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={isBlackGold ? theme.gold : "gray.300"} opacity={0.5} />
|
||||
<Text color={theme.textSecondary} fontSize="lg" fontWeight="medium">
|
||||
暂无相关新闻
|
||||
</Text>
|
||||
<Text fontSize="sm" color={theme.textMuted}>
|
||||
{searchQuery ? "尝试修改搜索关键词" : "该公司暂无新闻动态"}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewsEventsTab;
|
||||
@@ -0,0 +1,34 @@
|
||||
// src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsEmptyState.tsx
|
||||
// 空状态组件
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Center, VStack, Icon, Text } from '@chakra-ui/react';
|
||||
import { FaNewspaper } from 'react-icons/fa';
|
||||
import type { NewsEmptyStateProps } from '../types';
|
||||
|
||||
const NewsEmptyState: React.FC<NewsEmptyStateProps> = ({
|
||||
searchQuery,
|
||||
theme,
|
||||
isBlackGold,
|
||||
}) => {
|
||||
return (
|
||||
<Center h="400px">
|
||||
<VStack spacing={3}>
|
||||
<Icon
|
||||
as={FaNewspaper}
|
||||
boxSize={16}
|
||||
color={isBlackGold ? theme.gold : 'gray.300'}
|
||||
opacity={0.5}
|
||||
/>
|
||||
<Text color={theme.textSecondary} fontSize="lg" fontWeight="medium">
|
||||
暂无相关新闻
|
||||
</Text>
|
||||
<Text fontSize="sm" color={theme.textMuted}>
|
||||
{searchQuery ? '尝试修改搜索关键词' : '该公司暂无新闻动态'}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NewsEmptyState);
|
||||
@@ -0,0 +1,193 @@
|
||||
// src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsEventCard.tsx
|
||||
// 新闻事件卡片组件
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Badge,
|
||||
Icon,
|
||||
Card,
|
||||
CardBody,
|
||||
Tag,
|
||||
} from '@chakra-ui/react';
|
||||
import { FaEye, FaFire, FaChartLine } from 'react-icons/fa';
|
||||
import type { NewsEventCardProps } from '../types';
|
||||
import {
|
||||
getEventTypeIcon,
|
||||
getImportanceBadgeStyle,
|
||||
formatDate,
|
||||
formatChange,
|
||||
getChangeColor,
|
||||
getKeywordText,
|
||||
} from '../utils';
|
||||
|
||||
const NewsEventCard: React.FC<NewsEventCardProps> = ({
|
||||
event,
|
||||
theme,
|
||||
isBlackGold,
|
||||
onClick,
|
||||
}) => {
|
||||
const importanceBadgeStyle = getImportanceBadgeStyle(event.importance, theme, isBlackGold);
|
||||
const EventTypeIcon = getEventTypeIcon(event.event_type);
|
||||
|
||||
const hasRelatedChanges =
|
||||
event.related_avg_chg !== null ||
|
||||
event.related_max_chg !== null ||
|
||||
event.related_week_chg !== null;
|
||||
|
||||
return (
|
||||
<Card
|
||||
variant="outline"
|
||||
bg={theme.cardBg}
|
||||
borderColor={theme.cardBorder}
|
||||
cursor="pointer"
|
||||
onClick={() => onClick(event.id)}
|
||||
_hover={{
|
||||
bg: theme.cardHoverBg,
|
||||
shadow: 'md',
|
||||
borderColor: theme.cardHoverBorder,
|
||||
}}
|
||||
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={theme.gold} boxSize={5} />
|
||||
<Text fontWeight="bold" fontSize="lg" lineHeight="1.3" color={theme.textPrimary}>
|
||||
{event.title}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{/* 标签栏 */}
|
||||
<HStack spacing={2} flexWrap="wrap">
|
||||
{event.importance && (
|
||||
<Badge
|
||||
{...(isBlackGold ? {} : { colorScheme: importanceBadgeStyle.colorScheme, variant: 'solid' })}
|
||||
bg={isBlackGold ? importanceBadgeStyle.bg : undefined}
|
||||
color={isBlackGold ? importanceBadgeStyle.color : undefined}
|
||||
px={2}
|
||||
>
|
||||
{event.importance}级
|
||||
</Badge>
|
||||
)}
|
||||
{event.event_type && (
|
||||
<Badge
|
||||
{...(isBlackGold ? {} : { colorScheme: 'blue', variant: 'outline' })}
|
||||
bg={isBlackGold ? 'rgba(59, 130, 246, 0.2)' : undefined}
|
||||
color={isBlackGold ? '#60A5FA' : undefined}
|
||||
borderColor={isBlackGold ? 'rgba(59, 130, 246, 0.3)' : undefined}
|
||||
>
|
||||
{event.event_type}
|
||||
</Badge>
|
||||
)}
|
||||
{event.invest_score && (
|
||||
<Badge
|
||||
{...(isBlackGold ? {} : { colorScheme: 'purple', variant: 'subtle' })}
|
||||
bg={isBlackGold ? 'rgba(139, 92, 246, 0.2)' : undefined}
|
||||
color={isBlackGold ? '#A78BFA' : undefined}
|
||||
>
|
||||
投资分: {event.invest_score}
|
||||
</Badge>
|
||||
)}
|
||||
{event.keywords?.slice(0, 4).map((keyword, kidx) => (
|
||||
<Tag
|
||||
key={kidx}
|
||||
size="sm"
|
||||
{...(isBlackGold ? {} : { colorScheme: 'cyan', variant: 'subtle' })}
|
||||
bg={isBlackGold ? theme.tagBg : undefined}
|
||||
color={isBlackGold ? theme.tagColor : undefined}
|
||||
>
|
||||
{getKeywordText(keyword)}
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
</VStack>
|
||||
|
||||
{/* 右侧信息栏 */}
|
||||
<VStack align="end" spacing={1} minW="100px">
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
{formatDate(event.created_at)}
|
||||
</Text>
|
||||
<HStack spacing={3}>
|
||||
{event.view_count !== undefined && (
|
||||
<HStack spacing={1}>
|
||||
<Icon as={FaEye} boxSize={3} color={theme.textMuted} />
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
{event.view_count}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
{event.hot_score !== undefined && (
|
||||
<HStack spacing={1}>
|
||||
<Icon as={FaFire} boxSize={3} color={theme.goldLight} />
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
{event.hot_score.toFixed(1)}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
{event.creator && (
|
||||
<Text fontSize="xs" color={theme.textMuted}>
|
||||
@{event.creator.username}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
{/* 描述 */}
|
||||
{event.description && (
|
||||
<Text fontSize="sm" color={theme.textSecondary} lineHeight="1.6">
|
||||
{event.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 收益率数据 */}
|
||||
{hasRelatedChanges && (
|
||||
<Box pt={2} borderTop="1px" borderColor={theme.cardBorder}>
|
||||
<HStack spacing={6} flexWrap="wrap">
|
||||
<HStack spacing={1}>
|
||||
<Icon as={FaChartLine} boxSize={3} color={theme.textMuted} />
|
||||
<Text fontSize="xs" color={theme.textMuted} fontWeight="medium">
|
||||
相关涨跌:
|
||||
</Text>
|
||||
</HStack>
|
||||
{event.related_avg_chg !== null && event.related_avg_chg !== undefined && (
|
||||
<HStack spacing={1}>
|
||||
<Text fontSize="xs" color={theme.textMuted}>平均</Text>
|
||||
<Text fontSize="sm" fontWeight="bold" color={getChangeColor(event.related_avg_chg)}>
|
||||
{formatChange(event.related_avg_chg)}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
{event.related_max_chg !== null && event.related_max_chg !== undefined && (
|
||||
<HStack spacing={1}>
|
||||
<Text fontSize="xs" color={theme.textMuted}>最大</Text>
|
||||
<Text fontSize="sm" fontWeight="bold" color={getChangeColor(event.related_max_chg)}>
|
||||
{formatChange(event.related_max_chg)}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
{event.related_week_chg !== null && event.related_week_chg !== undefined && (
|
||||
<HStack spacing={1}>
|
||||
<Text fontSize="xs" color={theme.textMuted}>周</Text>
|
||||
<Text fontSize="sm" fontWeight="bold" color={getChangeColor(event.related_week_chg)}>
|
||||
{formatChange(event.related_week_chg)}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NewsEventCard);
|
||||
@@ -0,0 +1,19 @@
|
||||
// src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsLoadingState.tsx
|
||||
// 加载状态组件
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Center, VStack, Spinner, Text } from '@chakra-ui/react';
|
||||
import type { NewsLoadingStateProps } from '../types';
|
||||
|
||||
const NewsLoadingState: React.FC<NewsLoadingStateProps> = ({ theme }) => {
|
||||
return (
|
||||
<Center h="400px">
|
||||
<VStack spacing={3}>
|
||||
<Spinner size="xl" color={theme.spinnerColor} thickness="4px" />
|
||||
<Text color={theme.textSecondary}>正在加载新闻...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NewsLoadingState);
|
||||
@@ -0,0 +1,141 @@
|
||||
// src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsPagination.tsx
|
||||
// 分页组件
|
||||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Box, HStack, Text, Button, Icon } from '@chakra-ui/react';
|
||||
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||
import type { NewsPaginationProps } from '../types';
|
||||
|
||||
const NewsPagination: React.FC<NewsPaginationProps> = ({
|
||||
pagination,
|
||||
onPageChange,
|
||||
isLoading,
|
||||
theme,
|
||||
isBlackGold,
|
||||
}) => {
|
||||
const { page: currentPage, pages: totalPages, has_next, has_prev } = pagination;
|
||||
|
||||
// 渲染分页按钮
|
||||
const pageButtons = useMemo(() => {
|
||||
const buttons: React.ReactNode[] = [];
|
||||
|
||||
// 显示当前页及前后各2页
|
||||
const startPage = Math.max(1, currentPage - 2);
|
||||
const endPage = Math.min(totalPages, currentPage + 2);
|
||||
|
||||
// 如果开始页大于1,显示省略号
|
||||
if (startPage > 1) {
|
||||
buttons.push(
|
||||
<Text key="start-ellipsis" fontSize="sm" color={theme.textMuted}>
|
||||
...
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const isActive = i === currentPage;
|
||||
buttons.push(
|
||||
<Button
|
||||
key={i}
|
||||
size="sm"
|
||||
bg={isActive ? theme.buttonBg : isBlackGold ? theme.inputBg : undefined}
|
||||
color={isActive ? theme.buttonText : theme.textSecondary}
|
||||
borderColor={isActive ? theme.gold : theme.cardBorder}
|
||||
borderWidth="1px"
|
||||
_hover={{
|
||||
bg: isActive ? theme.buttonHoverBg : theme.cardHoverBg,
|
||||
borderColor: theme.gold,
|
||||
}}
|
||||
onClick={() => onPageChange(i)}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
{i}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果结束页小于总页数,显示省略号
|
||||
if (endPage < totalPages) {
|
||||
buttons.push(
|
||||
<Text key="end-ellipsis" fontSize="sm" color={theme.textMuted}>
|
||||
...
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}, [currentPage, totalPages, theme, isBlackGold, isLoading, onPageChange]);
|
||||
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
return (
|
||||
<Box pt={4}>
|
||||
<HStack justify="space-between" align="center" flexWrap="wrap">
|
||||
{/* 分页信息 */}
|
||||
<Text fontSize="sm" color={theme.textSecondary}>
|
||||
第 {currentPage} / {totalPages} 页
|
||||
</Text>
|
||||
|
||||
{/* 分页按钮 */}
|
||||
<HStack spacing={2}>
|
||||
<Button
|
||||
size="sm"
|
||||
bg={isBlackGold ? theme.inputBg : undefined}
|
||||
color={theme.textSecondary}
|
||||
borderColor={theme.cardBorder}
|
||||
borderWidth="1px"
|
||||
_hover={{ bg: theme.cardHoverBg, borderColor: theme.gold }}
|
||||
onClick={() => onPageChange(1)}
|
||||
isDisabled={!has_prev || isLoading}
|
||||
leftIcon={<Icon as={FaChevronLeft} />}
|
||||
>
|
||||
首页
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
bg={isBlackGold ? theme.inputBg : undefined}
|
||||
color={theme.textSecondary}
|
||||
borderColor={theme.cardBorder}
|
||||
borderWidth="1px"
|
||||
_hover={{ bg: theme.cardHoverBg, borderColor: theme.gold }}
|
||||
onClick={() => onPageChange(currentPage - 1)}
|
||||
isDisabled={!has_prev || isLoading}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
|
||||
{/* 页码按钮 */}
|
||||
{pageButtons}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
bg={isBlackGold ? theme.inputBg : undefined}
|
||||
color={theme.textSecondary}
|
||||
borderColor={theme.cardBorder}
|
||||
borderWidth="1px"
|
||||
_hover={{ bg: theme.cardHoverBg, borderColor: theme.gold }}
|
||||
onClick={() => onPageChange(currentPage + 1)}
|
||||
isDisabled={!has_next || isLoading}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
bg={isBlackGold ? theme.inputBg : undefined}
|
||||
color={theme.textSecondary}
|
||||
borderColor={theme.cardBorder}
|
||||
borderWidth="1px"
|
||||
_hover={{ bg: theme.cardHoverBg, borderColor: theme.gold }}
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
isDisabled={!has_next || isLoading}
|
||||
rightIcon={<Icon as={FaChevronRight} />}
|
||||
>
|
||||
末页
|
||||
</Button>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NewsPagination);
|
||||
@@ -0,0 +1,77 @@
|
||||
// src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsSearchBar.tsx
|
||||
// 新闻搜索栏组件
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { HStack, Input, InputGroup, InputLeftElement, Button, Text, Icon } from '@chakra-ui/react';
|
||||
import { SearchIcon } from '@chakra-ui/icons';
|
||||
import { FaNewspaper } from 'react-icons/fa';
|
||||
import type { NewsSearchBarProps } from '../types';
|
||||
|
||||
const NewsSearchBar: React.FC<NewsSearchBarProps> = ({
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
onSearch,
|
||||
isLoading,
|
||||
total,
|
||||
theme,
|
||||
isBlackGold,
|
||||
}) => {
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onSearchChange(e.target.value);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
onSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<HStack justify="space-between" flexWrap="wrap">
|
||||
<HStack flex={1} minW="300px">
|
||||
<InputGroup>
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<SearchIcon color={theme.textMuted} />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
placeholder="搜索相关新闻..."
|
||||
value={searchQuery}
|
||||
onChange={handleInputChange}
|
||||
onKeyPress={handleKeyPress}
|
||||
bg={theme.inputBg}
|
||||
borderColor={theme.inputBorder}
|
||||
color={theme.textPrimary}
|
||||
_placeholder={{ color: theme.textMuted }}
|
||||
_hover={{ borderColor: theme.gold }}
|
||||
_focus={{ borderColor: theme.gold, boxShadow: `0 0 0 1px ${theme.gold}` }}
|
||||
/>
|
||||
</InputGroup>
|
||||
<Button
|
||||
bg={theme.buttonBg}
|
||||
color={theme.buttonText}
|
||||
_hover={{ bg: theme.buttonHoverBg }}
|
||||
onClick={onSearch}
|
||||
isLoading={isLoading}
|
||||
minW="80px"
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
{total > 0 && (
|
||||
<HStack spacing={2}>
|
||||
<Icon as={FaNewspaper} color={theme.gold} />
|
||||
<Text fontSize="sm" color={theme.textSecondary}>
|
||||
共找到{' '}
|
||||
<Text as="span" fontWeight="bold" color={theme.gold}>
|
||||
{total}
|
||||
</Text>{' '}
|
||||
条新闻
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NewsSearchBar);
|
||||
@@ -0,0 +1,8 @@
|
||||
// src/views/Company/components/DynamicTracking/NewsEventsTab/components/index.ts
|
||||
// 组件导出
|
||||
|
||||
export { default as NewsSearchBar } from './NewsSearchBar';
|
||||
export { default as NewsEventCard } from './NewsEventCard';
|
||||
export { default as NewsPagination } from './NewsPagination';
|
||||
export { default as NewsEmptyState } from './NewsEmptyState';
|
||||
export { default as NewsLoadingState } from './NewsLoadingState';
|
||||
@@ -0,0 +1,68 @@
|
||||
// src/views/Company/components/DynamicTracking/NewsEventsTab/constants.ts
|
||||
// 新闻动态 - 主题配置常量
|
||||
|
||||
import type { ThemeConfig } from './types';
|
||||
|
||||
/**
|
||||
* 黑金主题配色(文字使用更亮的金色提高对比度)
|
||||
*/
|
||||
export const BLACK_GOLD_THEME: ThemeConfig = {
|
||||
bg: '#0A0E17',
|
||||
cardBg: '#1A1F2E',
|
||||
cardHoverBg: '#212633',
|
||||
cardBorder: 'rgba(212, 175, 55, 0.2)',
|
||||
cardHoverBorder: '#F4D03F',
|
||||
textPrimary: '#E8E9ED',
|
||||
textSecondary: '#A0A4B8',
|
||||
textMuted: '#6B7280',
|
||||
gold: '#F4D03F',
|
||||
goldLight: '#FFD54F',
|
||||
inputBg: '#151922',
|
||||
inputBorder: '#2D3748',
|
||||
buttonBg: '#D4AF37',
|
||||
buttonText: '#0A0E17',
|
||||
buttonHoverBg: '#FFD54F',
|
||||
badgeS: { bg: 'rgba(255, 195, 0, 0.2)', color: '#FFD54F' },
|
||||
badgeA: { bg: 'rgba(249, 115, 22, 0.2)', color: '#FB923C' },
|
||||
badgeB: { bg: 'rgba(59, 130, 246, 0.2)', color: '#60A5FA' },
|
||||
badgeC: { bg: 'rgba(107, 114, 128, 0.2)', color: '#9CA3AF' },
|
||||
tagBg: 'rgba(212, 175, 55, 0.15)',
|
||||
tagColor: '#F4D03F',
|
||||
spinnerColor: '#F4D03F',
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认主题配色
|
||||
*/
|
||||
export const DEFAULT_THEME: ThemeConfig = {
|
||||
bg: 'white',
|
||||
cardBg: 'white',
|
||||
cardHoverBg: 'gray.50',
|
||||
cardBorder: 'gray.200',
|
||||
cardHoverBorder: 'blue.300',
|
||||
textPrimary: 'gray.800',
|
||||
textSecondary: 'gray.600',
|
||||
textMuted: 'gray.500',
|
||||
gold: 'blue.500',
|
||||
goldLight: 'blue.400',
|
||||
inputBg: 'white',
|
||||
inputBorder: 'gray.200',
|
||||
buttonBg: 'blue.500',
|
||||
buttonText: 'white',
|
||||
buttonHoverBg: 'blue.600',
|
||||
badgeS: { bg: 'red.100', color: 'red.600' },
|
||||
badgeA: { bg: 'orange.100', color: 'orange.600' },
|
||||
badgeB: { bg: 'yellow.100', color: 'yellow.600' },
|
||||
badgeC: { bg: 'green.100', color: 'green.600' },
|
||||
tagBg: 'cyan.50',
|
||||
tagColor: 'cyan.600',
|
||||
spinnerColor: 'blue.500',
|
||||
};
|
||||
|
||||
/**
|
||||
* 主题预设映射
|
||||
*/
|
||||
export const THEME_PRESETS: Record<string, ThemeConfig> = {
|
||||
blackGold: BLACK_GOLD_THEME,
|
||||
default: DEFAULT_THEME,
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
// src/views/Company/components/DynamicTracking/NewsEventsTab/index.tsx
|
||||
// 新闻动态 Tab 组件 - 黑金主题
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { VStack, Card, CardBody } from '@chakra-ui/react';
|
||||
import { getEventDetailUrl } from '@/utils/idEncoder';
|
||||
import { THEME_PRESETS } from './constants';
|
||||
import {
|
||||
NewsSearchBar,
|
||||
NewsEventCard,
|
||||
NewsPagination,
|
||||
NewsEmptyState,
|
||||
NewsLoadingState,
|
||||
} from './components';
|
||||
import type { NewsEventsTabProps, NewsPagination as NewsPaginationType } from './types';
|
||||
|
||||
/**
|
||||
* 默认分页配置
|
||||
*/
|
||||
const DEFAULT_PAGINATION: NewsPaginationType = {
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
total: 0,
|
||||
pages: 0,
|
||||
has_next: false,
|
||||
has_prev: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* 新闻动态 Tab 组件
|
||||
*
|
||||
* Props:
|
||||
* - newsEvents: 新闻事件列表数组
|
||||
* - newsLoading: 加载状态
|
||||
* - newsPagination: 分页信息
|
||||
* - searchQuery: 搜索关键词
|
||||
* - onSearchChange: 搜索输入回调
|
||||
* - onSearch: 搜索提交回调
|
||||
* - onPageChange: 分页回调
|
||||
* - cardBg: 卡片背景色
|
||||
* - themePreset: 主题预设 'blackGold' | 'default'
|
||||
*/
|
||||
const NewsEventsTab: React.FC<NewsEventsTabProps> = ({
|
||||
newsEvents = [],
|
||||
newsLoading = false,
|
||||
newsPagination = DEFAULT_PAGINATION,
|
||||
searchQuery = '',
|
||||
onSearchChange,
|
||||
onSearch,
|
||||
onPageChange,
|
||||
cardBg,
|
||||
themePreset = 'default',
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 获取主题配色
|
||||
const theme = THEME_PRESETS[themePreset] || THEME_PRESETS.default;
|
||||
const isBlackGold = themePreset === 'blackGold';
|
||||
|
||||
// 点击事件卡片,跳转到详情页
|
||||
const handleEventClick = useCallback(
|
||||
(eventId: string | number | undefined) => {
|
||||
if (eventId) {
|
||||
navigate(getEventDetailUrl(eventId));
|
||||
}
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
// 处理搜索输入
|
||||
const handleSearchChange = useCallback(
|
||||
(value: string) => {
|
||||
onSearchChange?.(value);
|
||||
},
|
||||
[onSearchChange]
|
||||
);
|
||||
|
||||
// 处理搜索提交
|
||||
const handleSearch = useCallback(() => {
|
||||
onSearch?.();
|
||||
}, [onSearch]);
|
||||
|
||||
// 处理分页
|
||||
const handlePageChange = useCallback(
|
||||
(page: number) => {
|
||||
onPageChange?.(page);
|
||||
// 滚动到列表顶部
|
||||
document.getElementById('news-list-top')?.scrollIntoView({ behavior: 'smooth' });
|
||||
},
|
||||
[onPageChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<VStack spacing={4} align="stretch">
|
||||
<Card
|
||||
bg={cardBg || theme.cardBg}
|
||||
shadow="md"
|
||||
borderColor={theme.cardBorder}
|
||||
borderWidth={isBlackGold ? '1px' : '0'}
|
||||
>
|
||||
<CardBody>
|
||||
<VStack spacing={4} align="stretch">
|
||||
{/* 搜索框和统计信息 */}
|
||||
<NewsSearchBar
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={handleSearchChange}
|
||||
onSearch={handleSearch}
|
||||
isLoading={newsLoading}
|
||||
total={newsPagination.total}
|
||||
theme={theme}
|
||||
isBlackGold={isBlackGold}
|
||||
/>
|
||||
|
||||
<div id="news-list-top" />
|
||||
|
||||
{/* 新闻列表 */}
|
||||
{newsLoading ? (
|
||||
<NewsLoadingState theme={theme} />
|
||||
) : newsEvents.length > 0 ? (
|
||||
<>
|
||||
<VStack spacing={3} align="stretch">
|
||||
{newsEvents.map((event, idx) => (
|
||||
<NewsEventCard
|
||||
key={event.id || idx}
|
||||
event={event}
|
||||
theme={theme}
|
||||
isBlackGold={isBlackGold}
|
||||
onClick={handleEventClick}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
|
||||
{/* 分页控件 */}
|
||||
<NewsPagination
|
||||
pagination={newsPagination}
|
||||
onPageChange={handlePageChange}
|
||||
isLoading={newsLoading}
|
||||
theme={theme}
|
||||
isBlackGold={isBlackGold}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<NewsEmptyState
|
||||
searchQuery={searchQuery}
|
||||
theme={theme}
|
||||
isBlackGold={isBlackGold}
|
||||
/>
|
||||
)}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NewsEventsTab);
|
||||
@@ -0,0 +1,167 @@
|
||||
// src/views/Company/components/DynamicTracking/NewsEventsTab/types.ts
|
||||
// 新闻动态 - 类型定义
|
||||
|
||||
import type { IconType } from 'react-icons';
|
||||
|
||||
/**
|
||||
* 徽章样式配置
|
||||
*/
|
||||
export interface BadgeStyle {
|
||||
bg: string;
|
||||
color: string;
|
||||
colorScheme?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题配置
|
||||
*/
|
||||
export interface ThemeConfig {
|
||||
bg: string;
|
||||
cardBg: string;
|
||||
cardHoverBg: string;
|
||||
cardBorder: string;
|
||||
cardHoverBorder: string;
|
||||
textPrimary: string;
|
||||
textSecondary: string;
|
||||
textMuted: string;
|
||||
gold: string;
|
||||
goldLight: string;
|
||||
inputBg: string;
|
||||
inputBorder: string;
|
||||
buttonBg: string;
|
||||
buttonText: string;
|
||||
buttonHoverBg: string;
|
||||
badgeS: BadgeStyle;
|
||||
badgeA: BadgeStyle;
|
||||
badgeB: BadgeStyle;
|
||||
badgeC: BadgeStyle;
|
||||
tagBg: string;
|
||||
tagColor: string;
|
||||
spinnerColor: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新闻事件创建者
|
||||
*/
|
||||
export interface NewsEventCreator {
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新闻事件关键词
|
||||
*/
|
||||
export interface NewsEventKeyword {
|
||||
concept?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新闻事件数据
|
||||
*/
|
||||
export interface NewsEvent {
|
||||
id?: string | number;
|
||||
title: string;
|
||||
description?: string;
|
||||
event_type?: string;
|
||||
importance?: 'S' | 'A' | 'B' | 'C';
|
||||
invest_score?: number;
|
||||
keywords?: (string | NewsEventKeyword)[];
|
||||
created_at?: string;
|
||||
view_count?: number;
|
||||
hot_score?: number;
|
||||
creator?: NewsEventCreator;
|
||||
related_avg_chg?: number | null;
|
||||
related_max_chg?: number | null;
|
||||
related_week_chg?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页信息
|
||||
*/
|
||||
export interface NewsPagination {
|
||||
page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题预设类型
|
||||
*/
|
||||
export type ThemePreset = 'blackGold' | 'default';
|
||||
|
||||
/**
|
||||
* NewsEventsTab 组件 Props
|
||||
*/
|
||||
export interface NewsEventsTabProps {
|
||||
/** 新闻事件列表 */
|
||||
newsEvents?: NewsEvent[];
|
||||
/** 加载状态 */
|
||||
newsLoading?: boolean;
|
||||
/** 分页信息 */
|
||||
newsPagination?: NewsPagination;
|
||||
/** 搜索关键词 */
|
||||
searchQuery?: string;
|
||||
/** 搜索输入回调 */
|
||||
onSearchChange?: (value: string) => void;
|
||||
/** 搜索提交回调 */
|
||||
onSearch?: () => void;
|
||||
/** 分页回调 */
|
||||
onPageChange?: (page: number) => void;
|
||||
/** 卡片背景色 */
|
||||
cardBg?: string;
|
||||
/** 主题预设 */
|
||||
themePreset?: ThemePreset;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索栏组件 Props
|
||||
*/
|
||||
export interface NewsSearchBarProps {
|
||||
searchQuery: string;
|
||||
onSearchChange: (value: string) => void;
|
||||
onSearch: () => void;
|
||||
isLoading: boolean;
|
||||
total: number;
|
||||
theme: ThemeConfig;
|
||||
isBlackGold: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件卡片组件 Props
|
||||
*/
|
||||
export interface NewsEventCardProps {
|
||||
event: NewsEvent;
|
||||
theme: ThemeConfig;
|
||||
isBlackGold: boolean;
|
||||
onClick: (eventId: string | number | undefined) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页组件 Props
|
||||
*/
|
||||
export interface NewsPaginationProps {
|
||||
pagination: NewsPagination;
|
||||
onPageChange: (page: number) => void;
|
||||
isLoading: boolean;
|
||||
theme: ThemeConfig;
|
||||
isBlackGold: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 空状态组件 Props
|
||||
*/
|
||||
export interface NewsEmptyStateProps {
|
||||
searchQuery: string;
|
||||
theme: ThemeConfig;
|
||||
isBlackGold: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载状态组件 Props
|
||||
*/
|
||||
export interface NewsLoadingStateProps {
|
||||
theme: ThemeConfig;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// src/views/Company/components/DynamicTracking/NewsEventsTab/utils.ts
|
||||
// 新闻动态 - 工具函数
|
||||
|
||||
import type { IconType } from 'react-icons';
|
||||
import {
|
||||
FaNewspaper,
|
||||
FaBullhorn,
|
||||
FaGavel,
|
||||
FaFlask,
|
||||
FaDollarSign,
|
||||
FaShieldAlt,
|
||||
FaFileAlt,
|
||||
FaIndustry,
|
||||
} from 'react-icons/fa';
|
||||
import type { ThemeConfig, BadgeStyle } from './types';
|
||||
|
||||
/**
|
||||
* 事件类型图标映射
|
||||
*/
|
||||
const EVENT_TYPE_ICONS: Record<string, IconType> = {
|
||||
企业公告: FaBullhorn,
|
||||
政策: FaGavel,
|
||||
技术突破: FaFlask,
|
||||
企业融资: FaDollarSign,
|
||||
政策监管: FaShieldAlt,
|
||||
政策动态: FaFileAlt,
|
||||
行业事件: FaIndustry,
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取事件类型对应的图标
|
||||
*/
|
||||
export const getEventTypeIcon = (eventType?: string): IconType => {
|
||||
if (!eventType) return FaNewspaper;
|
||||
return EVENT_TYPE_ICONS[eventType] || FaNewspaper;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取重要性徽章样式
|
||||
*/
|
||||
export const getImportanceBadgeStyle = (
|
||||
importance: string | undefined,
|
||||
theme: ThemeConfig,
|
||||
isBlackGold: boolean
|
||||
): BadgeStyle => {
|
||||
if (isBlackGold) {
|
||||
const styles: Record<string, BadgeStyle> = {
|
||||
S: theme.badgeS,
|
||||
A: theme.badgeA,
|
||||
B: theme.badgeB,
|
||||
C: theme.badgeC,
|
||||
};
|
||||
return styles[importance || ''] || { bg: 'rgba(107, 114, 128, 0.2)', color: '#9CA3AF' };
|
||||
}
|
||||
|
||||
// 默认主题使用 colorScheme
|
||||
const colorMap: Record<string, string> = {
|
||||
S: 'red',
|
||||
A: 'orange',
|
||||
B: 'yellow',
|
||||
C: 'green',
|
||||
};
|
||||
return { colorScheme: colorMap[importance || ''] || 'gray', bg: '', color: '' };
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
export const formatDate = (dateStr?: string): string => {
|
||||
if (!dateStr) return '';
|
||||
return new Date(dateStr).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化涨跌幅
|
||||
*/
|
||||
export const formatChange = (value: number | null | undefined): string => {
|
||||
if (value === null || value === undefined) return '-';
|
||||
const prefix = value > 0 ? '+' : '';
|
||||
return `${prefix}${value.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取涨跌幅颜色
|
||||
*/
|
||||
export const getChangeColor = (value: number | null | undefined): string => {
|
||||
if (value === null || value === undefined) return '#9CA3AF';
|
||||
return value > 0 ? '#EF4444' : '#10B981';
|
||||
};
|
||||
|
||||
/**
|
||||
* 提取关键词显示文本
|
||||
*/
|
||||
export const getKeywordText = (keyword: string | { concept?: string; name?: string }): string => {
|
||||
if (typeof keyword === 'string') return keyword;
|
||||
return keyword?.concept || keyword?.name || '未知';
|
||||
};
|
||||
@@ -4,7 +4,7 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { logger } from '@utils/logger';
|
||||
import axios from '@utils/axiosConfig';
|
||||
import NewsEventsTab from '../../CompanyOverview/NewsEventsTab';
|
||||
import NewsEventsTab from '../NewsEventsTab';
|
||||
|
||||
const NewsPanel = ({ stockCode }) => {
|
||||
const [newsEvents, setNewsEvents] = useState([]);
|
||||
|
||||
Reference in New Issue
Block a user