Files
vf_react/src/mocks/handlers/account.js
zdl 023684b8b7 feat: 事件关注功能优化 - Redux 乐观更新 + Mock 数据状态同步
1. communityDataSlice 添加事件关注乐观更新
   - pending: 立即切换 isFollowing 状态
   - rejected: 回滚到之前状态
   - fulfilled: 使用 API 返回的准确数据覆盖

2. Mock 数据添加内存状态管理
   - 新增 followedEventsSet 和 followedEventsMap 存储
   - toggleEventFollowStatus: 切换关注状态
   - isEventFollowed: 检查是否已关注
   - getFollowedEvents: 获取关注事件列表

3. Mock handlers 使用内存状态
   - follow handler: 使用 toggleEventFollowStatus
   - following handler: 使用 getFollowedEvents 动态返回
   - 事件详情: 返回正确的 is_following 状态

修复: 关注事件后导航栏"自选事件"列表不同步更新的问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 16:34:36 +08:00

826 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/mocks/handlers/account.js
import { http, HttpResponse, delay } from 'msw';
import { getCurrentUser } from '../data/users';
import {
mockWatchlist,
mockRealtimeQuotes,
mockFollowingEvents,
mockEventComments,
mockInvestmentPlans,
mockCalendarEvents,
mockSubscriptionCurrent,
getCalendarEventsByDateRange,
getFollowedEvents
} from '../data/account';
// 模拟网络延迟(毫秒)
const NETWORK_DELAY = 300;
export const accountHandlers = [
// ==================== 用户资料管理 ====================
// 1. 获取资料完整度
http.get('/api/account/profile-completeness', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '用户未登录'
}, { status: 401 });
}
console.log('[Mock] 获取资料完整度:', currentUser);
const isWechatUser = currentUser.has_wechat || !!currentUser.wechat_openid;
const completeness = {
hasPassword: !!currentUser.password_hash || !isWechatUser,
hasPhone: !!currentUser.phone,
hasEmail: !!currentUser.email && currentUser.email.includes('@') && !currentUser.email.endsWith('@valuefrontier.temp'),
isWechatUser: isWechatUser
};
const totalItems = 3;
const completedItems = [completeness.hasPassword, completeness.hasPhone, completeness.hasEmail].filter(Boolean).length;
const completenessPercentage = Math.round((completedItems / totalItems) * 100);
let needsAttention = false;
const missingItems = [];
if (isWechatUser && completenessPercentage < 100) {
needsAttention = true;
if (!completeness.hasPassword) missingItems.push('登录密码');
if (!completeness.hasPhone) missingItems.push('手机号');
if (!completeness.hasEmail) missingItems.push('邮箱');
}
const result = {
success: true,
data: {
completeness,
completenessPercentage,
needsAttention,
missingItems,
isComplete: completedItems === totalItems,
showReminder: needsAttention
}
};
console.log('[Mock] 资料完整度结果:', result.data);
return HttpResponse.json(result);
}),
// 2. 更新用户资料
http.put('/api/account/profile', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '用户未登录'
}, { status: 401 });
}
const body = await request.json();
console.log('[Mock] 更新用户资料:', body);
Object.assign(currentUser, body);
return HttpResponse.json({
success: true,
message: '资料更新成功',
data: currentUser
});
}),
// 3. 获取用户资料
http.get('/api/account/profile', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '用户未登录'
}, { status: 401 });
}
console.log('[Mock] 获取用户资料:', currentUser);
return HttpResponse.json({
success: true,
data: currentUser
});
}),
// ==================== 自选股管理 ====================
// 4. 获取自选股列表
http.get('/api/account/watchlist', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
// console.log('[Mock] 获取自选股列表'); // 已关闭:减少日志
return HttpResponse.json({
success: true,
data: mockWatchlist
});
}),
// 5. 获取自选股实时行情
http.get('/api/account/watchlist/realtime', async () => {
await delay(200);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
console.log('[Mock] 获取自选股实时行情');
return HttpResponse.json({
success: true,
data: mockRealtimeQuotes
});
}),
// 6. 添加自选股
http.post('/api/account/watchlist', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const body = await request.json();
const { stock_code, stock_name } = body;
console.log('[Mock] 添加自选股:', { stock_code, stock_name });
const newItem = {
id: mockWatchlist.length + 1,
user_id: currentUser.id,
stock_code,
stock_name,
added_at: new Date().toISOString(),
industry: '未知',
current_price: null,
change_percent: null
};
mockWatchlist.push(newItem);
// 同步添加到 mockRealtimeQuotes导航栏自选股菜单使用此数组
mockRealtimeQuotes.push({
stock_code: stock_code,
stock_name: stock_name,
current_price: null,
change_percent: 0,
change: 0,
volume: 0,
turnover: 0,
high: 0,
low: 0,
open: 0,
prev_close: 0,
update_time: new Date().toTimeString().slice(0, 8)
});
return HttpResponse.json({
success: true,
message: '添加成功',
data: newItem
});
}),
// 7. 删除自选股
http.delete('/api/account/watchlist/:id', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const { id } = params;
console.log('[Mock] 删除自选股:', id);
// 支持按 stock_code 或 id 匹配删除
const index = mockWatchlist.findIndex(item =>
item.stock_code === id || item.id === parseInt(id)
);
if (index !== -1) {
const stockCode = mockWatchlist[index].stock_code;
mockWatchlist.splice(index, 1);
// 同步从 mockRealtimeQuotes 移除
const quotesIndex = mockRealtimeQuotes.findIndex(item => item.stock_code === stockCode);
if (quotesIndex !== -1) {
mockRealtimeQuotes.splice(quotesIndex, 1);
}
}
return HttpResponse.json({
success: true,
message: '删除成功'
});
}),
// ==================== 事件关注管理 ====================
// 8. 获取关注的事件(使用内存状态动态返回)
http.get('/api/account/events/following', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
// 从内存存储获取已关注的事件列表
const followedEvents = getFollowedEvents();
console.log('[Mock] 获取关注的事件, 数量:', followedEvents.length);
return HttpResponse.json({
success: true,
data: followedEvents
});
}),
// 9. 获取事件评论
http.get('/api/account/events/comments', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
console.log('[Mock] 获取事件评论');
return HttpResponse.json({
success: true,
data: mockEventComments
});
}),
// 10. 获取事件帖子(用户发布的评论/帖子)
http.get('/api/account/events/posts', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
console.log('[Mock] 获取事件帖子');
return HttpResponse.json({
success: true,
data: mockEventComments // 复用 mockEventComments 数据
});
}),
// ==================== 投资计划与复盘 ====================
// 10. 获取投资计划列表
http.get('/api/account/investment-plans', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
console.log('[Mock] 获取投资计划列表');
return HttpResponse.json({
success: true,
data: mockInvestmentPlans
});
}),
// 11. 创建投资计划
http.post('/api/account/investment-plans', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const body = await request.json();
console.log('[Mock] 创建投资计划:', body);
const newPlan = {
id: mockInvestmentPlans.length + 301,
user_id: currentUser.id,
...body,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
mockInvestmentPlans.push(newPlan);
return HttpResponse.json({
success: true,
message: '创建成功',
data: newPlan
});
}),
// 12. 更新投资计划
http.put('/api/account/investment-plans/:id', async ({ request, params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const { id } = params;
const body = await request.json();
console.log('[Mock] 更新投资计划:', { id, body });
const index = mockInvestmentPlans.findIndex(plan => plan.id === parseInt(id));
if (index !== -1) {
mockInvestmentPlans[index] = {
...mockInvestmentPlans[index],
...body,
updated_at: new Date().toISOString()
};
return HttpResponse.json({
success: true,
message: '更新成功',
data: mockInvestmentPlans[index]
});
}
return HttpResponse.json({
success: false,
error: '计划不存在'
}, { status: 404 });
}),
// 13. 删除投资计划
http.delete('/api/account/investment-plans/:id', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const { id } = params;
console.log('[Mock] 删除投资计划:', id);
const index = mockInvestmentPlans.findIndex(plan => plan.id === parseInt(id));
if (index !== -1) {
mockInvestmentPlans.splice(index, 1);
}
return HttpResponse.json({
success: true,
message: '删除成功'
});
}),
// ==================== 投资日历 ====================
// 14. 获取日历事件(可选日期范围)- 合并投资计划和日历事件
http.get('/api/account/calendar/events', async ({ request }) => {
await delay(NETWORK_DELAY);
// Mock 模式下允许无登录访问,使用默认用户 id: 1
const currentUser = getCurrentUser() || { id: 1 };
const url = new URL(request.url);
const startDate = url.searchParams.get('start_date');
const endDate = url.searchParams.get('end_date');
console.log('[Mock] 获取日历事件:', { startDate, endDate });
// 1. 获取日历事件
let calendarEvents = mockCalendarEvents;
if (startDate && endDate) {
calendarEvents = getCalendarEventsByDateRange(currentUser.id, startDate, endDate);
}
// 2. 获取投资计划和复盘,转换为日历事件格式
// Mock 模式:不过滤 user_id显示所有 mock 数据(方便开发测试)
const investmentPlansAsEvents = mockInvestmentPlans
.map(plan => ({
id: plan.id,
user_id: plan.user_id,
title: plan.title,
date: plan.target_date || plan.date,
event_date: plan.target_date || plan.date,
type: plan.type, // 'plan' or 'review'
category: plan.type === 'plan' ? 'investment_plan' : 'investment_review',
description: plan.content || '',
importance: 3, // 默认重要度
source: 'user', // 标记为用户创建
stocks: plan.stocks || [],
tags: plan.tags || [],
status: plan.status,
created_at: plan.created_at,
updated_at: plan.updated_at
}));
// 3. 合并两个数据源
const allEvents = [...calendarEvents, ...investmentPlansAsEvents];
// 4. 如果提供了日期范围,对合并后的数据进行过滤
let filteredEvents = allEvents;
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
filteredEvents = allEvents.filter(event => {
const eventDate = new Date(event.date || event.event_date);
return eventDate >= start && eventDate <= end;
});
}
console.log('[Mock] 日历事件详情:', {
currentUserId: currentUser.id,
calendarEvents: calendarEvents.length,
investmentPlansAsEvents: investmentPlansAsEvents.length,
total: filteredEvents.length,
plansCount: filteredEvents.filter(e => e.type === 'plan').length,
reviewsCount: filteredEvents.filter(e => e.type === 'review').length
});
return HttpResponse.json({
success: true,
data: filteredEvents
});
}),
// 15. 创建日历事件
http.post('/api/account/calendar/events', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const body = await request.json();
console.log('[Mock] 创建日历事件:', body);
const newEvent = {
id: mockCalendarEvents.length + 401,
user_id: currentUser.id,
...body,
source: 'user', // 用户创建的事件标记为 'user'
created_at: new Date().toISOString()
};
mockCalendarEvents.push(newEvent);
return HttpResponse.json({
success: true,
message: '创建成功',
data: newEvent
});
}),
// 16. 更新日历事件
http.put('/api/account/calendar/events/:id', async ({ request, params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const { id } = params;
const body = await request.json();
console.log('[Mock] 更新日历事件:', { id, body });
const index = mockCalendarEvents.findIndex(event => event.id === parseInt(id));
if (index !== -1) {
mockCalendarEvents[index] = {
...mockCalendarEvents[index],
...body
};
return HttpResponse.json({
success: true,
message: '更新成功',
data: mockCalendarEvents[index]
});
}
return HttpResponse.json({
success: false,
error: '事件不存在'
}, { status: 404 });
}),
// 17. 删除日历事件
http.delete('/api/account/calendar/events/:id', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const { id } = params;
console.log('[Mock] 删除日历事件:', id);
const index = mockCalendarEvents.findIndex(event => event.id === parseInt(id));
if (index !== -1) {
mockCalendarEvents.splice(index, 1);
}
return HttpResponse.json({
success: true,
message: '删除成功'
});
}),
// ==================== 订阅信息 ====================
// 18. 获取订阅信息
http.get('/api/subscription/info', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: true,
data: {
type: 'free',
status: 'active',
is_active: true,
days_left: 0,
end_date: null
}
});
}
console.log('[Mock] 获取订阅信息:', currentUser);
const subscriptionInfo = {
type: currentUser.subscription_type || 'free',
status: currentUser.subscription_status || 'active',
is_active: currentUser.is_subscription_active !== false,
days_left: currentUser.subscription_days_left || 0,
end_date: currentUser.subscription_end_date || null
};
console.log('[Mock] 订阅信息结果:', subscriptionInfo);
return HttpResponse.json({
success: true,
data: subscriptionInfo
});
}),
// 19. 获取当前订阅详情
http.get('/api/subscription/current', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
console.warn('[Mock API] 获取订阅详情失败: 用户未登录');
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
// 基于当前用户的订阅类型返回详情
const userSubscriptionType = (currentUser.subscription_type || 'free').toLowerCase();
const subscriptionDetails = {
...mockSubscriptionCurrent,
type: userSubscriptionType,
status: currentUser.subscription_status || 'active',
is_active: currentUser.is_subscription_active !== false,
days_left: currentUser.subscription_days_left || 0,
end_date: currentUser.subscription_end_date || null
};
return HttpResponse.json({
success: true,
data: subscriptionDetails
});
}),
// 20. 获取订阅权限
http.get('/api/subscription/permissions', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: true,
data: {
permissions: {
'related_stocks': false,
'related_concepts': false,
'transmission_chain': false,
'historical_events': 'limited',
'concept_html_detail': false,
'concept_stats_panel': false,
'concept_related_stocks': false,
'concept_timeline': false,
'hot_stocks': false
}
}
});
}
const subscriptionType = (currentUser.subscription_type || 'free').toLowerCase();
let permissions = {};
if (subscriptionType === 'free') {
permissions = {
'related_stocks': false,
'related_concepts': false,
'transmission_chain': false,
'historical_events': 'limited',
'concept_html_detail': false,
'concept_stats_panel': false,
'concept_related_stocks': false,
'concept_timeline': false,
'hot_stocks': false
};
} else if (subscriptionType === 'pro') {
permissions = {
'related_stocks': true,
'related_concepts': true,
'transmission_chain': false,
'historical_events': 'full',
'concept_html_detail': true,
'concept_stats_panel': true,
'concept_related_stocks': true,
'concept_timeline': false,
'hot_stocks': true
};
} else if (subscriptionType === 'max') {
permissions = {
'related_stocks': true,
'related_concepts': true,
'transmission_chain': true,
'historical_events': 'full',
'concept_html_detail': true,
'concept_stats_panel': true,
'concept_related_stocks': true,
'concept_timeline': true,
'hot_stocks': true
};
}
console.log('[Mock] 订阅权限:', { subscriptionType, permissions });
return HttpResponse.json({
success: true,
data: {
subscription_type: subscriptionType,
permissions
}
});
}),
// 21. 获取订阅套餐列表
http.get('/api/subscription/plans', async () => {
await delay(NETWORK_DELAY);
const plans = [
{
id: 1,
name: 'pro',
display_name: 'Pro 专业版',
description: '事件关联股票深度分析 | 历史事件智能对比复盘 | 事件概念关联与挖掘 | 概念板块个股追踪 | 概念深度研报与解读 | 个股异动实时预警',
monthly_price: 299,
yearly_price: 2699,
pricing_options: [
{ cycle_key: 'monthly', label: '月付', months: 1, price: 299, original_price: null, discount_percent: 0 },
{ cycle_key: 'quarterly', label: '季付', months: 3, price: 799, original_price: 897, discount_percent: 11 },
{ cycle_key: 'semiannual', label: '半年付', months: 6, price: 1499, original_price: 1794, discount_percent: 16 },
{ cycle_key: 'yearly', label: '年付', months: 12, price: 2699, original_price: 3588, discount_percent: 25 }
],
features: [
'新闻信息流',
'历史事件对比',
'事件传导链分析(AI)',
'事件-相关标的分析',
'相关概念展示',
'AI复盘功能',
'企业概览',
'个股深度分析(AI) - 50家/月',
'高效数据筛选工具',
'概念中心(548大概念)',
'历史时间轴查询 - 100天',
'涨停板块数据分析',
'个股涨停分析'
],
sort_order: 1
},
{
id: 2,
name: 'max',
display_name: 'Max 旗舰版',
description: '包含Pro版全部功能 | 事件传导链路智能分析 | 概念演变时间轴追溯 | 个股全方位深度研究 | 价小前投研助手无限使用 | 新功能优先体验权 | 专属客服一对一服务',
monthly_price: 599,
yearly_price: 5399,
pricing_options: [
{ cycle_key: 'monthly', label: '月付', months: 1, price: 599, original_price: null, discount_percent: 0 },
{ cycle_key: 'quarterly', label: '季付', months: 3, price: 1599, original_price: 1797, discount_percent: 11 },
{ cycle_key: 'semiannual', label: '半年付', months: 6, price: 2999, original_price: 3594, discount_percent: 17 },
{ cycle_key: 'yearly', label: '年付', months: 12, price: 5399, original_price: 7188, discount_percent: 25 }
],
features: [
'新闻信息流',
'历史事件对比',
'事件传导链分析(AI)',
'事件-相关标的分析',
'相关概念展示',
'板块深度分析(AI)',
'AI复盘功能',
'企业概览',
'个股深度分析(AI) - 无限制',
'高效数据筛选工具',
'概念中心(548大概念)',
'历史时间轴查询 - 无限制',
'概念高频更新',
'涨停板块数据分析',
'个股涨停分析'
],
sort_order: 2
}
];
console.log('[Mock] 获取订阅套餐列表:', plans.length, '个套餐');
return HttpResponse.json({
success: true,
data: plans
});
}),
];