From 2ace0e35efdf41f9e723a4bf75b200542a18a291 Mon Sep 17 00:00:00 2001 From: Lxy Date: Wed, 18 Feb 2026 23:33:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E8=87=AA=E9=80=89?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.jsx | 2 + src/components/layout/MainLayout.jsx | 18 +- src/pages/dashboard/Dashboard.css | 15 +- src/pages/dashboard/Dashboard.jsx | 71 ++++++- src/pages/watchlist/Watchlist.css | 271 ++++++++++++++++++++++++++ src/pages/watchlist/Watchlist.jsx | 274 +++++++++++++++++++++++++++ src/store/futuresSlice.js | 67 ++++++- 7 files changed, 700 insertions(+), 18 deletions(-) create mode 100644 src/pages/watchlist/Watchlist.css create mode 100644 src/pages/watchlist/Watchlist.jsx diff --git a/src/App.jsx b/src/App.jsx index f244d7d..28d34f6 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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() { } /> + } /> } /> } /> } /> diff --git a/src/components/layout/MainLayout.jsx b/src/components/layout/MainLayout.jsx index 3555b87..f2e53f5 100644 --- a/src/components/layout/MainLayout.jsx +++ b/src/components/layout/MainLayout.jsx @@ -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: , + label: 自选合约, + }, + { + key: '3', icon: , label: 详情分析, }, { - key: '3', + key: '4', icon: , label: 风控管理, }, { - key: '4', + key: '5', icon: , label: 配置管理, }, diff --git a/src/pages/dashboard/Dashboard.css b/src/pages/dashboard/Dashboard.css index 3a27923..bfa7ab3 100644 --- a/src/pages/dashboard/Dashboard.css +++ b/src/pages/dashboard/Dashboard.css @@ -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 { diff --git a/src/pages/dashboard/Dashboard.jsx b/src/pages/dashboard/Dashboard.jsx index 5c832f7..b4ee1e4 100644 --- a/src/pages/dashboard/Dashboard.jsx +++ b/src/pages/dashboard/Dashboard.jsx @@ -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 = () => { - {/* 查看详细分析按钮 */} - + {/* 操作按钮 */} +
+ + {watchlists.length > 1 && !item.isInWatchlist ? ( + { + if (open) { + setShowWatchlistMenu(item.code); + } else { + setShowWatchlistMenu(null); + } + }} + overlay={ + + {watchlists.map(watchlist => ( + handleAddToWatchlist(item.code, watchlist.id, e)}> + {watchlist.name} + + ))} + + } + trigger={['click']} + > + + + ) : ( + + )} +
))} diff --git a/src/pages/watchlist/Watchlist.css b/src/pages/watchlist/Watchlist.css new file mode 100644 index 0000000..341e76b --- /dev/null +++ b/src/pages/watchlist/Watchlist.css @@ -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; + } +} \ No newline at end of file diff --git a/src/pages/watchlist/Watchlist.jsx b/src/pages/watchlist/Watchlist.jsx new file mode 100644 index 0000000..90d9c82 --- /dev/null +++ b/src/pages/watchlist/Watchlist.jsx @@ -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 ? : ; + }; + + 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 ( +
+ +
+ ); + } + + const watchlistData = getWatchlistData(); + + return ( +
+ {/* 页面头部 */} +
+
+

自选合约

+
+ +
+
+
+ + +
+
+ + {watchlistData.length === 0 ? ( +
+ +
+ +

暂无自选合约

+

请在市场概览页面添加合约到自选

+ +
+
+
+ ) : ( + <> + {/* 自选合约列表 */} + + {watchlistData.map(item => ( + + handleFutureClick(item)} + > + {/* 品种名称和胜率 */} +
+
+

{item.name}-{item.code}

+
+
+ 胜率: {item.winRate}% +
+
+ + {/* ATR/ADX和涨跌幅 */} +
+
+ ATR: {item.atr} | ADX: {item.adx} +
+
+ ● {Math.abs(item.changePercent)}% +
+
+ + {/* 当前价格和支撑压力位 */} +
+
+ {item.currentPrice.toFixed(2)} +
+
+ 支撑: {item.tradingAdvice?.support?.toFixed(2) || '-'} + 压力: {item.tradingAdvice?.resistance?.toFixed(2) || '-'} +
+
+ + {/* 多周期趋势 */} +
+ 多周期趋势: +
+
+ {Object.entries(item.trends).map(([period, trend]) => ( +
+
{period.replace('MIN', 'min').replace('HOUR', 'min')}
+
+ {trend.direction} +
+
+ {trend.status} +
+
+ RSI: {trend.rsi} +
+
+ ))} +
+ + {/* 整体预判 */} +
+ 整体预判: + + {item.overallView} + +
+ + {/* AI分析 */} +
+

AI分析:

+
+ {item.aiAnalysis?.split(' | ').map((item, index) => ( +
{item}
+ ))} +
+
+ + {/* 操作按钮 */} +
+ + +
+
+ + ))} +
+ + )} + + {/* 创建新自选组合模态框 */} + setShowCreateModal(false)} + footer={[ + , + + ]} + > + setNewWatchlistName(e.target.value)} + style={{ marginBottom: 16 }} + /> + +
+ ); +}; + +export default Watchlist; \ No newline at end of file diff --git a/src/store/futuresSlice.js b/src/store/futuresSlice.js index 63c8b69..6b4dc76 100644 --- a/src/store/futuresSlice.js +++ b/src/store/futuresSlice.js @@ -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;