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
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; |