Merge branch 'feature_bugfix/251201_vf_h5_ui' into feature_2025/251209_stock_pref

* feature_bugfix/251201_vf_h5_ui:
  feat: 事件关注功能优化 - Redux 乐观更新 + Mock 数据状态同步
  feat: 投资日历自选股功能优化 - Redux 集成 + 乐观更新
  fix: 修复投资日历切换月份时自动打开事件弹窗的问题
  fix: 修复 CompanyOverview 中 Hooks 顺序错误
This commit is contained in:
zdl
2025-12-09 16:36:46 +08:00
7 changed files with 220 additions and 55 deletions

View File

@@ -1,5 +1,7 @@
// 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 {
Card, Calendar, Badge, Modal, Table, Tabs, Tag, Button, List, Spin, Empty,
Drawer, Typography, Divider, Space, Tooltip, message, Alert
@@ -24,6 +26,10 @@ const { TabPane } = Tabs;
const { Text, Title, Paragraph } = Typography;
const InvestmentCalendar = () => {
// Redux 状态
const dispatch = useDispatch();
const reduxWatchlist = useSelector(state => state.stock.watchlist);
// 权限控制
const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription();
const [upgradeModalOpen, setUpgradeModalOpen] = useState(false);
@@ -45,7 +51,6 @@ const InvestmentCalendar = () => {
const [selectedStock, setSelectedStock] = useState(null);
const [selectedEventTime, setSelectedEventTime] = useState(null); // 记录事件时间
const [followingIds, setFollowingIds] = useState([]); // 正在处理关注的事件ID列表
const [addingToWatchlist, setAddingToWatchlist] = 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(() => {
loadEventCounts(currentMonth);
}, [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
const cellRender = (current, info) => {
// 只处理日期单元格,月份单元格返回默认
@@ -220,7 +244,12 @@ const InvestmentCalendar = () => {
};
// 处理日期选择
const handleDateSelect = (value) => {
// info.source 区分选择来源:'date' = 点击日期,'month'/'year' = 切换月份/年份
const handleDateSelect = (value, info) => {
// 只有点击日期单元格时才打开弹窗,切换月份/年份时不打开
if (info?.source !== 'date') {
return;
}
setSelectedDate(value);
loadDateEvents(value);
setModalVisible(true);
@@ -379,42 +408,35 @@ const InvestmentCalendar = () => {
}
};
// 添加单只股票到自选(支持新旧格式
// 添加单只股票到自选(乐观更新,无需 loading 状态
const addSingleToWatchlist = async (stock) => {
// 兼容新旧格式
const code = stock.code || stock[0];
const name = stock.name || stock[1];
const stockCode = getSixDigitCode(code);
setAddingToWatchlist(prev => ({ ...prev, [stockCode]: true }));
// 检查是否已在自选中
if (isStockInWatchlist(code)) {
message.info(`${name} 已在自选中`);
return;
}
try {
const response = await fetch('/api/account/watchlist', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
stock_code: stockCode, // 使用六位代码
stock_name: name // 股票名称
})
});
// 乐观更新dispatch 后 Redux 立即更新状态UI 立即响应
await dispatch(toggleWatchlist({
stockCode,
stockName: name,
isInWatchlist: false // false 表示添加
})).unwrap();
const data = await response.json();
if (data.success) {
message.success(`已将 ${name}(${stockCode}) 添加到自选`);
} else {
message.error(data.error || '添加失败');
}
message.success(`已将 ${name}(${stockCode}) 添加到自选`);
} catch (error) {
// 失败时 Redux 会自动回滚状态
logger.error('InvestmentCalendar', 'addSingleToWatchlist', error, {
stockCode,
stockName: name
});
message.error('添加失败,请重试');
} finally {
setAddingToWatchlist(prev => ({ ...prev, [stockCode]: false }));
}
};
@@ -793,17 +815,16 @@ const InvestmentCalendar = () => {
key: 'action',
width: 100,
render: (_, record) => {
const stockCode = getSixDigitCode(record.code);
const isAdding = addingToWatchlist[stockCode] || false;
const inWatchlist = isStockInWatchlist(record.code);
return (
<Button
type="default"
type={inWatchlist ? "primary" : "default"}
size="small"
loading={isAdding}
onClick={() => addSingleToWatchlist(record)}
disabled={inWatchlist}
>
加自选
{inWatchlist ? '已关注' : '加自选'}
</Button>
);
}