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([]);