update pay promo
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// src/components/Navbars/SearchBar/SearchBar.js
|
||||
// 全局股票搜索栏 - 模糊搜索 + 下拉选择
|
||||
// 设计风格:玻璃态融合 + 微妙金色点缀
|
||||
|
||||
import React, { useRef, useEffect, useCallback } from "react";
|
||||
import React, { useRef, useEffect, useCallback, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
@@ -27,16 +28,14 @@ export function SearchBar(props) {
|
||||
const { variant, children, ...rest } = props;
|
||||
const navigate = useNavigate();
|
||||
const containerRef = useRef(null);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// 颜色配置 - 固定使用深色主题(增强可见性)
|
||||
const searchIconColor = "#D4AF37";
|
||||
const inputBg = "rgba(26, 32, 44, 0.9)";
|
||||
const dropdownBg = "#1a1a2e";
|
||||
const borderColor = "rgba(212, 175, 55, 0.5)";
|
||||
// 颜色配置 - 玻璃态风格,低调但醒目
|
||||
const accentColor = "#D4AF37"; // 金色强调
|
||||
const dropdownBg = "rgba(20, 25, 35, 0.98)";
|
||||
const hoverBg = "whiteAlpha.100";
|
||||
const textColor = "white";
|
||||
const subTextColor = "whiteAlpha.600";
|
||||
const accentColor = "#D4AF37";
|
||||
|
||||
// 使用搜索 Hook
|
||||
const {
|
||||
@@ -54,6 +53,7 @@ export function SearchBar(props) {
|
||||
const handleClickOutside = (event) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
||||
setShowResults(false);
|
||||
setIsFocused(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
@@ -63,6 +63,7 @@ export function SearchBar(props) {
|
||||
// 选择股票 - 跳转到详情页
|
||||
const handleSelectStock = useCallback((stock) => {
|
||||
clearSearch();
|
||||
setIsFocused(false);
|
||||
// 跳转到股票详情页
|
||||
navigate(`/company/${stock.stock_code}`);
|
||||
}, [navigate, clearSearch]);
|
||||
@@ -73,95 +74,148 @@ export function SearchBar(props) {
|
||||
handleSelectStock(searchResults[0]);
|
||||
} else if (e.key === "Escape") {
|
||||
setShowResults(false);
|
||||
setIsFocused(false);
|
||||
}
|
||||
}, [searchResults, handleSelectStock, setShowResults]);
|
||||
|
||||
// 处理焦点
|
||||
const handleFocus = () => {
|
||||
setIsFocused(true);
|
||||
if (searchQuery && searchResults.length > 0) {
|
||||
setShowResults(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
// 延迟关闭,让点击事件先执行
|
||||
setTimeout(() => {
|
||||
if (!containerRef.current?.contains(document.activeElement)) {
|
||||
setIsFocused(false);
|
||||
}
|
||||
}, 150);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box ref={containerRef} position="relative" {...rest}>
|
||||
<InputGroup borderRadius="8px" w={{ base: "180px", md: "220px", lg: "280px" }}>
|
||||
<InputLeftElement pointerEvents="none">
|
||||
<Search color={searchIconColor} size={16} />
|
||||
{/* 搜索框容器 - 带微妙动画 */}
|
||||
<Box
|
||||
position="relative"
|
||||
borderRadius="full"
|
||||
bg={isFocused ? "rgba(255, 255, 255, 0.12)" : "rgba(255, 255, 255, 0.06)"}
|
||||
backdropFilter="blur(10px)"
|
||||
border="1px solid"
|
||||
borderColor={isFocused ? "rgba(212, 175, 55, 0.4)" : "rgba(255, 255, 255, 0.1)"}
|
||||
boxShadow={isFocused ? "0 0 20px rgba(212, 175, 55, 0.15)" : "none"}
|
||||
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||
_hover={{
|
||||
bg: "rgba(255, 255, 255, 0.1)",
|
||||
borderColor: "rgba(212, 175, 55, 0.3)",
|
||||
}}
|
||||
>
|
||||
<InputGroup w={{ base: "160px", md: "200px", lg: "240px" }}>
|
||||
<InputLeftElement pointerEvents="none" h="full">
|
||||
<Search
|
||||
size={15}
|
||||
color={isFocused ? accentColor : "rgba(255, 255, 255, 0.5)"}
|
||||
style={{ transition: "color 0.3s" }}
|
||||
/>
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
variant="search"
|
||||
variant="unstyled"
|
||||
fontSize="sm"
|
||||
bg={inputBg}
|
||||
color="white"
|
||||
placeholder="搜索股票代码/名称..."
|
||||
placeholder="搜索股票..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => searchQuery && searchResults.length > 0 && setShowResults(true)}
|
||||
border="1px solid"
|
||||
borderColor={borderColor}
|
||||
_placeholder={{ color: "gray.400" }}
|
||||
_hover={{ borderColor: accentColor, bg: "rgba(26, 32, 44, 1)" }}
|
||||
_focus={{
|
||||
borderColor: accentColor,
|
||||
boxShadow: `0 0 0 1px ${accentColor}`,
|
||||
bg: "rgba(26, 32, 44, 1)"
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
pl={10}
|
||||
pr={searchQuery ? 10 : 4}
|
||||
py={2}
|
||||
h="36px"
|
||||
_placeholder={{
|
||||
color: "rgba(255, 255, 255, 0.4)",
|
||||
fontSize: "sm",
|
||||
}}
|
||||
/>
|
||||
{(searchQuery || isSearching) && (
|
||||
<InputRightElement>
|
||||
<InputRightElement h="full">
|
||||
{isSearching ? (
|
||||
<Spinner size="sm" color={accentColor} />
|
||||
<Spinner size="xs" color={accentColor} />
|
||||
) : (
|
||||
<IconButton
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
icon={<X size={10} />}
|
||||
icon={<X size={12} color="rgba(255, 255, 255, 0.5)" />}
|
||||
onClick={clearSearch}
|
||||
aria-label="清除搜索"
|
||||
_hover={{ bg: "transparent" }}
|
||||
borderRadius="full"
|
||||
minW="auto"
|
||||
h="20px"
|
||||
w="20px"
|
||||
_hover={{ bg: "whiteAlpha.200" }}
|
||||
/>
|
||||
)}
|
||||
</InputRightElement>
|
||||
)}
|
||||
</InputGroup>
|
||||
</Box>
|
||||
|
||||
{/* 搜索结果下拉 */}
|
||||
{/* 搜索结果下拉 - 玻璃态设计 */}
|
||||
{showResults && (
|
||||
<Box
|
||||
position="absolute"
|
||||
top="100%"
|
||||
left={0}
|
||||
mt={2}
|
||||
top="calc(100% + 8px)"
|
||||
left="50%"
|
||||
transform="translateX(-50%)"
|
||||
w="320px"
|
||||
bg={dropdownBg}
|
||||
backdropFilter="blur(20px)"
|
||||
border="1px solid"
|
||||
borderColor={borderColor}
|
||||
borderRadius="md"
|
||||
boxShadow="lg"
|
||||
borderColor="rgba(212, 175, 55, 0.2)"
|
||||
borderRadius="xl"
|
||||
boxShadow="0 10px 40px rgba(0, 0, 0, 0.4), 0 0 20px rgba(212, 175, 55, 0.1)"
|
||||
maxH="400px"
|
||||
overflowY="auto"
|
||||
zIndex={9999}
|
||||
css={{
|
||||
'&::-webkit-scrollbar': { width: '4px' },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: 'rgba(212, 175, 55, 0.3)',
|
||||
borderRadius: '2px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{searchResults.length > 0 ? (
|
||||
<List spacing={0}>
|
||||
<List spacing={0} py={2}>
|
||||
{searchResults.map((stock, index) => (
|
||||
<ListItem
|
||||
key={stock.stock_code}
|
||||
px={4}
|
||||
py={3}
|
||||
py={2.5}
|
||||
mx={2}
|
||||
cursor="pointer"
|
||||
_hover={{ bg: hoverBg }}
|
||||
borderRadius="lg"
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
bg: "rgba(212, 175, 55, 0.1)",
|
||||
}}
|
||||
onClick={() => handleSelectStock(stock)}
|
||||
borderBottomWidth={index < searchResults.length - 1 ? "1px" : "0"}
|
||||
borderColor={borderColor}
|
||||
>
|
||||
<Flex align="center" justify="space-between">
|
||||
<VStack align="start" spacing={0} flex={1}>
|
||||
<Text fontWeight="bold" color={textColor} fontSize="sm">
|
||||
<Text fontWeight="semibold" color={textColor} fontSize="sm">
|
||||
{stock.stock_name}
|
||||
</Text>
|
||||
<HStack spacing={2}>
|
||||
<Text fontSize="xs" color={subTextColor}>
|
||||
<Text fontSize="xs" color={accentColor} fontFamily="mono">
|
||||
{stock.stock_code}
|
||||
</Text>
|
||||
{stock.pinyin_abbr && (
|
||||
<Text fontSize="xs" color={subTextColor}>
|
||||
({stock.pinyin_abbr.toUpperCase()})
|
||||
{stock.pinyin_abbr.toUpperCase()}
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
@@ -169,9 +223,11 @@ export function SearchBar(props) {
|
||||
{stock.exchange && (
|
||||
<Tag
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
variant="subtle"
|
||||
bg="rgba(212, 175, 55, 0.15)"
|
||||
color={accentColor}
|
||||
fontSize="xs"
|
||||
fontWeight="medium"
|
||||
borderRadius="md"
|
||||
>
|
||||
{stock.exchange}
|
||||
</Tag>
|
||||
@@ -181,10 +237,13 @@ export function SearchBar(props) {
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<Center p={4}>
|
||||
<Center p={6}>
|
||||
<VStack spacing={2}>
|
||||
<Search size={24} color="rgba(255, 255, 255, 0.2)" />
|
||||
<Text color={subTextColor} fontSize="sm">
|
||||
{searchQuery ? "未找到相关股票" : "输入股票代码或名称搜索"}
|
||||
{searchQuery ? "未找到相关股票" : "输入代码或名称搜索"}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -223,53 +223,12 @@ const [currentMode, setCurrentMode] = useState('vertical');
|
||||
setCurrentMode(mode);
|
||||
}, [mode]);
|
||||
|
||||
// 看涨看跌投票处理
|
||||
const handleVoteChange = useCallback(async ({ eventId, voteType }) => {
|
||||
if (!isLoggedIn) {
|
||||
toast({
|
||||
title: '请先登录',
|
||||
description: '登录后才能参与投票',
|
||||
status: 'warning',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getApiBase()}/api/events/${eventId}/sentiment-vote`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ vote_type: voteType }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || '投票失败');
|
||||
}
|
||||
|
||||
toast({
|
||||
title: voteType === 'bullish' ? '已看涨' : '已看跌',
|
||||
status: 'success',
|
||||
duration: 1500,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// 刷新当前页数据以更新投票计数
|
||||
handlePageChange(currentPage, true);
|
||||
} catch (error) {
|
||||
console.error('投票失败:', error);
|
||||
toast({
|
||||
title: '投票失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}, [isLoggedIn, toast, handlePageChange, currentPage]);
|
||||
// 看涨看跌投票回调(API 调用已在卡片组件内部处理,这里仅用于可选的列表刷新)
|
||||
const handleVoteChange = useCallback(({ eventId, voteType }) => {
|
||||
console.log('[DynamicNewsCard] 投票完成回调', { eventId, voteType });
|
||||
// 投票已在 HorizontalDynamicNewsEventCard 组件内部处理
|
||||
// 这里可以选择是否刷新整个列表(暂不刷新,避免干扰用户体验)
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* ⚡【核心逻辑】执行刷新的回调函数(包含原有的智能刷新逻辑)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// src/views/Community/components/EventCard/HorizontalDynamicNewsEventCard.js
|
||||
// 横向布局的动态新闻事件卡片组件(时间在左,卡片在右)
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
HStack,
|
||||
Card,
|
||||
@@ -12,10 +12,13 @@ import {
|
||||
Tooltip,
|
||||
Badge,
|
||||
useBreakpointValue,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { getImportanceConfig } from '@constants/importanceLevels';
|
||||
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
||||
import { useDevice } from '@hooks/useDevice';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
import { getApiBase } from '@utils/apiConfig';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 导入子组件
|
||||
@@ -71,6 +74,93 @@ const HorizontalDynamicNewsEventCard = React.memo(({
|
||||
}) => {
|
||||
const importance = getImportanceConfig(event.importance);
|
||||
const { isMobile } = useDevice();
|
||||
const { isAuthenticated: isLoggedIn } = useAuth();
|
||||
const toast = useToast();
|
||||
|
||||
// 本地状态管理投票数据(实时更新 UI)
|
||||
const [localBullish, setLocalBullish] = useState(event.bullish_count || 0);
|
||||
const [localBearish, setLocalBearish] = useState(event.bearish_count || 0);
|
||||
const [isVoting, setIsVoting] = useState(false);
|
||||
|
||||
// 同步 props 变化到本地状态
|
||||
useEffect(() => {
|
||||
setLocalBullish(event.bullish_count || 0);
|
||||
}, [event.bullish_count]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalBearish(event.bearish_count || 0);
|
||||
}, [event.bearish_count]);
|
||||
|
||||
// 投票处理函数(直接调用 API 并更新本地状态)
|
||||
const handleVote = useCallback(async (voteType) => {
|
||||
if (!isLoggedIn) {
|
||||
toast({
|
||||
title: '请先登录',
|
||||
description: '登录后才能参与投票',
|
||||
status: 'warning',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVoting) return;
|
||||
setIsVoting(true);
|
||||
|
||||
// 乐观更新
|
||||
const oldBullish = localBullish;
|
||||
const oldBearish = localBearish;
|
||||
if (voteType === 'bullish') {
|
||||
setLocalBullish(prev => prev + 1);
|
||||
} else {
|
||||
setLocalBearish(prev => prev + 1);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getApiBase()}/api/events/${event.id}/sentiment-vote`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ vote_type: voteType }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
throw new Error(data.error || '投票失败');
|
||||
}
|
||||
|
||||
// 用服务端返回的真实数据更新
|
||||
if (data.data) {
|
||||
setLocalBullish(data.data.bullish_count ?? localBullish);
|
||||
setLocalBearish(data.data.bearish_count ?? localBearish);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: voteType === 'bullish' ? '已看涨' : '已看跌',
|
||||
status: 'success',
|
||||
duration: 1500,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// 通知父组件(可选,用于刷新列表)
|
||||
onVoteChange?.({ eventId: event.id, voteType });
|
||||
} catch (error) {
|
||||
// 回滚
|
||||
setLocalBullish(oldBullish);
|
||||
setLocalBearish(oldBearish);
|
||||
|
||||
toast({
|
||||
title: '投票失败',
|
||||
description: error.message,
|
||||
status: 'error',
|
||||
duration: 2000,
|
||||
isClosable: true,
|
||||
});
|
||||
} finally {
|
||||
setIsVoting(false);
|
||||
}
|
||||
}, [event.id, isLoggedIn, isVoting, localBullish, localBearish, toast, onVoteChange]);
|
||||
|
||||
// 专业配色 - 黑色、灰色、金色主题
|
||||
const cardBg = PROFESSIONAL_COLORS.background.card;
|
||||
@@ -275,11 +365,13 @@ const HorizontalDynamicNewsEventCard = React.memo(({
|
||||
spacing={0}
|
||||
_hover={{ bg: '#9B2C2C' }}
|
||||
transition="all 0.2s"
|
||||
onClick={() => onVoteChange?.({ eventId: event.id, voteType: 'bullish' })}
|
||||
onClick={() => handleVote('bullish')}
|
||||
disabled={isVoting}
|
||||
opacity={isVoting ? 0.7 : 1}
|
||||
>
|
||||
<HStack spacing={1}>
|
||||
<TrendingUp size={16} />
|
||||
<Text fontSize="sm" fontWeight="semibold">{formatCompactNumber(event.bullish_count)}</Text>
|
||||
<Text fontSize="sm" fontWeight="semibold">{formatCompactNumber(localBullish)}</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs">看涨</Text>
|
||||
</VStack>
|
||||
@@ -296,11 +388,13 @@ const HorizontalDynamicNewsEventCard = React.memo(({
|
||||
spacing={0}
|
||||
_hover={{ bg: '#2F855A' }}
|
||||
transition="all 0.2s"
|
||||
onClick={() => onVoteChange?.({ eventId: event.id, voteType: 'bearish' })}
|
||||
onClick={() => handleVote('bearish')}
|
||||
disabled={isVoting}
|
||||
opacity={isVoting ? 0.7 : 1}
|
||||
>
|
||||
<HStack spacing={1}>
|
||||
<TrendingDown size={16} />
|
||||
<Text fontSize="sm" fontWeight="semibold">{formatCompactNumber(event.bearish_count)}</Text>
|
||||
<Text fontSize="sm" fontWeight="semibold">{formatCompactNumber(localBearish)}</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs">看跌</Text>
|
||||
</VStack>
|
||||
|
||||
Reference in New Issue
Block a user