diff --git a/src/views/Company/components/CompanyOverview/NewsEventsTab.js b/src/views/Company/components/CompanyOverview/NewsEventsTab.js deleted file mode 100644 index c4677efe..00000000 --- a/src/views/Company/components/CompanyOverview/NewsEventsTab.js +++ /dev/null @@ -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( - - ... - - ); - } - - for (let i = startPage; i <= endPage; i++) { - const isActive = i === currentPage; - pageButtons.push( - - ); - } - - // 如果结束页小于总页数,显示省略号 - if (endPage < totalPages) { - pageButtons.push( - - ... - - ); - } - - return pageButtons; - }; - - return ( - - - - - {/* 搜索框和统计信息 */} - - - - - - - - - - - - {newsPagination.total > 0 && ( - - - - 共找到{" "} - - {newsPagination.total} - {" "} - 条新闻 - - - )} - - -
- - {/* 新闻列表 */} - {newsLoading ? ( -
- - - 正在加载新闻... - -
- ) : newsEvents.length > 0 ? ( - <> - - {newsEvents.map((event, idx) => { - const importanceBadgeStyle = getImportanceBadgeStyle( - event.importance - ); - const eventTypeIcon = getEventTypeIcon(event.event_type); - - return ( - handleEventClick(event.id)} - _hover={{ - bg: theme.cardHoverBg, - shadow: "md", - borderColor: theme.cardHoverBorder, - }} - transition="all 0.2s" - > - - - {/* 标题栏 */} - - - - - - {event.title} - - - - {/* 标签栏 */} - - {event.importance && ( - - {event.importance}级 - - )} - {event.event_type && ( - - {event.event_type} - - )} - {event.invest_score && ( - - 投资分: {event.invest_score} - - )} - {event.keywords && event.keywords.length > 0 && ( - <> - {event.keywords - .slice(0, 4) - .map((keyword, kidx) => ( - - {typeof keyword === "string" - ? keyword - : keyword?.concept || - keyword?.name || - "未知"} - - ))} - - )} - - - - {/* 右侧信息栏 */} - - - {event.created_at - ? new Date( - event.created_at - ).toLocaleDateString("zh-CN", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }) - : ""} - - - {event.view_count !== undefined && ( - - - - {event.view_count} - - - )} - {event.hot_score !== undefined && ( - - - - {event.hot_score.toFixed(1)} - - - )} - - {event.creator && ( - - @{event.creator.username} - - )} - - - - {/* 描述 */} - {event.description && ( - - {event.description} - - )} - - {/* 收益率数据 */} - {(event.related_avg_chg !== null || - event.related_max_chg !== null || - event.related_week_chg !== null) && ( - - - - - - 相关涨跌: - - - {event.related_avg_chg !== null && - event.related_avg_chg !== undefined && ( - - - 平均 - - 0 - ? "#EF4444" - : "#10B981" - } - > - {event.related_avg_chg > 0 ? "+" : ""} - {event.related_avg_chg.toFixed(2)}% - - - )} - {event.related_max_chg !== null && - event.related_max_chg !== undefined && ( - - - 最大 - - 0 - ? "#EF4444" - : "#10B981" - } - > - {event.related_max_chg > 0 ? "+" : ""} - {event.related_max_chg.toFixed(2)}% - - - )} - {event.related_week_chg !== null && - event.related_week_chg !== undefined && ( - - - 周 - - 0 - ? "#EF4444" - : "#10B981" - } - > - {event.related_week_chg > 0 - ? "+" - : ""} - {event.related_week_chg.toFixed(2)}% - - - )} - - - )} - - - - ); - })} - - - {/* 分页控件 */} - {newsPagination.pages > 1 && ( - - - {/* 分页信息 */} - - 第 {newsPagination.page} / {newsPagination.pages} 页 - - - {/* 分页按钮 */} - - - - - {/* 页码按钮 */} - {renderPaginationButtons()} - - - - - - - )} - - ) : ( -
- - - - 暂无相关新闻 - - - {searchQuery ? "尝试修改搜索关键词" : "该公司暂无新闻动态"} - - -
- )} - - - - - ); -}; - -export default NewsEventsTab; diff --git a/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsEmptyState.tsx b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsEmptyState.tsx new file mode 100644 index 00000000..44f6a849 --- /dev/null +++ b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsEmptyState.tsx @@ -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 = ({ + searchQuery, + theme, + isBlackGold, +}) => { + return ( +
+ + + + 暂无相关新闻 + + + {searchQuery ? '尝试修改搜索关键词' : '该公司暂无新闻动态'} + + +
+ ); +}; + +export default memo(NewsEmptyState); diff --git a/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsEventCard.tsx b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsEventCard.tsx new file mode 100644 index 00000000..126508f4 --- /dev/null +++ b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsEventCard.tsx @@ -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 = ({ + 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 ( + onClick(event.id)} + _hover={{ + bg: theme.cardHoverBg, + shadow: 'md', + borderColor: theme.cardHoverBorder, + }} + transition="all 0.2s" + > + + + {/* 标题栏 */} + + + + + + {event.title} + + + + {/* 标签栏 */} + + {event.importance && ( + + {event.importance}级 + + )} + {event.event_type && ( + + {event.event_type} + + )} + {event.invest_score && ( + + 投资分: {event.invest_score} + + )} + {event.keywords?.slice(0, 4).map((keyword, kidx) => ( + + {getKeywordText(keyword)} + + ))} + + + + {/* 右侧信息栏 */} + + + {formatDate(event.created_at)} + + + {event.view_count !== undefined && ( + + + + {event.view_count} + + + )} + {event.hot_score !== undefined && ( + + + + {event.hot_score.toFixed(1)} + + + )} + + {event.creator && ( + + @{event.creator.username} + + )} + + + + {/* 描述 */} + {event.description && ( + + {event.description} + + )} + + {/* 收益率数据 */} + {hasRelatedChanges && ( + + + + + + 相关涨跌: + + + {event.related_avg_chg !== null && event.related_avg_chg !== undefined && ( + + 平均 + + {formatChange(event.related_avg_chg)} + + + )} + {event.related_max_chg !== null && event.related_max_chg !== undefined && ( + + 最大 + + {formatChange(event.related_max_chg)} + + + )} + {event.related_week_chg !== null && event.related_week_chg !== undefined && ( + + + + {formatChange(event.related_week_chg)} + + + )} + + + )} + + + + ); +}; + +export default memo(NewsEventCard); diff --git a/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsLoadingState.tsx b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsLoadingState.tsx new file mode 100644 index 00000000..7b1054bf --- /dev/null +++ b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsLoadingState.tsx @@ -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 = ({ theme }) => { + return ( +
+ + + 正在加载新闻... + +
+ ); +}; + +export default memo(NewsLoadingState); diff --git a/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsPagination.tsx b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsPagination.tsx new file mode 100644 index 00000000..27d26e81 --- /dev/null +++ b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsPagination.tsx @@ -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 = ({ + 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( + + ... + + ); + } + + for (let i = startPage; i <= endPage; i++) { + const isActive = i === currentPage; + buttons.push( + + ); + } + + // 如果结束页小于总页数,显示省略号 + if (endPage < totalPages) { + buttons.push( + + ... + + ); + } + + return buttons; + }, [currentPage, totalPages, theme, isBlackGold, isLoading, onPageChange]); + + if (totalPages <= 1) return null; + + return ( + + + {/* 分页信息 */} + + 第 {currentPage} / {totalPages} 页 + + + {/* 分页按钮 */} + + + + + {/* 页码按钮 */} + {pageButtons} + + + + + + + ); +}; + +export default memo(NewsPagination); diff --git a/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsSearchBar.tsx b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsSearchBar.tsx new file mode 100644 index 00000000..a915f5db --- /dev/null +++ b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/NewsSearchBar.tsx @@ -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 = ({ + searchQuery, + onSearchChange, + onSearch, + isLoading, + total, + theme, + isBlackGold, +}) => { + const handleInputChange = (e: React.ChangeEvent) => { + onSearchChange(e.target.value); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onSearch(); + } + }; + + return ( + + + + + + + + + + + + {total > 0 && ( + + + + 共找到{' '} + + {total} + {' '} + 条新闻 + + + )} + + ); +}; + +export default memo(NewsSearchBar); diff --git a/src/views/Company/components/DynamicTracking/NewsEventsTab/components/index.ts b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/index.ts new file mode 100644 index 00000000..3ab81b5a --- /dev/null +++ b/src/views/Company/components/DynamicTracking/NewsEventsTab/components/index.ts @@ -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'; diff --git a/src/views/Company/components/DynamicTracking/NewsEventsTab/constants.ts b/src/views/Company/components/DynamicTracking/NewsEventsTab/constants.ts new file mode 100644 index 00000000..2b1f8135 --- /dev/null +++ b/src/views/Company/components/DynamicTracking/NewsEventsTab/constants.ts @@ -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 = { + blackGold: BLACK_GOLD_THEME, + default: DEFAULT_THEME, +}; diff --git a/src/views/Company/components/DynamicTracking/NewsEventsTab/index.tsx b/src/views/Company/components/DynamicTracking/NewsEventsTab/index.tsx new file mode 100644 index 00000000..535deae5 --- /dev/null +++ b/src/views/Company/components/DynamicTracking/NewsEventsTab/index.tsx @@ -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 = ({ + 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 ( + + + + + {/* 搜索框和统计信息 */} + + +
+ + {/* 新闻列表 */} + {newsLoading ? ( + + ) : newsEvents.length > 0 ? ( + <> + + {newsEvents.map((event, idx) => ( + + ))} + + + {/* 分页控件 */} + + + ) : ( + + )} + + + + + ); +}; + +export default memo(NewsEventsTab); diff --git a/src/views/Company/components/DynamicTracking/NewsEventsTab/types.ts b/src/views/Company/components/DynamicTracking/NewsEventsTab/types.ts new file mode 100644 index 00000000..4fe69202 --- /dev/null +++ b/src/views/Company/components/DynamicTracking/NewsEventsTab/types.ts @@ -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; +} diff --git a/src/views/Company/components/DynamicTracking/NewsEventsTab/utils.ts b/src/views/Company/components/DynamicTracking/NewsEventsTab/utils.ts new file mode 100644 index 00000000..4f3c5098 --- /dev/null +++ b/src/views/Company/components/DynamicTracking/NewsEventsTab/utils.ts @@ -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 = { + 企业公告: 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 = { + 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 = { + 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 || '未知'; +}; diff --git a/src/views/Company/components/DynamicTracking/components/NewsPanel.js b/src/views/Company/components/DynamicTracking/components/NewsPanel.js index e01310cf..708df46b 100644 --- a/src/views/Company/components/DynamicTracking/components/NewsPanel.js +++ b/src/views/Company/components/DynamicTracking/components/NewsPanel.js @@ -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([]);