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
+ });
+ })
+];