You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

516 lines
18 KiB

import React, { useState, useEffect, useRef } from 'react';
import { Card, Button, Row, Col, Select, Tabs, Tag, Statistic, Alert, Spin } from 'antd';
import { useParams, useNavigate } from 'react-router-dom';
import { LineChartOutlined, BarChartOutlined, AreaChartOutlined, ArrowUpOutlined, AlertOutlined, RobotOutlined, SafetyOutlined } from '@ant-design/icons';
import { generateFutureData, generateKlineData } from '../../utils/mockData';
import { createChart } from 'lightweight-charts';
import './Detail.css';
const { Option } = Select;
const { TabPane } = Tabs;
const Detail = () => {
const navigate = useNavigate();
const { code } = useParams();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [currentPeriod, setCurrentPeriod] = useState('1H');
const [currentIndicator, setCurrentIndicator] = useState('MA');
const chartRef = useRef(null);
const chartInstance = useRef(null);
console.log('Detail page loaded with code:', code);
useEffect(() => {
// 从后端API获取品种详情数据
const fetchFutureDetail = async () => {
try {
console.log('Fetching detail for code:', code);
const response = await fetch(`http://localhost:3007/api/market/detail/${code}`);
if (!response.ok) {
throw new Error('获取品种详情失败');
}
const result = await response.json();
console.log('Received detail data:', result);
setData(result.data);
setLoading(false);
} catch (error) {
console.error('获取品种详情失败:', error);
// 失败时使用模拟数据
const futureData = generateFutureData(code, code); // 使用code作为品种名称
setData(futureData);
setLoading(false);
}
};
fetchFutureDetail();
}, [code]);
// 初始化和更新K线图表
useEffect(() => {
if (!data || !chartRef.current) return;
// 销毁旧图表
if (chartInstance.current) {
chartInstance.current.destroy();
}
// 从后端API获取K线数据
const fetchKlineData = async () => {
try {
console.log('Fetching Kline data for code:', code, 'period:', currentPeriod);
const response = await fetch(`http://localhost:3007/api/market/klines/${code}?period=${currentPeriod}`);
if (!response.ok) {
throw new Error('获取K线数据失败');
}
const result = await response.json();
console.log('Received Kline data:', result);
// 检查数据格式
if (result.data && Array.isArray(result.data) && result.data.length > 0) {
return result.data;
} else {
console.warn('Kline data is empty or invalid, using mock data');
// 失败时使用模拟数据
return generateKlineData(30);
}
} catch (error) {
console.error('获取K线数据失败:', error);
// 失败时使用模拟数据
return generateKlineData(30);
}
};
fetchKlineData().then(klineData => {
// 检查chartRef.current是否存在
if (!chartRef.current) return;
// 创建图表
const chart = createChart(chartRef.current, {
width: chartRef.current.clientWidth,
height: 400,
timeScale: {
timeVisible: true,
secondsVisible: false,
},
grid: {
vertLines: {
color: 'rgba(42, 46, 57, 0.1)',
},
horzLines: {
color: 'rgba(42, 46, 57, 0.1)',
},
},
});
// 检查chart对象是否正确创建
console.log('Chart object:', chart);
console.log('Chart methods:', Object.keys(chart));
// 添加K线系列
try {
const candlestickSeries = chart.addCandlestickSeries({
upColor: '#52c41a',
downColor: '#ff4d4f',
borderVisible: false,
wickUpColor: '#52c41a',
wickDownColor: '#ff4d4f',
});
// 设置K线数据
candlestickSeries.setData(klineData.map(item => ({
time: new Date(item.timestamp * 1000).toISOString().split('T')[0],
open: item.open,
high: item.high,
low: item.low,
close: item.close,
})));
// 根据当前指标添加相应的技术指标
if (currentIndicator === 'MA') {
// 添加MA5
const ma5Series = chart.addLineSeries({
color: '#1890ff',
lineWidth: 1,
});
// 计算MA5数据
const ma5Data = [];
for (let i = 4; i < klineData.length; i++) {
const sum = klineData.slice(i - 4, i + 1).reduce((acc, item) => acc + item.close, 0);
ma5Data.push({
time: new Date(klineData[i].timestamp * 1000).toISOString().split('T')[0],
value: sum / 5,
});
}
ma5Series.setData(ma5Data);
// 添加MA10
const ma10Series = chart.addLineSeries({
color: '#faad14',
lineWidth: 1,
});
// 计算MA10数据
const ma10Data = [];
for (let i = 9; i < klineData.length; i++) {
const sum = klineData.slice(i - 9, i + 1).reduce((acc, item) => acc + item.close, 0);
ma10Data.push({
time: new Date(klineData[i].timestamp * 1000).toISOString().split('T')[0],
value: sum / 10,
});
}
ma10Series.setData(ma10Data);
}
} catch (error) {
console.error('Error creating chart series:', error);
// 失败时使用模拟数据创建简单的线图
const lineSeries = chart.addLineSeries({
color: '#1890ff',
lineWidth: 1,
});
lineSeries.setData(klineData.map(item => ({
time: new Date(item.timestamp * 1000).toISOString().split('T')[0],
value: item.close,
})));
}
// 保存图表实例
chartInstance.current = chart;
// 处理窗口大小变化
const handleResize = () => {
if (chartInstance.current) {
chartInstance.current.resize(chartRef.current.clientWidth, 400);
}
};
window.addEventListener('resize', handleResize);
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
if (chartInstance.current) {
chartInstance.current.destroy();
}
};
});
}, [data, currentPeriod, currentIndicator, code]);
const handleBack = () => {
navigate('/');
};
const handlePeriodChange = (value) => {
setCurrentPeriod(value);
};
const handleIndicatorChange = (value) => {
setCurrentIndicator(value);
};
if (loading) {
return (
<div className="loading-container">
<Spin size="large" tip="加载数据中..." />
</div>
);
}
if (!data) {
return (
<div className="error-container">
<Alert message="未找到品种数据" type="error" />
<Button type="primary" onClick={handleBack} style={{ marginTop: 16 }}>
返回主页
</Button>
</div>
);
}
return (
<div className="detail">
{/* 页面头部 */}
<div className="detail-header">
<Button type="default" onClick={handleBack} style={{ marginBottom: 16 }}>
返回主页
</Button>
<div className="header-info">
<h2>{data.name} ({data.code})</h2>
<div className="price-info">
<Statistic
title="当前价格"
value={data.currentPrice}
precision={2}
valueStyle={{ color: data.changePercent >= 0 ? '#52c41a' : '#ff4d4f' }}
/>
<Statistic
title="涨跌幅"
value={data.changePercent}
suffix="%"
valueStyle={{ color: data.changePercent >= 0 ? '#52c41a' : '#ff4d4f' }}
/>
<Statistic
title="胜率"
value={data.winRate}
suffix="%"
valueStyle={{ color: '#1890ff' }}
/>
</div>
</div>
</div>
{/* K线图表区 */}
<Card className="detail-card" style={{ marginBottom: 24 }}>
<div className="chart-header">
<div className="chart-title">
<h3>K线图表</h3>
<div className="chart-controls">
<span style={{ marginRight: 16 }}>周期:</span>
<Select
value={currentPeriod}
onChange={handlePeriodChange}
style={{ width: 120, marginRight: 16 }}
>
<Option value="5M">5分钟</Option>
<Option value="30M">30分钟</Option>
<Option value="1H">1小时</Option>
<Option value="1D">1</Option>
<Option value="1W">1</Option>
</Select>
<span style={{ marginRight: 16 }}>指标:</span>
<Select
value={currentIndicator}
onChange={handleIndicatorChange}
style={{ width: 120 }}
>
<Option value="MA">MA</Option>
<Option value="MACD">MACD</Option>
<Option value="KDJ">KDJ</Option>
<Option value="RSI">RSI</Option>
<Option value="BOLL">布林带</Option>
</Select>
</div>
</div>
</div>
<div className="kline-chart">
{/* TradingView Lightweight Charts */}
<div ref={chartRef} className="chart-container"></div>
</div>
</Card>
{/* 技术指标区 */}
<Card className="detail-card" style={{ marginBottom: 24 }}>
<div className="section-header">
<h3>技术指标</h3>
<BarChartOutlined />
</div>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={8} lg={6}>
<div className="indicator-item">
<div className="indicator-label">MA5</div>
<div className="indicator-value">{data.technicalIndicators?.ma5 || 'N/A'}</div>
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div className="indicator-item">
<div className="indicator-label">MA10</div>
<div className="indicator-value">{data.technicalIndicators?.ma10 || 'N/A'}</div>
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div className="indicator-item">
<div className="indicator-label">MACD</div>
<div className="indicator-value">{data.technicalIndicators?.macd || 'N/A'}</div>
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div className="indicator-item">
<div className="indicator-label">RSI</div>
<div className="indicator-value">{data.technicalIndicators?.rsi || 'N/A'}</div>
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div className="indicator-item">
<div className="indicator-label">KDJ</div>
<div className="indicator-value">{data.technicalIndicators?.kdj || 'N/A'}</div>
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div className="indicator-item">
<div className="indicator-label">布林带</div>
<div className="indicator-value">{data.technicalIndicators?.bollinger || 'N/A'}</div>
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div className="indicator-item">
<div className="indicator-label">ATR</div>
<div className="indicator-value">{data.atr}</div>
</div>
</Col>
<Col xs={24} sm={12} md={8} lg={6}>
<div className="indicator-item">
<div className="indicator-label">ADX</div>
<div className="indicator-value">{data.adx}</div>
</div>
</Col>
</Row>
</Card>
{/* 多周期趋势和AI研判区 */}
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
{/* 多周期趋势 */}
<Col xs={24} lg={12}>
<Card className="detail-card">
<div className="section-header">
<h3>多周期趋势</h3>
<AreaChartOutlined />
</div>
<Row gutter={[16, 16]}>
{data.trends && Object.entries(data.trends).map(([period, trend]) => (
<Col xs={12} sm={6} key={period}>
<div className="trend-card">
<div className="trend-header">
<h4>{period.replace('MIN', 'min').replace('HOUR', 'min')}</h4>
<Tag color={trend.direction === '看多' ? 'green' : trend.direction === '看空' ? 'red' : 'orange'}>
{trend.direction}
</Tag>
</div>
<div className="trend-status">{trend.status}</div>
<div className="trend-rsi">RSI: {trend.rsi}</div>
</div>
</Col>
))}
</Row>
</Card>
</Col>
{/* AI研判区 */}
<Col xs={24} lg={12}>
<Card className="detail-card">
<div className="section-header">
<h3>AI研判</h3>
<RobotOutlined />
</div>
<div className="ai-analysis">
<div className="ai-overview">
<Statistic
title="趋势预测"
value={data.aiPrediction?.trend || '中性'}
valueStyle={{ color: data.aiPrediction?.trend === '上涨' ? '#52c41a' : data.aiPrediction?.trend === '下跌' ? '#ff4d4f' : '#1890ff' }}
/>
<Statistic
title="预测胜率"
value={data.aiPrediction?.winRate || 0}
suffix="%"
valueStyle={{ color: '#1890ff' }}
/>
<Statistic
title="预期收益"
value={data.aiPrediction?.expectedReturn || 0}
suffix="%"
valueStyle={{ color: '#52c41a' }}
/>
</div>
<div className="ai-details">
<h4>AI分析</h4>
<p>{data.aiAnalysis || 'AI正在分析中...'}</p>
<h4>关键因素</h4>
<div className="factor-tags">
{data.aiPrediction?.keyFactors?.map((factor, index) => (
<Tag key={index} color="blue">{factor}</Tag>
))}
</div>
</div>
</div>
</Card>
</Col>
</Row>
{/* 交易建议区和风险评估区 */}
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
{/* 交易建议区 */}
<Col xs={24} lg={12}>
<Card className="detail-card">
<div className="section-header">
<h3>交易建议</h3>
<ArrowUpOutlined />
</div>
<div className="trading-advice">
<Row gutter={[16, 16]}>
<Col xs={24} sm={8}>
<div className="advice-item">
<div className="advice-label">入场价</div>
<div className="advice-value">{data.tradingAdvice?.entryPrice || 'N/A'}</div>
</div>
</Col>
<Col xs={24} sm={8}>
<div className="advice-item">
<div className="advice-label">止损价</div>
<div className="advice-value" style={{ color: '#ff4d4f' }}>
{data.tradingAdvice?.stopLoss || 'N/A'}
</div>
</div>
</Col>
<Col xs={24} sm={8}>
<div className="advice-item">
<div className="advice-label">目标价</div>
<div className="advice-value" style={{ color: '#52c41a' }}>
{data.tradingAdvice?.targetPrice || 'N/A'}
</div>
</div>
</Col>
</Row>
<div className="advice-details">
<h4>操作建议</h4>
<p>{data.tradingAdvice?.strategy || 'AI正在生成策略...'}</p>
</div>
</div>
</Card>
</Col>
{/* 风险评估区 */}
<Col xs={24} lg={12}>
<Card className="detail-card">
<div className="section-header">
<h3>风险评估</h3>
<SafetyOutlined />
</div>
<div className="risk-assessment">
<Row gutter={[16, 16]}>
<Col xs={24} sm={8}>
<div className="risk-item">
<div className="risk-label">风险等级</div>
<Tag color={data.riskLevel === '高' ? 'red' : data.riskLevel === '中等' ? 'orange' : 'green'}>
{data.riskLevel}
</Tag>
</div>
</Col>
<Col xs={24} sm={8}>
<div className="risk-item">
<div className="risk-label">波动率</div>
<div className="risk-value">{data.volatility || 'N/A'}%</div>
</div>
</Col>
<Col xs={24} sm={8}>
<div className="risk-item">
<div className="risk-label">最大回撤</div>
<div className="risk-value">{data.maxDrawdown || 'N/A'}%</div>
</div>
</Col>
</Row>
<div className="risk-details">
<h4>风险提示</h4>
<Alert
message={data.riskAlert || '无明显风险'}
type={data.riskLevel === '高' ? 'error' : data.riskLevel === '中等' ? 'warning' : 'info'}
showIcon
/>
</div>
</div>
</Card>
</Col>
</Row>
</div>
);
};
export default Detail;