From ac421011ebe7dedf72d4a5e1e4fe7d6ac0c12cb5 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 4 Dec 2025 11:57:30 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E5=88=9A=E8=BF=9B=E9=A1=B5=E9=9D=A2=E5=90=91?= =?UTF-8?q?=E4=B8=8A=E6=BB=9A=E5=8A=A8=E4=BA=86=E4=B8=80=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/Community/index.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/views/Community/index.js b/src/views/Community/index.js index 3059c6d0..073ce724 100644 --- a/src/views/Community/index.js +++ b/src/views/Community/index.js @@ -107,28 +107,6 @@ const Community = () => { } }, [events, loading, pagination, filters]); - // ⚡ 首次进入页面时滚动到内容区域(考虑导航栏高度) - const hasScrolled = useRef(false); - useEffect(() => { - // 只在第一次挂载时执行滚动 - if (hasScrolled.current) return; - - // 延迟执行,确保DOM已完全渲染 - const timer = setTimeout(() => { - if (containerRef.current) { - hasScrolled.current = true; - // 滚动到容器顶部,自动考虑导航栏的高度 - containerRef.current.scrollIntoView({ - behavior: 'auto', - block: 'start', - inline: 'nearest' - }); - } - }, 100); - - return () => clearTimeout(timer); - }, []); // 空依赖数组,只在组件挂载时执行一次 - /** * ⚡ 【核心逻辑】注册 Socket 新事件回调 - 当收到新事件时智能刷新列表 * From f521b89c2796f5cf2749719a94ee8a0436a02792 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 4 Dec 2025 12:20:27 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E8=87=AA=E9=80=89=E8=82=A1=E6=B2=A1=E5=8F=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mocks/handlers/account.js | 29 +++++- .../DynamicNewsDetailPanel.js | 95 +++++++++++++------ .../DynamicNewsDetail/StockListItem.js | 2 +- 3 files changed, 94 insertions(+), 32 deletions(-) diff --git a/src/mocks/handlers/account.js b/src/mocks/handlers/account.js index bc60c6b0..2e4d46c8 100644 --- a/src/mocks/handlers/account.js +++ b/src/mocks/handlers/account.js @@ -188,6 +188,22 @@ export const accountHandlers = [ mockWatchlist.push(newItem); + // 同步添加到 mockRealtimeQuotes(导航栏自选股菜单使用此数组) + mockRealtimeQuotes.push({ + stock_code: stock_code, + stock_name: stock_name, + current_price: null, + change_percent: 0, + change: 0, + volume: 0, + turnover: 0, + high: 0, + low: 0, + open: 0, + prev_close: 0, + update_time: new Date().toTimeString().slice(0, 8) + }); + return HttpResponse.json({ success: true, message: '添加成功', @@ -210,9 +226,20 @@ export const accountHandlers = [ const { id } = params; console.log('[Mock] 删除自选股:', id); - const index = mockWatchlist.findIndex(item => item.id === parseInt(id)); + // 支持按 stock_code 或 id 匹配删除 + const index = mockWatchlist.findIndex(item => + item.stock_code === id || item.id === parseInt(id) + ); + if (index !== -1) { + const stockCode = mockWatchlist[index].stock_code; mockWatchlist.splice(index, 1); + + // 同步从 mockRealtimeQuotes 移除 + const quotesIndex = mockRealtimeQuotes.findIndex(item => item.stock_code === stockCode); + if (quotesIndex !== -1) { + mockRealtimeQuotes.splice(quotesIndex, 1); + } } return HttpResponse.json({ diff --git a/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js b/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js index d2deb64a..214d24ba 100644 --- a/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js +++ b/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js @@ -17,11 +17,11 @@ import { useColorModeValue, useToast, } from '@chakra-ui/react'; -import { getImportanceConfig } from '../../../../constants/importanceLevels'; -import { eventService } from '../../../../services/eventService'; +import { getImportanceConfig } from '@constants/importanceLevels'; +import { eventService } from '@services/eventService'; import { useEventStocks } from '../StockDetailPanel/hooks/useEventStocks'; -import { toggleEventFollow, selectEventFollowStatus } from '../../../../store/slices/communityDataSlice'; -import { useAuth } from '../../../../contexts/AuthContext'; +import { toggleEventFollow, selectEventFollowStatus } from '@store/slices/communityDataSlice'; +import { useAuth } from '@contexts/AuthContext'; import EventHeaderInfo from './EventHeaderInfo'; import CompactMetaBar from './CompactMetaBar'; import EventDescriptionSection from './EventDescriptionSection'; @@ -29,12 +29,13 @@ import RelatedConceptsSection from './RelatedConceptsSection'; import RelatedStocksSection from './RelatedStocksSection'; import CompactStockItem from './CompactStockItem'; import CollapsibleSection from './CollapsibleSection'; -import HistoricalEvents from '../../../EventDetail/components/HistoricalEvents'; -import TransmissionChainAnalysis from '../../../EventDetail/components/TransmissionChainAnalysis'; -import SubscriptionBadge from '../../../../components/SubscriptionBadge'; -import SubscriptionUpgradeModal from '../../../../components/SubscriptionUpgradeModal'; -import { PROFESSIONAL_COLORS } from '../../../../constants/professionalTheme'; -import EventCommentSection from '../../../../components/EventCommentSection'; +import HistoricalEvents from '@views/EventDetail/components/HistoricalEvents'; +import TransmissionChainAnalysis from '@views/EventDetail/components/TransmissionChainAnalysis'; +import SubscriptionBadge from '@components/SubscriptionBadge'; +import SubscriptionUpgradeModal from '@components/SubscriptionUpgradeModal'; +import { PROFESSIONAL_COLORS } from '@constants/professionalTheme'; +import { getApiBase } from '@utils/apiConfig'; +import EventCommentSection from '@components/EventCommentSection'; /** * 动态新闻详情面板主组件 @@ -255,37 +256,71 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { }, [dispatch, event?.id]); // 切换自选股 - const handleWatchlistToggle = useCallback(async (stockCode, isInWatchlist) => { + const handleWatchlistToggle = useCallback(async (stockCode, stockName, isInWatchlist) => { try { - const newWatchlist = new Set(watchlistSet); + const base = getApiBase(); if (isInWatchlist) { - newWatchlist.delete(stockCode); - toast({ - title: '已移除自选股', - status: 'info', - duration: 2000, - isClosable: true, + // 移除自选股 + const resp = await fetch(base + `/api/account/watchlist/${stockCode}`, { + method: 'DELETE', + credentials: 'include' }); + const data = await resp.json().catch(() => ({})); + if (resp.ok && data.success !== false) { + const newWatchlist = new Set(watchlistSet); + newWatchlist.delete(stockCode); + setWatchlistSet(newWatchlist); + localStorage.setItem('stock_watchlist', JSON.stringify(Array.from(newWatchlist))); + toast({ + title: '已移除自选股', + status: 'info', + duration: 2000, + isClosable: true, + }); + } else { + toast({ + title: '移除失败', + status: 'error', + duration: 2000, + isClosable: true, + }); + } } else { - newWatchlist.add(stockCode); - toast({ - title: '已添加至自选股', - status: 'success', - duration: 2000, - isClosable: true, + // 添加自选股 + const resp = await fetch(base + '/api/account/watchlist/add', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ stock_code: stockCode, stock_name: stockName }) }); + const data = await resp.json().catch(() => ({})); + if (resp.ok && data.success) { + const newWatchlist = new Set(watchlistSet); + newWatchlist.add(stockCode); + setWatchlistSet(newWatchlist); + localStorage.setItem('stock_watchlist', JSON.stringify(Array.from(newWatchlist))); + toast({ + title: '已添加至自选股', + status: 'success', + duration: 2000, + isClosable: true, + }); + } else { + toast({ + title: '添加失败', + status: 'error', + duration: 2000, + isClosable: true, + }); + } } - - setWatchlistSet(newWatchlist); - localStorage.setItem('stock_watchlist', JSON.stringify(Array.from(newWatchlist))); } catch (error) { console.error('切换自选股失败:', error); toast({ - title: '操作失败', - description: error.message, + title: '网络错误', status: 'error', - duration: 3000, + duration: 2000, isClosable: true, }); } diff --git a/src/views/Community/components/DynamicNewsDetail/StockListItem.js b/src/views/Community/components/DynamicNewsDetail/StockListItem.js index 35fe821e..8e3b2a52 100644 --- a/src/views/Community/components/DynamicNewsDetail/StockListItem.js +++ b/src/views/Community/components/DynamicNewsDetail/StockListItem.js @@ -74,7 +74,7 @@ const StockListItem = ({ const handleWatchlistClick = (e) => { e.stopPropagation(); - onWatchlistToggle?.(stock.stock_code, isInWatchlist); + onWatchlistToggle?.(stock.stock_code, stock.stock_name, isInWatchlist); }; // 格式化涨跌幅显示 From 3f518def09562903ad606b67040ec9bde1d6f1e4 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 4 Dec 2025 12:33:59 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E9=A2=84=E5=8A=A0=E8=BD=BD=E8=A1=8C?= =?UTF-8?q?=E4=B8=9A=E6=95=B0=E6=8D=AE=EF=BC=88=E8=A7=A3=E5=86=B3=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E7=82=B9=E5=87=BB=E6=97=A0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/Community/components/CompactSearchBox.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/views/Community/components/CompactSearchBox.js b/src/views/Community/components/CompactSearchBox.js index 1cb9d51a..95dc54c1 100644 --- a/src/views/Community/components/CompactSearchBox.js +++ b/src/views/Community/components/CompactSearchBox.js @@ -93,6 +93,13 @@ const CompactSearchBox = ({ loadStocks(); }, []); + // 预加载行业数据(解决第一次点击无数据问题) + useEffect(() => { + if (!industryData || industryData.length === 0) { + dispatch(fetchIndustryData()); + } + }, [dispatch, industryData]); + // 初始化筛选条件 const findIndustryPath = useCallback((targetCode, data, currentPath = []) => { if (!data || data.length === 0) return null; From e8c21f7863cf966293f776b27a0da9e6cbb99f5d Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Thu, 4 Dec 2025 13:29:59 +0800 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20DynamicNewsDetailPanel=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 useReducer 整合 7 个折叠状态为统一的 sectionState - 提取自选股逻辑到 useWatchlist Hook,移除 70 行重复代码 - 扩展 useWatchlist 添加 handleAddToWatchlist、isInWatchlist 方法 - 清理未使用的导入(HStack、useColorModeValue) - 移除调试 console.log 日志 - RelatedStocksSection 改用 isInWatchlist 函数替代 watchlistSet 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/hooks/useWatchlist.js | 47 +++- .../DynamicNewsDetailPanel.js | 260 +++++++----------- .../DynamicNewsDetail/RelatedStocksSection.js | 6 +- 3 files changed, 141 insertions(+), 172 deletions(-) diff --git a/src/hooks/useWatchlist.js b/src/hooks/useWatchlist.js index 32eb7356..d9c83941 100644 --- a/src/hooks/useWatchlist.js +++ b/src/hooks/useWatchlist.js @@ -10,7 +10,7 @@ const WATCHLIST_PAGE_SIZE = 10; /** * 自选股管理 Hook - * 提供自选股加载、分页、移除等功能 + * 提供自选股加载、分页、添加、移除等功能 * * @returns {{ * watchlistQuotes: Array, @@ -19,7 +19,9 @@ const WATCHLIST_PAGE_SIZE = 10; * setWatchlistPage: Function, * WATCHLIST_PAGE_SIZE: number, * loadWatchlistQuotes: Function, - * handleRemoveFromWatchlist: Function + * handleAddToWatchlist: Function, + * handleRemoveFromWatchlist: Function, + * isInWatchlist: Function * }} */ export const useWatchlist = () => { @@ -58,6 +60,32 @@ export const useWatchlist = () => { } }, []); + // 添加到自选股 + const handleAddToWatchlist = useCallback(async (stockCode, stockName) => { + try { + const base = getApiBase(); + const resp = await fetch(base + '/api/account/watchlist/add', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ stock_code: stockCode, stock_name: stockName }) + }); + const data = await resp.json().catch(() => ({})); + if (resp.ok && data.success) { + // 刷新自选股列表 + loadWatchlistQuotes(); + toast({ title: '已添加至自选股', status: 'success', duration: 1500 }); + return true; + } else { + toast({ title: '添加失败', status: 'error', duration: 2000 }); + return false; + } + } catch (e) { + toast({ title: '网络错误,添加失败', status: 'error', duration: 2000 }); + return false; + } + }, [toast, loadWatchlistQuotes]); + // 从自选股移除 const handleRemoveFromWatchlist = useCallback(async (stockCode) => { try { @@ -85,9 +113,20 @@ export const useWatchlist = () => { } } catch (e) { toast({ title: '网络错误,移除失败', status: 'error', duration: 2000 }); + return false; } }, [toast]); + // 判断股票是否在自选股中 + const isInWatchlist = useCallback((stockCode) => { + const normalize6 = (code) => { + const m = String(code || '').match(/(\d{6})/); + return m ? m[1] : String(code || ''); + }; + const target = normalize6(stockCode); + return watchlistQuotes.some(item => normalize6(item.stock_code) === target); + }, [watchlistQuotes]); + return { watchlistQuotes, watchlistLoading, @@ -95,6 +134,8 @@ export const useWatchlist = () => { setWatchlistPage, WATCHLIST_PAGE_SIZE, loadWatchlistQuotes, - handleRemoveFromWatchlist + handleAddToWatchlist, + handleRemoveFromWatchlist, + isInWatchlist }; }; diff --git a/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js b/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js index 214d24ba..6e96beef 100644 --- a/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js +++ b/src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js @@ -1,21 +1,18 @@ // src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js // 动态新闻详情面板主组件(组装所有子组件) -import React, { useState, useCallback, useEffect } from 'react'; +import React, { useState, useCallback, useEffect, useReducer } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { - Box, Card, CardBody, VStack, - HStack, Text, Spinner, Center, Wrap, WrapItem, - useColorModeValue, - useToast, + Box, } from '@chakra-ui/react'; import { getImportanceConfig } from '@constants/importanceLevels'; import { eventService } from '@services/eventService'; @@ -34,9 +31,52 @@ import TransmissionChainAnalysis from '@views/EventDetail/components/Transmissio import SubscriptionBadge from '@components/SubscriptionBadge'; import SubscriptionUpgradeModal from '@components/SubscriptionUpgradeModal'; import { PROFESSIONAL_COLORS } from '@constants/professionalTheme'; -import { getApiBase } from '@utils/apiConfig'; +import { useWatchlist } from '@hooks/useWatchlist'; import EventCommentSection from '@components/EventCommentSection'; +// 折叠区块状态管理 - 使用 useReducer 整合 +const initialSectionState = { + stocks: { isOpen: true, hasLoaded: false, hasLoadedQuotes: false }, + concepts: { isOpen: false }, + historical: { isOpen: false, hasLoaded: false }, + transmission: { isOpen: false, hasLoaded: false } +}; + +const sectionReducer = (state, action) => { + switch (action.type) { + case 'TOGGLE': + return { + ...state, + [action.section]: { + ...state[action.section], + isOpen: !state[action.section].isOpen + } + }; + case 'SET_LOADED': + return { + ...state, + [action.section]: { + ...state[action.section], + hasLoaded: true + } + }; + case 'SET_QUOTES_LOADED': + return { + ...state, + stocks: { ...state.stocks, hasLoadedQuotes: true } + }; + case 'RESET_ALL': + return { + stocks: { isOpen: true, hasLoaded: false, hasLoadedQuotes: false }, + concepts: { isOpen: false }, + historical: { isOpen: false, hasLoaded: false }, + transmission: { isOpen: false, hasLoaded: false } + }; + default: + return state; + } +}; + /** * 动态新闻详情面板主组件 * @param {Object} props @@ -49,7 +89,14 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { const cardBg = PROFESSIONAL_COLORS.background.card; const borderColor = PROFESSIONAL_COLORS.border.default; const textColor = PROFESSIONAL_COLORS.text.secondary; - const toast = useToast(); + + // 使用 useWatchlist Hook 管理自选股 + const { + handleAddToWatchlist, + handleRemoveFromWatchlist, + isInWatchlist, + loadWatchlistQuotes + } = useWatchlist(); // 获取用户会员等级(修复:字段名从 subscription_tier 改为 subscription_type) const userTier = user?.subscription_type || 'free'; @@ -102,11 +149,6 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { const response = await eventService.getEventDetail(event.id); if (response.success) { setFullEventDetail(response.data); - console.log('%c📊 [浏览量] 事件详情加载成功', 'color: #10B981; font-weight: bold;', { - eventId: event.id, - viewCount: response.data.view_count, - title: response.data.title - }); } } catch (error) { console.error('[DynamicNewsDetailPanel] loadEventDetail 失败:', error, { @@ -123,30 +165,8 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { const canAccessHistorical = hasAccess('pro'); const canAccessTransmission = hasAccess('max'); - // 子区块折叠状态管理 + 加载追踪 - // 相关股票默认展开 - const [isStocksOpen, setIsStocksOpen] = useState(true); - const [hasLoadedStocks, setHasLoadedStocks] = useState(false); // 股票列表是否已加载(获取数量) - const [hasLoadedQuotes, setHasLoadedQuotes] = useState(false); // 行情数据是否已加载 - - const [isConceptsOpen, setIsConceptsOpen] = useState(false); - - // 历史事件默认折叠,但预加载数量 - const [isHistoricalOpen, setIsHistoricalOpen] = useState(false); - const [hasLoadedHistorical, setHasLoadedHistorical] = useState(false); - - const [isTransmissionOpen, setIsTransmissionOpen] = useState(false); - const [hasLoadedTransmission, setHasLoadedTransmission] = useState(false); - - // 自选股管理(使用 localStorage) - const [watchlistSet, setWatchlistSet] = useState(() => { - try { - const saved = localStorage.getItem('stock_watchlist'); - return saved ? new Set(JSON.parse(saved)) : new Set(); - } catch { - return new Set(); - } - }); + // 子区块折叠状态管理 - 使用 useReducer 整合 + const [sectionState, dispatchSection] = useReducer(sectionReducer, initialSectionState); // 锁定点击处理 - 弹出升级弹窗 const handleLockedClick = useCallback((featureName, requiredLevel) => { @@ -167,87 +187,62 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { }, []); // 相关股票 - 展开时加载行情(需要 PRO 权限) - // 股票列表在事件切换时预加载(显示数量),行情在展开时才加载 const handleStocksToggle = useCallback(() => { - const newState = !isStocksOpen; - setIsStocksOpen(newState); + const willOpen = !sectionState.stocks.isOpen; + dispatchSection({ type: 'TOGGLE', section: 'stocks' }); // 展开时加载行情数据(如果还没加载过) - if (newState && !hasLoadedQuotes && stocks.length > 0) { - console.log('%c📈 [相关股票] 首次展开,加载行情数据', 'color: #10B981; font-weight: bold;', { - eventId: event?.id, - stockCount: stocks.length - }); + if (willOpen && !sectionState.stocks.hasLoadedQuotes && stocks.length > 0) { refreshQuotes(); - setHasLoadedQuotes(true); + dispatchSection({ type: 'SET_QUOTES_LOADED' }); } - }, [isStocksOpen, hasLoadedQuotes, stocks.length, refreshQuotes, event?.id]); + }, [sectionState.stocks, stocks.length, refreshQuotes]); // 相关概念 - 展开/收起(无需加载) const handleConceptsToggle = useCallback(() => { - setIsConceptsOpen(!isConceptsOpen); - }, [isConceptsOpen]); + dispatchSection({ type: 'TOGGLE', section: 'concepts' }); + }, []); // 历史事件对比 - 数据已预加载,只需切换展开状态 const handleHistoricalToggle = useCallback(() => { - const newState = !isHistoricalOpen; - setIsHistoricalOpen(newState); - - // 数据已在事件切换时预加载,这里只需展开 - if (newState) { - console.log('%c📜 [历史事件] 展开(数据已预加载)', 'color: #3B82F6; font-weight: bold;', { - eventId: event?.id, - count: historicalEvents?.length || 0 - }); - } - }, [isHistoricalOpen, event?.id, historicalEvents?.length]); + dispatchSection({ type: 'TOGGLE', section: 'historical' }); + }, []); // 传导链分析 - 展开时加载 const handleTransmissionToggle = useCallback(() => { - const newState = !isTransmissionOpen; - setIsTransmissionOpen(newState); + const willOpen = !sectionState.transmission.isOpen; + dispatchSection({ type: 'TOGGLE', section: 'transmission' }); - if (newState && !hasLoadedTransmission) { - console.log('%c🔗 [传导链] 首次展开,加载传导链数据', 'color: #8B5CF6; font-weight: bold;', { eventId: event?.id }); + if (willOpen && !sectionState.transmission.hasLoaded) { loadChainAnalysis(); - setHasLoadedTransmission(true); + dispatchSection({ type: 'SET_LOADED', section: 'transmission' }); } - }, [isTransmissionOpen, hasLoadedTransmission, loadChainAnalysis, event?.id]); + }, [sectionState.transmission, loadChainAnalysis]); // 事件切换时重置所有子模块状态 useEffect(() => { - console.log('%c🔄 [事件切换] 重置所有子模块状态', 'color: #F59E0B; font-weight: bold;', { eventId: event?.id }); - - // 🎯 加载事件详情(增加浏览量) + // 加载事件详情(增加浏览量) loadEventDetail(); - // 重置所有加载状态 - setHasLoadedStocks(false); - setHasLoadedQuotes(false); // 重置行情加载状态 - setHasLoadedHistorical(false); - setHasLoadedTransmission(false); + // 加载自选股数据(用于判断股票是否已关注) + loadWatchlistQuotes(); + + // 重置所有折叠区块状态 + dispatchSection({ type: 'RESET_ALL' }); // 相关股票默认展开,预加载股票列表和行情数据 - setIsStocksOpen(true); if (canAccessStocks) { - console.log('%c📊 [相关股票] 事件切换,预加载股票列表和行情数据', 'color: #10B981; font-weight: bold;', { eventId: event?.id }); loadStocksData(); - setHasLoadedStocks(true); - // 由于默认展开,直接加载行情数据 - setHasLoadedQuotes(true); + dispatchSection({ type: 'SET_LOADED', section: 'stocks' }); + dispatchSection({ type: 'SET_QUOTES_LOADED' }); } // 历史事件默认折叠,但预加载数据(显示数量吸引点击) - setIsHistoricalOpen(false); if (canAccessHistorical) { - console.log('%c📜 [历史事件] 事件切换,预加载历史事件(获取数量)', 'color: #3B82F6; font-weight: bold;', { eventId: event?.id }); loadHistoricalData(); - setHasLoadedHistorical(true); + dispatchSection({ type: 'SET_LOADED', section: 'historical' }); } - - setIsConceptsOpen(false); - setIsTransmissionOpen(false); - }, [event?.id, canAccessStocks, canAccessHistorical, userTier, loadStocksData, loadHistoricalData, loadEventDetail]); + }, [event?.id, canAccessStocks, canAccessHistorical, userTier, loadStocksData, loadHistoricalData, loadEventDetail, loadWatchlistQuotes]); // 切换关注状态 const handleToggleFollow = useCallback(async () => { @@ -255,76 +250,14 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { dispatch(toggleEventFollow(event.id)); }, [dispatch, event?.id]); - // 切换自选股 - const handleWatchlistToggle = useCallback(async (stockCode, stockName, isInWatchlist) => { - try { - const base = getApiBase(); - - if (isInWatchlist) { - // 移除自选股 - const resp = await fetch(base + `/api/account/watchlist/${stockCode}`, { - method: 'DELETE', - credentials: 'include' - }); - const data = await resp.json().catch(() => ({})); - if (resp.ok && data.success !== false) { - const newWatchlist = new Set(watchlistSet); - newWatchlist.delete(stockCode); - setWatchlistSet(newWatchlist); - localStorage.setItem('stock_watchlist', JSON.stringify(Array.from(newWatchlist))); - toast({ - title: '已移除自选股', - status: 'info', - duration: 2000, - isClosable: true, - }); - } else { - toast({ - title: '移除失败', - status: 'error', - duration: 2000, - isClosable: true, - }); - } - } else { - // 添加自选股 - const resp = await fetch(base + '/api/account/watchlist/add', { - method: 'POST', - credentials: 'include', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ stock_code: stockCode, stock_name: stockName }) - }); - const data = await resp.json().catch(() => ({})); - if (resp.ok && data.success) { - const newWatchlist = new Set(watchlistSet); - newWatchlist.add(stockCode); - setWatchlistSet(newWatchlist); - localStorage.setItem('stock_watchlist', JSON.stringify(Array.from(newWatchlist))); - toast({ - title: '已添加至自选股', - status: 'success', - duration: 2000, - isClosable: true, - }); - } else { - toast({ - title: '添加失败', - status: 'error', - duration: 2000, - isClosable: true, - }); - } - } - } catch (error) { - console.error('切换自选股失败:', error); - toast({ - title: '网络错误', - status: 'error', - duration: 2000, - isClosable: true, - }); + // 切换自选股(使用 useWatchlist Hook) + const handleWatchlistToggle = useCallback(async (stockCode, stockName, currentlyInWatchlist) => { + if (currentlyInWatchlist) { + await handleRemoveFromWatchlist(stockCode); + } else { + await handleAddToWatchlist(stockCode, stockName); } - }, [watchlistSet, toast]); + }, [handleAddToWatchlist, handleRemoveFromWatchlist]); // 空状态 if (!event) { @@ -373,15 +306,10 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { {/* 相关股票(可折叠) - 懒加载 - 需要 PRO 权限 - 支持精简/详细模式 */} { - if (!canAccessStocks) { - return ; - } - return null; - })()} + subscriptionBadge={!canAccessStocks ? : null} isLocked={!canAccessStocks} onLockedClick={() => handleLockedClick('相关股票', 'pro')} showModeToggle={canAccessStocks} @@ -416,7 +344,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { stocks={stocks} quotes={quotes} eventTime={event.created_at} - watchlistSet={watchlistSet} + isInWatchlist={isInWatchlist} onWatchlistToggle={handleWatchlistToggle} /> )} @@ -427,7 +355,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { eventTitle={event.title} effectiveTradingDate={event.trading_date || event.created_at} eventTime={event.created_at} - isOpen={isConceptsOpen} + isOpen={sectionState.concepts.isOpen} onToggle={handleConceptsToggle} subscriptionBadge={!canAccessConcepts ? : null} isLocked={!canAccessConcepts} @@ -437,7 +365,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { {/* 历史事件对比(可折叠) - 懒加载 - 需要 PRO 权限 */} : null} @@ -460,7 +388,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { {/* 传导链分析(可折叠) - 懒加载 - 需要 MAX 权限 */} : null} isLocked={!canAccessTransmission} @@ -488,7 +416,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => { featureName={upgradeModal.featureName} currentLevel={userTier} /> - ): null } + ) : null} ); }; diff --git a/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js b/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js index 49e1c25f..fe5c97e4 100644 --- a/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js +++ b/src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js @@ -15,14 +15,14 @@ import { logger } from '../../../../utils/logger'; * @param {Array} props.stocks - 股票数组 * @param {Object} props.quotes - 股票行情字典 { [stockCode]: { change: number } } * @param {string} props.eventTime - 事件时间 - * @param {Set} props.watchlistSet - 自选股代码集合 + * @param {Function} props.isInWatchlist - 检查股票是否在自选股中的函数 * @param {Function} props.onWatchlistToggle - 切换自选股回调 */ const RelatedStocksSection = ({ stocks, quotes = {}, eventTime = null, - watchlistSet = new Set(), + isInWatchlist = () => false, onWatchlistToggle }) => { // 分时图数据状态:{ [stockCode]: data[] } @@ -167,7 +167,7 @@ const RelatedStocksSection = ({ stock={stock} quote={quotes[stock.stock_code]} eventTime={eventTime} - isInWatchlist={watchlistSet.has(stock.stock_code)} + isInWatchlist={isInWatchlist(stock.stock_code)} onWatchlistToggle={onWatchlistToggle} timelineData={timelineDataMap[stock.stock_code]} timelineLoading={shouldShowTimelineLoading && !timelineDataMap[stock.stock_code]}