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:
zdl
2025-12-19 15:03:36 +08:00
parent b0e5184b36
commit 2cb236e5ba
12 changed files with 966 additions and 664 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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';

View File

@@ -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,
};

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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 || '未知';
};