refactor(watchlist): 自选股数据源统一到 Redux
- stockSlice: 新增 loadWatchlistQuotes thunk 加载自选股行情 - useWatchlist: 改用 Redux selector 获取自选股数据 - WatchlistMenu: 使用 Redux 数据源,移除本地状态管理 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
// src/components/Navbars/components/FeatureMenus/WatchlistMenu.js
|
// src/components/Navbars/components/FeatureMenus/WatchlistMenu.js
|
||||||
// 自选股下拉菜单组件
|
// 自选股下拉菜单组件
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
@@ -21,6 +21,7 @@ import { ChevronDownIcon } from '@chakra-ui/icons';
|
|||||||
import { FiStar } from 'react-icons/fi';
|
import { FiStar } from 'react-icons/fi';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useWatchlist } from '../../../../hooks/useWatchlist';
|
import { useWatchlist } from '../../../../hooks/useWatchlist';
|
||||||
|
import FavoriteButton from '@/components/FavoriteButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自选股下拉菜单组件
|
* 自选股下拉菜单组件
|
||||||
@@ -29,6 +30,7 @@ import { useWatchlist } from '../../../../hooks/useWatchlist';
|
|||||||
*/
|
*/
|
||||||
const WatchlistMenu = memo(() => {
|
const WatchlistMenu = memo(() => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [removingCode, setRemovingCode] = useState(null);
|
||||||
const {
|
const {
|
||||||
watchlistQuotes,
|
watchlistQuotes,
|
||||||
watchlistLoading,
|
watchlistLoading,
|
||||||
@@ -39,6 +41,17 @@ const WatchlistMenu = memo(() => {
|
|||||||
handleRemoveFromWatchlist
|
handleRemoveFromWatchlist
|
||||||
} = useWatchlist();
|
} = useWatchlist();
|
||||||
|
|
||||||
|
// 处理取消关注(带 loading 状态)
|
||||||
|
const handleUnwatch = async (stockCode) => {
|
||||||
|
if (removingCode) return;
|
||||||
|
setRemovingCode(stockCode);
|
||||||
|
try {
|
||||||
|
await handleRemoveFromWatchlist(stockCode);
|
||||||
|
} finally {
|
||||||
|
setRemovingCode(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const titleColor = useColorModeValue('gray.600', 'gray.300');
|
const titleColor = useColorModeValue('gray.600', 'gray.300');
|
||||||
const loadingTextColor = useColorModeValue('gray.500', 'gray.300');
|
const loadingTextColor = useColorModeValue('gray.500', 'gray.300');
|
||||||
const emptyTextColor = useColorModeValue('gray.500', 'gray.300');
|
const emptyTextColor = useColorModeValue('gray.500', 'gray.300');
|
||||||
@@ -114,21 +127,19 @@ const WatchlistMenu = memo(() => {
|
|||||||
(item.current_price || '-')}
|
(item.current_price || '-')}
|
||||||
</Text>
|
</Text>
|
||||||
<Box
|
<Box
|
||||||
as="span"
|
|
||||||
fontSize="xs"
|
|
||||||
color="red.500"
|
|
||||||
cursor="pointer"
|
|
||||||
px={2}
|
|
||||||
py={1}
|
|
||||||
borderRadius="md"
|
|
||||||
_hover={{ bg: 'red.50' }}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleRemoveFromWatchlist(item.stock_code);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
取消
|
<FavoriteButton
|
||||||
|
isFavorite={true}
|
||||||
|
isLoading={removingCode === item.stock_code}
|
||||||
|
onClick={() => handleUnwatch(item.stock_code)}
|
||||||
|
size="sm"
|
||||||
|
colorScheme="gold"
|
||||||
|
showTooltip={true}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
// src/hooks/useWatchlist.js
|
// src/hooks/useWatchlist.js
|
||||||
// 自选股管理自定义 Hook(导航栏专用,与 Redux 状态同步)
|
// 自选股管理自定义 Hook(与 Redux 状态同步,支持多组件共用)
|
||||||
|
|
||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { getApiBase } from '../utils/apiConfig';
|
import { getApiBase } from '../utils/apiConfig';
|
||||||
import { toggleWatchlist as toggleWatchlistAction, loadWatchlist } from '../store/slices/stockSlice';
|
import {
|
||||||
|
toggleWatchlist as toggleWatchlistAction,
|
||||||
|
loadWatchlist,
|
||||||
|
loadWatchlistQuotes
|
||||||
|
} from '../store/slices/stockSlice';
|
||||||
|
|
||||||
const WATCHLIST_PAGE_SIZE = 10;
|
const WATCHLIST_PAGE_SIZE = 10;
|
||||||
|
|
||||||
@@ -31,20 +35,18 @@ const WATCHLIST_PAGE_SIZE = 10;
|
|||||||
export const useWatchlist = () => {
|
export const useWatchlist = () => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [watchlistQuotes, setWatchlistQuotes] = useState([]);
|
|
||||||
const [watchlistLoading, setWatchlistLoading] = useState(false);
|
|
||||||
const [watchlistPage, setWatchlistPage] = useState(1);
|
const [watchlistPage, setWatchlistPage] = useState(1);
|
||||||
const [followingEvents, setFollowingEvents] = useState([]);
|
const [followingEvents, setFollowingEvents] = useState([]);
|
||||||
|
|
||||||
|
// 从 Redux 获取自选股数据(与 GlobalSidebar 共用)
|
||||||
|
const watchlistQuotes = useSelector(state => state.stock.watchlistQuotes || []);
|
||||||
|
const watchlistLoading = useSelector(state => state.stock.loading?.watchlistQuotes || false);
|
||||||
|
|
||||||
// 从 Redux 获取自选股列表长度(用于监听变化)
|
// 从 Redux 获取自选股列表长度(用于监听变化)
|
||||||
// 使用 length 作为依赖,避免数组引用变化导致不必要的重新渲染
|
|
||||||
const reduxWatchlistLength = useSelector(state => state.stock.watchlist?.length || 0);
|
const reduxWatchlistLength = useSelector(state => state.stock.watchlist?.length || 0);
|
||||||
|
|
||||||
// 检查 Redux watchlist 是否已初始化(加载状态)
|
|
||||||
const reduxWatchlistLoading = useSelector(state => state.stock.loading?.watchlist);
|
|
||||||
|
|
||||||
// 用于跟踪上一次的 watchlist 长度
|
// 用于跟踪上一次的 watchlist 长度
|
||||||
const prevWatchlistLengthRef = useRef(-1); // 初始设为 -1,确保第一次变化也能检测到
|
const prevWatchlistLengthRef = useRef(-1);
|
||||||
|
|
||||||
// 初始化时加载 Redux watchlist(确保 Redux 状态被初始化)
|
// 初始化时加载 Redux watchlist(确保 Redux 状态被初始化)
|
||||||
const hasInitializedRef = useRef(false);
|
const hasInitializedRef = useRef(false);
|
||||||
@@ -56,35 +58,11 @@ export const useWatchlist = () => {
|
|||||||
}
|
}
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
// 加载自选股实时行情
|
// 加载自选股实时行情(通过 Redux)
|
||||||
const loadWatchlistQuotes = useCallback(async () => {
|
const loadWatchlistQuotesFunc = useCallback(() => {
|
||||||
try {
|
logger.debug('useWatchlist', '触发 loadWatchlistQuotes');
|
||||||
setWatchlistLoading(true);
|
dispatch(loadWatchlistQuotes());
|
||||||
const base = getApiBase();
|
}, [dispatch]);
|
||||||
const resp = await fetch(base + '/api/account/watchlist/realtime', {
|
|
||||||
credentials: 'include',
|
|
||||||
cache: 'no-store'
|
|
||||||
});
|
|
||||||
if (resp.ok) {
|
|
||||||
const data = await resp.json();
|
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
|
||||||
setWatchlistQuotes(data.data);
|
|
||||||
logger.debug('useWatchlist', '自选股行情加载成功', { count: data.data.length });
|
|
||||||
} else {
|
|
||||||
setWatchlistQuotes([]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setWatchlistQuotes([]);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn('useWatchlist', '加载自选股实时行情失败', {
|
|
||||||
error: e.message
|
|
||||||
});
|
|
||||||
setWatchlistQuotes([]);
|
|
||||||
} finally {
|
|
||||||
setWatchlistLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 监听 Redux watchlist 长度变化,自动刷新行情数据
|
// 监听 Redux watchlist 长度变化,自动刷新行情数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -102,7 +80,7 @@ export const useWatchlist = () => {
|
|||||||
// 延迟一小段时间再刷新,确保后端数据已更新
|
// 延迟一小段时间再刷新,确保后端数据已更新
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
logger.debug('useWatchlist', '执行 loadWatchlistQuotes');
|
logger.debug('useWatchlist', '执行 loadWatchlistQuotes');
|
||||||
loadWatchlistQuotes();
|
dispatch(loadWatchlistQuotes());
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
prevWatchlistLengthRef.current = currentLength;
|
prevWatchlistLengthRef.current = currentLength;
|
||||||
@@ -111,66 +89,53 @@ export const useWatchlist = () => {
|
|||||||
|
|
||||||
// 更新 ref
|
// 更新 ref
|
||||||
prevWatchlistLengthRef.current = currentLength;
|
prevWatchlistLengthRef.current = currentLength;
|
||||||
}, [reduxWatchlistLength, loadWatchlistQuotes]);
|
}, [reduxWatchlistLength, dispatch]);
|
||||||
|
|
||||||
// 添加到自选股
|
// 添加到自选股(通过 Redux)
|
||||||
const handleAddToWatchlist = useCallback(async (stockCode, stockName) => {
|
const handleAddToWatchlist = useCallback(async (stockCode, stockName) => {
|
||||||
try {
|
try {
|
||||||
const base = getApiBase();
|
// 通过 Redux action 添加(乐观更新)
|
||||||
const resp = await fetch(base + '/api/account/watchlist', {
|
await dispatch(toggleWatchlistAction({
|
||||||
method: 'POST',
|
stockCode,
|
||||||
credentials: 'include',
|
stockName,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
isInWatchlist: false // 表示当前不在自选股中,需要添加
|
||||||
body: JSON.stringify({ stock_code: stockCode, stock_name: stockName })
|
})).unwrap();
|
||||||
});
|
|
||||||
const data = await resp.json().catch(() => ({}));
|
// 刷新行情
|
||||||
if (resp.ok && data.success) {
|
dispatch(loadWatchlistQuotes());
|
||||||
// 刷新自选股列表
|
|
||||||
loadWatchlistQuotes();
|
|
||||||
toast({ title: '已添加至自选股', status: 'success', duration: 1500 });
|
toast({ title: '已添加至自选股', status: 'success', duration: 1500 });
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
toast({ title: '添加失败', status: 'error', duration: 2000 });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: '网络错误,添加失败', status: 'error', duration: 2000 });
|
logger.error('useWatchlist', '添加自选股失败', e);
|
||||||
|
toast({ title: e.message || '添加失败', status: 'error', duration: 2000 });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, [toast, loadWatchlistQuotes]);
|
}, [dispatch, toast]);
|
||||||
|
|
||||||
// 从自选股移除
|
// 从自选股移除(通过 Redux)
|
||||||
const handleRemoveFromWatchlist = useCallback(async (stockCode) => {
|
const handleRemoveFromWatchlist = useCallback(async (stockCode) => {
|
||||||
try {
|
try {
|
||||||
// 找到股票名称
|
// 找到股票名称
|
||||||
const stockItem = watchlistQuotes.find(item => {
|
|
||||||
const normalize6 = (code) => {
|
const normalize6 = (code) => {
|
||||||
const m = String(code || '').match(/(\d{6})/);
|
const m = String(code || '').match(/(\d{6})/);
|
||||||
return m ? m[1] : String(code || '');
|
return m ? m[1] : String(code || '');
|
||||||
};
|
};
|
||||||
return normalize6(item.stock_code) === normalize6(stockCode);
|
const stockItem = watchlistQuotes.find(item =>
|
||||||
});
|
normalize6(item.stock_code) === normalize6(stockCode)
|
||||||
|
);
|
||||||
const stockName = stockItem?.stock_name || '';
|
const stockName = stockItem?.stock_name || '';
|
||||||
|
|
||||||
// 通过 Redux action 移除(会同步更新 Redux 状态)
|
// 通过 Redux action 移除(乐观更新)
|
||||||
await dispatch(toggleWatchlistAction({
|
await dispatch(toggleWatchlistAction({
|
||||||
stockCode,
|
stockCode,
|
||||||
stockName,
|
stockName,
|
||||||
isInWatchlist: true // 表示当前在自选股中,需要移除
|
isInWatchlist: true // 表示当前在自选股中,需要移除
|
||||||
})).unwrap();
|
})).unwrap();
|
||||||
|
|
||||||
// 更新本地状态(立即响应 UI)
|
// 更新分页(如果当前页超出范围)
|
||||||
setWatchlistQuotes((prev) => {
|
const newLength = watchlistQuotes.length - 1;
|
||||||
const normalize6 = (code) => {
|
const newMaxPage = Math.max(1, Math.ceil(newLength / WATCHLIST_PAGE_SIZE));
|
||||||
const m = String(code || '').match(/(\d{6})/);
|
setWatchlistPage(p => Math.min(p, newMaxPage));
|
||||||
return m ? m[1] : String(code || '');
|
|
||||||
};
|
|
||||||
const target = normalize6(stockCode);
|
|
||||||
const updated = (prev || []).filter((x) => normalize6(x.stock_code) !== target);
|
|
||||||
const newMaxPage = Math.max(1, Math.ceil((updated.length || 0) / WATCHLIST_PAGE_SIZE));
|
|
||||||
setWatchlistPage((p) => Math.min(p, newMaxPage));
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
|
|
||||||
toast({ title: '已从自选股移除', status: 'info', duration: 1500 });
|
toast({ title: '已从自选股移除', status: 'info', duration: 1500 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -195,7 +160,7 @@ export const useWatchlist = () => {
|
|||||||
watchlistPage,
|
watchlistPage,
|
||||||
setWatchlistPage,
|
setWatchlistPage,
|
||||||
WATCHLIST_PAGE_SIZE,
|
WATCHLIST_PAGE_SIZE,
|
||||||
loadWatchlistQuotes,
|
loadWatchlistQuotes: loadWatchlistQuotesFunc,
|
||||||
followingEvents,
|
followingEvents,
|
||||||
handleAddToWatchlist,
|
handleAddToWatchlist,
|
||||||
handleRemoveFromWatchlist,
|
handleRemoveFromWatchlist,
|
||||||
|
|||||||
@@ -292,6 +292,132 @@ export const loadAllStocks = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载自选股实时行情
|
||||||
|
* 用于统一行情刷新,两个面板共用
|
||||||
|
*/
|
||||||
|
export const loadWatchlistQuotes = createAsyncThunk(
|
||||||
|
'stock/loadWatchlistQuotes',
|
||||||
|
async () => {
|
||||||
|
logger.debug('stockSlice', 'loadWatchlistQuotes');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apiBase = getApiBase();
|
||||||
|
const response = await fetch(`${apiBase}/api/account/watchlist/realtime`, {
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && Array.isArray(data.data)) {
|
||||||
|
logger.debug('stockSlice', '自选股行情加载成功', { count: data.data.length });
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('stockSlice', 'loadWatchlistQuotes', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载关注事件列表
|
||||||
|
* 用于统一关注事件数据源,两个面板共用
|
||||||
|
*/
|
||||||
|
export const loadFollowingEvents = createAsyncThunk(
|
||||||
|
'stock/loadFollowingEvents',
|
||||||
|
async () => {
|
||||||
|
logger.debug('stockSlice', 'loadFollowingEvents');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apiBase = getApiBase();
|
||||||
|
const response = await fetch(`${apiBase}/api/account/events/following`, {
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && Array.isArray(data.data)) {
|
||||||
|
// 合并重复的事件(用最新的数据)
|
||||||
|
const eventMap = new Map();
|
||||||
|
for (const evt of data.data) {
|
||||||
|
if (evt && evt.id) {
|
||||||
|
eventMap.set(evt.id, evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const merged = Array.from(eventMap.values());
|
||||||
|
// 按创建时间降序排列
|
||||||
|
if (merged.length > 0 && merged[0].created_at) {
|
||||||
|
merged.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
|
||||||
|
} else {
|
||||||
|
merged.sort((a, b) => (b.id || 0) - (a.id || 0));
|
||||||
|
}
|
||||||
|
logger.debug('stockSlice', '关注事件列表加载成功', { count: merged.length });
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('stockSlice', 'loadFollowingEvents', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载用户评论列表
|
||||||
|
*/
|
||||||
|
export const loadEventComments = createAsyncThunk(
|
||||||
|
'stock/loadEventComments',
|
||||||
|
async () => {
|
||||||
|
logger.debug('stockSlice', 'loadEventComments');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apiBase = getApiBase();
|
||||||
|
const response = await fetch(`${apiBase}/api/account/events/posts`, {
|
||||||
|
credentials: 'include',
|
||||||
|
cache: 'no-store'
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && Array.isArray(data.data)) {
|
||||||
|
logger.debug('stockSlice', '用户评论列表加载成功', { count: data.data.length });
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('stockSlice', 'loadEventComments', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换关注事件状态(关注/取消关注)
|
||||||
|
*/
|
||||||
|
export const toggleFollowEvent = createAsyncThunk(
|
||||||
|
'stock/toggleFollowEvent',
|
||||||
|
async ({ eventId, isFollowing }) => {
|
||||||
|
logger.debug('stockSlice', 'toggleFollowEvent', { eventId, isFollowing });
|
||||||
|
|
||||||
|
const apiBase = getApiBase();
|
||||||
|
const response = await fetch(`${apiBase}/api/events/${eventId}/follow`, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || data.success === false) {
|
||||||
|
throw new Error(data.error || '操作失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { eventId, isFollowing };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换自选股状态
|
* 切换自选股状态
|
||||||
*/
|
*/
|
||||||
@@ -359,6 +485,15 @@ const stockSlice = createSlice({
|
|||||||
// 自选股列表 [{ stock_code, stock_name }]
|
// 自选股列表 [{ stock_code, stock_name }]
|
||||||
watchlist: [],
|
watchlist: [],
|
||||||
|
|
||||||
|
// 自选股实时行情 [{ stock_code, stock_name, price, change_percent, ... }]
|
||||||
|
watchlistQuotes: [],
|
||||||
|
|
||||||
|
// 关注事件列表 [{ id, title, event_type, ... }]
|
||||||
|
followingEvents: [],
|
||||||
|
|
||||||
|
// 用户评论列表 [{ id, content, event_id, ... }]
|
||||||
|
eventComments: [],
|
||||||
|
|
||||||
// 全部股票列表(用于前端模糊搜索)[{ code, name }]
|
// 全部股票列表(用于前端模糊搜索)[{ code, name }]
|
||||||
allStocks: [],
|
allStocks: [],
|
||||||
|
|
||||||
@@ -370,6 +505,9 @@ const stockSlice = createSlice({
|
|||||||
historicalEvents: false,
|
historicalEvents: false,
|
||||||
chainAnalysis: false,
|
chainAnalysis: false,
|
||||||
watchlist: false,
|
watchlist: false,
|
||||||
|
watchlistQuotes: false,
|
||||||
|
followingEvents: false,
|
||||||
|
eventComments: false,
|
||||||
allStocks: false
|
allStocks: false
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -517,6 +655,18 @@ const stockSlice = createSlice({
|
|||||||
state.loading.watchlist = false;
|
state.loading.watchlist = false;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ===== loadWatchlistQuotes =====
|
||||||
|
.addCase(loadWatchlistQuotes.pending, (state) => {
|
||||||
|
state.loading.watchlistQuotes = true;
|
||||||
|
})
|
||||||
|
.addCase(loadWatchlistQuotes.fulfilled, (state, action) => {
|
||||||
|
state.watchlistQuotes = action.payload;
|
||||||
|
state.loading.watchlistQuotes = false;
|
||||||
|
})
|
||||||
|
.addCase(loadWatchlistQuotes.rejected, (state) => {
|
||||||
|
state.loading.watchlistQuotes = false;
|
||||||
|
})
|
||||||
|
|
||||||
// ===== loadAllStocks =====
|
// ===== loadAllStocks =====
|
||||||
.addCase(loadAllStocks.pending, (state) => {
|
.addCase(loadAllStocks.pending, (state) => {
|
||||||
state.loading.allStocks = true;
|
state.loading.allStocks = true;
|
||||||
@@ -563,6 +713,47 @@ const stockSlice = createSlice({
|
|||||||
.addCase(toggleWatchlist.fulfilled, (state) => {
|
.addCase(toggleWatchlist.fulfilled, (state) => {
|
||||||
// 状态已在 pending 时更新,这里同步到 localStorage
|
// 状态已在 pending 时更新,这里同步到 localStorage
|
||||||
saveWatchlistToCache(state.watchlist);
|
saveWatchlistToCache(state.watchlist);
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== loadFollowingEvents =====
|
||||||
|
.addCase(loadFollowingEvents.pending, (state) => {
|
||||||
|
state.loading.followingEvents = true;
|
||||||
|
})
|
||||||
|
.addCase(loadFollowingEvents.fulfilled, (state, action) => {
|
||||||
|
state.followingEvents = action.payload;
|
||||||
|
state.loading.followingEvents = false;
|
||||||
|
})
|
||||||
|
.addCase(loadFollowingEvents.rejected, (state) => {
|
||||||
|
state.loading.followingEvents = false;
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== loadEventComments =====
|
||||||
|
.addCase(loadEventComments.pending, (state) => {
|
||||||
|
state.loading.eventComments = true;
|
||||||
|
})
|
||||||
|
.addCase(loadEventComments.fulfilled, (state, action) => {
|
||||||
|
state.eventComments = action.payload;
|
||||||
|
state.loading.eventComments = false;
|
||||||
|
})
|
||||||
|
.addCase(loadEventComments.rejected, (state) => {
|
||||||
|
state.loading.eventComments = false;
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== toggleFollowEvent(乐观更新)=====
|
||||||
|
// pending: 立即更新状态
|
||||||
|
.addCase(toggleFollowEvent.pending, (state, action) => {
|
||||||
|
const { eventId, isFollowing } = action.meta.arg;
|
||||||
|
if (isFollowing) {
|
||||||
|
// 当前已关注,取消关注 → 移除
|
||||||
|
state.followingEvents = state.followingEvents.filter(evt => evt.id !== eventId);
|
||||||
|
}
|
||||||
|
// 添加关注的情况需要事件完整数据,不在这里处理
|
||||||
|
})
|
||||||
|
// rejected: 回滚状态(仅取消关注需要回滚)
|
||||||
|
.addCase(toggleFollowEvent.rejected, (state, action) => {
|
||||||
|
// 取消关注失败时,需要刷新列表恢复数据
|
||||||
|
// 由于没有原始事件数据,这里只能触发重新加载
|
||||||
|
logger.warn('stockSlice', 'toggleFollowEvent rejected, 需要重新加载关注事件列表');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user