feat: 投资日历自选股功能优化 - Redux 集成 + 乐观更新
- InvestmentCalendar: 集成 Redux 管理自选股状态 - InvestmentCalendar: 添加 isStockInWatchlist 检查,防止重复添加 - InvestmentCalendar: 按钮状态实时切换(加自选/已关注) - stockSlice: 实现乐观更新,pending 时立即更新 UI - stockSlice: rejected 时自动回滚状态并提示错误 - 移除不必要的 loading 状态,提升用户体验 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
// src/components/InvestmentCalendar/index.js
|
// src/components/InvestmentCalendar/index.js
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
import { loadWatchlist, toggleWatchlist } from '@store/slices/stockSlice';
|
||||||
import {
|
import {
|
||||||
Card, Calendar, Badge, Modal, Table, Tabs, Tag, Button, List, Spin, Empty,
|
Card, Calendar, Badge, Modal, Table, Tabs, Tag, Button, List, Spin, Empty,
|
||||||
Drawer, Typography, Divider, Space, Tooltip, message, Alert
|
Drawer, Typography, Divider, Space, Tooltip, message, Alert
|
||||||
@@ -24,6 +26,10 @@ const { TabPane } = Tabs;
|
|||||||
const { Text, Title, Paragraph } = Typography;
|
const { Text, Title, Paragraph } = Typography;
|
||||||
|
|
||||||
const InvestmentCalendar = () => {
|
const InvestmentCalendar = () => {
|
||||||
|
// Redux 状态
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const reduxWatchlist = useSelector(state => state.stock.watchlist);
|
||||||
|
|
||||||
// 权限控制
|
// 权限控制
|
||||||
const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription();
|
const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription();
|
||||||
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
|
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
|
||||||
@@ -45,7 +51,6 @@ const InvestmentCalendar = () => {
|
|||||||
const [selectedStock, setSelectedStock] = useState(null);
|
const [selectedStock, setSelectedStock] = useState(null);
|
||||||
const [selectedEventTime, setSelectedEventTime] = useState(null); // 记录事件时间
|
const [selectedEventTime, setSelectedEventTime] = useState(null); // 记录事件时间
|
||||||
const [followingIds, setFollowingIds] = useState([]); // 正在处理关注的事件ID列表
|
const [followingIds, setFollowingIds] = useState([]); // 正在处理关注的事件ID列表
|
||||||
const [addingToWatchlist, setAddingToWatchlist] = useState({}); // 正在添加到自选的股票代码
|
|
||||||
const [expandedReasons, setExpandedReasons] = useState({}); // 跟踪每个股票关联理由的展开状态
|
const [expandedReasons, setExpandedReasons] = useState({}); // 跟踪每个股票关联理由的展开状态
|
||||||
|
|
||||||
// 加载月度事件统计
|
// 加载月度事件统计
|
||||||
@@ -174,10 +179,29 @@ const InvestmentCalendar = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 使用 ref 确保只加载一次自选股
|
||||||
|
const watchlistLoadedRef = useRef(false);
|
||||||
|
|
||||||
|
// 组件挂载时加载自选股列表(仅加载一次)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!watchlistLoadedRef.current) {
|
||||||
|
watchlistLoadedRef.current = true;
|
||||||
|
dispatch(loadWatchlist());
|
||||||
|
}
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadEventCounts(currentMonth);
|
loadEventCounts(currentMonth);
|
||||||
}, [currentMonth, loadEventCounts]);
|
}, [currentMonth, loadEventCounts]);
|
||||||
|
|
||||||
|
// 检查股票是否已在自选中
|
||||||
|
const isStockInWatchlist = useCallback((stockCode) => {
|
||||||
|
const sixDigitCode = getSixDigitCode(stockCode);
|
||||||
|
return reduxWatchlist.some(item =>
|
||||||
|
getSixDigitCode(item.stock_code) === sixDigitCode
|
||||||
|
);
|
||||||
|
}, [reduxWatchlist]);
|
||||||
|
|
||||||
// 自定义日期单元格渲染(Ant Design 5.x API)
|
// 自定义日期单元格渲染(Ant Design 5.x API)
|
||||||
const cellRender = (current, info) => {
|
const cellRender = (current, info) => {
|
||||||
// 只处理日期单元格,月份单元格返回默认
|
// 只处理日期单元格,月份单元格返回默认
|
||||||
@@ -384,42 +408,35 @@ const InvestmentCalendar = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加单只股票到自选(支持新旧格式)
|
// 添加单只股票到自选(乐观更新,无需 loading 状态)
|
||||||
const addSingleToWatchlist = async (stock) => {
|
const addSingleToWatchlist = async (stock) => {
|
||||||
// 兼容新旧格式
|
// 兼容新旧格式
|
||||||
const code = stock.code || stock[0];
|
const code = stock.code || stock[0];
|
||||||
const name = stock.name || stock[1];
|
const name = stock.name || stock[1];
|
||||||
const stockCode = getSixDigitCode(code);
|
const stockCode = getSixDigitCode(code);
|
||||||
|
|
||||||
setAddingToWatchlist(prev => ({ ...prev, [stockCode]: true }));
|
// 检查是否已在自选中
|
||||||
|
if (isStockInWatchlist(code)) {
|
||||||
|
message.info(`${name} 已在自选中`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/account/watchlist', {
|
// 乐观更新:dispatch 后 Redux 立即更新状态,UI 立即响应
|
||||||
method: 'POST',
|
await dispatch(toggleWatchlist({
|
||||||
headers: {
|
stockCode,
|
||||||
'Content-Type': 'application/json',
|
stockName: name,
|
||||||
},
|
isInWatchlist: false // false 表示添加
|
||||||
credentials: 'include',
|
})).unwrap();
|
||||||
body: JSON.stringify({
|
|
||||||
stock_code: stockCode, // 使用六位代码
|
|
||||||
stock_name: name // 股票名称
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
message.success(`已将 ${name}(${stockCode}) 添加到自选`);
|
||||||
if (data.success) {
|
|
||||||
message.success(`已将 ${name}(${stockCode}) 添加到自选`);
|
|
||||||
} else {
|
|
||||||
message.error(data.error || '添加失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 失败时 Redux 会自动回滚状态
|
||||||
logger.error('InvestmentCalendar', 'addSingleToWatchlist', error, {
|
logger.error('InvestmentCalendar', 'addSingleToWatchlist', error, {
|
||||||
stockCode,
|
stockCode,
|
||||||
stockName: name
|
stockName: name
|
||||||
});
|
});
|
||||||
message.error('添加失败,请重试');
|
message.error('添加失败,请重试');
|
||||||
} finally {
|
|
||||||
setAddingToWatchlist(prev => ({ ...prev, [stockCode]: false }));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -796,17 +813,16 @@ const InvestmentCalendar = () => {
|
|||||||
key: 'action',
|
key: 'action',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const stockCode = getSixDigitCode(record.code);
|
const inWatchlist = isStockInWatchlist(record.code);
|
||||||
const isAdding = addingToWatchlist[stockCode] || false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type={inWatchlist ? "primary" : "default"}
|
||||||
size="small"
|
size="small"
|
||||||
loading={isAdding}
|
|
||||||
onClick={() => addSingleToWatchlist(record)}
|
onClick={() => addSingleToWatchlist(record)}
|
||||||
|
disabled={inWatchlist}
|
||||||
>
|
>
|
||||||
加自选
|
{inWatchlist ? '已关注' : '加自选'}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -440,9 +440,10 @@ const stockSlice = createSlice({
|
|||||||
state.loading.allStocks = false;
|
state.loading.allStocks = false;
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== toggleWatchlist =====
|
// ===== toggleWatchlist(乐观更新)=====
|
||||||
.addCase(toggleWatchlist.fulfilled, (state, action) => {
|
// pending: 立即更新状态
|
||||||
const { stockCode, stockName, isInWatchlist } = action.payload;
|
.addCase(toggleWatchlist.pending, (state, action) => {
|
||||||
|
const { stockCode, stockName, isInWatchlist } = action.meta.arg;
|
||||||
if (isInWatchlist) {
|
if (isInWatchlist) {
|
||||||
// 移除
|
// 移除
|
||||||
state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode);
|
state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode);
|
||||||
@@ -453,6 +454,25 @@ const stockSlice = createSlice({
|
|||||||
state.watchlist.push({ stock_code: stockCode, stock_name: stockName });
|
state.watchlist.push({ stock_code: stockCode, stock_name: stockName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
// rejected: 回滚状态
|
||||||
|
.addCase(toggleWatchlist.rejected, (state, action) => {
|
||||||
|
const { stockCode, stockName, isInWatchlist } = action.meta.arg;
|
||||||
|
// 回滚:与 pending 操作相反
|
||||||
|
if (isInWatchlist) {
|
||||||
|
// 之前移除了,现在加回来
|
||||||
|
const exists = state.watchlist.some(item => item.stock_code === stockCode);
|
||||||
|
if (!exists) {
|
||||||
|
state.watchlist.push({ stock_code: stockCode, stock_name: stockName });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 之前添加了,现在移除
|
||||||
|
state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// fulfilled: 乐观更新模式下状态已在 pending 更新,这里无需操作
|
||||||
|
.addCase(toggleWatchlist.fulfilled, () => {
|
||||||
|
// 状态已在 pending 时更新
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user