feat: 添加mock数据,给导航添加选中标识
This commit is contained in:
@@ -446,10 +446,10 @@ export default function AuthFormContent() {
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">完善个人信息</AlertDialogHeader>
|
||||
<AlertDialogBody>您已成功注册!是否前往个人中心设置昵称和其他信息?</AlertDialogBody>
|
||||
<AlertDialogBody>您已成功注册!是否前往个人资料设置昵称和其他信息?</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); }}>稍后再说</Button>
|
||||
<Button colorScheme="green" onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); setTimeout(() => { navigate('/admin/profile'); }, 300); }} ml={3}>去设置</Button>
|
||||
<Button colorScheme="green" onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); setTimeout(() => { navigate('/home/profile'); }, 300); }} ml={3}>去设置</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { FormControl, FormErrorMessage, HStack, Input, Button } from "@chakra-ui/react";
|
||||
import { FormControl, FormErrorMessage, HStack, Input, Button, Spinner } from "@chakra-ui/react";
|
||||
|
||||
/**
|
||||
* 通用验证码输入组件
|
||||
@@ -30,6 +30,17 @@ export default function VerificationCodeInput({
|
||||
}
|
||||
};
|
||||
|
||||
// 计算按钮显示的文本(避免在 JSX 中使用条件渲染)
|
||||
const getButtonText = () => {
|
||||
if (isSending) {
|
||||
return "发送中";
|
||||
}
|
||||
if (countdown > 0) {
|
||||
return countdownText(countdown);
|
||||
}
|
||||
return buttonText;
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl isRequired={isRequired} isInvalid={!!error}>
|
||||
<HStack>
|
||||
@@ -41,13 +52,14 @@ export default function VerificationCodeInput({
|
||||
maxLength={6}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
colorScheme={colorScheme}
|
||||
onClick={handleSendCode}
|
||||
isDisabled={countdown > 0 || isLoading}
|
||||
isLoading={isSending}
|
||||
isDisabled={countdown > 0 || isLoading || isSending}
|
||||
minW="120px"
|
||||
leftIcon={isSending ? <Spinner size="sm" /> : undefined}
|
||||
>
|
||||
{countdown > 0 ? countdownText(countdown) : buttonText}
|
||||
{getButtonText()}
|
||||
</Button>
|
||||
</HStack>
|
||||
<FormErrorMessage>{error}</FormErrorMessage>
|
||||
|
||||
@@ -17,10 +17,25 @@ class ErrorBoundary extends React.Component {
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// 开发环境:不拦截错误,让 React DevTools 显示完整堆栈
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return { hasError: false };
|
||||
}
|
||||
// 生产环境:拦截错误,显示友好界面
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
// 开发环境:打印错误到控制台,但不显示错误边界
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('🔴 ErrorBoundary 捕获到错误(开发模式,不拦截):');
|
||||
console.error('错误:', error);
|
||||
console.error('错误信息:', errorInfo);
|
||||
// 不更新 state,让错误继续抛出
|
||||
return;
|
||||
}
|
||||
|
||||
// 生产环境:保存错误信息到 state
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: errorInfo
|
||||
@@ -28,6 +43,12 @@ class ErrorBoundary extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
// 开发环境:直接渲染子组件,不显示错误边界
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
// 生产环境:如果有错误,显示错误边界
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Container maxW="lg" py={20}>
|
||||
@@ -57,7 +78,7 @@ class ErrorBoundary extends React.Component {
|
||||
<Box fontWeight="bold" mb={2}>错误详情:</Box>
|
||||
<Box as="pre" whiteSpace="pre-wrap">
|
||||
{this.state.error && this.state.error.toString()}
|
||||
{this.state.errorInfo.componentStack}
|
||||
{this.state.errorInfo && this.state.errorInfo.componentStack}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -238,6 +238,9 @@ const NavItems = ({ isAuthenticated, user }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 计算 API 基础地址(移到组件外部,避免每次 render 重新创建)
|
||||
const getApiBase = () => (process.env.NODE_ENV === 'production' ? '' : (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'));
|
||||
|
||||
export default function HomeNavbar() {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const navigate = useNavigate();
|
||||
@@ -269,6 +272,10 @@ export default function HomeNavbar() {
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
// 重置资料完整性检查标志
|
||||
hasCheckedCompleteness.current = false;
|
||||
setProfileCompleteness(null);
|
||||
setShowCompletenessAlert(false);
|
||||
// logout函数已经包含了跳转逻辑,这里不需要额外处理
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
@@ -293,13 +300,13 @@ export default function HomeNavbar() {
|
||||
const [profileCompleteness, setProfileCompleteness] = useState(null);
|
||||
const [showCompletenessAlert, setShowCompletenessAlert] = useState(false);
|
||||
|
||||
// 计算 API 基础地址(与 Center.js 一致的策略)
|
||||
const getApiBase = () => (process.env.NODE_ENV === 'production' ? '' : (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'));
|
||||
// 添加标志位:追踪是否已经检查过资料完整性(避免重复请求)
|
||||
const hasCheckedCompleteness = React.useRef(false);
|
||||
|
||||
const loadWatchlistQuotes = useCallback(async () => {
|
||||
try {
|
||||
setWatchlistLoading(true);
|
||||
const base = getApiBase();
|
||||
const base = getApiBase(); // 使用外部函数
|
||||
const resp = await fetch(base + '/api/account/watchlist/realtime', {
|
||||
credentials: 'include',
|
||||
cache: 'no-store',
|
||||
@@ -321,7 +328,7 @@ export default function HomeNavbar() {
|
||||
} finally {
|
||||
setWatchlistLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, []); // getApiBase 是外部函数,不需要作为依赖
|
||||
|
||||
const loadFollowingEvents = useCallback(async () => {
|
||||
try {
|
||||
@@ -369,7 +376,7 @@ export default function HomeNavbar() {
|
||||
} finally {
|
||||
setEventsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, []); // getApiBase 是外部函数,不需要作为依赖
|
||||
|
||||
// 从自选股移除
|
||||
const handleRemoveFromWatchlist = useCallback(async (stockCode) => {
|
||||
@@ -399,7 +406,7 @@ export default function HomeNavbar() {
|
||||
} catch (e) {
|
||||
toast({ title: '网络错误,移除失败', status: 'error', duration: 2000 });
|
||||
}
|
||||
}, [getApiBase, toast, WATCHLIST_PAGE_SIZE]);
|
||||
}, [toast]); // WATCHLIST_PAGE_SIZE 是常量,getApiBase 是外部函数,不需要作为依赖
|
||||
|
||||
// 取消关注事件
|
||||
const handleUnfollowEvent = useCallback(async (eventId) => {
|
||||
@@ -424,13 +431,20 @@ export default function HomeNavbar() {
|
||||
} catch (e) {
|
||||
toast({ title: '网络错误,操作失败', status: 'error', duration: 2000 });
|
||||
}
|
||||
}, [getApiBase, toast, EVENTS_PAGE_SIZE]);
|
||||
}, [toast]); // EVENTS_PAGE_SIZE 是常量,getApiBase 是外部函数,不需要作为依赖
|
||||
|
||||
// 检查用户资料完整性
|
||||
const checkProfileCompleteness = useCallback(async () => {
|
||||
if (!isAuthenticated || !user) return;
|
||||
|
||||
// 如果已经检查过,跳过(避免重复请求)
|
||||
if (hasCheckedCompleteness.current) {
|
||||
console.log('[Profile] 已检查过资料完整性,跳过重复请求');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[Profile] 开始检查资料完整性...');
|
||||
const base = getApiBase();
|
||||
const resp = await fetch(base + '/api/account/profile-completeness', {
|
||||
credentials: 'include'
|
||||
@@ -442,12 +456,25 @@ export default function HomeNavbar() {
|
||||
setProfileCompleteness(data.data);
|
||||
// 只有微信用户且资料不完整时才显示提醒
|
||||
setShowCompletenessAlert(data.data.needsAttention);
|
||||
// 标记为已检查
|
||||
hasCheckedCompleteness.current = true;
|
||||
console.log('[Profile] 资料完整性检查完成:', data.data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('检查资料完整性失败:', error);
|
||||
}
|
||||
}, [isAuthenticated, user, getApiBase]);
|
||||
}, [isAuthenticated, user]); // 移除 getApiBase 依赖,因为它现在在组件外部
|
||||
|
||||
// 监听用户变化,重置检查标志(用户切换或退出登录时)
|
||||
React.useEffect(() => {
|
||||
if (!isAuthenticated || !user) {
|
||||
// 用户退出登录,重置标志
|
||||
hasCheckedCompleteness.current = false;
|
||||
setProfileCompleteness(null);
|
||||
setShowCompletenessAlert(false);
|
||||
}
|
||||
}, [isAuthenticated, user?.id]); // 监听用户 ID 变化
|
||||
|
||||
// 用户登录后检查资料完整性
|
||||
React.useEffect(() => {
|
||||
|
||||
32
src/index.js
32
src/index.js
@@ -11,14 +11,26 @@ import './styles/brainwave-colors.css';
|
||||
// Import the main App component
|
||||
import App from './App';
|
||||
|
||||
// Create root
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
// 启动 Mock Service Worker(如果启用)
|
||||
async function startApp() {
|
||||
// 只在开发环境启动 MSW
|
||||
if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_ENABLE_MOCK === 'true') {
|
||||
const { startMockServiceWorker } = await import('./mocks/browser');
|
||||
await startMockServiceWorker();
|
||||
}
|
||||
|
||||
// Render the app with Router wrapper
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</React.StrictMode>
|
||||
);
|
||||
// Create root
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
|
||||
// Render the app with Router wrapper
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
// 启动应用
|
||||
startApp();
|
||||
61
src/mocks/browser.js
Normal file
61
src/mocks/browser.js
Normal 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
107
src/mocks/data/users.js
Normal 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] 清除当前登录用户');
|
||||
}
|
||||
248
src/mocks/handlers/account.js
Normal file
248
src/mocks/handlers/account.js
Normal 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
330
src/mocks/handlers/auth.js
Normal 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. 检查 Session(AuthContext 使用的正确端点)
|
||||
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: '退出成功'
|
||||
});
|
||||
})
|
||||
];
|
||||
16
src/mocks/handlers/index.js
Normal file
16
src/mocks/handlers/index.js
Normal 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,
|
||||
];
|
||||
Reference in New Issue
Block a user