refactor(Planning): 投资规划中心重构为 Redux 状态管理
- 新增 planningSlice 管理计划/复盘数据 - InvestmentPlanningCenter 改用 Redux 而非本地 state - 列表和日历视图共享同一数据源,保持同步 - 优化 Mock handlers,改进事件 ID 生成和调试日志 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -770,6 +770,68 @@ export const mockInvestmentPlans = [
|
|||||||
updated_at: '2024-10-08T10:00:00Z',
|
updated_at: '2024-10-08T10:00:00Z',
|
||||||
tags: ['季度复盘', '半导体', 'Q3'],
|
tags: ['季度复盘', '半导体', 'Q3'],
|
||||||
stocks: ['688981.SH', '002371.SZ']
|
stocks: ['688981.SH', '002371.SZ']
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== 今日数据(用于日历视图展示) ====================
|
||||||
|
{
|
||||||
|
id: 320,
|
||||||
|
user_id: 1,
|
||||||
|
type: 'plan',
|
||||||
|
title: '今日交易计划 - 年末布局',
|
||||||
|
content: `【今日目标】
|
||||||
|
重点关注年末资金流向,寻找低位优质标的布局机会。
|
||||||
|
|
||||||
|
【操作计划】
|
||||||
|
1. 白酒板块:观察茅台、五粮液走势,若出现回调可适当加仓
|
||||||
|
2. 新能源:宁德时代逢低补仓,目标价位160元附近
|
||||||
|
3. AI算力:关注寒武纪的突破信号
|
||||||
|
|
||||||
|
【资金安排】
|
||||||
|
- 当前仓位:65%
|
||||||
|
- 可动用资金:35%
|
||||||
|
- 计划使用资金:15%(分3笔建仓)
|
||||||
|
|
||||||
|
【风险控制】
|
||||||
|
- 单笔止损:-3%
|
||||||
|
- 日内最大亏损:-5%
|
||||||
|
- 不追涨,只接回调`,
|
||||||
|
target_date: '2025-12-23',
|
||||||
|
status: 'active',
|
||||||
|
created_at: '2025-12-23T08:30:00Z',
|
||||||
|
updated_at: '2025-12-23T08:30:00Z',
|
||||||
|
tags: ['日计划', '年末布局'],
|
||||||
|
stocks: ['600519.SH', '300750.SZ', '688256.SH']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 321,
|
||||||
|
user_id: 1,
|
||||||
|
type: 'review',
|
||||||
|
title: '今日交易复盘 - 市场震荡',
|
||||||
|
content: `【操作回顾】
|
||||||
|
1. 上午10:30 在茅台1580元位置加仓0.5%
|
||||||
|
2. 下午14:00 宁德时代触及160元支撑位,建仓1%
|
||||||
|
3. AI算力板块异动,寒武纪涨幅超5%,观望未操作
|
||||||
|
|
||||||
|
【盈亏分析】
|
||||||
|
- 茅台加仓部分:浮盈+0.8%
|
||||||
|
- 宁德时代:浮亏-0.3%(正常波动范围内)
|
||||||
|
- 当日账户变动:+0.15%
|
||||||
|
|
||||||
|
【经验总结】
|
||||||
|
- 茅台买点把握较好,符合预期的回调位置
|
||||||
|
- 宁德时代略显急躁,可以再等一等
|
||||||
|
- AI算力虽然错过涨幅,但不追高的纪律执行到位
|
||||||
|
|
||||||
|
【明日计划】
|
||||||
|
- 继续持有今日新增仓位
|
||||||
|
- 如茅台继续上涨至1620,可考虑获利了结一半
|
||||||
|
- 关注周五PMI数据公布对市场影响`,
|
||||||
|
target_date: '2025-12-23',
|
||||||
|
status: 'completed',
|
||||||
|
created_at: '2025-12-23T15:30:00Z',
|
||||||
|
updated_at: '2025-12-23T16:00:00Z',
|
||||||
|
tags: ['日复盘', '年末交易'],
|
||||||
|
stocks: ['600519.SH', '300750.SZ']
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1101,6 +1163,87 @@ export const mockCalendarEvents = [
|
|||||||
is_recurring: true,
|
is_recurring: true,
|
||||||
recurrence_rule: 'weekly',
|
recurrence_rule: 'weekly',
|
||||||
created_at: '2025-01-01T10:00:00Z'
|
created_at: '2025-01-01T10:00:00Z'
|
||||||
|
},
|
||||||
|
|
||||||
|
// ==================== 今日事件(2025-12-23) ====================
|
||||||
|
{
|
||||||
|
id: 409,
|
||||||
|
user_id: 1,
|
||||||
|
title: '比亚迪全球发布会',
|
||||||
|
date: '2025-12-23',
|
||||||
|
event_date: '2025-12-23',
|
||||||
|
type: 'earnings',
|
||||||
|
category: 'company_event',
|
||||||
|
description: `比亚迪将于今日14:00召开全球发布会,预计发布新一代刀片电池技术和2026年新车规划。
|
||||||
|
|
||||||
|
重点关注:
|
||||||
|
1. 刀片电池2.0技术参数:能量密度提升预期
|
||||||
|
2. 2026年新车型规划:高端品牌仰望系列
|
||||||
|
3. 海外市场扩张计划:欧洲建厂进度
|
||||||
|
4. 年度交付量预告
|
||||||
|
|
||||||
|
投资建议:
|
||||||
|
- 关注发布会后股价走势
|
||||||
|
- 若技术突破超预期,可考虑加仓
|
||||||
|
- 设置止损位:当前价-5%`,
|
||||||
|
stock_code: '002594.SZ',
|
||||||
|
stock_name: '比亚迪',
|
||||||
|
importance: 5,
|
||||||
|
source: 'future',
|
||||||
|
stocks: ['002594.SZ', '300750.SZ', '601238.SH'],
|
||||||
|
created_at: '2025-12-20T10:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 410,
|
||||||
|
user_id: 1,
|
||||||
|
title: '12月LPR报价公布',
|
||||||
|
date: '2025-12-23',
|
||||||
|
event_date: '2025-12-23',
|
||||||
|
type: 'policy',
|
||||||
|
category: 'macro_policy',
|
||||||
|
description: `中国人民银行将于今日9:30公布12月贷款市场报价利率(LPR)。
|
||||||
|
|
||||||
|
市场预期:
|
||||||
|
- 1年期LPR:3.10%(维持不变)
|
||||||
|
- 5年期以上LPR:3.60%(维持不变)
|
||||||
|
|
||||||
|
影响板块:
|
||||||
|
1. 银行板块:利差压力关注
|
||||||
|
2. 房地产:按揭成本影响
|
||||||
|
3. 基建:融资成本变化
|
||||||
|
|
||||||
|
投资策略:
|
||||||
|
- 若降息,利好成长股,可加仓科技板块
|
||||||
|
- 若维持,银行股防守价值凸显`,
|
||||||
|
importance: 5,
|
||||||
|
source: 'future',
|
||||||
|
stocks: ['601398.SH', '600036.SH', '000001.SZ'],
|
||||||
|
created_at: '2025-12-20T08:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 411,
|
||||||
|
user_id: 1,
|
||||||
|
title: 'A股年末交易策略会议',
|
||||||
|
date: '2025-12-23',
|
||||||
|
event_date: '2025-12-23',
|
||||||
|
type: 'reminder',
|
||||||
|
category: 'personal',
|
||||||
|
description: `个人备忘:年末交易策略规划
|
||||||
|
|
||||||
|
待办事项:
|
||||||
|
1. 回顾2025年度投资收益
|
||||||
|
2. 分析持仓股票基本面变化
|
||||||
|
3. 制定2026年Q1布局计划
|
||||||
|
4. 检查止盈止损纪律执行情况
|
||||||
|
|
||||||
|
重点关注:
|
||||||
|
- 白酒板块持仓是否需要调整
|
||||||
|
- 新能源板块估值是否合理
|
||||||
|
- 是否需要增加防守性配置`,
|
||||||
|
importance: 3,
|
||||||
|
source: 'user',
|
||||||
|
stocks: [],
|
||||||
|
created_at: '2025-12-22T20:00:00Z'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -351,15 +351,21 @@ export const accountHandlers = [
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
console.log('[Mock] 创建投资计划:', body);
|
console.log('[Mock] 创建投资计划:', body);
|
||||||
|
|
||||||
|
// 生成唯一 ID(使用时间戳避免冲突)
|
||||||
|
const newId = Date.now();
|
||||||
|
|
||||||
const newPlan = {
|
const newPlan = {
|
||||||
id: mockInvestmentPlans.length + 301,
|
id: newId,
|
||||||
user_id: currentUser.id,
|
user_id: currentUser.id,
|
||||||
...body,
|
...body,
|
||||||
|
// 确保 target_date 字段存在(兼容前端发送的 date 字段)
|
||||||
|
target_date: body.target_date || body.date,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
mockInvestmentPlans.push(newPlan);
|
mockInvestmentPlans.push(newPlan);
|
||||||
|
console.log('[Mock] 新增计划/复盘,当前总数:', mockInvestmentPlans.length);
|
||||||
|
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -488,13 +494,22 @@ export const accountHandlers = [
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打印今天的事件(方便调试)
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
const todayEvents = filteredEvents.filter(e =>
|
||||||
|
(e.date === today || e.event_date === today)
|
||||||
|
);
|
||||||
|
|
||||||
console.log('[Mock] 日历事件详情:', {
|
console.log('[Mock] 日历事件详情:', {
|
||||||
currentUserId: currentUser.id,
|
currentUserId: currentUser.id,
|
||||||
calendarEvents: calendarEvents.length,
|
calendarEvents: calendarEvents.length,
|
||||||
investmentPlansAsEvents: investmentPlansAsEvents.length,
|
investmentPlansAsEvents: investmentPlansAsEvents.length,
|
||||||
total: filteredEvents.length,
|
total: filteredEvents.length,
|
||||||
plansCount: filteredEvents.filter(e => e.type === 'plan').length,
|
plansCount: filteredEvents.filter(e => e.type === 'plan').length,
|
||||||
reviewsCount: filteredEvents.filter(e => e.type === 'review').length
|
reviewsCount: filteredEvents.filter(e => e.type === 'review').length,
|
||||||
|
today,
|
||||||
|
todayEventsCount: todayEvents.length,
|
||||||
|
todayEventTitles: todayEvents.map(e => `[${e.type}] ${e.title}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import stockReducer from './slices/stockSlice';
|
|||||||
import authModalReducer from './slices/authModalSlice';
|
import authModalReducer from './slices/authModalSlice';
|
||||||
import subscriptionReducer from './slices/subscriptionSlice';
|
import subscriptionReducer from './slices/subscriptionSlice';
|
||||||
import deviceReducer from './slices/deviceSlice'; // ✅ 设备检测状态管理
|
import deviceReducer from './slices/deviceSlice'; // ✅ 设备检测状态管理
|
||||||
|
import planningReducer from './slices/planningSlice'; // ✅ 投资规划中心状态管理
|
||||||
import { eventsApi } from './api/eventsApi'; // ✅ RTK Query API
|
import { eventsApi } from './api/eventsApi'; // ✅ RTK Query API
|
||||||
|
|
||||||
// ⚡ 基础 reducers(首屏必需)
|
// ⚡ 基础 reducers(首屏必需)
|
||||||
@@ -19,6 +20,7 @@ const staticReducers = {
|
|||||||
authModal: authModalReducer, // ✅ 认证弹窗状态管理
|
authModal: authModalReducer, // ✅ 认证弹窗状态管理
|
||||||
subscription: subscriptionReducer, // ✅ 订阅信息状态管理
|
subscription: subscriptionReducer, // ✅ 订阅信息状态管理
|
||||||
device: deviceReducer, // ✅ 设备检测状态管理(移动端/桌面端)
|
device: deviceReducer, // ✅ 设备检测状态管理(移动端/桌面端)
|
||||||
|
planning: planningReducer, // ✅ 投资规划中心状态管理
|
||||||
[eventsApi.reducerPath]: eventsApi.reducer, // ✅ RTK Query 事件 API
|
[eventsApi.reducerPath]: eventsApi.reducer, // ✅ RTK Query 事件 API
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
266
src/store/slices/planningSlice.ts
Normal file
266
src/store/slices/planningSlice.ts
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
/**
|
||||||
|
* planningSlice - 投资规划中心 Redux Slice
|
||||||
|
* 管理计划、复盘和日历事件数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import type { InvestmentEvent } from '@/types';
|
||||||
|
import { getApiBase } from '@/utils/apiConfig';
|
||||||
|
import { logger } from '@/utils/logger';
|
||||||
|
|
||||||
|
// ==================== State 类型定义 ====================
|
||||||
|
|
||||||
|
interface PlanningState {
|
||||||
|
/** 所有事件(计划 + 复盘 + 系统事件) */
|
||||||
|
allEvents: InvestmentEvent[];
|
||||||
|
/** 加载状态 */
|
||||||
|
loading: boolean;
|
||||||
|
/** 错误信息 */
|
||||||
|
error: string | null;
|
||||||
|
/** 最后更新时间 */
|
||||||
|
lastUpdated: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 初始状态 ====================
|
||||||
|
|
||||||
|
const initialState: PlanningState = {
|
||||||
|
allEvents: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== Async Thunks ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载所有事件数据
|
||||||
|
*/
|
||||||
|
export const fetchAllEvents = createAsyncThunk(
|
||||||
|
'planning/fetchAllEvents',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const base = getApiBase();
|
||||||
|
const response = await fetch(`${base}/api/account/calendar/events`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
logger.debug('planningSlice', '数据加载成功', {
|
||||||
|
count: data.data?.length || 0,
|
||||||
|
});
|
||||||
|
return data.data || [];
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || '加载失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('planningSlice', 'fetchAllEvents', error);
|
||||||
|
return rejectWithValue(error instanceof Error ? error.message : '加载失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建计划/复盘
|
||||||
|
*/
|
||||||
|
export const createEvent = createAsyncThunk(
|
||||||
|
'planning/createEvent',
|
||||||
|
async (eventData: Partial<InvestmentEvent>, { rejectWithValue, dispatch }) => {
|
||||||
|
try {
|
||||||
|
const base = getApiBase();
|
||||||
|
const response = await fetch(`${base}/api/account/investment-plans`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(eventData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
logger.info('planningSlice', '创建成功', { title: eventData.title });
|
||||||
|
// 创建成功后重新加载所有数据
|
||||||
|
dispatch(fetchAllEvents());
|
||||||
|
return data.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || '创建失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('planningSlice', 'createEvent', error);
|
||||||
|
return rejectWithValue(error instanceof Error ? error.message : '创建失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新计划/复盘
|
||||||
|
*/
|
||||||
|
export const updateEvent = createAsyncThunk(
|
||||||
|
'planning/updateEvent',
|
||||||
|
async ({ id, data }: { id: number; data: Partial<InvestmentEvent> }, { rejectWithValue, dispatch }) => {
|
||||||
|
try {
|
||||||
|
const base = getApiBase();
|
||||||
|
const response = await fetch(`${base}/api/account/investment-plans/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
logger.info('planningSlice', '更新成功', { id });
|
||||||
|
// 更新成功后重新加载所有数据
|
||||||
|
dispatch(fetchAllEvents());
|
||||||
|
return result.data;
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || '更新失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('planningSlice', 'updateEvent', error);
|
||||||
|
return rejectWithValue(error instanceof Error ? error.message : '更新失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除计划/复盘
|
||||||
|
*/
|
||||||
|
export const deleteEvent = createAsyncThunk(
|
||||||
|
'planning/deleteEvent',
|
||||||
|
async (id: number, { rejectWithValue, dispatch }) => {
|
||||||
|
try {
|
||||||
|
const base = getApiBase();
|
||||||
|
const response = await fetch(`${base}/api/account/investment-plans/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
logger.info('planningSlice', '删除成功', { id });
|
||||||
|
// 删除成功后重新加载所有数据
|
||||||
|
dispatch(fetchAllEvents());
|
||||||
|
return id;
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('planningSlice', 'deleteEvent', error);
|
||||||
|
return rejectWithValue(error instanceof Error ? error.message : '删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== Slice ====================
|
||||||
|
|
||||||
|
const planningSlice = createSlice({
|
||||||
|
name: 'planning',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
/** 清空数据 */
|
||||||
|
clearEvents: (state) => {
|
||||||
|
state.allEvents = [];
|
||||||
|
state.error = null;
|
||||||
|
},
|
||||||
|
/** 直接设置事件(用于乐观更新) */
|
||||||
|
setEvents: (state, action: PayloadAction<InvestmentEvent[]>) => {
|
||||||
|
state.allEvents = action.payload;
|
||||||
|
state.lastUpdated = Date.now();
|
||||||
|
},
|
||||||
|
/** 添加单个事件(乐观更新) */
|
||||||
|
addEvent: (state, action: PayloadAction<InvestmentEvent>) => {
|
||||||
|
state.allEvents.push(action.payload);
|
||||||
|
state.lastUpdated = Date.now();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
// fetchAllEvents
|
||||||
|
.addCase(fetchAllEvents.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
state.error = null;
|
||||||
|
})
|
||||||
|
.addCase(fetchAllEvents.fulfilled, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.allEvents = action.payload;
|
||||||
|
state.lastUpdated = Date.now();
|
||||||
|
})
|
||||||
|
.addCase(fetchAllEvents.rejected, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
})
|
||||||
|
// createEvent
|
||||||
|
.addCase(createEvent.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
})
|
||||||
|
.addCase(createEvent.fulfilled, (state) => {
|
||||||
|
state.loading = false;
|
||||||
|
})
|
||||||
|
.addCase(createEvent.rejected, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
})
|
||||||
|
// updateEvent
|
||||||
|
.addCase(updateEvent.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
})
|
||||||
|
.addCase(updateEvent.fulfilled, (state) => {
|
||||||
|
state.loading = false;
|
||||||
|
})
|
||||||
|
.addCase(updateEvent.rejected, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
})
|
||||||
|
// deleteEvent
|
||||||
|
.addCase(deleteEvent.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
})
|
||||||
|
.addCase(deleteEvent.fulfilled, (state) => {
|
||||||
|
state.loading = false;
|
||||||
|
})
|
||||||
|
.addCase(deleteEvent.rejected, (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.error = action.payload as string;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== 导出 ====================
|
||||||
|
|
||||||
|
export const { clearEvents, setEvents, addEvent } = planningSlice.actions;
|
||||||
|
|
||||||
|
export default planningSlice.reducer;
|
||||||
|
|
||||||
|
// ==================== Selectors ====================
|
||||||
|
|
||||||
|
export const selectAllEvents = (state: { planning: PlanningState }) => state.planning.allEvents;
|
||||||
|
export const selectPlanningLoading = (state: { planning: PlanningState }) => state.planning.loading;
|
||||||
|
export const selectPlanningError = (state: { planning: PlanningState }) => state.planning.error;
|
||||||
|
export const selectPlans = (state: { planning: PlanningState }) =>
|
||||||
|
state.planning.allEvents.filter(e => e.type === 'plan' && e.source !== 'future');
|
||||||
|
export const selectReviews = (state: { planning: PlanningState }) =>
|
||||||
|
state.planning.allEvents.filter(e => e.type === 'review' && e.source !== 'future');
|
||||||
@@ -23,6 +23,8 @@ import type { EventClickArg } from '@fullcalendar/core';
|
|||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
import { useAppSelector } from '@/store/hooks';
|
||||||
|
import { selectAllEvents } from '@/store/slices/planningSlice';
|
||||||
import { usePlanningData } from './PlanningContext';
|
import { usePlanningData } from './PlanningContext';
|
||||||
import { EventDetailModal } from './EventDetailModal';
|
import { EventDetailModal } from './EventDetailModal';
|
||||||
import type { InvestmentEvent } from '@/types';
|
import type { InvestmentEvent } from '@/types';
|
||||||
@@ -53,8 +55,11 @@ interface CalendarEvent {
|
|||||||
* 日历视图面板,显示所有投资事件
|
* 日历视图面板,显示所有投资事件
|
||||||
*/
|
*/
|
||||||
export const CalendarPanel: React.FC = () => {
|
export const CalendarPanel: React.FC = () => {
|
||||||
|
// 从 Redux 获取数据(确保与列表视图同步)
|
||||||
|
const allEvents = useAppSelector(selectAllEvents);
|
||||||
|
|
||||||
|
// UI 相关状态仍从 Context 获取
|
||||||
const {
|
const {
|
||||||
allEvents,
|
|
||||||
borderColor,
|
borderColor,
|
||||||
secondaryText,
|
secondaryText,
|
||||||
setViewMode,
|
setViewMode,
|
||||||
@@ -143,11 +148,33 @@ export const CalendarPanel: React.FC = () => {
|
|||||||
opacity: '1 !important',
|
opacity: '1 !important',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 星期头(周日、周一等)- 金色
|
||||||
|
'.fc-col-header-cell-cushion': {
|
||||||
|
color: '#D4AF37 !important',
|
||||||
|
fontWeight: '600 !important',
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
|
// 日期数字 - 增强可见性
|
||||||
|
'.fc-daygrid-day-number': {
|
||||||
|
color: 'rgba(255, 255, 255, 0.9) !important',
|
||||||
|
fontWeight: '600 !important',
|
||||||
|
fontSize: '14px !important',
|
||||||
|
padding: '4px 8px !important',
|
||||||
|
},
|
||||||
|
// 非当前月份日期(灰色)
|
||||||
|
'.fc-day-other .fc-daygrid-day-number': {
|
||||||
|
color: 'rgba(255, 255, 255, 0.35) !important',
|
||||||
|
},
|
||||||
// 今天日期高亮边框
|
// 今天日期高亮边框
|
||||||
'.fc-daygrid-day.fc-day-today': {
|
'.fc-daygrid-day.fc-day-today': {
|
||||||
border: '2px solid #D4AF37 !important',
|
border: '2px solid #D4AF37 !important',
|
||||||
backgroundColor: 'rgba(212, 175, 55, 0.1) !important',
|
backgroundColor: 'rgba(212, 175, 55, 0.1) !important',
|
||||||
},
|
},
|
||||||
|
// 今天日期数字 - 金色
|
||||||
|
'.fc-daygrid-day.fc-day-today .fc-daygrid-day-number': {
|
||||||
|
color: '#D4AF37 !important',
|
||||||
|
fontWeight: '700 !important',
|
||||||
|
},
|
||||||
// 标题金色渐变
|
// 标题金色渐变
|
||||||
'.fc .fc-toolbar-title': {
|
'.fc .fc-toolbar-title': {
|
||||||
background: 'linear-gradient(135deg, #D4AF37 0%, #F5E6A3 100%)',
|
background: 'linear-gradient(135deg, #D4AF37 0%, #F5E6A3 100%)',
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import 'dayjs/locale/zh-cn';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { useAppDispatch } from '@/store/hooks';
|
import { useAppDispatch } from '@/store/hooks';
|
||||||
import { usePlanningData } from './PlanningContext';
|
import { fetchAllEvents } from '@/store/slices/planningSlice';
|
||||||
import './EventFormModal.less';
|
import './EventFormModal.less';
|
||||||
import type { InvestmentEvent, EventType } from '@/types';
|
import type { InvestmentEvent, EventType } from '@/types';
|
||||||
import { logger } from '@/utils/logger';
|
import { logger } from '@/utils/logger';
|
||||||
@@ -186,7 +186,6 @@ export const EventFormModal: React.FC<EventFormModalProps> = ({
|
|||||||
label = '事件',
|
label = '事件',
|
||||||
apiEndpoint = 'investment-plans',
|
apiEndpoint = 'investment-plans',
|
||||||
}) => {
|
}) => {
|
||||||
const { loadAllData } = usePlanningData();
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [form] = Form.useForm<FormData>();
|
const [form] = Form.useForm<FormData>();
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -336,7 +335,8 @@ export const EventFormModal: React.FC<EventFormModalProps> = ({
|
|||||||
message.success(mode === 'edit' ? '修改成功' : '添加成功');
|
message.success(mode === 'edit' ? '修改成功' : '添加成功');
|
||||||
onClose();
|
onClose();
|
||||||
onSuccess();
|
onSuccess();
|
||||||
loadAllData();
|
// 使用 Redux 刷新数据,确保列表和日历同步
|
||||||
|
dispatch(fetchAllEvents());
|
||||||
} else {
|
} else {
|
||||||
throw new Error('保存失败');
|
throw new Error('保存失败');
|
||||||
}
|
}
|
||||||
@@ -352,7 +352,7 @@ export const EventFormModal: React.FC<EventFormModalProps> = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}, [form, eventType, apiEndpoint, mode, editingEvent, label, onClose, onSuccess, loadAllData, allStocks, watchlist]);
|
}, [form, eventType, apiEndpoint, mode, editingEvent, label, onClose, onSuccess, dispatch, allStocks, watchlist]);
|
||||||
|
|
||||||
// 监听键盘快捷键 Ctrl + Enter
|
// 监听键盘快捷键 Ctrl + Enter
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* EventPanel - 通用事件面板组件
|
* EventPanel - 通用事件面板组件 (Redux 版本)
|
||||||
* 用于显示、编辑和管理投资计划或复盘
|
* 用于显示、编辑和管理投资计划或复盘
|
||||||
*
|
*
|
||||||
* 通过 props 配置差异化行为:
|
* 通过 props 配置差异化行为:
|
||||||
@@ -17,15 +17,22 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
Center,
|
Center,
|
||||||
Icon,
|
Icon,
|
||||||
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { FiFileText } from 'react-icons/fi';
|
import { FiFileText } from 'react-icons/fi';
|
||||||
|
|
||||||
import { usePlanningData } from './PlanningContext';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
|
import {
|
||||||
|
fetchAllEvents,
|
||||||
|
deleteEvent,
|
||||||
|
selectPlans,
|
||||||
|
selectReviews,
|
||||||
|
selectPlanningLoading,
|
||||||
|
} from '@/store/slices/planningSlice';
|
||||||
import { EventFormModal } from './EventFormModal';
|
import { EventFormModal } from './EventFormModal';
|
||||||
import { FUIEventCard } from './FUIEventCard';
|
import { FUIEventCard } from './FUIEventCard';
|
||||||
import type { InvestmentEvent } from '@/types';
|
import type { InvestmentEvent } from '@/types';
|
||||||
import { logger } from '@/utils/logger';
|
import { logger } from '@/utils/logger';
|
||||||
import { getApiBase } from '@/utils/apiConfig';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EventPanel Props
|
* EventPanel Props
|
||||||
@@ -51,15 +58,16 @@ export const EventPanel: React.FC<EventPanelProps> = ({
|
|||||||
label,
|
label,
|
||||||
openModalTrigger,
|
openModalTrigger,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const dispatch = useAppDispatch();
|
||||||
allEvents,
|
const toast = useToast();
|
||||||
loadAllData,
|
|
||||||
loading,
|
// Redux 状态
|
||||||
toast,
|
const plans = useAppSelector(selectPlans);
|
||||||
textColor,
|
const reviews = useAppSelector(selectReviews);
|
||||||
secondaryText,
|
const loading = useAppSelector(selectPlanningLoading);
|
||||||
cardBg,
|
|
||||||
} = usePlanningData();
|
// 根据类型选择事件列表
|
||||||
|
const events = type === 'plan' ? plans : reviews;
|
||||||
|
|
||||||
// 弹窗状态
|
// 弹窗状态
|
||||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||||
@@ -69,9 +77,6 @@ export const EventPanel: React.FC<EventPanelProps> = ({
|
|||||||
// 使用 ref 记录上一次的 trigger 值,避免组件挂载时误触发
|
// 使用 ref 记录上一次的 trigger 值,避免组件挂载时误触发
|
||||||
const prevTriggerRef = useRef<number>(openModalTrigger || 0);
|
const prevTriggerRef = useRef<number>(openModalTrigger || 0);
|
||||||
|
|
||||||
// 筛选事件列表(按类型过滤,排除系统事件)
|
|
||||||
const events = allEvents.filter(event => event.type === type && event.source !== 'future');
|
|
||||||
|
|
||||||
// 监听外部触发打开新建模态框(修复 bug:只在值变化时触发)
|
// 监听外部触发打开新建模态框(修复 bug:只在值变化时触发)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openModalTrigger !== undefined && openModalTrigger > prevTriggerRef.current) {
|
if (openModalTrigger !== undefined && openModalTrigger > prevTriggerRef.current) {
|
||||||
@@ -99,27 +104,18 @@ export const EventPanel: React.FC<EventPanelProps> = ({
|
|||||||
setEditingItem(null);
|
setEditingItem(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除数据
|
// 删除数据 - 使用 Redux action
|
||||||
const handleDelete = async (id: number): Promise<void> => {
|
const handleDelete = async (id: number): Promise<void> => {
|
||||||
if (!window.confirm('确定要删除吗?')) return;
|
if (!window.confirm('确定要删除吗?')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const base = getApiBase();
|
await dispatch(deleteEvent(id)).unwrap();
|
||||||
|
logger.info('EventPanel', `删除${label}成功`, { itemId: id });
|
||||||
const response = await fetch(base + `/api/account/investment-plans/${id}`, {
|
toast({
|
||||||
method: 'DELETE',
|
title: '删除成功',
|
||||||
credentials: 'include',
|
status: 'success',
|
||||||
|
duration: 2000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
logger.info('EventPanel', `删除${label}成功`, { itemId: id });
|
|
||||||
toast({
|
|
||||||
title: '删除成功',
|
|
||||||
status: 'success',
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
loadAllData();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('EventPanel', 'handleDelete', error, { itemId: id });
|
logger.error('EventPanel', 'handleDelete', error, { itemId: id });
|
||||||
toast({
|
toast({
|
||||||
@@ -130,11 +126,19 @@ export const EventPanel: React.FC<EventPanelProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const handleRefresh = useCallback(() => {
|
||||||
|
dispatch(fetchAllEvents());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
// 使用 useCallback 优化回调函数
|
// 使用 useCallback 优化回调函数
|
||||||
const handleEdit = useCallback((item: InvestmentEvent) => {
|
const handleEdit = useCallback((item: InvestmentEvent) => {
|
||||||
handleOpenModal(item);
|
handleOpenModal(item);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 颜色主题
|
||||||
|
const secondaryText = 'rgba(255, 255, 255, 0.6)';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<VStack align="stretch" spacing={4}>
|
<VStack align="stretch" spacing={4}>
|
||||||
@@ -172,7 +176,7 @@ export const EventPanel: React.FC<EventPanelProps> = ({
|
|||||||
mode={modalMode}
|
mode={modalMode}
|
||||||
eventType={type}
|
eventType={type}
|
||||||
editingEvent={editingItem}
|
editingEvent={editingItem}
|
||||||
onSuccess={loadAllData}
|
onSuccess={handleRefresh}
|
||||||
label={label}
|
label={label}
|
||||||
apiEndpoint="investment-plans"
|
apiEndpoint="investment-plans"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -124,22 +124,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 星期头
|
// 星期头(周日、周一等)
|
||||||
.fc-col-header-cell-cushion {
|
.fc-col-header-cell-cushion {
|
||||||
color: @text-secondary;
|
color: @gold-400 !important;
|
||||||
font-weight: 500;
|
font-weight: 600 !important;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 日期数字
|
// 日期数字(1日、2日等)- 增强可见性
|
||||||
.fc-daygrid-day-number {
|
.fc-daygrid-day-number {
|
||||||
color: @text-primary;
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
font-weight: 500;
|
font-weight: 600 !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前月份的日期更亮
|
||||||
|
.fc-day.fc-day-future .fc-daygrid-day-number,
|
||||||
|
.fc-day.fc-day-past .fc-daygrid-day-number {
|
||||||
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非当前月份的日期(灰色)
|
||||||
|
.fc-day-other .fc-daygrid-day-number {
|
||||||
|
color: rgba(255, 255, 255, 0.35) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 今天高亮
|
// 今天高亮
|
||||||
.fc-daygrid-day.fc-day-today {
|
.fc-daygrid-day.fc-day-today {
|
||||||
background-color: @today-bg !important;
|
background-color: @today-bg !important;
|
||||||
border: 2px solid @gold-400 !important;
|
border: 2px solid @gold-400 !important;
|
||||||
|
|
||||||
|
.fc-daygrid-day-number {
|
||||||
|
color: @gold-400 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件样式
|
// 事件样式
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* InvestmentPlanningCenter - 投资规划中心主组件 (TypeScript 重构版)
|
* InvestmentPlanningCenter - 投资规划中心主组件 (Redux 版本)
|
||||||
*
|
*
|
||||||
* 性能优化:
|
* 使用 Redux 管理数据,确保列表和日历视图数据同步
|
||||||
* - 使用 React.lazy() 懒加载子面板,减少初始加载时间
|
|
||||||
* - 使用 TypeScript 提供类型安全
|
|
||||||
*
|
*
|
||||||
* 组件架构:
|
* 组件架构:
|
||||||
* - InvestmentPlanningCenter (主组件)
|
* - InvestmentPlanningCenter (主组件)
|
||||||
* - CalendarPanel (日历面板,懒加载)
|
* - CalendarPanel (日历面板,懒加载)
|
||||||
* - EventPanel (通用事件面板,用于计划和复盘)
|
* - EventPanel (通用事件面板,用于计划和复盘)
|
||||||
* - PlanningContext (数据共享层)
|
* - PlanningContext (UI 状态共享)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useMemo, Suspense, lazy } from 'react';
|
import React, { useState, useEffect, useMemo, Suspense, lazy } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Heading,
|
Heading,
|
||||||
@@ -30,7 +28,6 @@ import {
|
|||||||
Center,
|
Center,
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Text,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
FiCalendar,
|
FiCalendar,
|
||||||
@@ -42,9 +39,15 @@ import { Target } from 'lucide-react';
|
|||||||
import GlassCard from '@components/GlassCard';
|
import GlassCard from '@components/GlassCard';
|
||||||
|
|
||||||
import { PlanningDataProvider } from './PlanningContext';
|
import { PlanningDataProvider } from './PlanningContext';
|
||||||
import type { InvestmentEvent, PlanningContextValue } from '@/types';
|
import type { PlanningContextValue } from '@/types';
|
||||||
import { logger } from '@/utils/logger';
|
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||||
import { getApiBase } from '@/utils/apiConfig';
|
import {
|
||||||
|
fetchAllEvents,
|
||||||
|
selectAllEvents,
|
||||||
|
selectPlanningLoading,
|
||||||
|
selectPlans,
|
||||||
|
selectReviews,
|
||||||
|
} from '@/store/slices/planningSlice';
|
||||||
import './InvestmentCalendar.less';
|
import './InvestmentCalendar.less';
|
||||||
|
|
||||||
// 懒加载子面板组件(实现代码分割)
|
// 懒加载子面板组件(实现代码分割)
|
||||||
@@ -68,64 +71,45 @@ const PanelLoadingFallback: React.FC = () => (
|
|||||||
* InvestmentPlanningCenter 主组件
|
* InvestmentPlanningCenter 主组件
|
||||||
*/
|
*/
|
||||||
const InvestmentPlanningCenter: React.FC = () => {
|
const InvestmentPlanningCenter: React.FC = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
// Redux 状态
|
||||||
|
const allEvents = useAppSelector(selectAllEvents);
|
||||||
|
const loading = useAppSelector(selectPlanningLoading);
|
||||||
|
const plans = useAppSelector(selectPlans);
|
||||||
|
const reviews = useAppSelector(selectReviews);
|
||||||
|
|
||||||
// 颜色主题
|
// 颜色主题
|
||||||
const bgColor = useColorModeValue('white', 'gray.800');
|
|
||||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||||
const textColor = useColorModeValue('gray.700', 'white');
|
const textColor = useColorModeValue('gray.700', 'white');
|
||||||
const secondaryText = useColorModeValue('gray.600', 'gray.400');
|
const secondaryText = useColorModeValue('gray.600', 'gray.400');
|
||||||
const cardBg = useColorModeValue('gray.50', 'gray.700');
|
const cardBg = useColorModeValue('gray.50', 'gray.700');
|
||||||
|
|
||||||
// 全局数据状态
|
// UI 状态
|
||||||
const [allEvents, setAllEvents] = useState<InvestmentEvent[]>([]);
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
const [viewMode, setViewMode] = useState<'calendar' | 'list'>('list');
|
const [viewMode, setViewMode] = useState<'calendar' | 'list'>('list');
|
||||||
const [listTab, setListTab] = useState<number>(0); // 0: 我的计划, 1: 我的复盘
|
const [listTab, setListTab] = useState<number>(0); // 0: 我的计划, 1: 我的复盘
|
||||||
const [openPlanModalTrigger, setOpenPlanModalTrigger] = useState<number>(0);
|
const [openPlanModalTrigger, setOpenPlanModalTrigger] = useState<number>(0);
|
||||||
const [openReviewModalTrigger, setOpenReviewModalTrigger] = useState<number>(0);
|
const [openReviewModalTrigger, setOpenReviewModalTrigger] = useState<number>(0);
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载所有事件数据(日历事件 + 计划 + 复盘)
|
|
||||||
*/
|
|
||||||
const loadAllData = useCallback(async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const base = getApiBase();
|
|
||||||
|
|
||||||
const response = await fetch(base + '/api/account/calendar/events', {
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
setAllEvents(data.data || []);
|
|
||||||
logger.debug('InvestmentPlanningCenter', '数据加载成功', {
|
|
||||||
count: data.data?.length || 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('InvestmentPlanningCenter', 'loadAllData', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 组件挂载时加载数据
|
// 组件挂载时加载数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAllData();
|
dispatch(fetchAllEvents());
|
||||||
}, [loadAllData]);
|
}, [dispatch]);
|
||||||
|
|
||||||
// 提供给子组件的 Context 值(使用 useMemo 缓存,避免子组件不必要的重渲染)
|
// 刷新数据的方法(供子组件调用)
|
||||||
|
const loadAllData = async (): Promise<void> => {
|
||||||
|
await dispatch(fetchAllEvents());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提供给子组件的 Context 值
|
||||||
const contextValue: PlanningContextValue = useMemo(
|
const contextValue: PlanningContextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
allEvents,
|
allEvents,
|
||||||
setAllEvents,
|
setAllEvents: () => {}, // Redux 管理,不需要 setter
|
||||||
loadAllData,
|
loadAllData,
|
||||||
loading,
|
loading,
|
||||||
setLoading,
|
setLoading: () => {}, // Redux 管理,不需要 setter
|
||||||
openPlanModalTrigger,
|
openPlanModalTrigger,
|
||||||
openReviewModalTrigger,
|
openReviewModalTrigger,
|
||||||
toast,
|
toast,
|
||||||
@@ -138,7 +122,6 @@ const InvestmentPlanningCenter: React.FC = () => {
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
allEvents,
|
allEvents,
|
||||||
loadAllData,
|
|
||||||
loading,
|
loading,
|
||||||
openPlanModalTrigger,
|
openPlanModalTrigger,
|
||||||
openReviewModalTrigger,
|
openReviewModalTrigger,
|
||||||
@@ -150,15 +133,6 @@ const InvestmentPlanningCenter: React.FC = () => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 计算各类型事件数量(使用 useMemo 缓存,避免每次渲染重复遍历数组)
|
|
||||||
const { planCount, reviewCount } = useMemo(
|
|
||||||
() => ({
|
|
||||||
planCount: allEvents.filter(e => e.type === 'plan').length,
|
|
||||||
reviewCount: allEvents.filter(e => e.type === 'review').length,
|
|
||||||
}),
|
|
||||||
[allEvents]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 金色主题色
|
// 金色主题色
|
||||||
const goldAccent = 'rgba(212, 175, 55, 0.9)';
|
const goldAccent = 'rgba(212, 175, 55, 0.9)';
|
||||||
|
|
||||||
@@ -245,7 +219,7 @@ const InvestmentPlanningCenter: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box as={Target} boxSize={{ base: 3, md: 4 }} mr={1} />
|
<Box as={Target} boxSize={{ base: 3, md: 4 }} mr={1} />
|
||||||
我的计划 ({planCount})
|
我的计划 ({plans.length})
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
fontSize={{ base: '11px', md: 'sm' }}
|
fontSize={{ base: '11px', md: 'sm' }}
|
||||||
@@ -260,7 +234,7 @@ const InvestmentPlanningCenter: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon as={FiFileText} mr={1} boxSize={{ base: 3, md: 4 }} />
|
<Icon as={FiFileText} mr={1} boxSize={{ base: 3, md: 4 }} />
|
||||||
我的复盘 ({reviewCount})
|
我的复盘 ({reviews.length})
|
||||||
</Tab>
|
</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user