diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index bddd445..2ecb7c7 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -1685,7 +1685,8 @@ "maxSize": "Max", "invalidPriceOrBalance": "Invalid price or balance", "insufficientBalanceForMax": "Insufficient balance for max size", - "maxSizeUpdated": "Updated to max size" + "maxSizeUpdated": "Updated to max size", + "periodChanged": "Period has changed, popup closed" } } } \ No newline at end of file diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index ca63a4e..853c74c 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -1685,7 +1685,8 @@ "maxSize": "最大", "invalidPriceOrBalance": "价格或余额无效", "insufficientBalanceForMax": "余额不足以购买最大数量", - "maxSizeUpdated": "已更新为最大数量" + "maxSizeUpdated": "已更新为最大数量", + "periodChanged": "周期已切换,弹窗已关闭" } } } \ No newline at end of file diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json index aa5020a..e4ea674 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -1685,7 +1685,8 @@ "maxSize": "最大", "invalidPriceOrBalance": "價格或餘額無效", "insufficientBalanceForMax": "餘額不足以購買最大數量", - "maxSizeUpdated": "已更新為最大數量" + "maxSizeUpdated": "已更新為最大數量", + "periodChanged": "週期已切換,彈窗已關閉" } } } \ No newline at end of file diff --git a/frontend/src/pages/CryptoTailMonitor.tsx b/frontend/src/pages/CryptoTailMonitor.tsx index 80db6d2..4ddacfd 100644 --- a/frontend/src/pages/CryptoTailMonitor.tsx +++ b/frontend/src/pages/CryptoTailMonitor.tsx @@ -17,6 +17,7 @@ import { InputNumber, message } from 'antd' +import { Popup as AntdMobilePopup } from 'antd-mobile' import { ClockCircleOutlined, SyncOutlined, InfoCircleOutlined, ShoppingCartOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { useMediaQuery } from 'react-responsive' @@ -33,6 +34,10 @@ import type { const { Title, Text } = Typography +// localStorage keys +const PERIOD_SWITCH_MODE_KEY = 'cryptoTailMonitor_periodSwitchMode' +const SELECTED_STRATEGY_ID_KEY = 'cryptoTailMonitor_selectedStrategyId' + /** 分时图数据点:时间戳、BTC 价格 USDC、市场 Up/Down 价格 0-1 */ interface PriceDataPoint { time: number @@ -49,8 +54,11 @@ const CryptoTailMonitor: React.FC = () => { const [strategies, setStrategies] = useState([]) const [strategiesLoading, setStrategiesLoading] = useState(false) - // 选中的策略 - const [selectedStrategyId, setSelectedStrategyId] = useState(null) + // 选中的策略(从 localStorage 恢复) + const [selectedStrategyId, setSelectedStrategyId] = useState(() => { + const cached = localStorage.getItem(SELECTED_STRATEGY_ID_KEY) + return cached != null ? parseInt(cached, 10) : null + }) // 监控数据 const [initData, setInitData] = useState(null) @@ -69,9 +77,6 @@ const CryptoTailMonitor: React.FC = () => { selectedStrategyIdRef.current = selectedStrategyId }, [selectedStrategyId]) - // localStorage key for period switch mode - const PERIOD_SWITCH_MODE_KEY = 'cryptoTailMonitor_periodSwitchMode' - // 记录首次数据进入时间(用于中途进入时的横轴起点) const [firstDataTime, setFirstDataTime] = useState(null) // 标记是否已切换过周期(切换后使用完整周期) @@ -100,6 +105,7 @@ const CryptoTailMonitor: React.FC = () => { totalAmount: string bestBid: string availableBalance: string + periodStartUnix: number | null }>({ visible: false, direction: 'UP', @@ -107,10 +113,21 @@ const CryptoTailMonitor: React.FC = () => { size: '', totalAmount: '', bestBid: '', - availableBalance: '' + availableBalance: '', + periodStartUnix: null }) const [ordering, setOrdering] = useState(false) + // 检测周期切换,关闭弹窗并提示用户 + useEffect(() => { + if (manualOrderModal.visible && manualOrderModal.periodStartUnix != null && pushData) { + if (pushData.periodStartUnix !== manualOrderModal.periodStartUnix) { + message.warning(t('cryptoTailMonitor.manualOrder.periodChanged')) + handleCloseManualOrderModal() + } + } + }, [pushData?.periodStartUnix, manualOrderModal.visible, manualOrderModal.periodStartUnix]) + // 获取策略列表 useEffect(() => { const fetchStrategies = async () => { @@ -118,10 +135,18 @@ const CryptoTailMonitor: React.FC = () => { try { const res = await apiService.cryptoTailStrategy.list({ enabled: true }) if (res.data.code === 0 && res.data.data) { - setStrategies(res.data.data.list ?? []) - // 自动选择第一个策略 - if (res.data.data.list?.length > 0 && !selectedStrategyId) { - setSelectedStrategyId(res.data.data.list[0].id) + const strategyList = res.data.data.list ?? [] + setStrategies(strategyList) + // 从 localStorage 恢复选中的策略 + const cachedId = localStorage.getItem(SELECTED_STRATEGY_ID_KEY) + const cachedStrategyId = cachedId != null ? parseInt(cachedId, 10) : null + // 检查缓存的策略是否在列表中 + const isValidCached = cachedStrategyId != null && strategyList.some(s => s.id === cachedStrategyId) + if (isValidCached) { + setSelectedStrategyId(cachedStrategyId) + } else if (strategyList.length > 0) { + // 自动选择第一个策略 + setSelectedStrategyId(strategyList[0].id) } } } catch (e) { @@ -133,6 +158,13 @@ const CryptoTailMonitor: React.FC = () => { fetchStrategies() }, []) + // 保存选中的策略到 localStorage + useEffect(() => { + if (selectedStrategyId != null) { + localStorage.setItem(SELECTED_STRATEGY_ID_KEY, String(selectedStrategyId)) + } + }, [selectedStrategyId]) + // 初始化监控数据 useEffect(() => { if (!selectedStrategyId) { @@ -818,7 +850,8 @@ const CryptoTailMonitor: React.FC = () => { size: defaultSize, totalAmount: defaultTotalAmount, bestBid, - availableBalance + availableBalance, + periodStartUnix: pushData.periodStartUnix }) } @@ -854,7 +887,8 @@ const CryptoTailMonitor: React.FC = () => { size: '', totalAmount: '', bestBid: '', - availableBalance: '' + availableBalance: '', + periodStartUnix: null }) } @@ -1160,7 +1194,7 @@ const CryptoTailMonitor: React.FC = () => { block style={{ backgroundColor: '#1890ff', borderColor: '#1890ff' }} > - {t('cryptoTailMonitor.manualOrder.buttonUp')} + {t('cryptoTailMonitor.manualOrder.buttonUp')} {pushData?.currentPriceUp ? `(${formatNumber(pushData.currentPriceUp, 2)})` : ''} @@ -1173,7 +1207,7 @@ const CryptoTailMonitor: React.FC = () => { block style={{ backgroundColor: '#fa8c16', borderColor: '#fa8c16' }} > - {t('cryptoTailMonitor.manualOrder.buttonDown')} + {t('cryptoTailMonitor.manualOrder.buttonDown')} {pushData?.currentPriceDown ? `(${formatNumber(pushData.currentPriceDown, 2)})` : ''} @@ -1211,48 +1245,162 @@ const CryptoTailMonitor: React.FC = () => { )} - {/* 手动下单确认弹窗 */} - - {t('cryptoTailMonitor.manualOrder.cancel')} - , - - ]} - width={isMobile ? '100%' : 480} - style={isMobile ? { maxWidth: '100%', top: 0, paddingBottom: 0 } : undefined} - > - {initData && ( - - - - - {t('cryptoTailMonitor.manualOrder.marketTitle')} - - {pushData?.marketTitle ?? initData.marketTitle} - - - - {t('cryptoTailMonitor.manualOrder.direction')} + {/* 手动下单确认弹窗 - 桌面端使用 Modal */} + {!isMobile && ( + + {t('cryptoTailMonitor.manualOrder.cancel')} + , + + ]} + width={480} + > + {initData && ( + + + + + {t('cryptoTailMonitor.manualOrder.marketTitle')} + + {pushData?.marketTitle ?? initData.marketTitle} + + + + {t('cryptoTailMonitor.manualOrder.direction')} + + + {manualOrderModal.direction === 'UP' + ? t('cryptoTailMonitor.manualOrder.directionUp') + : t('cryptoTailMonitor.manualOrder.directionDown')} + + + + + {t('cryptoTailMonitor.manualOrder.availableBalance')} + + + {manualOrderModal.availableBalance ? formatNumber(manualOrderModal.availableBalance, 2) : '-'} {t('cryptoTailMonitor.manualOrder.orderUnit')} + + + + + {t('cryptoTailMonitor.manualOrder.orderPrice')} ({t('cryptoTailMonitor.manualOrder.orderUnit')}) + + + + + + + + + {t('cryptoTailMonitor.manualOrder.orderSize')} ({t('cryptoTailMonitor.manualOrder.sizeUnit')}) + + + + + + + + + {t('cryptoTailMonitor.manualOrder.totalAmount')} + + + {manualOrderModal.totalAmount} {t('cryptoTailMonitor.manualOrder.orderUnit')} + + + + + {t('cryptoTailMonitor.manualOrder.account')} + + {initData.accountName || `#${initData.accountId}`} + + + + )} + + )} + + {/* 移动端 BottomSheet 弹窗 */} + {isMobile && ( + + {initData && ( +
+ {/* 标题栏 */} +
+ + {t('cryptoTailMonitor.manualOrder.confirmTitle')} + +
+ + {/* 市场信息 + 方向 + 余额(一行) */} +
+
+ {t('cryptoTailMonitor.manualOrder.marketTitle')}: + {pushData?.marketTitle ?? initData.marketTitle} +
@@ -1260,22 +1408,26 @@ const CryptoTailMonitor: React.FC = () => { ? t('cryptoTailMonitor.manualOrder.directionUp') : t('cryptoTailMonitor.manualOrder.directionDown')} - - - - {t('cryptoTailMonitor.manualOrder.availableBalance')} +
+ + {/* 可用余额 */} +
+ + {t('cryptoTailMonitor.manualOrder.availableBalance')}: - + {manualOrderModal.availableBalance ? formatNumber(manualOrderModal.availableBalance, 2) : '-'} {t('cryptoTailMonitor.manualOrder.orderUnit')} - - - - {t('cryptoTailMonitor.manualOrder.orderPrice')} ({t('cryptoTailMonitor.manualOrder.orderUnit')}) +
+ + {/* 价格输入 */} +
+ + {t('cryptoTailMonitor.manualOrder.orderPrice')} { precision={4} placeholder="0.0000" /> - - - - - {t('cryptoTailMonitor.manualOrder.orderSize')} ({t('cryptoTailMonitor.manualOrder.sizeUnit')}) +
+ + {/* 数量输入 */} +
+ + {t('cryptoTailMonitor.manualOrder.orderSize')} - - - - - {t('cryptoTailMonitor.manualOrder.totalAmount')} - - - {manualOrderModal.totalAmount} {t('cryptoTailMonitor.manualOrder.orderUnit')} - - - - - {t('cryptoTailMonitor.manualOrder.account')} - - {initData.accountName || `#${initData.accountId}`} - - - - )} - +
+ + {/* 总金额 + 账户(一行) */} +
+
+ {t('cryptoTailMonitor.manualOrder.totalAmount')}: + + {manualOrderModal.totalAmount} {t('cryptoTailMonitor.manualOrder.orderUnit')} + +
+
+ {t('cryptoTailMonitor.manualOrder.account')}: + {initData.accountName || `#${initData.accountId}`} +
+
+ + {/* 确认按钮 */} + +
+ )} +
+ )} {/* 移动端底部悬浮按钮 */} {isMobile && ( @@ -1351,7 +1521,7 @@ const CryptoTailMonitor: React.FC = () => { loading={ordering} style={{ flex: 1, backgroundColor: '#1890ff', borderColor: '#1890ff', height: 44, borderRadius: '6px 0 0 6px' }} > - {t('cryptoTailMonitor.manualOrder.buttonUp')} + {t('cryptoTailMonitor.manualOrder.buttonUp')} {pushData?.currentPriceUp ? `(${formatNumber(pushData.currentPriceUp, 2)})` : ''}