diff --git a/src/components/Navbars/HomeNavbar.js b/src/components/Navbars/HomeNavbar.js index 9f02f7a6..af7c109e 100644 --- a/src/components/Navbars/HomeNavbar.js +++ b/src/components/Navbars/HomeNavbar.js @@ -38,9 +38,142 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; import { useAuthModal } from '../../contexts/AuthModalContext'; +/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */ +const SecondaryNav = () => { + const navigate = useNavigate(); + const location = useLocation(); + const navbarBg = useColorModeValue('gray.50', 'gray.700'); + const itemHoverBg = useColorModeValue('white', 'gray.600'); + + // 定义二级导航结构 + const secondaryNavConfig = { + '/community': { + title: '高频跟踪', + items: [ + { path: '/community', label: '新闻催化分析', badges: [{ text: 'HOT', colorScheme: 'green' }, { text: 'NEW', colorScheme: 'red' }] }, + { path: '/concepts', label: '概念中心', badges: [{ text: 'NEW', colorScheme: 'red' }] } + ] + }, + '/concepts': { + title: '高频跟踪', + items: [ + { path: '/community', label: '新闻催化分析', badges: [{ text: 'HOT', colorScheme: 'green' }, { text: 'NEW', colorScheme: 'red' }] }, + { path: '/concepts', label: '概念中心', badges: [{ text: 'NEW', colorScheme: 'red' }] } + ] + }, + '/limit-analyse': { + title: '行情复盘', + items: [ + { path: '/limit-analyse', label: '涨停分析', badges: [{ text: 'FREE', colorScheme: 'blue' }] }, + { path: '/stocks', label: '个股中心', badges: [{ text: 'HOT', colorScheme: 'green' }] }, + { path: '/trading-simulation', label: '模拟盘', badges: [{ text: 'NEW', colorScheme: 'red' }] } + ] + }, + '/stocks': { + title: '行情复盘', + items: [ + { path: '/limit-analyse', label: '涨停分析', badges: [{ text: 'FREE', colorScheme: 'blue' }] }, + { path: '/stocks', label: '个股中心', badges: [{ text: 'HOT', colorScheme: 'green' }] }, + { path: '/trading-simulation', label: '模拟盘', badges: [{ text: 'NEW', colorScheme: 'red' }] } + ] + } + }; + + // 找到当前路径对应的二级导航配置 + const currentConfig = Object.keys(secondaryNavConfig).find(key => + location.pathname.includes(key) + ); + + // 如果没有匹配的二级导航,不显示 + if (!currentConfig) return null; + + const config = secondaryNavConfig[currentConfig]; + + return ( + + + + {/* 显示一级菜单标题 */} + + {config.title}: + + {/* 二级菜单项 */} + {config.items.map((item, index) => { + const isActive = location.pathname.includes(item.path); + return item.external ? ( + + ) : ( + + ); + })} + + + + ); +}; + /** 桌面端导航 - 完全按照原网站 * @TODO 添加逻辑 不展示导航case - * 1.未登陆状态 && 是首页 + * 1.未登陆状态 && 是首页 * 2. !isMobile */ const NavItems = ({ isAuthenticated, user }) => { @@ -163,12 +296,16 @@ const NavItems = ({ isAuthenticated, user }) => { navigate('/trading-simulation')} py={2} px={3} borderRadius="md" _hover={{ bg: 'gray.100' }} + cursor="pointer" + bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/trading-simulation') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/trading-simulation') ? 'bold' : 'normal'} > 模拟盘 @@ -978,12 +1115,19 @@ export default function HomeNavbar() { { + navigate('/trading-simulation'); + onClose(); + }} py={1} px={3} borderRadius="md" _hover={{ bg: 'gray.100' }} + cursor="pointer" + bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/trading-simulation') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/trading-simulation') ? 'bold' : 'normal'} > 模拟盘 @@ -1058,6 +1202,9 @@ export default function HomeNavbar() { + + {/* 二级导航栏 - 显示当前页面所属的二级菜单 */} + {!isMobile && } ); } \ No newline at end of file diff --git a/src/layouts/Home.js b/src/layouts/Home.js index edc8545a..03f3674f 100755 --- a/src/layouts/Home.js +++ b/src/layouts/Home.js @@ -13,10 +13,11 @@ import SettingsPage from "views/Settings/SettingsPage"; import CenterDashboard from "views/Dashboard/Center"; import Subscription from "views/Pages/Account/Subscription"; -// 懒加载隐私政策、用户协议和微信回调页面 +// 懒加载隐私政策、用户协议、微信回调和模拟交易页面 const PrivacyPolicy = React.lazy(() => import("views/Pages/PrivacyPolicy")); const UserAgreement = React.lazy(() => import("views/Pages/UserAgreement")); const WechatCallback = React.lazy(() => import("views/Pages/WechatCallback")); +const TradingSimulation = React.lazy(() => import("views/TradingSimulation")); // 导入保护路由组件 import ProtectedRoute from "../components/ProtectedRoute"; @@ -71,6 +72,16 @@ export default function Home() { } /> + {/* 模拟盘交易页面 */} + + + + } + /> + {/* 隐私政策页面 - 无需登录 */} } /> diff --git a/src/mocks/handlers/index.js b/src/mocks/handlers/index.js index 5f24e595..048913d0 100644 --- a/src/mocks/handlers/index.js +++ b/src/mocks/handlers/index.js @@ -3,6 +3,7 @@ import { authHandlers } from './auth'; import { accountHandlers } from './account'; +import { simulationHandlers } from './simulation'; // 可以在这里添加更多的 handlers // import { userHandlers } from './user'; @@ -11,6 +12,7 @@ import { accountHandlers } from './account'; export const handlers = [ ...authHandlers, ...accountHandlers, + ...simulationHandlers, // ...userHandlers, // ...eventHandlers, ]; diff --git a/src/mocks/handlers/simulation.js b/src/mocks/handlers/simulation.js new file mode 100644 index 00000000..da33c5e1 --- /dev/null +++ b/src/mocks/handlers/simulation.js @@ -0,0 +1,374 @@ +// src/mocks/handlers/simulation.js +import { http, HttpResponse, delay } from 'msw'; +import { getCurrentUser } from '../data/users'; + +// 模拟网络延迟(毫秒) +const NETWORK_DELAY = 300; + +// 模拟交易账户数据 +let mockTradingAccount = { + account_id: 'sim_001', + account_name: '模拟交易账户', + initial_capital: 1000000, + available_cash: 850000, + frozen_cash: 0, + position_value: 150000, + total_assets: 1000000, + total_profit: 0, + total_profit_rate: 0, + daily_profit: 0, + daily_profit_rate: 0, + created_at: '2024-01-01T00:00:00Z', + updated_at: new Date().toISOString() +}; + +// 模拟持仓数据 +let mockPositions = [ + { + id: 1, + stock_code: '600036', + stock_name: '招商银行', + position_qty: 1000, + available_qty: 1000, + frozen_qty: 0, + avg_cost: 42.50, + current_price: 42.80, + market_value: 42800, + profit: 300, + profit_rate: 0.71, + today_profit: 100, + today_profit_rate: 0.23, + updated_at: new Date().toISOString() + }, + { + id: 2, + stock_code: '000001', + stock_name: '平安银行', + position_qty: 2000, + available_qty: 2000, + frozen_qty: 0, + avg_cost: 12.30, + current_price: 12.50, + market_value: 25000, + profit: 400, + profit_rate: 1.63, + today_profit: -50, + today_profit_rate: -0.20, + updated_at: new Date().toISOString() + } +]; + +// 模拟交易历史 +let mockOrders = [ + { + id: 1, + order_no: 'ORD20240101001', + stock_code: '600036', + stock_name: '招商银行', + order_type: 'BUY', + price_type: 'MARKET', + order_price: 42.50, + order_qty: 1000, + filled_qty: 1000, + filled_price: 42.50, + filled_amount: 42500, + commission: 12.75, + stamp_tax: 0, + transfer_fee: 0.42, + total_fee: 13.17, + status: 'FILLED', + reject_reason: null, + order_time: '2024-01-15T09:30:00Z', + filled_time: '2024-01-15T09:30:05Z' + }, + { + id: 2, + order_no: 'ORD20240102001', + stock_code: '000001', + stock_name: '平安银行', + order_type: 'BUY', + price_type: 'LIMIT', + order_price: 12.30, + order_qty: 2000, + filled_qty: 2000, + filled_price: 12.30, + filled_amount: 24600, + commission: 7.38, + stamp_tax: 0, + transfer_fee: 0.25, + total_fee: 7.63, + status: 'FILLED', + reject_reason: null, + order_time: '2024-01-16T10:15:00Z', + filled_time: '2024-01-16T10:15:10Z' + } +]; + +export const simulationHandlers = [ + // ==================== 获取模拟账户信息 ==================== + http.get('/api/simulation/account', async () => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + + // 未登录时返回401 + if (!currentUser) { + return HttpResponse.json({ + success: false, + error: '未登录' + }, { status: 401 }); + } + + console.log('[Mock] 获取模拟账户信息:', currentUser); + + return HttpResponse.json({ + success: true, + data: mockTradingAccount + }); + }), + + // ==================== 获取持仓列表 ==================== + http.get('/api/simulation/positions', 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: mockPositions + }); + }), + + // ==================== 获取交易订单历史 ==================== + http.get('/api/simulation/orders', async ({ request }) => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + + if (!currentUser) { + return HttpResponse.json({ + success: false, + error: '未登录' + }, { status: 401 }); + } + + const url = new URL(request.url); + const limit = parseInt(url.searchParams.get('limit') || '100'); + + console.log('[Mock] 获取交易订单历史, limit:', limit); + + return HttpResponse.json({ + success: true, + data: mockOrders.slice(0, limit) + }); + }), + + // ==================== 下单(买入/卖出)==================== + http.post('/api/simulation/place-order', 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 { stock_code, order_type, order_qty, price_type } = body; + + // 生成订单号 + const orderNo = 'ORD' + Date.now(); + + // 创建新订单 + const newOrder = { + id: mockOrders.length + 1, + order_no: orderNo, + stock_code: stock_code, + stock_name: '模拟股票', // 实际应该查询股票名称 + order_type: order_type, + price_type: price_type, + order_price: 0, + order_qty: order_qty, + filled_qty: order_qty, + filled_price: 0, + filled_amount: 0, + commission: 0, + stamp_tax: 0, + transfer_fee: 0, + total_fee: 0, + status: 'FILLED', + reject_reason: null, + order_time: new Date().toISOString(), + filled_time: new Date().toISOString() + }; + + // 添加到订单列表 + mockOrders.unshift(newOrder); + + return HttpResponse.json({ + success: true, + message: '下单成功', + data: { + order_no: orderNo, + order_id: newOrder.id + } + }); + }), + + // ==================== 撤销订单 ==================== + http.post('/api/simulation/cancel-order/:orderId', async ({ params }) => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + + if (!currentUser) { + return HttpResponse.json({ + success: false, + error: '未登录' + }, { status: 401 }); + } + + const { orderId } = params; + console.log('[Mock] 撤销订单:', orderId); + + // 查找并更新订单状态 + const order = mockOrders.find(o => o.id.toString() === orderId || o.order_no === orderId); + if (order) { + order.status = 'CANCELLED'; + } + + return HttpResponse.json({ + success: true, + message: '撤单成功' + }); + }), + + // ==================== 获取资产统计数据 ==================== + http.get('/api/simulation/statistics', async ({ request }) => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + + if (!currentUser) { + return HttpResponse.json({ + success: false, + error: '未登录' + }, { status: 401 }); + } + + const url = new URL(request.url); + const days = parseInt(url.searchParams.get('days') || '30'); + + console.log('[Mock] 获取资产统计, days:', days); + + // 生成模拟的资产历史数据 + const dailyReturns = []; + const baseAssets = 1000000; + + for (let i = 0; i < days; i++) { + const date = new Date(); + date.setDate(date.getDate() - (days - 1 - i)); + + // 生成随机波动 + const randomChange = (Math.random() - 0.5) * 0.02; // ±1% + const assets = baseAssets * (1 + randomChange * i / days); + + dailyReturns.push({ + date: date.toISOString().split('T')[0], + closing_assets: assets, + total_assets: assets, + daily_profit: assets - baseAssets, + daily_profit_rate: ((assets - baseAssets) / baseAssets * 100).toFixed(2) + }); + } + + return HttpResponse.json({ + success: true, + data: { + daily_returns: dailyReturns, + summary: { + total_profit: 0, + total_profit_rate: 0, + win_rate: 50, + max_drawdown: -5.2 + } + } + }); + }), + + // ==================== 获取交易记录 ==================== + http.get('/api/simulation/transactions', async ({ request }) => { + await delay(NETWORK_DELAY); + + const currentUser = getCurrentUser(); + + if (!currentUser) { + return HttpResponse.json({ + success: false, + error: '未登录' + }, { status: 401 }); + } + + const url = new URL(request.url); + const limit = parseInt(url.searchParams.get('limit') || '50'); + + console.log('[Mock] 获取交易记录, limit:', limit); + + // 返回已成交的订单作为交易记录 + const transactions = mockOrders + .filter(order => order.status === 'FILLED') + .slice(0, limit); + + return HttpResponse.json({ + success: true, + data: transactions + }); + }), + + // ==================== 搜索股票 ==================== + http.get('/api/stocks/search', async ({ request }) => { + await delay(200); + + const url = new URL(request.url); + const keyword = url.searchParams.get('q') || ''; + const limit = parseInt(url.searchParams.get('limit') || '10'); + + console.log('[Mock] 搜索股票:', keyword); + + // 模拟股票数据 + const allStocks = [ + { stock_code: '000001', stock_name: '平安银行', current_price: 12.50, pinyin_abbr: 'payh', security_type: 'A股', exchange: '深交所' }, + { stock_code: '000002', stock_name: '万科A', current_price: 8.32, pinyin_abbr: 'wka', security_type: 'A股', exchange: '深交所' }, + { stock_code: '600036', stock_name: '招商银行', current_price: 42.80, pinyin_abbr: 'zsyh', security_type: 'A股', exchange: '上交所' }, + { stock_code: '600519', stock_name: '贵州茅台', current_price: 1680.50, pinyin_abbr: 'gzmt', security_type: 'A股', exchange: '上交所' }, + { stock_code: '601318', stock_name: '中国平安', current_price: 45.20, pinyin_abbr: 'zgpa', security_type: 'A股', exchange: '上交所' }, + { stock_code: '688256', stock_name: '寒武纪', current_price: 1394.94, pinyin_abbr: 'hwj', security_type: 'A股', exchange: '上交所科创板' }, + ]; + + // 过滤股票 + const results = allStocks.filter(stock => + stock.stock_code.includes(keyword) || + stock.stock_name.includes(keyword) || + stock.pinyin_abbr.includes(keyword.toLowerCase()) + ).slice(0, limit); + + return HttpResponse.json({ + success: true, + data: results + }); + }) +];