refactor: DynamicNewsDetailPanel 组件优化
- 使用 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 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ const WATCHLIST_PAGE_SIZE = 10;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 自选股管理 Hook
|
* 自选股管理 Hook
|
||||||
* 提供自选股加载、分页、移除等功能
|
* 提供自选股加载、分页、添加、移除等功能
|
||||||
*
|
*
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* watchlistQuotes: Array,
|
* watchlistQuotes: Array,
|
||||||
@@ -19,7 +19,9 @@ const WATCHLIST_PAGE_SIZE = 10;
|
|||||||
* setWatchlistPage: Function,
|
* setWatchlistPage: Function,
|
||||||
* WATCHLIST_PAGE_SIZE: number,
|
* WATCHLIST_PAGE_SIZE: number,
|
||||||
* loadWatchlistQuotes: Function,
|
* loadWatchlistQuotes: Function,
|
||||||
* handleRemoveFromWatchlist: Function
|
* handleAddToWatchlist: Function,
|
||||||
|
* handleRemoveFromWatchlist: Function,
|
||||||
|
* isInWatchlist: Function
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export const useWatchlist = () => {
|
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) => {
|
const handleRemoveFromWatchlist = useCallback(async (stockCode) => {
|
||||||
try {
|
try {
|
||||||
@@ -85,9 +113,20 @@ export const useWatchlist = () => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: '网络错误,移除失败', status: 'error', duration: 2000 });
|
toast({ title: '网络错误,移除失败', status: 'error', duration: 2000 });
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}, [toast]);
|
}, [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 {
|
return {
|
||||||
watchlistQuotes,
|
watchlistQuotes,
|
||||||
watchlistLoading,
|
watchlistLoading,
|
||||||
@@ -95,6 +134,8 @@ export const useWatchlist = () => {
|
|||||||
setWatchlistPage,
|
setWatchlistPage,
|
||||||
WATCHLIST_PAGE_SIZE,
|
WATCHLIST_PAGE_SIZE,
|
||||||
loadWatchlistQuotes,
|
loadWatchlistQuotes,
|
||||||
handleRemoveFromWatchlist
|
handleAddToWatchlist,
|
||||||
|
handleRemoveFromWatchlist,
|
||||||
|
isInWatchlist
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
// src/views/Community/components/DynamicNewsDetail/DynamicNewsDetailPanel.js
|
// 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 { useDispatch, useSelector } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
Box,
|
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
VStack,
|
VStack,
|
||||||
HStack,
|
|
||||||
Text,
|
Text,
|
||||||
Spinner,
|
Spinner,
|
||||||
Center,
|
Center,
|
||||||
Wrap,
|
Wrap,
|
||||||
WrapItem,
|
WrapItem,
|
||||||
useColorModeValue,
|
Box,
|
||||||
useToast,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { getImportanceConfig } from '@constants/importanceLevels';
|
import { getImportanceConfig } from '@constants/importanceLevels';
|
||||||
import { eventService } from '@services/eventService';
|
import { eventService } from '@services/eventService';
|
||||||
@@ -34,9 +31,52 @@ import TransmissionChainAnalysis from '@views/EventDetail/components/Transmissio
|
|||||||
import SubscriptionBadge from '@components/SubscriptionBadge';
|
import SubscriptionBadge from '@components/SubscriptionBadge';
|
||||||
import SubscriptionUpgradeModal from '@components/SubscriptionUpgradeModal';
|
import SubscriptionUpgradeModal from '@components/SubscriptionUpgradeModal';
|
||||||
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
import { PROFESSIONAL_COLORS } from '@constants/professionalTheme';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { useWatchlist } from '@hooks/useWatchlist';
|
||||||
import EventCommentSection from '@components/EventCommentSection';
|
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
|
* @param {Object} props
|
||||||
@@ -49,7 +89,14 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
const cardBg = PROFESSIONAL_COLORS.background.card;
|
const cardBg = PROFESSIONAL_COLORS.background.card;
|
||||||
const borderColor = PROFESSIONAL_COLORS.border.default;
|
const borderColor = PROFESSIONAL_COLORS.border.default;
|
||||||
const textColor = PROFESSIONAL_COLORS.text.secondary;
|
const textColor = PROFESSIONAL_COLORS.text.secondary;
|
||||||
const toast = useToast();
|
|
||||||
|
// 使用 useWatchlist Hook 管理自选股
|
||||||
|
const {
|
||||||
|
handleAddToWatchlist,
|
||||||
|
handleRemoveFromWatchlist,
|
||||||
|
isInWatchlist,
|
||||||
|
loadWatchlistQuotes
|
||||||
|
} = useWatchlist();
|
||||||
|
|
||||||
// 获取用户会员等级(修复:字段名从 subscription_tier 改为 subscription_type)
|
// 获取用户会员等级(修复:字段名从 subscription_tier 改为 subscription_type)
|
||||||
const userTier = user?.subscription_type || 'free';
|
const userTier = user?.subscription_type || 'free';
|
||||||
@@ -102,11 +149,6 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
const response = await eventService.getEventDetail(event.id);
|
const response = await eventService.getEventDetail(event.id);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setFullEventDetail(response.data);
|
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) {
|
} catch (error) {
|
||||||
console.error('[DynamicNewsDetailPanel] loadEventDetail 失败:', error, {
|
console.error('[DynamicNewsDetailPanel] loadEventDetail 失败:', error, {
|
||||||
@@ -123,30 +165,8 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
const canAccessHistorical = hasAccess('pro');
|
const canAccessHistorical = hasAccess('pro');
|
||||||
const canAccessTransmission = hasAccess('max');
|
const canAccessTransmission = hasAccess('max');
|
||||||
|
|
||||||
// 子区块折叠状态管理 + 加载追踪
|
// 子区块折叠状态管理 - 使用 useReducer 整合
|
||||||
// 相关股票默认展开
|
const [sectionState, dispatchSection] = useReducer(sectionReducer, initialSectionState);
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 锁定点击处理 - 弹出升级弹窗
|
// 锁定点击处理 - 弹出升级弹窗
|
||||||
const handleLockedClick = useCallback((featureName, requiredLevel) => {
|
const handleLockedClick = useCallback((featureName, requiredLevel) => {
|
||||||
@@ -167,87 +187,62 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 相关股票 - 展开时加载行情(需要 PRO 权限)
|
// 相关股票 - 展开时加载行情(需要 PRO 权限)
|
||||||
// 股票列表在事件切换时预加载(显示数量),行情在展开时才加载
|
|
||||||
const handleStocksToggle = useCallback(() => {
|
const handleStocksToggle = useCallback(() => {
|
||||||
const newState = !isStocksOpen;
|
const willOpen = !sectionState.stocks.isOpen;
|
||||||
setIsStocksOpen(newState);
|
dispatchSection({ type: 'TOGGLE', section: 'stocks' });
|
||||||
|
|
||||||
// 展开时加载行情数据(如果还没加载过)
|
// 展开时加载行情数据(如果还没加载过)
|
||||||
if (newState && !hasLoadedQuotes && stocks.length > 0) {
|
if (willOpen && !sectionState.stocks.hasLoadedQuotes && stocks.length > 0) {
|
||||||
console.log('%c📈 [相关股票] 首次展开,加载行情数据', 'color: #10B981; font-weight: bold;', {
|
|
||||||
eventId: event?.id,
|
|
||||||
stockCount: stocks.length
|
|
||||||
});
|
|
||||||
refreshQuotes();
|
refreshQuotes();
|
||||||
setHasLoadedQuotes(true);
|
dispatchSection({ type: 'SET_QUOTES_LOADED' });
|
||||||
}
|
}
|
||||||
}, [isStocksOpen, hasLoadedQuotes, stocks.length, refreshQuotes, event?.id]);
|
}, [sectionState.stocks, stocks.length, refreshQuotes]);
|
||||||
|
|
||||||
// 相关概念 - 展开/收起(无需加载)
|
// 相关概念 - 展开/收起(无需加载)
|
||||||
const handleConceptsToggle = useCallback(() => {
|
const handleConceptsToggle = useCallback(() => {
|
||||||
setIsConceptsOpen(!isConceptsOpen);
|
dispatchSection({ type: 'TOGGLE', section: 'concepts' });
|
||||||
}, [isConceptsOpen]);
|
}, []);
|
||||||
|
|
||||||
// 历史事件对比 - 数据已预加载,只需切换展开状态
|
// 历史事件对比 - 数据已预加载,只需切换展开状态
|
||||||
const handleHistoricalToggle = useCallback(() => {
|
const handleHistoricalToggle = useCallback(() => {
|
||||||
const newState = !isHistoricalOpen;
|
dispatchSection({ type: 'TOGGLE', section: 'historical' });
|
||||||
setIsHistoricalOpen(newState);
|
}, []);
|
||||||
|
|
||||||
// 数据已在事件切换时预加载,这里只需展开
|
|
||||||
if (newState) {
|
|
||||||
console.log('%c📜 [历史事件] 展开(数据已预加载)', 'color: #3B82F6; font-weight: bold;', {
|
|
||||||
eventId: event?.id,
|
|
||||||
count: historicalEvents?.length || 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [isHistoricalOpen, event?.id, historicalEvents?.length]);
|
|
||||||
|
|
||||||
// 传导链分析 - 展开时加载
|
// 传导链分析 - 展开时加载
|
||||||
const handleTransmissionToggle = useCallback(() => {
|
const handleTransmissionToggle = useCallback(() => {
|
||||||
const newState = !isTransmissionOpen;
|
const willOpen = !sectionState.transmission.isOpen;
|
||||||
setIsTransmissionOpen(newState);
|
dispatchSection({ type: 'TOGGLE', section: 'transmission' });
|
||||||
|
|
||||||
if (newState && !hasLoadedTransmission) {
|
if (willOpen && !sectionState.transmission.hasLoaded) {
|
||||||
console.log('%c🔗 [传导链] 首次展开,加载传导链数据', 'color: #8B5CF6; font-weight: bold;', { eventId: event?.id });
|
|
||||||
loadChainAnalysis();
|
loadChainAnalysis();
|
||||||
setHasLoadedTransmission(true);
|
dispatchSection({ type: 'SET_LOADED', section: 'transmission' });
|
||||||
}
|
}
|
||||||
}, [isTransmissionOpen, hasLoadedTransmission, loadChainAnalysis, event?.id]);
|
}, [sectionState.transmission, loadChainAnalysis]);
|
||||||
|
|
||||||
// 事件切换时重置所有子模块状态
|
// 事件切换时重置所有子模块状态
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('%c🔄 [事件切换] 重置所有子模块状态', 'color: #F59E0B; font-weight: bold;', { eventId: event?.id });
|
// 加载事件详情(增加浏览量)
|
||||||
|
|
||||||
// 🎯 加载事件详情(增加浏览量)
|
|
||||||
loadEventDetail();
|
loadEventDetail();
|
||||||
|
|
||||||
// 重置所有加载状态
|
// 加载自选股数据(用于判断股票是否已关注)
|
||||||
setHasLoadedStocks(false);
|
loadWatchlistQuotes();
|
||||||
setHasLoadedQuotes(false); // 重置行情加载状态
|
|
||||||
setHasLoadedHistorical(false);
|
// 重置所有折叠区块状态
|
||||||
setHasLoadedTransmission(false);
|
dispatchSection({ type: 'RESET_ALL' });
|
||||||
|
|
||||||
// 相关股票默认展开,预加载股票列表和行情数据
|
// 相关股票默认展开,预加载股票列表和行情数据
|
||||||
setIsStocksOpen(true);
|
|
||||||
if (canAccessStocks) {
|
if (canAccessStocks) {
|
||||||
console.log('%c📊 [相关股票] 事件切换,预加载股票列表和行情数据', 'color: #10B981; font-weight: bold;', { eventId: event?.id });
|
|
||||||
loadStocksData();
|
loadStocksData();
|
||||||
setHasLoadedStocks(true);
|
dispatchSection({ type: 'SET_LOADED', section: 'stocks' });
|
||||||
// 由于默认展开,直接加载行情数据
|
dispatchSection({ type: 'SET_QUOTES_LOADED' });
|
||||||
setHasLoadedQuotes(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 历史事件默认折叠,但预加载数据(显示数量吸引点击)
|
// 历史事件默认折叠,但预加载数据(显示数量吸引点击)
|
||||||
setIsHistoricalOpen(false);
|
|
||||||
if (canAccessHistorical) {
|
if (canAccessHistorical) {
|
||||||
console.log('%c📜 [历史事件] 事件切换,预加载历史事件(获取数量)', 'color: #3B82F6; font-weight: bold;', { eventId: event?.id });
|
|
||||||
loadHistoricalData();
|
loadHistoricalData();
|
||||||
setHasLoadedHistorical(true);
|
dispatchSection({ type: 'SET_LOADED', section: 'historical' });
|
||||||
}
|
}
|
||||||
|
}, [event?.id, canAccessStocks, canAccessHistorical, userTier, loadStocksData, loadHistoricalData, loadEventDetail, loadWatchlistQuotes]);
|
||||||
setIsConceptsOpen(false);
|
|
||||||
setIsTransmissionOpen(false);
|
|
||||||
}, [event?.id, canAccessStocks, canAccessHistorical, userTier, loadStocksData, loadHistoricalData, loadEventDetail]);
|
|
||||||
|
|
||||||
// 切换关注状态
|
// 切换关注状态
|
||||||
const handleToggleFollow = useCallback(async () => {
|
const handleToggleFollow = useCallback(async () => {
|
||||||
@@ -255,76 +250,14 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
dispatch(toggleEventFollow(event.id));
|
dispatch(toggleEventFollow(event.id));
|
||||||
}, [dispatch, event?.id]);
|
}, [dispatch, event?.id]);
|
||||||
|
|
||||||
// 切换自选股
|
// 切换自选股(使用 useWatchlist Hook)
|
||||||
const handleWatchlistToggle = useCallback(async (stockCode, stockName, isInWatchlist) => {
|
const handleWatchlistToggle = useCallback(async (stockCode, stockName, currentlyInWatchlist) => {
|
||||||
try {
|
if (currentlyInWatchlist) {
|
||||||
const base = getApiBase();
|
await handleRemoveFromWatchlist(stockCode);
|
||||||
|
} else {
|
||||||
if (isInWatchlist) {
|
await handleAddToWatchlist(stockCode, stockName);
|
||||||
// 移除自选股
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [watchlistSet, toast]);
|
}, [handleAddToWatchlist, handleRemoveFromWatchlist]);
|
||||||
|
|
||||||
// 空状态
|
// 空状态
|
||||||
if (!event) {
|
if (!event) {
|
||||||
@@ -373,15 +306,10 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
{/* 相关股票(可折叠) - 懒加载 - 需要 PRO 权限 - 支持精简/详细模式 */}
|
{/* 相关股票(可折叠) - 懒加载 - 需要 PRO 权限 - 支持精简/详细模式 */}
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title="相关股票"
|
title="相关股票"
|
||||||
isOpen={isStocksOpen}
|
isOpen={sectionState.stocks.isOpen}
|
||||||
onToggle={handleStocksToggle}
|
onToggle={handleStocksToggle}
|
||||||
count={stocks?.length || 0}
|
count={stocks?.length || 0}
|
||||||
subscriptionBadge={(() => {
|
subscriptionBadge={!canAccessStocks ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
||||||
if (!canAccessStocks) {
|
|
||||||
return <SubscriptionBadge tier="pro" size="sm" />;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})()}
|
|
||||||
isLocked={!canAccessStocks}
|
isLocked={!canAccessStocks}
|
||||||
onLockedClick={() => handleLockedClick('相关股票', 'pro')}
|
onLockedClick={() => handleLockedClick('相关股票', 'pro')}
|
||||||
showModeToggle={canAccessStocks}
|
showModeToggle={canAccessStocks}
|
||||||
@@ -416,7 +344,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
stocks={stocks}
|
stocks={stocks}
|
||||||
quotes={quotes}
|
quotes={quotes}
|
||||||
eventTime={event.created_at}
|
eventTime={event.created_at}
|
||||||
watchlistSet={watchlistSet}
|
isInWatchlist={isInWatchlist}
|
||||||
onWatchlistToggle={handleWatchlistToggle}
|
onWatchlistToggle={handleWatchlistToggle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -427,7 +355,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
eventTitle={event.title}
|
eventTitle={event.title}
|
||||||
effectiveTradingDate={event.trading_date || event.created_at}
|
effectiveTradingDate={event.trading_date || event.created_at}
|
||||||
eventTime={event.created_at}
|
eventTime={event.created_at}
|
||||||
isOpen={isConceptsOpen}
|
isOpen={sectionState.concepts.isOpen}
|
||||||
onToggle={handleConceptsToggle}
|
onToggle={handleConceptsToggle}
|
||||||
subscriptionBadge={!canAccessConcepts ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
subscriptionBadge={!canAccessConcepts ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
||||||
isLocked={!canAccessConcepts}
|
isLocked={!canAccessConcepts}
|
||||||
@@ -437,7 +365,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
{/* 历史事件对比(可折叠) - 懒加载 - 需要 PRO 权限 */}
|
{/* 历史事件对比(可折叠) - 懒加载 - 需要 PRO 权限 */}
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title="历史事件对比"
|
title="历史事件对比"
|
||||||
isOpen={isHistoricalOpen}
|
isOpen={sectionState.historical.isOpen}
|
||||||
onToggle={handleHistoricalToggle}
|
onToggle={handleHistoricalToggle}
|
||||||
count={historicalEvents?.length || 0}
|
count={historicalEvents?.length || 0}
|
||||||
subscriptionBadge={!canAccessHistorical ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
subscriptionBadge={!canAccessHistorical ? <SubscriptionBadge tier="pro" size="sm" /> : null}
|
||||||
@@ -460,7 +388,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
{/* 传导链分析(可折叠) - 懒加载 - 需要 MAX 权限 */}
|
{/* 传导链分析(可折叠) - 懒加载 - 需要 MAX 权限 */}
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title="传导链分析"
|
title="传导链分析"
|
||||||
isOpen={isTransmissionOpen}
|
isOpen={sectionState.transmission.isOpen}
|
||||||
onToggle={handleTransmissionToggle}
|
onToggle={handleTransmissionToggle}
|
||||||
subscriptionBadge={!canAccessTransmission ? <SubscriptionBadge tier="max" size="sm" /> : null}
|
subscriptionBadge={!canAccessTransmission ? <SubscriptionBadge tier="max" size="sm" /> : null}
|
||||||
isLocked={!canAccessTransmission}
|
isLocked={!canAccessTransmission}
|
||||||
@@ -488,7 +416,7 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
featureName={upgradeModal.featureName}
|
featureName={upgradeModal.featureName}
|
||||||
currentLevel={userTier}
|
currentLevel={userTier}
|
||||||
/>
|
/>
|
||||||
): null }
|
) : null}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ import { logger } from '../../../../utils/logger';
|
|||||||
* @param {Array<Object>} props.stocks - 股票数组
|
* @param {Array<Object>} props.stocks - 股票数组
|
||||||
* @param {Object} props.quotes - 股票行情字典 { [stockCode]: { change: number } }
|
* @param {Object} props.quotes - 股票行情字典 { [stockCode]: { change: number } }
|
||||||
* @param {string} props.eventTime - 事件时间
|
* @param {string} props.eventTime - 事件时间
|
||||||
* @param {Set} props.watchlistSet - 自选股代码集合
|
* @param {Function} props.isInWatchlist - 检查股票是否在自选股中的函数
|
||||||
* @param {Function} props.onWatchlistToggle - 切换自选股回调
|
* @param {Function} props.onWatchlistToggle - 切换自选股回调
|
||||||
*/
|
*/
|
||||||
const RelatedStocksSection = ({
|
const RelatedStocksSection = ({
|
||||||
stocks,
|
stocks,
|
||||||
quotes = {},
|
quotes = {},
|
||||||
eventTime = null,
|
eventTime = null,
|
||||||
watchlistSet = new Set(),
|
isInWatchlist = () => false,
|
||||||
onWatchlistToggle
|
onWatchlistToggle
|
||||||
}) => {
|
}) => {
|
||||||
// 分时图数据状态:{ [stockCode]: data[] }
|
// 分时图数据状态:{ [stockCode]: data[] }
|
||||||
@@ -167,7 +167,7 @@ const RelatedStocksSection = ({
|
|||||||
stock={stock}
|
stock={stock}
|
||||||
quote={quotes[stock.stock_code]}
|
quote={quotes[stock.stock_code]}
|
||||||
eventTime={eventTime}
|
eventTime={eventTime}
|
||||||
isInWatchlist={watchlistSet.has(stock.stock_code)}
|
isInWatchlist={isInWatchlist(stock.stock_code)}
|
||||||
onWatchlistToggle={onWatchlistToggle}
|
onWatchlistToggle={onWatchlistToggle}
|
||||||
timelineData={timelineDataMap[stock.stock_code]}
|
timelineData={timelineDataMap[stock.stock_code]}
|
||||||
timelineLoading={shouldShowTimelineLoading && !timelineDataMap[stock.stock_code]}
|
timelineLoading={shouldShowTimelineLoading && !timelineDataMap[stock.stock_code]}
|
||||||
|
|||||||
Reference in New Issue
Block a user