update pay function

This commit is contained in:
2025-11-23 16:34:45 +08:00
parent 23a94d5ab2
commit a778f94b68

View File

@@ -17,8 +17,8 @@ import {
import { import {
FaBuilding, FaMapMarkerAlt, FaChartLine, FaLightbulb, FaRocket, FaBuilding, FaMapMarkerAlt, FaChartLine, FaLightbulb, FaRocket,
FaNetworkWired, FaChevronDown, FaChevronUp, FaCog, FaTrophy, FaNetworkWired, FaChevronDown, FaChevronUp, FaChevronLeft, FaChevronRight,
FaShieldAlt, FaBrain, FaChartPie, FaHistory, FaCheckCircle, FaCog, FaTrophy, FaShieldAlt, FaBrain, FaChartPie, FaHistory, FaCheckCircle,
FaExclamationCircle, FaArrowUp, FaArrowDown, FaLink, FaStar, FaExclamationCircle, FaArrowUp, FaArrowDown, FaLink, FaStar,
FaUserTie, FaIndustry, FaDollarSign, FaBalanceScale, FaChartBar, FaUserTie, FaIndustry, FaDollarSign, FaBalanceScale, FaChartBar,
FaEye, FaFlask, FaHandshake, FaUsers, FaClock, FaCalendarAlt, FaEye, FaFlask, FaHandshake, FaUsers, FaClock, FaCalendarAlt,
@@ -26,7 +26,7 @@ import {
FaUniversity, FaGraduationCap, FaVenusMars, FaPassport, FaFileAlt, FaUniversity, FaGraduationCap, FaVenusMars, FaPassport, FaFileAlt,
FaNewspaper, FaBullhorn, FaUserShield, FaShareAlt, FaSitemap, FaNewspaper, FaBullhorn, FaUserShield, FaShareAlt, FaSitemap,
FaSearch, FaDownload, FaExternalLinkAlt, FaInfoCircle, FaCrown, FaSearch, FaDownload, FaExternalLinkAlt, FaInfoCircle, FaCrown,
FaCertificate, FaAward, FaExpandAlt, FaCompressAlt FaCertificate, FaAward, FaExpandAlt, FaCompressAlt, FaGavel, FaFire
} from 'react-icons/fa'; } from 'react-icons/fa';
import { import {
@@ -797,6 +797,14 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
const [newsEvents, setNewsEvents] = useState([]); const [newsEvents, setNewsEvents] = useState([]);
const [newsLoading, setNewsLoading] = useState(false); const [newsLoading, setNewsLoading] = useState(false);
const [newsSearchQuery, setNewsSearchQuery] = useState(''); const [newsSearchQuery, setNewsSearchQuery] = useState('');
const [newsPagination, setNewsPagination] = useState({
page: 1,
per_page: 10,
total: 0,
pages: 0,
has_next: false,
has_prev: false
});
const [error, setError] = useState(null); const [error, setError] = useState(null);
@@ -886,38 +894,53 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
}, [stockCode]); }, [stockCode]);
// 加载新闻事件 // 加载新闻事件
const loadNewsEvents = async (searchQuery = '') => { const loadNewsEvents = async (page = 1, searchQuery = '') => {
setNewsLoading(true); setNewsLoading(true);
try { try {
// 构建查询参数 // 构建查询参数
const params = new URLSearchParams({ const params = new URLSearchParams({
per_page: '20', page: page.toString(),
per_page: '10',
sort: 'new', sort: 'new',
include_creator: 'false', include_creator: 'true',
include_stats: 'true' include_stats: 'true'
}); });
// 如果有搜索关键词,添加搜索参数 // 搜索关键词优先级:
if (searchQuery) { // 1. 用户输入的搜索关键词
params.append('q', searchQuery); // 2. 股票简称
params.append('search_fields', 'title,description,content'); const queryText = searchQuery || basicInfo?.SECNAME || '';
} if (queryText) {
params.append('q', queryText);
// 如果有股票代码添加相关筛选这里假设事件表有stock_code字段
// 如果没有直接的stock_code字段可以通过tags或keywords搜索
if (basicInfo?.SECNAME) {
params.append('keywords', basicInfo.SECNAME);
} }
const response = await fetch(`${API_BASE_URL}/api/events?${params.toString()}`); const response = await fetch(`${API_BASE_URL}/api/events?${params.toString()}`);
const data = await response.json(); const data = await response.json();
// API可能返回 data.data.events 或 data.events // API返回 data.data.events
const events = data.data?.events || data.events || []; const events = data.data?.events || data.events || [];
const pagination = data.data?.pagination || {
page: 1,
per_page: 10,
total: 0,
pages: 0,
has_next: false,
has_prev: false
};
setNewsEvents(events); setNewsEvents(events);
setNewsPagination(pagination);
} catch (err) { } catch (err) {
logger.error('CompanyOverview', 'loadNewsEvents', err, { stockCode, searchQuery }); logger.error('CompanyOverview', 'loadNewsEvents', err, { stockCode, searchQuery, page });
setNewsEvents([]); setNewsEvents([]);
setNewsPagination({
page: 1,
per_page: 10,
total: 0,
pages: 0,
has_next: false,
has_prev: false
});
} finally { } finally {
setNewsLoading(false); setNewsLoading(false);
} }
@@ -926,13 +949,20 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
// 当基本信息加载完成后,加载新闻事件 // 当基本信息加载完成后,加载新闻事件
useEffect(() => { useEffect(() => {
if (basicInfo) { if (basicInfo) {
loadNewsEvents(); loadNewsEvents(1);
} }
}, [basicInfo]); }, [basicInfo]);
// 处理搜索 // 处理搜索
const handleNewsSearch = () => { const handleNewsSearch = () => {
loadNewsEvents(newsSearchQuery); loadNewsEvents(1, newsSearchQuery);
};
// 处理分页
const handleNewsPageChange = (newPage) => {
loadNewsEvents(newPage, newsSearchQuery);
// 滚动到新闻列表顶部
document.getElementById('news-list-top')?.scrollIntoView({ behavior: 'smooth' });
}; };
// 管理层职位分类 // 管理层职位分类
@@ -2183,172 +2213,319 @@ const CompanyAnalysisComplete = ({ stockCode: propStockCode }) => {
{/* 新闻动态标签页 */} {/* 新闻动态标签页 */}
<TabPanel p={0} pt={6}> <TabPanel p={0} pt={6}>
<Card bg={cardBg} shadow="md"> <VStack spacing={4} align="stretch">
<CardBody> <Card bg={cardBg} shadow="md">
<VStack spacing={4} align="stretch"> <CardBody>
{/* 搜索框 */} <VStack spacing={4} align="stretch">
<HStack> {/* 搜索框和统计信息 */}
<InputGroup> <HStack justify="space-between" flexWrap="wrap">
<InputLeftElement pointerEvents="none"> <HStack flex={1} minW="300px">
<SearchIcon color="gray.400" /> <InputGroup>
</InputLeftElement> <InputLeftElement pointerEvents="none">
<Input <SearchIcon color="gray.400" />
placeholder="搜索相关新闻..." </InputLeftElement>
value={newsSearchQuery} <Input
onChange={(e) => setNewsSearchQuery(e.target.value)} placeholder="搜索相关新闻..."
onKeyPress={(e) => e.key === 'Enter' && handleNewsSearch()} value={newsSearchQuery}
/> onChange={(e) => setNewsSearchQuery(e.target.value)}
</InputGroup> onKeyPress={(e) => e.key === 'Enter' && handleNewsSearch()}
<Button />
colorScheme="blue" </InputGroup>
onClick={handleNewsSearch} <Button
isLoading={newsLoading} colorScheme="blue"
> onClick={handleNewsSearch}
搜索 isLoading={newsLoading}
</Button> minW="80px"
</HStack> >
搜索
</Button>
</HStack>
{/* 新闻列表 */} {newsPagination.total > 0 && (
{newsLoading ? ( <HStack spacing={2}>
<Center h="300px"> <Icon as={FaNewspaper} color="blue.500" />
<VStack spacing={3}> <Text fontSize="sm" color="gray.600">
<Spinner size="xl" color="blue.500" thickness="4px" /> 共找到 <Text as="span" fontWeight="bold" color="blue.600">{newsPagination.total}</Text>
<Text>正在加载新闻...</Text> </Text>
</VStack> </HStack>
</Center> )}
) : newsEvents.length > 0 ? ( </HStack>
<VStack spacing={3} align="stretch">
{newsEvents.map((event, idx) => {
const importanceColor = {
'S': 'red',
'A': 'orange',
'B': 'yellow',
'C': 'green'
}[event.importance] || 'gray';
return ( <div id="news-list-top" />
<Card
key={idx} {/* 新闻列表 */}
variant="outline" {newsLoading ? (
size="sm" <Center h="400px">
cursor="pointer" <VStack spacing={3}>
_hover={{ bg: 'gray.50', shadow: 'md' }} <Spinner size="xl" color="blue.500" thickness="4px" />
transition="all 0.2s" <Text color="gray.600">正在加载新闻...</Text>
> </VStack>
<CardBody p={4}> </Center>
<VStack align="stretch" spacing={3}> ) : newsEvents.length > 0 ? (
<HStack justify="space-between" align="start"> <>
<VStack align="start" spacing={1} flex={1}> <VStack spacing={3} align="stretch">
<HStack> {newsEvents.map((event, idx) => {
<Icon as={FaNewspaper} color="blue.500" boxSize={4} /> const importanceColor = {
<Text fontWeight="bold" fontSize="md" noOfLines={2}> 'S': 'red',
{event.title} 'A': 'orange',
</Text> 'B': 'yellow',
</HStack> 'C': 'green'
<HStack spacing={2} flexWrap="wrap"> }[event.importance] || 'gray';
{event.importance && (
<Badge colorScheme={importanceColor} size="sm"> const eventTypeIcon = {
重要度: {event.importance} '企业公告': FaBullhorn,
</Badge> '政策': FaGavel,
)} '技术突破': FaFlask,
{event.event_type && ( '企业融资': FaDollarSign,
<Badge colorScheme="blue" size="sm"> '政策监管': FaShieldAlt,
{event.event_type} '政策动态': FaFileAlt,
</Badge> '行业事件': FaIndustry
)} }[event.event_type] || FaNewspaper;
{event.tags && event.tags.length > 0 && (
<> return (
{event.tags.slice(0, 3).map((tag, tidx) => ( <Card
<Tag key={tidx} size="sm" variant="subtle"> key={event.id || idx}
{tag} variant="outline"
</Tag> _hover={{ bg: useColorModeValue('gray.50', 'gray.700'), shadow: 'md', borderColor: 'blue.300' }}
))} transition="all 0.2s"
</> >
)} <CardBody p={4}>
</HStack> <VStack align="stretch" spacing={3}>
</VStack> {/* 标题栏 */}
<VStack align="end" spacing={1}> <HStack justify="space-between" align="start">
<Text fontSize="xs" color="gray.500"> <VStack align="start" spacing={2} flex={1}>
{event.created_at ? new Date(event.created_at).toLocaleDateString('zh-CN') : ''} <HStack>
</Text> <Icon as={eventTypeIcon} color="blue.500" boxSize={5} />
{event.view_count && ( <Text fontWeight="bold" fontSize="lg" lineHeight="1.3">
<HStack spacing={1}> {event.title}
<Icon as={FaEye} boxSize={3} color="gray.400" /> </Text>
</HStack>
{/* 标签栏 */}
<HStack spacing={2} flexWrap="wrap">
{event.importance && (
<Badge colorScheme={importanceColor} variant="solid" px={2}>
{event.importance}
</Badge>
)}
{event.event_type && (
<Badge colorScheme="blue" variant="outline">
{event.event_type}
</Badge>
)}
{event.invest_score && (
<Badge colorScheme="purple" variant="subtle">
投资分: {event.invest_score}
</Badge>
)}
{event.keywords && event.keywords.length > 0 && (
<>
{event.keywords.slice(0, 4).map((keyword, kidx) => (
<Tag key={kidx} size="sm" colorScheme="cyan" variant="subtle">
{keyword}
</Tag>
))}
</>
)}
</HStack>
</VStack>
{/* 右侧信息栏 */}
<VStack align="end" spacing={1} minW="100px">
<Text fontSize="xs" color="gray.500"> <Text fontSize="xs" color="gray.500">
{event.view_count} {event.created_at ? new Date(event.created_at).toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}) : ''}
</Text> </Text>
</HStack> <HStack spacing={3}>
{event.view_count !== undefined && (
<HStack spacing={1}>
<Icon as={FaEye} boxSize={3} color="gray.400" />
<Text fontSize="xs" color="gray.500">{event.view_count}</Text>
</HStack>
)}
{event.hot_score !== undefined && (
<HStack spacing={1}>
<Icon as={FaFire} boxSize={3} color="orange.400" />
<Text fontSize="xs" color="gray.500">{event.hot_score.toFixed(1)}</Text>
</HStack>
)}
</HStack>
{event.creator && (
<Text fontSize="xs" color="gray.400">
@{event.creator.username}
</Text>
)}
</VStack>
</HStack>
{/* 描述 */}
{event.description && (
<Text fontSize="sm" color="gray.700" lineHeight="1.6">
{event.description}
</Text>
)}
{/* 收益率数据 */}
{(event.related_avg_chg !== null || event.related_max_chg !== null || event.related_week_chg !== null) && (
<Box pt={2} borderTop="1px" borderColor="gray.200">
<HStack spacing={6} flexWrap="wrap">
<HStack spacing={1}>
<Icon as={FaChartLine} boxSize={3} color="gray.500" />
<Text fontSize="xs" color="gray.500" fontWeight="medium">相关涨跌:</Text>
</HStack>
{event.related_avg_chg !== null && event.related_avg_chg !== undefined && (
<HStack spacing={1}>
<Text fontSize="xs" color="gray.500">平均</Text>
<Text
fontSize="sm"
fontWeight="bold"
color={event.related_avg_chg > 0 ? 'red.500' : 'green.500'}
>
{event.related_avg_chg > 0 ? '+' : ''}{event.related_avg_chg.toFixed(2)}%
</Text>
</HStack>
)}
{event.related_max_chg !== null && event.related_max_chg !== undefined && (
<HStack spacing={1}>
<Text fontSize="xs" color="gray.500">最大</Text>
<Text
fontSize="sm"
fontWeight="bold"
color={event.related_max_chg > 0 ? 'red.500' : 'green.500'}
>
{event.related_max_chg > 0 ? '+' : ''}{event.related_max_chg.toFixed(2)}%
</Text>
</HStack>
)}
{event.related_week_chg !== null && event.related_week_chg !== undefined && (
<HStack spacing={1}>
<Text fontSize="xs" color="gray.500"></Text>
<Text
fontSize="sm"
fontWeight="bold"
color={event.related_week_chg > 0 ? 'red.500' : 'green.500'}
>
{event.related_week_chg > 0 ? '+' : ''}{event.related_week_chg.toFixed(2)}%
</Text>
</HStack>
)}
</HStack>
</Box>
)} )}
</VStack> </VStack>
</HStack> </CardBody>
</Card>
);
})}
</VStack>
{event.description && ( {/* 分页控件 */}
<Text fontSize="sm" color="gray.600" noOfLines={2}> {newsPagination.pages > 1 && (
{event.description} <Box pt={4}>
</Text> <HStack justify="space-between" align="center" flexWrap="wrap">
)} {/* 分页信息 */}
<Text fontSize="sm" color="gray.600">
{newsPagination.page} / {newsPagination.pages}
</Text>
{(event.related_avg_chg !== null || event.related_max_chg !== null || event.related_week_chg !== null) && ( {/* 分页按钮 */}
<HStack spacing={4} pt={2} borderTop="1px" borderColor="gray.200"> <HStack spacing={2}>
<Text fontSize="xs" color="gray.500">相关涨跌:</Text> <Button
{event.related_avg_chg !== null && event.related_avg_chg !== undefined && ( size="sm"
<HStack spacing={1}> onClick={() => handleNewsPageChange(1)}
<Text fontSize="xs" color="gray.500">平均:</Text> isDisabled={!newsPagination.has_prev || newsLoading}
<Text leftIcon={<Icon as={FaChevronLeft} />}
fontSize="xs" >
fontWeight="bold" 首页
color={event.related_avg_chg > 0 ? 'red.500' : 'green.500'} </Button>
> <Button
{event.related_avg_chg > 0 ? '+' : ''}{event.related_avg_chg}% size="sm"
</Text> onClick={() => handleNewsPageChange(newsPagination.page - 1)}
</HStack> isDisabled={!newsPagination.has_prev || newsLoading}
)} >
{event.related_max_chg !== null && event.related_max_chg !== undefined && ( 上一页
<HStack spacing={1}> </Button>
<Text fontSize="xs" color="gray.500">最大:</Text>
<Text {/* 页码按钮 */}
fontSize="xs" {(() => {
fontWeight="bold" const currentPage = newsPagination.page;
color={event.related_max_chg > 0 ? 'red.500' : 'green.500'} const totalPages = newsPagination.pages;
> const pageButtons = [];
{event.related_max_chg > 0 ? '+' : ''}{event.related_max_chg}%
</Text> // 显示当前页及前后各2页
</HStack> let startPage = Math.max(1, currentPage - 2);
)} let endPage = Math.min(totalPages, currentPage + 2);
{event.related_week_chg !== null && event.related_week_chg !== undefined && (
<HStack spacing={1}> // 如果开始页大于1显示省略号
<Text fontSize="xs" color="gray.500">周涨幅:</Text> if (startPage > 1) {
<Text pageButtons.push(
fontSize="xs" <Text key="start-ellipsis" fontSize="sm" color="gray.400">...</Text>
fontWeight="bold" );
color={event.related_week_chg > 0 ? 'red.500' : 'green.500'} }
>
{event.related_week_chg > 0 ? '+' : ''}{event.related_week_chg}% for (let i = startPage; i <= endPage; i++) {
</Text> pageButtons.push(
</HStack> <Button
)} key={i}
</HStack> size="sm"
)} variant={i === currentPage ? 'solid' : 'outline'}
</VStack> colorScheme={i === currentPage ? 'blue' : 'gray'}
</CardBody> onClick={() => handleNewsPageChange(i)}
</Card> isDisabled={newsLoading}
); >
})} {i}
</VStack> </Button>
) : ( );
<Center h="300px"> }
<VStack spacing={3}>
<Icon as={FaNewspaper} boxSize={16} color="gray.300" /> // 如果结束页小于总页数,显示省略号
<Text color="gray.500">暂无相关新闻</Text> if (endPage < totalPages) {
<Text fontSize="sm" color="gray.400"> pageButtons.push(
{newsSearchQuery ? '尝试修改搜索关键词' : '该公司暂无新闻动态'} <Text key="end-ellipsis" fontSize="sm" color="gray.400">...</Text>
</Text> );
</VStack> }
</Center>
)} return pageButtons;
</VStack> })()}
</CardBody>
</Card> <Button
size="sm"
onClick={() => handleNewsPageChange(newsPagination.page + 1)}
isDisabled={!newsPagination.has_next || newsLoading}
>
下一页
</Button>
<Button
size="sm"
onClick={() => handleNewsPageChange(newsPagination.pages)}
isDisabled={!newsPagination.has_next || newsLoading}
rightIcon={<Icon as={FaChevronRight} />}
>
末页
</Button>
</HStack>
</HStack>
</Box>
)}
</>
) : (
<Center h="400px">
<VStack spacing={3}>
<Icon as={FaNewspaper} boxSize={16} color="gray.300" />
<Text color="gray.500" fontSize="lg" fontWeight="medium">暂无相关新闻</Text>
<Text fontSize="sm" color="gray.400">
{newsSearchQuery ? '尝试修改搜索关键词' : '该公司暂无新闻动态'}
</Text>
</VStack>
</Center>
)}
</VStack>
</CardBody>
</Card>
</VStack>
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
</Tabs> </Tabs>