fix: 增加自选模块

master
Lxy 3 months ago
parent 76957ba5b3
commit 2ace0e35ef

@ -7,6 +7,7 @@ import Dashboard from './pages/dashboard/Dashboard';
import Detail from './pages/detail/Detail';
import RiskControl from './pages/risk-control/RiskControl';
import Config from './pages/config/Config';
import Watchlist from './pages/watchlist/Watchlist';
import './App.css';
function App() {
@ -16,6 +17,7 @@ function App() {
<MainLayout>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/watchlist" element={<Watchlist />} />
<Route path="/detail/:code" element={<Detail />} />
<Route path="/risk-control" element={<RiskControl />} />
<Route path="/config" element={<Config />} />

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Layout, Menu, Button, Input, Avatar, Badge, Switch, ConfigProvider } from 'antd';
import { SearchOutlined, BellOutlined, UserOutlined, MenuFoldOutlined, MenuUnfoldOutlined, HomeOutlined, BarChartOutlined, SafetyOutlined, SettingOutlined, SunOutlined, MoonOutlined } from '@ant-design/icons';
import { SearchOutlined, BellOutlined, UserOutlined, MenuFoldOutlined, MenuUnfoldOutlined, HomeOutlined, BarChartOutlined, SafetyOutlined, SettingOutlined, SunOutlined, MoonOutlined, StarOutlined } from '@ant-design/icons';
import { Link, useLocation } from 'react-router-dom';
import './MainLayout.css';
@ -15,9 +15,10 @@ const MainLayout = ({ children }) => {
const getSelectedKey = () => {
const path = location.pathname;
if (path === '/') return '1';
if (path.includes('/detail/')) return '2';
if (path === '/risk-control') return '3';
if (path === '/config') return '4';
if (path === '/watchlist') return '2';
if (path.includes('/detail/')) return '3';
if (path === '/risk-control') return '4';
if (path === '/config') return '5';
return '1';
};
@ -29,16 +30,21 @@ const MainLayout = ({ children }) => {
},
{
key: '2',
icon: <StarOutlined />,
label: <Link to="/watchlist">自选合约</Link>,
},
{
key: '3',
icon: <BarChartOutlined />,
label: <Link to="/detail/AG">详情分析</Link>,
},
{
key: '3',
key: '4',
icon: <SafetyOutlined />,
label: <Link to="/risk-control">风控管理</Link>,
},
{
key: '4',
key: '5',
icon: <SettingOutlined />,
label: <Link to="/config">配置管理</Link>,
},

@ -273,12 +273,25 @@
/* 查看详细分析按钮 */
.detail-button-new {
width: 100%;
flex: 1;
border-radius: 4px;
font-size: 14px;
padding: 10px;
}
.watchlist-button-new {
flex: 1;
border-radius: 4px;
font-size: 14px;
padding: 10px;
}
.future-actions-new {
display: flex;
gap: 12px;
margin-top: 16px;
}
/* 加载容器 */
.loading-container {

@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Card, Row, Col, Statistic, Button, Select, Tag, message, Spin, Alert } from 'antd';
import { Card, Row, Col, Statistic, Button, Select, Tag, message, Spin, Alert, Dropdown, Menu } from 'antd';
import { ReloadOutlined, ArrowUpOutlined, ArrowDownOutlined, FireOutlined, AlertOutlined, RobotOutlined } from '@ant-design/icons';
import { fetchFuturesOverview, fetchRiskAlerts, fetchAIMarketAnalysis } from '../../store/futuresSlice';
import { fetchFuturesOverview, fetchRiskAlerts, fetchAIMarketAnalysis, toggleWatchlist } from '../../store/futuresSlice';
import { useNavigate } from 'react-router-dom';
import './Dashboard.css';
@ -11,9 +11,10 @@ const { Option } = Select;
const Dashboard = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { overview, riskAlerts, aiAnalysis, loading } = useSelector(state => state.futures);
const { overview, riskAlerts, aiAnalysis, loading, watchlists, currentWatchlist } = useSelector(state => state.futures);
const [filterType, setFilterType] = useState('all');
const [sortBy, setSortBy] = useState('winRate');
const [showWatchlistMenu, setShowWatchlistMenu] = useState(null);
useEffect(() => {
dispatch(fetchFuturesOverview());
@ -32,6 +33,25 @@ const Dashboard = () => {
navigate(`/detail/${future.code}`);
};
const handleToggleWatchlist = (future, e) => {
e.stopPropagation();
if (watchlists.length > 1 && !future.isInWatchlist) {
//
setShowWatchlistMenu(future.code);
} else {
//
dispatch(toggleWatchlist({ code: future.code, watchlistId: currentWatchlist }));
message.success(future.isInWatchlist ? '已从自选移除' : '已添加到自选');
}
};
const handleAddToWatchlist = (code, watchlistId, e) => {
e.stopPropagation();
dispatch(toggleWatchlist({ code, watchlistId }));
message.success('已添加到自选');
setShowWatchlistMenu(null);
};
const getChangeColor = (changePercent) => {
return changePercent >= 0 ? '#52c41a' : '#ff4d4f';
};
@ -291,10 +311,47 @@ const Dashboard = () => {
</div>
</div>
{/* 查看详细分析按钮 */}
<Button type="primary" className="detail-button-new" onClick={() => handleFutureClick(item)}>
查看详细分析
</Button>
{/* 操作按钮 */}
<div className="future-actions-new">
<Button type="primary" className="detail-button-new" onClick={() => handleFutureClick(item)}>
查看详细分析
</Button>
{watchlists.length > 1 && !item.isInWatchlist ? (
<Dropdown
open={showWatchlistMenu === item.code}
onOpenChange={(open) => {
if (open) {
setShowWatchlistMenu(item.code);
} else {
setShowWatchlistMenu(null);
}
}}
overlay={
<Menu>
{watchlists.map(watchlist => (
<Menu.Item key={watchlist.id} onClick={(e) => handleAddToWatchlist(item.code, watchlist.id, e)}>
{watchlist.name}
</Menu.Item>
))}
</Menu>
}
trigger={['click']}
>
<Button type="dashed" className="watchlist-button-new">
添加自选
</Button>
</Dropdown>
) : (
<Button
type={item.isInWatchlist ? "default" : "dashed"}
className="watchlist-button-new"
onClick={(e) => handleToggleWatchlist(item, e)}
disabled={item.isInWatchlist}
>
{item.isInWatchlist ? '已添加' : '添加自选'}
</Button>
)}
</div>
</Card>
</Col>
))}

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

@ -49,7 +49,15 @@ const futuresSlice = createSlice({
riskAlerts: [],
aiAnalysis: null,
loading: false,
error: null
error: null,
watchlists: [
{
id: 'default',
name: '默认自选',
codes: []
}
],
currentWatchlist: 'default'
},
reducers: {
selectFuture: (state, action) => {
@ -57,6 +65,49 @@ const futuresSlice = createSlice({
},
clearSelectedFuture: (state) => {
state.selectedFuture = null;
},
toggleWatchlist: (state, action) => {
const { code, watchlistId = state.currentWatchlist } = action.payload;
const targetWatchlist = state.watchlists.find(wl => wl.id === watchlistId);
if (targetWatchlist) {
const index = targetWatchlist.codes.indexOf(code);
if (index > -1) {
targetWatchlist.codes.splice(index, 1);
} else {
targetWatchlist.codes.push(code);
}
}
// 更新overview中每个合约的自选状态
state.overview = state.overview.map(item => ({
...item,
isInWatchlist: state.watchlists.some(wl => wl.codes.includes(item.code))
}));
// 更新selectedFuture的自选状态
if (state.selectedFuture && state.selectedFuture.code === code) {
state.selectedFuture.isInWatchlist = state.watchlists.some(wl => wl.codes.includes(code));
}
},
createWatchlist: (state, action) => {
const { name } = action.payload;
const newWatchlist = {
id: `wl-${Date.now()}`,
name,
codes: []
};
state.watchlists.push(newWatchlist);
state.currentWatchlist = newWatchlist.id;
},
selectWatchlist: (state, action) => {
state.currentWatchlist = action.payload;
},
deleteWatchlist: (state, action) => {
const watchlistId = action.payload;
if (watchlistId !== 'default') {
state.watchlists = state.watchlists.filter(wl => wl.id !== watchlistId);
if (state.currentWatchlist === watchlistId) {
state.currentWatchlist = 'default';
}
}
}
},
extraReducers: (builder) => {
@ -68,7 +119,11 @@ const futuresSlice = createSlice({
})
.addCase(fetchFuturesOverview.fulfilled, (state, action) => {
state.loading = false;
state.overview = action.payload;
// 更新overview数据并添加自选状态
state.overview = action.payload.map(item => ({
...item,
isInWatchlist: state.watchlists.some(wl => wl.codes.includes(item.code))
}));
})
.addCase(fetchFuturesOverview.rejected, (state, action) => {
state.loading = false;
@ -82,7 +137,11 @@ const futuresSlice = createSlice({
})
.addCase(fetchFutureDetail.fulfilled, (state, action) => {
state.loading = false;
state.selectedFuture = action.payload;
// 更新selectedFuture数据并添加自选状态
state.selectedFuture = {
...action.payload,
isInWatchlist: state.watchlists.some(wl => wl.codes.includes(action.payload.code))
};
console.log('fetchFutureDetail fulfilled with:', action.payload);
})
.addCase(fetchFutureDetail.rejected, (state, action) => {
@ -121,5 +180,5 @@ const futuresSlice = createSlice({
}
});
export const { selectFuture, clearSelectedFuture } = futuresSlice.actions;
export const { selectFuture, clearSelectedFuture, toggleWatchlist, createWatchlist, selectWatchlist, deleteWatchlist } = futuresSlice.actions;
export default futuresSlice.reducer;

Loading…
Cancel
Save