Files
vf_react/src/views/TradingSimulation/index.js
zdl 2ca58cdff7 fix(TradingSimulation): 修复 React Hooks 调用顺序错误
提取 JSX 中直接调用的 useColorModeValue 到组件顶部,避免 Hooks 顺序不一致。

修改内容:
- 在第 95 行添加 contentTextColor 常量
- 替换第 350 行 Heading 中的内联 Hook 调用
- 替换第 361 行 Text 中的内联 Hook 调用

修复警告:React has detected a change in the order of Hooks called by TradingSimulation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 19:08:24 +08:00

412 lines
12 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/views/TradingSimulation/index.js - 模拟盘系统主页面
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Flex,
Grid,
GridItem,
Tab,
Tabs,
TabList,
TabPanels,
TabPanel,
Card,
CardHeader,
CardBody,
Heading,
Text,
Button,
Badge,
VStack,
HStack,
Stat,
StatLabel,
StatNumber,
StatHelpText,
StatArrow,
useColorModeValue,
Spinner,
Alert,
AlertIcon,
AlertTitle,
AlertDescription,
} from '@chakra-ui/react';
import { useAuth } from '../../contexts/AuthContext';
import { logger } from '../../utils/logger';
// 导入子组件
import AccountOverview from './components/AccountOverview';
import TradingPanel from './components/TradingPanel';
import PositionsList from './components/PositionsList';
import TradingHistory from './components/TradingHistory';
import MarginTrading from './components/MarginTrading';
// 导入现有的高质量组件
import LineChart from '../../components/Charts/LineChart';
// 导航栏已由 MainLayout 提供,无需在此导入
// 模拟盘账户管理 Hook
import { useTradingAccount } from './hooks/useTradingAccount';
import { useTradingSimulationEvents } from './hooks/useTradingSimulationEvents';
export default function TradingSimulation() {
// ========== 1. 所有 Hooks 必须放在最顶部,不能有任何条件判断 ==========
const { user, isAuthenticated } = useAuth();
const [activeTab, setActiveTab] = useState(0);
const [assetHistory, setAssetHistory] = useState([]); // 移到这里!
// ⚡ 提取 userId 为独立变量,避免 user 对象引用变化导致无限循环
const userId = user?.id;
const prevUserIdRef = React.useRef(userId);
const prevIsAuthenticatedRef = React.useRef(isAuthenticated);
// 使用模拟账户管理 Hook
const {
account,
positions,
tradingHistory,
isLoading,
error,
buyStock,
sellStock,
cancelOrder,
refreshAccount,
searchStocks,
getAssetHistory
} = useTradingAccount();
// 🎯 初始化模拟盘埋点Hook传入账户信息
const tradingEvents = useTradingSimulationEvents({
portfolio: account ? {
totalValue: account.total_assets,
availableCash: account.available_cash,
holdingsCount: positions?.length || 0
} : null
});
// 所有的 useColorModeValue 也必须在顶部
const bgColor = useColorModeValue('gray.50', 'gray.900');
const cardBg = useColorModeValue('white', 'gray.800');
const xAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
const yAxisLabelColor = useColorModeValue('#718096', '#A0AEC0');
const gridBorderColor = useColorModeValue('#E2E8F0', '#4A5568');
const contentTextColor = useColorModeValue('gray.700', 'white');
// ========== 2. 所有 useEffect 也必须在条件返回之前 ==========
useEffect(() => {
// 调试模式:即使没有登录也加载模拟数据
refreshAccount();
}, [refreshAccount]);
// 调试:观察认证状态变化
useEffect(() => {
const userIdChanged = prevUserIdRef.current !== userId;
const authChanged = prevIsAuthenticatedRef.current !== isAuthenticated;
if (userIdChanged || authChanged) {
prevUserIdRef.current = userId;
prevIsAuthenticatedRef.current = isAuthenticated;
logger.debug('TradingSimulation', '组件挂载,认证状态检查', {
isAuthenticated,
userId,
userName: user?.name
});
}
}, [isAuthenticated, userId, user]); // ⚡ 使用 userId防重复通过 ref 判断
// 获取资产历史数据的 useEffect
useEffect(() => {
if (account) {
getAssetHistory(30).then(data => {
setAssetHistory(data || []);
logger.debug('TradingSimulation', '资产历史数据加载成功', {
accountId: account.id,
dataPoints: data?.length || 0
});
}).catch(err => {
logger.error('TradingSimulation', 'getAssetHistory', err, {
accountId: account?.id,
days: 30
});
setAssetHistory([]);
});
}
}, [account, getAssetHistory]);
// ========== 3. 数据处理和计算(不是 Hooks可以放在这里==========
// 准备资产走势图表数据(使用真实数据,安全处理)
const hasAssetData = Array.isArray(assetHistory) && assetHistory.length > 0;
const assetTrendData = hasAssetData ? [{
name: "总资产",
data: assetHistory.map(item => {
// 安全地获取数据避免undefined错误
if (!item) return 0;
return item.closing_assets || item.total_assets || 0;
})
}] : [];
const assetTrendOptions = hasAssetData ? {
chart: {
toolbar: { show: false },
height: 350
},
tooltip: {
theme: "dark",
y: {
formatter: function(val) {
return '¥' + (val || 0).toLocaleString()
}
}
},
dataLabels: {
enabled: false,
},
stroke: {
curve: "smooth",
width: 3
},
xaxis: {
type: "datetime",
categories: assetHistory.map(item => {
// 安全地获取日期
if (!item) return '';
return item.date || '';
}),
labels: {
style: {
colors: xAxisLabelColor,
fontSize: "12px",
},
},
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
},
yaxis: {
labels: {
style: {
colors: yAxisLabelColor,
fontSize: "12px",
},
formatter: function(val) {
return '¥' + ((val || 0) / 10000).toFixed(1) + 'w'
}
},
},
legend: {
show: false,
},
grid: {
strokeDashArray: 5,
borderColor: gridBorderColor
},
fill: {
type: "gradient",
gradient: {
shade: "light",
type: "vertical",
shadeIntensity: 0.5,
gradientToColors: undefined,
inverseColors: true,
opacityFrom: 0.8,
opacityTo: 0,
stops: [],
},
colors: [account?.totalProfit >= 0 ? "#48BB78" : "#F56565"],
},
colors: [account?.totalProfit >= 0 ? "#48BB78" : "#F56565"],
} : {};
// ========== 4. 现在可以安全地进行条件返回了 ==========
if (isLoading) {
return (
<Container maxW="7xl" py={8}>
<Flex justify="center" align="center" minH="400px">
<VStack spacing={4}>
<Spinner size="xl" color="blue.500" />
<Text>正在加载模拟盘数据...</Text>
</VStack>
</Flex>
</Container>
);
}
if (error) {
return (
<Container maxW="7xl" py={8}>
<Alert status="error">
<AlertIcon />
<AlertTitle>加载失败</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
</Container>
);
}
// ========== 5. 主要渲染逻辑 ==========
return (
<Box minH="100vh" bg={bgColor}>
{/* 导航栏已由 MainLayout 提供 */}
<Container maxW="7xl" py={8}>
{!isAuthenticated ? (
<Alert status="warning">
<AlertIcon />
<AlertTitle>需要登录</AlertTitle>
<AlertDescription>
请先登录以访问模拟盘系统
</AlertDescription>
</Alert>
) : (
<VStack spacing={8} align="stretch">
{/* 现代化页面标题 */}
<Box textAlign="center" py={6}>
<Heading
size="2xl"
bgGradient="linear(to-r, blue.400, purple.500)"
bgClip="text"
fontWeight="extrabold"
mb={3}
>
智能模拟交易平台
</Heading>
<Text fontSize="xl" color="gray.600" maxW="2xl" mx="auto">
体验真实交易环境提升投资技能零风险练手
</Text>
</Box>
{/* 主要功能区域 - 放在上面 */}
<Tabs
index={activeTab}
onChange={(index) => {
setActiveTab(index);
// 🎯 追踪 Tab 切换
const tabNames = ['trading', 'holdings', 'history', 'margin'];
tradingEvents.trackTabClicked(tabNames[index]);
}}
variant="soft-rounded"
colorScheme="blue"
size="lg"
>
<TabList bg={cardBg} p={2} borderRadius="xl" shadow="sm">
<Tab fontWeight="bold">💹 交易面板</Tab>
<Tab fontWeight="bold">📊 我的持仓</Tab>
<Tab fontWeight="bold">📋 交易历史</Tab>
<Tab fontWeight="bold">💰 融资融券</Tab>
</TabList>
<TabPanels>
{/* 交易面板 */}
<TabPanel px={0}>
<TradingPanel
account={account}
onBuyStock={buyStock}
onSellStock={sellStock}
searchStocks={searchStocks}
tradingEvents={tradingEvents}
/>
</TabPanel>
{/* 我的持仓 */}
<TabPanel px={0}>
<PositionsList
positions={positions}
account={account}
onSellStock={sellStock}
tradingEvents={tradingEvents}
/>
</TabPanel>
{/* 交易历史 */}
<TabPanel px={0}>
<TradingHistory
history={tradingHistory}
onCancelOrder={cancelOrder}
tradingEvents={tradingEvents}
/>
</TabPanel>
{/* 融资融券 */}
<TabPanel px={0}>
<MarginTrading
account={account}
onMarginBuy={buyStock}
onShortSell={sellStock}
/>
</TabPanel>
</TabPanels>
</Tabs>
{/* 统计数据区域 - 放在下面 */}
<VStack spacing={6} align="stretch">
{/* 账户概览统计 */}
<Box>
<Heading size="lg" mb={4} color={contentTextColor}>
📊 账户统计分析
</Heading>
<AccountOverview account={account} tradingEvents={tradingEvents} />
</Box>
{/* 资产走势图表 - 只在有数据时显示 */}
{hasAssetData && (
<Card>
<CardHeader>
<HStack justify="space-between">
<Text fontSize="lg" fontWeight="bold" color={contentTextColor}>
📈 资产走势分析
</Text>
<Badge colorScheme="purple" variant="outline">
{assetHistory.length}
</Badge>
</HStack>
</CardHeader>
<CardBody>
<Box h="350px">
<LineChart
chartData={assetTrendData}
chartOptions={assetTrendOptions}
/>
</Box>
</CardBody>
</Card>
)}
{/* 无数据提示 */}
{!hasAssetData && account && (
<Card>
<CardBody>
<VStack spacing={4} py={8}>
<Text fontSize="lg" color="gray.500">📊 暂无历史数据</Text>
<Text fontSize="sm" color="gray.400" textAlign="center">
开始交易后这里将显示您的资产走势图表和详细统计分析
</Text>
</VStack>
</CardBody>
</Card>
)}
</VStack>
{/* 风险提示 */}
<Alert status="info" variant="left-accent">
<AlertIcon />
<Box>
<AlertTitle>风险提示</AlertTitle>
<AlertDescription>
本系统为模拟交易所有数据仅供学习和练习使用不构成实际投资建议
模拟盘起始资金为100万元人民币
</AlertDescription>
</Box>
</Alert>
</VStack>
)}
</Container>
</Box>
);
}