feat: 添加mock数据,给导航添加选中标识

This commit is contained in:
zdl
2025-10-17 15:01:35 +08:00
parent bc407d2a35
commit 2d9d047a9f
16 changed files with 2120 additions and 25 deletions

61
src/mocks/browser.js Normal file
View File

@@ -0,0 +1,61 @@
// src/mocks/browser.js
// 浏览器环境的 MSW Worker
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
// 创建 Service Worker 实例
export const worker = setupWorker(...handlers);
// 启动 Mock Service Worker
export async function startMockServiceWorker() {
// 只在开发环境且 REACT_APP_ENABLE_MOCK=true 时启动
const shouldEnableMock = process.env.REACT_APP_ENABLE_MOCK === 'true';
if (!shouldEnableMock) {
console.log('[MSW] Mock 已禁用 (REACT_APP_ENABLE_MOCK=false)');
return;
}
try {
await worker.start({
// 不显示未拦截的请求警告(可选)
onUnhandledRequest: 'bypass',
// 自定义 Service Worker URL如果需要
serviceWorker: {
url: '/mockServiceWorker.js',
},
// 静默模式(不在控制台打印启动消息)
quiet: false,
});
console.log(
'%c[MSW] Mock Service Worker 已启动 🎭',
'color: #4CAF50; font-weight: bold; font-size: 14px;'
);
console.log(
'%c提示: 所有 API 请求将使用本地 Mock 数据',
'color: #FF9800; font-size: 12px;'
);
console.log(
'%c要禁用 Mock请设置 REACT_APP_ENABLE_MOCK=false',
'color: #2196F3; font-size: 12px;'
);
} catch (error) {
console.error('[MSW] 启动失败:', error);
}
}
// 停止 Mock Service Worker
export function stopMockServiceWorker() {
worker.stop();
console.log('[MSW] Mock Service Worker 已停止');
}
// 重置所有 Handlers
export function resetMockHandlers() {
worker.resetHandlers();
console.log('[MSW] Handlers 已重置');
}

107
src/mocks/data/users.js Normal file
View File

@@ -0,0 +1,107 @@
// Mock 用户数据
export const mockUsers = {
// 免费用户 - 手机号登录
'13800138000': {
id: 1,
phone: '13800138000',
nickname: '测试用户',
email: 'test@example.com',
avatar_url: 'https://i.pravatar.cc/150?img=1',
has_wechat: false,
created_at: '2024-01-01T00:00:00Z',
// 会员信息 - 免费用户
subscription_type: 'free',
subscription_status: 'active',
subscription_end_date: null,
is_subscription_active: true,
subscription_days_left: 0
},
// Pro 会员 - 手机号登录
'13900139000': {
id: 2,
phone: '13900139000',
nickname: 'Pro会员',
email: 'pro@example.com',
avatar_url: 'https://i.pravatar.cc/150?img=2',
has_wechat: true,
created_at: '2024-01-15T00:00:00Z',
// 会员信息 - Pro 会员
subscription_type: 'pro',
subscription_status: 'active',
subscription_end_date: '2025-12-31T23:59:59Z',
is_subscription_active: true,
subscription_days_left: 90
},
// Max 会员 - 手机号登录
'13700137000': {
id: 3,
phone: '13700137000',
nickname: 'Max会员',
email: 'max@example.com',
avatar_url: 'https://i.pravatar.cc/150?img=3',
has_wechat: false,
created_at: '2024-02-01T00:00:00Z',
// 会员信息 - Max 会员
subscription_type: 'max',
subscription_status: 'active',
subscription_end_date: '2026-12-31T23:59:59Z',
is_subscription_active: true,
subscription_days_left: 365
},
// 过期会员 - 测试过期状态
'13600136000': {
id: 4,
phone: '13600136000',
nickname: '过期会员',
email: 'expired@example.com',
avatar_url: 'https://i.pravatar.cc/150?img=4',
has_wechat: false,
created_at: '2023-01-01T00:00:00Z',
// 会员信息 - 已过期
subscription_type: 'pro',
subscription_status: 'expired',
subscription_end_date: '2024-01-01T00:00:00Z',
is_subscription_active: false,
subscription_days_left: -300
}
};
// Mock 验证码存储(实际项目中应该在后端验证)
export const mockVerificationCodes = new Map();
// 生成随机6位验证码
export function generateVerificationCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// 微信 session 存储
export const mockWechatSessions = new Map();
// 生成微信 session ID
export function generateWechatSessionId() {
return 'wx_' + Math.random().toString(36).substring(2, 15);
}
// ==================== 当前登录用户状态管理 ====================
// 用于跟踪当前登录的用户Mock 模式下的全局状态)
let currentLoggedInUser = null;
// 设置当前登录用户
export function setCurrentUser(user) {
currentLoggedInUser = user;
console.log('[Mock State] 设置当前登录用户:', user);
}
// 获取当前登录用户
export function getCurrentUser() {
return currentLoggedInUser;
}
// 清除当前登录用户
export function clearCurrentUser() {
currentLoggedInUser = null;
console.log('[Mock State] 清除当前登录用户');
}

