parent
76957ba5b3
commit
2ace0e35ef
@ -0,0 +1,271 @@
|
||||
.watchlist {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.watchlist-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.watchlist-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.watchlist-selector {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.watchlist-header-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.watchlist-header h2 {
|
||||
margin: 0;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.watchlist-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.empty-watchlist {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.empty-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.empty-content h3 {
|
||||
margin: 0;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.empty-content p {
|
||||
margin: 0;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
/* 品种卡片样式 */
|
||||
.future-card {
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.future-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 品种名称和胜率 */
|
||||
.future-header-new {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.future-name-new h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.future-winrate-new {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
/* ATR/ADX和涨跌幅 */
|
||||
.future-info-new {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.future-indicators-new {
|
||||
}
|
||||
|
||||
.future-change-new {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 当前价格和支撑压力位 */
|
||||
.future-price-container-new {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.future-price-new {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.future-levels-new {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.level-label-new {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* 多周期趋势 */
|
||||
.future-trend-title-new {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.future-trends-new {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.trend-item-new {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trend-period-new {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.trend-direction-new {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.trend-status-new {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.trend-rsi-new {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
/* 整体预判 */
|
||||
.future-overview-new {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.overview-label-new {
|
||||
color: #262626;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* AI分析 */
|
||||
.future-ai-analysis-new {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ai-title-new {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.ai-content-new {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.future-actions-new {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-button-new {
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.watchlist-button-new {
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 加载容器 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.watchlist-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.watchlist-header-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.future-trends {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.future-actions-new {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,274 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Card, Row, Col, Button, Select, Tag, message, Spin, Input, Modal, Popconfirm } from 'antd';
|
||||
import { ReloadOutlined, ArrowUpOutlined, ArrowDownOutlined, StarOutlined, DeleteOutlined, PlusOutlined, MoreOutlined } from '@ant-design/icons';
|
||||
import { fetchFuturesOverview, toggleWatchlist, createWatchlist, selectWatchlist, deleteWatchlist } from '../../store/futuresSlice';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import './Watchlist.css';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const Watchlist = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { overview, watchlists, currentWatchlist, loading } = useSelector(state => state.futures);
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [newWatchlistName, setNewWatchlistName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchFuturesOverview());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
dispatch(fetchFuturesOverview());
|
||||
message.success('数据已刷新');
|
||||
};
|
||||
|
||||
const handleFutureClick = (future) => {
|
||||
navigate(`/detail/${future.code}`);
|
||||
};
|
||||
|
||||
const handleToggleWatchlist = (future, e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(toggleWatchlist({ code: future.code, watchlistId: currentWatchlist }));
|
||||
message.success(future.isInWatchlist ? '已从自选移除' : '已添加到自选');
|
||||
};
|
||||
|
||||
const handleCreateWatchlist = () => {
|
||||
if (newWatchlistName.trim()) {
|
||||
dispatch(createWatchlist({ name: newWatchlistName.trim() }));
|
||||
message.success('新自选组合创建成功');
|
||||
setShowCreateModal(false);
|
||||
setNewWatchlistName('');
|
||||
} else {
|
||||
message.error('请输入自选组合名称');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectWatchlist = (watchlistId) => {
|
||||
dispatch(selectWatchlist(watchlistId));
|
||||
};
|
||||
|
||||
const handleDeleteWatchlist = (watchlistId) => {
|
||||
dispatch(deleteWatchlist(watchlistId));
|
||||
message.success('自选组合删除成功');
|
||||
};
|
||||
|
||||
const getChangeColor = (changePercent) => {
|
||||
return changePercent >= 0 ? '#52c41a' : '#ff4d4f';
|
||||
};
|
||||
|
||||
const getChangeIcon = (changePercent) => {
|
||||
return changePercent >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />;
|
||||
};
|
||||
|
||||
const getTrendColor = (direction) => {
|
||||
if (direction === '看多') return '#52c41a';
|
||||
if (direction === '看空') return '#ff4d4f';
|
||||
return '#faad14';
|
||||
};
|
||||
|
||||
const getWatchlistData = () => {
|
||||
const currentWl = watchlists.find(wl => wl.id === currentWatchlist);
|
||||
if (!currentWl) return [];
|
||||
return overview.filter(item => currentWl.codes.includes(item.code));
|
||||
};
|
||||
|
||||
if (loading && overview.length === 0) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<Spin size="large" tip="加载数据中..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const watchlistData = getWatchlistData();
|
||||
|
||||
return (
|
||||
<div className="watchlist">
|
||||
{/* 页面头部 */}
|
||||
<div className="watchlist-header">
|
||||
<div className="watchlist-header-left">
|
||||
<h2>自选合约</h2>
|
||||
<div className="watchlist-selector">
|
||||
<Select
|
||||
value={currentWatchlist}
|
||||
onChange={handleSelectWatchlist}
|
||||
style={{ width: 200, marginLeft: 16 }}
|
||||
>
|
||||
{watchlists.map(watchlist => (
|
||||
<Option key={watchlist.id} value={watchlist.id}>
|
||||
{watchlist.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="watchlist-header-actions">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
style={{ marginRight: 16 }}
|
||||
>
|
||||
新建自选组合
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
刷新数据
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{watchlistData.length === 0 ? (
|
||||
<div className="empty-watchlist">
|
||||
<Card className="empty-card">
|
||||
<div className="empty-content">
|
||||
<StarOutlined style={{ fontSize: 48, color: '#d9d9d9' }} />
|
||||
<h3>暂无自选合约</h3>
|
||||
<p>请在市场概览页面添加合约到自选</p>
|
||||
<Button type="primary" onClick={() => navigate('/')}>
|
||||
前往市场概览
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 自选合约列表 */}
|
||||
<Row gutter={[16, 16]}>
|
||||
{watchlistData.map(item => (
|
||||
<Col xs={24} sm={12} md={8} lg={6} key={item.code}>
|
||||
<Card
|
||||
className="future-card"
|
||||
hoverable
|
||||
onClick={() => handleFutureClick(item)}
|
||||
>
|
||||
{/* 品种名称和胜率 */}
|
||||
<div className="future-header-new">
|
||||
<div className="future-name-new">
|
||||
<h3>{item.name}-{item.code}</h3>
|
||||
</div>
|
||||
<div className="future-winrate-new">
|
||||
胜率: {item.winRate}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ATR/ADX和涨跌幅 */}
|
||||
<div className="future-info-new">
|
||||
<div className="future-indicators-new">
|
||||
ATR: {item.atr} | ADX: {item.adx}
|
||||
</div>
|
||||
<div className="future-change-new" style={{ color: getChangeColor(item.changePercent) }}>
|
||||
● {Math.abs(item.changePercent)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 当前价格和支撑压力位 */}
|
||||
<div className="future-price-container-new">
|
||||
<div className="future-price-new" style={{ color: getChangeColor(item.changePercent) }}>
|
||||
{item.currentPrice.toFixed(2)}
|
||||
</div>
|
||||
<div className="future-levels-new">
|
||||
<span className="level-label-new">支撑: {item.tradingAdvice?.support?.toFixed(2) || '-'}</span>
|
||||
<span className="level-label-new">压力: {item.tradingAdvice?.resistance?.toFixed(2) || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 多周期趋势 */}
|
||||
<div className="future-trend-title-new">
|
||||
多周期趋势:
|
||||
</div>
|
||||
<div className="future-trends-new">
|
||||
{Object.entries(item.trends).map(([period, trend]) => (
|
||||
<div key={period} className="trend-item-new">
|
||||
<div className="trend-period-new">{period.replace('MIN', 'min').replace('HOUR', 'min')}</div>
|
||||
<div className="trend-direction-new" style={{ color: getTrendColor(trend.direction) }}>
|
||||
{trend.direction}
|
||||
</div>
|
||||
<div className="trend-status-new">
|
||||
{trend.status}
|
||||
</div>
|
||||
<div className="trend-rsi-new">
|
||||
RSI: {trend.rsi}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 整体预判 */}
|
||||
<div className="future-overview-new">
|
||||
<span className="overview-label-new">整体预判:</span>
|
||||
<Tag color={item.overallView === '多头排列' ? 'green' : item.overallView === '空头排列' ? 'red' : 'orange'}>
|
||||
{item.overallView}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* AI分析 */}
|
||||
<div className="future-ai-analysis-new">
|
||||
<h4 className="ai-title-new">AI分析:</h4>
|
||||
<div className="ai-content-new">
|
||||
{item.aiAnalysis?.split(' | ').map((item, index) => (
|
||||
<div key={index}>{item}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="future-actions-new">
|
||||
<Button
|
||||
type="primary"
|
||||
className="detail-button-new"
|
||||
onClick={() => handleFutureClick(item)}
|
||||
>
|
||||
查看详细分析
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
className="watchlist-button-new"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
dispatch(toggleWatchlist(item.code));
|
||||
}}
|
||||
>
|
||||
删除自选
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 创建新自选组合模态框 */}
|
||||
<Modal
|
||||
title="创建新自选组合"
|
||||
open={showCreateModal}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={() => setShowCreateModal(false)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="create" type="primary" onClick={handleCreateWatchlist}>
|
||||
创建
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入自选组合名称"
|
||||
value={newWatchlistName}
|
||||
onChange={(e) => setNewWatchlistName(e.target.value)}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Watchlist;
|
||||
Loading…
Reference in new issue