From b57cd3019c92bce0c26e36328878f3563e1a60f9 Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Tue, 3 Feb 2026 15:32:52 +0800 Subject: [PATCH] update pay promo --- src/components/Navbars/SearchBar/SearchBar.js | 197 ++++++++++++------ .../components/DynamicNews/DynamicNewsCard.js | 53 +---- .../HorizontalDynamicNewsEventCard.js | 104 ++++++++- 3 files changed, 233 insertions(+), 121 deletions(-) diff --git a/src/components/Navbars/SearchBar/SearchBar.js b/src/components/Navbars/SearchBar/SearchBar.js index e0576769..a3f7c6de 100755 --- a/src/components/Navbars/SearchBar/SearchBar.js +++ b/src/components/Navbars/SearchBar/SearchBar.js @@ -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 ( - - - - - 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)" - }} - /> - {(searchQuery || isSearching) && ( - - {isSearching ? ( - - ) : ( - } - onClick={clearSearch} - aria-label="清除搜索" - _hover={{ bg: "transparent" }} - /> - )} - - )} - + {/* 搜索框容器 - 带微妙动画 */} + + + + + + handleSearch(e.target.value)} + onKeyDown={handleKeyDown} + 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) && ( + + {isSearching ? ( + + ) : ( + } + onClick={clearSearch} + aria-label="清除搜索" + borderRadius="full" + minW="auto" + h="20px" + w="20px" + _hover={{ bg: "whiteAlpha.200" }} + /> + )} + + )} + + - {/* 搜索结果下拉 */} + {/* 搜索结果下拉 - 玻璃态设计 */} {showResults && ( {searchResults.length > 0 ? ( - + {searchResults.map((stock, index) => ( handleSelectStock(stock)} - borderBottomWidth={index < searchResults.length - 1 ? "1px" : "0"} - borderColor={borderColor} > - + {stock.stock_name} - + {stock.stock_code} {stock.pinyin_abbr && ( - ({stock.pinyin_abbr.toUpperCase()}) + {stock.pinyin_abbr.toUpperCase()} )} @@ -169,9 +223,11 @@ export function SearchBar(props) { {stock.exchange && ( {stock.exchange} @@ -181,10 +237,13 @@ export function SearchBar(props) { ))} ) : ( -
- - {searchQuery ? "未找到相关股票" : "输入股票代码或名称搜索"} - +
+ + + + {searchQuery ? "未找到相关股票" : "输入代码或名称搜索"} + +
)} diff --git a/src/views/Community/components/DynamicNews/DynamicNewsCard.js b/src/views/Community/components/DynamicNews/DynamicNewsCard.js index bd2ec9f1..f83591f7 100644 --- a/src/views/Community/components/DynamicNews/DynamicNewsCard.js +++ b/src/views/Community/components/DynamicNews/DynamicNewsCard.js @@ -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 组件内部处理 + // 这里可以选择是否刷新整个列表(暂不刷新,避免干扰用户体验) + }, []); /** * ⚡【核心逻辑】执行刷新的回调函数(包含原有的智能刷新逻辑) diff --git a/src/views/Community/components/EventCard/HorizontalDynamicNewsEventCard.js b/src/views/Community/components/EventCard/HorizontalDynamicNewsEventCard.js index a4da031d..588e5260 100644 --- a/src/views/Community/components/EventCard/HorizontalDynamicNewsEventCard.js +++ b/src/views/Community/components/EventCard/HorizontalDynamicNewsEventCard.js @@ -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} > - {formatCompactNumber(event.bullish_count)} + {formatCompactNumber(localBullish)} 看涨 @@ -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} > - {formatCompactNumber(event.bearish_count)} + {formatCompactNumber(localBearish)} 看跌