diff --git a/src/components/Calendar/FullCalendarPro.tsx b/src/components/Calendar/FullCalendarPro.tsx index 10bf5264..80208c45 100644 --- a/src/components/Calendar/FullCalendarPro.tsx +++ b/src/components/Calendar/FullCalendarPro.tsx @@ -49,6 +49,7 @@ export interface FullCalendarProProps { start: Date; end: Date; dates: string[]; + clickedDate: string; }) => void; /** 月份变化回调 */ onMonthChange?: (year: number, month: number) => void; @@ -372,16 +373,33 @@ export const FullCalendarPro: React.FC = ({ [onDateClick] ); - // 处理事件点击 + // 处理事件点击(计算点击的具体日期) const handleEventClick = useCallback( (arg: EventClickArg) => { const { extendedProps } = arg.event; - if (arg.event.start && arg.event.end) { + const dates = extendedProps.dates as string[]; + + if (arg.event.start && dates?.length > 0) { + // 计算点击的是哪一天 + let clickedDate = dates[0]; // 默认第一天 + + if (dates.length > 1) { + const rect = arg.el.getBoundingClientRect(); + const clickX = arg.jsEvent.clientX - rect.left; + const dayWidth = rect.width / dates.length; + const dayIndex = Math.min( + Math.floor(clickX / dayWidth), + dates.length - 1 + ); + clickedDate = dates[dayIndex]; + } + onEventClick?.({ title: arg.event.title, start: arg.event.start, end: arg.event.end, - dates: extendedProps.dates as string[], + dates: dates, + clickedDate: clickedDate, }); } }, diff --git a/src/store/slices/communityDataSlice.js b/src/store/slices/communityDataSlice.js index 1c44dc12..c745d681 100644 --- a/src/store/slices/communityDataSlice.js +++ b/src/store/slices/communityDataSlice.js @@ -508,6 +508,35 @@ const communityDataSlice = createSlice({ const paginationKey = mode === 'four-row' ? 'fourRowPagination' : 'verticalPagination'; state[paginationKey].current_page = page; logger.debug('CommunityData', '同步更新分页页码(缓存场景)', { mode, page }); + }, + + /** + * 乐观更新事件投票计数(同步) + * @param {Object} action.payload - { eventId, voteType, delta } + * - eventId: 事件ID + * - voteType: 'bullish' | 'bearish' + * - delta: 增量(+1 或 -1) + */ + updateEventVoteOptimistic: (state, action) => { + const { eventId, voteType, delta = 1 } = action.payload; + const countKey = voteType === 'bullish' ? 'bullish_count' : 'bearish_count'; + + // 更新纵向模式的事件数据 + Object.keys(state.verticalEventsByPage || {}).forEach(page => { + const events = state.verticalEventsByPage[page]; + const eventIndex = events.findIndex(e => e.id === eventId); + if (eventIndex !== -1) { + events[eventIndex][countKey] = (events[eventIndex][countKey] || 0) + delta; + } + }); + + // 更新平铺模式的事件数据 + const fourRowIndex = (state.fourRowEvents || []).findIndex(e => e.id === eventId); + if (fourRowIndex !== -1) { + state.fourRowEvents[fourRowIndex][countKey] = (state.fourRowEvents[fourRowIndex][countKey] || 0) + delta; + } + + logger.debug('CommunityData', '乐观更新事件投票', { eventId, voteType, delta }); } }, @@ -714,7 +743,7 @@ const communityDataSlice = createSlice({ // ==================== 导出 ==================== -export const { clearCache, clearSpecificCache, preloadData, setEventFollowStatus, updatePaginationPage } = communityDataSlice.actions; +export const { clearCache, clearSpecificCache, preloadData, setEventFollowStatus, updatePaginationPage, updateEventVoteOptimistic } = communityDataSlice.actions; // ==================== 基础选择器(Selectors)==================== export const selectPopularKeywords = (state) => state.communityData.popularKeywords; diff --git a/src/views/Community/components/DynamicNews/DynamicNewsCard.js b/src/views/Community/components/DynamicNews/DynamicNewsCard.js index f39b22ae..794860ed 100644 --- a/src/views/Community/components/DynamicNews/DynamicNewsCard.js +++ b/src/views/Community/components/DynamicNews/DynamicNewsCard.js @@ -35,7 +35,8 @@ import { toggleEventFollow, selectEventFollowStatus, selectVerticalEventsWithLoading, - selectFourRowEventsWithLoading + selectFourRowEventsWithLoading, + updateEventVoteOptimistic } from '@store/slices/communityDataSlice'; import { usePagination } from './hooks/usePagination'; import { PAGINATION_CONFIG, DISPLAY_MODES, REFRESH_DEBOUNCE_DELAY } from './constants'; @@ -223,7 +224,7 @@ const [currentMode, setCurrentMode] = useState('vertical'); setCurrentMode(mode); }, [mode]); - // 看涨看跌投票处理 + // 看涨看跌投票处理(乐观更新) const handleVoteChange = useCallback(async ({ eventId, voteType }) => { if (!isLoggedIn) { toast({ @@ -236,6 +237,10 @@ const [currentMode, setCurrentMode] = useState('vertical'); return; } + // 🚀 乐观更新:立即 +1,无需等待 API 响应 + dispatch(updateEventVoteOptimistic({ eventId, voteType, delta: 1 })); + + // 后台默默发送 API 请求 try { const response = await fetch(`${getApiBase()}/api/events/${eventId}/sentiment-vote`, { method: 'POST', @@ -250,17 +255,12 @@ const [currentMode, setCurrentMode] = useState('vertical'); throw new Error(data.error || '投票失败'); } - toast({ - title: voteType === 'bullish' ? '已看涨' : '已看跌', - status: 'success', - duration: 1500, - isClosable: true, - }); - - // 刷新当前页数据以更新投票计数 - handlePageChange(currentPage, true); + // 成功:静默完成,不显示 toast(避免干扰用户) + console.log('[投票] 成功', { eventId, voteType }); } catch (error) { console.error('投票失败:', error); + // ❌ 失败时回滚:-1 撤销乐观更新 + dispatch(updateEventVoteOptimistic({ eventId, voteType, delta: -1 })); toast({ title: '投票失败', description: error.message, @@ -269,7 +269,7 @@ const [currentMode, setCurrentMode] = useState('vertical'); isClosable: true, }); } - }, [isLoggedIn, toast, handlePageChange, currentPage]); + }, [isLoggedIn, toast, dispatch]); /** * ⚡【核心逻辑】执行刷新的回调函数(包含原有的智能刷新逻辑) diff --git a/src/views/Community/components/HeroPanel/components/CombinedCalendar.js b/src/views/Community/components/HeroPanel/components/CombinedCalendar.js index e9409d9d..a1a218a6 100644 --- a/src/views/Community/components/HeroPanel/components/CombinedCalendar.js +++ b/src/views/Community/components/HeroPanel/components/CombinedCalendar.js @@ -14,7 +14,6 @@ import dayjs from "dayjs"; import { GLASS_BLUR } from "@/constants/glassConfig"; import { eventService } from "@services/eventService"; import { getApiBase } from "@utils/apiConfig"; -import { getConceptHtmlUrl } from "@utils/textUtils"; import { textColors } from "../constants"; import { formatDateStr } from "../utils"; @@ -146,15 +145,21 @@ const CombinedCalendar = memo(({ DetailModal }) => { setCurrentMonth(new Date(year, month - 1, 1)); }, []); - // 处理概念条点击 - 打开概念详情页 - const handleEventClick = useCallback((event) => { - // event.title 格式: "概念名 (N天)" 或 "概念名" - const conceptName = event.title.replace(/\s*\(\d+天\)$/, ""); - const url = getConceptHtmlUrl(conceptName); - if (url) { - window.open(url, "_blank"); - } - }, []); + // 处理概念条点击 - 弹出当天弹窗 + const handleEventClick = useCallback( + (event) => { + // 使用 clickedDate(YYYYMMDD 格式)构造 Date 对象 + const dateStr = event.clickedDate || event.dates?.[0]; + if (dateStr) { + const year = parseInt(dateStr.slice(0, 4), 10); + const month = parseInt(dateStr.slice(4, 6), 10) - 1; + const day = parseInt(dateStr.slice(6, 8), 10); + const date = new Date(year, month, day); + handleDateClick(date); + } + }, + [handleDateClick] + ); return ( <> diff --git a/src/views/Community/components/SearchFilters/CompactSearchBox.js b/src/views/Community/components/SearchFilters/CompactSearchBox.js index 65590524..f336605f 100644 --- a/src/views/Community/components/SearchFilters/CompactSearchBox.js +++ b/src/views/Community/components/SearchFilters/CompactSearchBox.js @@ -388,18 +388,19 @@ const CompactSearchBox = ({ return (
- {/* 第一行:搜索框 + 日期筛选 */} + {/* 单行布局:搜索框 + 时间筛选 + 行业/等级/排序/重置 */} + {/* 搜索框 */} e.key === "Enter" && handleMainSearch()} - style={{ flex: 1, minWidth: isMobile ? 100 : 200 }} + style={{ flex: 1, minWidth: isMobile ? 80 : 120 }} className="gold-placeholder" allowClear={{ clearIcon: ( @@ -433,13 +434,14 @@ const CompactSearchBox = ({ {!isMobile && ( )} -
+ {/* 时间筛选 */} +
- - {/* 第二行:筛选条件 */} - {mode !== "mainline" && ( - - + {/* 筛选控件 */} + {mode !== "mainline" && ( + <> + {!isMobile && ( + + )} + + {/* 行业筛选 */} + {/* 事件等级 */} @@ -506,13 +515,12 @@ const CompactSearchBox = ({ ))} - - + {/* 排序 */} {SORT_OPTIONS.map((opt) => ( @@ -525,17 +533,19 @@ const CompactSearchBox = ({ ))} - - - - )} + {/* 重置 */} + +
); };