View File

@@ -0,0 +1,248 @@
// src/mocks/handlers/account.js
import { http, HttpResponse, delay } from 'msw';
import { getCurrentUser } from '../data/users';
// 模拟网络延迟(毫秒)
const NETWORK_DELAY = 300;
export const accountHandlers = [
// ==================== 用户资料管理 ====================
// 1. 获取资料完整度
http.get('/api/account/profile-completeness', async () => {
await delay(NETWORK_DELAY);
// 获取当前登录用户
const currentUser = getCurrentUser();
// 如果没有登录,返回 401
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 = [];
// Mock 模式下,对微信用户进行简化判断
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);
// 在 Mock 模式下,我们直接更新当前用户对象(实际应该调用 setCurrentUser
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/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
});
}),
// 5. 获取订阅权限
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
}
});
})
];

330
src/mocks/handlers/auth.js Normal file
View File

@@ -0,0 +1,330 @@
// src/mocks/handlers/auth.js
import { http, HttpResponse, delay } from 'msw';
import {
mockUsers,
mockVerificationCodes,
generateVerificationCode,
mockWechatSessions,
generateWechatSessionId,
setCurrentUser,
getCurrentUser,
clearCurrentUser
} from '../data/users';
// 模拟网络延迟(毫秒)
const NETWORK_DELAY = 500;
export const authHandlers = [
// ==================== 手机验证码登录 ====================
// 1. 发送验证码
http.post('/api/auth/send-verification-code', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { credential, type, purpose } = body;
console.log('[Mock] 发送验证码:', { credential, type, purpose });
// 生成验证码
const code = generateVerificationCode();
mockVerificationCodes.set(credential, {
code,
expiresAt: Date.now() + 5 * 60 * 1000 // 5分钟后过期
});
console.log(`[Mock] 验证码已生成: ${credential} -> ${code}`);
return HttpResponse.json({
success: true,
message: `验证码已发送到 ${credential}Mock: ${code}`,
// 开发环境下返回验证码,方便测试
dev_code: code
});
}),
// 2. 验证码登录
http.post('/api/auth/login-with-code', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { credential, verification_code, login_type } = body;
console.log('[Mock] 验证码登录:', { credential, verification_code, login_type });
// 验证验证码
const storedCode = mockVerificationCodes.get(credential);
if (!storedCode) {
return HttpResponse.json({
success: false,
error: '验证码不存在或已过期'
}, { status: 400 });
}
if (storedCode.expiresAt < Date.now()) {
mockVerificationCodes.delete(credential);
return HttpResponse.json({
success: false,
error: '验证码已过期'
}, { status: 400 });
}
if (storedCode.code !== verification_code) {
return HttpResponse.json({
success: false,
error: '验证码错误'
}, { status: 400 });
}
// 验证成功,删除验证码
mockVerificationCodes.delete(credential);
// 查找或创建用户
let user = mockUsers[credential];
let isNewUser = false;
if (!user) {
// 新用户
isNewUser = true;
const id = Object.keys(mockUsers).length + 1;
user = {
id,
phone: credential,
nickname: `用户${id}`,
email: null,
avatar_url: `https://i.pravatar.cc/150?img=${id}`,
has_wechat: false,
created_at: new Date().toISOString()
};
mockUsers[credential] = user;
console.log('[Mock] 创建新用户:', user);
}
console.log('[Mock] 登录成功:', user);
// 设置当前登录用户
setCurrentUser(user);
return HttpResponse.json({
success: true,
message: isNewUser ? '注册成功' : '登录成功',
isNewUser,
user,
token: `mock_token_${user.id}_${Date.now()}`
});
}),
// ==================== 微信登录 ====================
// 3. 获取微信 PC 二维码
http.get('/api/auth/wechat/qrcode', async () => {
await delay(NETWORK_DELAY);
const sessionId = generateWechatSessionId();
// 创建微信 session
mockWechatSessions.set(sessionId, {
status: 'waiting', // waiting, scanned, confirmed, expired
createdAt: Date.now(),
user: null
});
// 模拟微信授权 URL实际是微信的 URL
const authUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=mock&redirect_uri=&response_type=code&scope=snsapi_login&state=${sessionId}#wechat_redirect`;
console.log('[Mock] 生成微信二维码:', { sessionId, authUrl });
// 10秒后自动模拟扫码方便测试
setTimeout(() => {
const session = mockWechatSessions.get(sessionId);
if (session && session.status === 'waiting') {
session.status = 'scanned';
console.log(`[Mock] 模拟用户扫码: ${sessionId}`);
// 再过5秒自动确认登录
setTimeout(() => {
const session2 = mockWechatSessions.get(sessionId);
if (session2 && session2.status === 'scanned') {
session2.status = 'confirmed';
session2.user = {
id: 999,
nickname: '微信用户',
wechat_openid: 'mock_openid_' + sessionId,
avatar_url: 'https://i.pravatar.cc/150?img=99',
phone: null,
email: null,
has_wechat: true,
created_at: new Date().toISOString()
};
console.log(`[Mock] 模拟用户确认登录: ${sessionId}`, session2.user);
}
}, 5000);
}
}, 10000);
return HttpResponse.json({
code: 0,
message: '成功',
data: {
auth_url: authUrl,
session_id: sessionId
}
});
}),
// 4. 检查微信扫码状态
http.post('/api/auth/wechat/check-status', async ({ request }) => {
await delay(200); // 轮询请求,延迟短一些
const body = await request.json();
const { session_id } = body;
const session = mockWechatSessions.get(session_id);
if (!session) {
return HttpResponse.json({
code: 404,
message: 'Session 不存在',
data: { status: 'expired' }
});
}
// 检查是否过期5分钟
if (Date.now() - session.createdAt > 5 * 60 * 1000) {
session.status = 'expired';
mockWechatSessions.delete(session_id);
}
console.log('[Mock] 检查微信状态:', { session_id, status: session.status });
return HttpResponse.json({
code: 0,
message: '成功',
data: {
status: session.status,
user: session.user
}
});
}),
// 5. 微信登录确认
http.post('/api/auth/wechat/login', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { session_id } = body;
const session = mockWechatSessions.get(session_id);
if (!session || session.status !== 'confirmed') {
return HttpResponse.json({
success: false,
error: '微信登录未确认或已过期'
}, { status: 400 });
}
const user = session.user;
// 清理 session
mockWechatSessions.delete(session_id);
console.log('[Mock] 微信登录成功:', user);
// 设置当前登录用户
setCurrentUser(user);
return HttpResponse.json({
success: true,
message: '微信登录成功',
user,
token: `mock_wechat_token_${user.id}_${Date.now()}`
});
}),
// 6. 获取微信 H5 授权 URL
http.post('/api/auth/wechat/h5-auth-url', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { redirect_url } = body;
const state = generateWechatSessionId();
const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=mock&redirect_uri=${encodeURIComponent(redirect_url)}&response_type=code&scope=snsapi_userinfo&state=${state}#wechat_redirect`;
console.log('[Mock] 生成微信 H5 授权 URL:', authUrl);
return HttpResponse.json({
code: 0,
message: '成功',
data: {
auth_url: authUrl,
state
}
});
}),
// ==================== Session 管理 ====================
// 7. 检查 SessionAuthContext 使用的正确端点)
http.get('/api/auth/session', async () => {
await delay(300);
// 获取当前登录用户
const currentUser = getCurrentUser();
console.log('[Mock] 检查 Session:', currentUser);
if (currentUser) {
return HttpResponse.json({
success: true,
isAuthenticated: true,
user: currentUser
});
} else {
return HttpResponse.json({
success: true,
isAuthenticated: false,
user: null
});
}
}),
// 8. 检查 Session旧端点保留兼容
http.get('/api/auth/check-session', async () => {
await delay(300);
// 获取当前登录用户
const currentUser = getCurrentUser();
console.log('[Mock] 检查 Session (旧端点):', currentUser);
if (currentUser) {
return HttpResponse.json({
success: true,
isAuthenticated: true,
user: currentUser
});
} else {
return HttpResponse.json({
success: true,
isAuthenticated: false,
user: null
});
}
}),
// 9. 退出登录
http.post('/api/auth/logout', async () => {
await delay(NETWORK_DELAY);
console.log('[Mock] 退出登录');
// 清除当前登录用户
clearCurrentUser();
return HttpResponse.json({
success: true,
message: '退出成功'
});
})
];

View File

@@ -0,0 +1,16 @@
// src/mocks/handlers/index.js
// 汇总所有 Mock Handlers
import { authHandlers } from './auth';
import { accountHandlers } from './account';
// 可以在这里添加更多的 handlers
// import { userHandlers } from './user';
// import { eventHandlers } from './event';
export const handlers = [
...authHandlers,
...accountHandlers,
// ...userHandlers,
// ...eventHandlers,
];