import { useEffect, useState, useMemo } from 'react' import { Card, Table, Tag, message, Space, Input, Radio, Select, Button, Row, Col, Empty, Modal, Form, Descriptions } from 'antd' import { SearchOutlined, AppstoreOutlined, UnorderedListOutlined, UpOutlined, DownOutlined } from '@ant-design/icons' import { useNavigate } from 'react-router-dom' import { apiService } from '../services/api' import type { AccountPosition, Account, PositionPushMessage, PositionSellRequest, MarketPriceResponse, RedeemablePositionsSummary, PositionRedeemRequest } from '../types' import { getPositionKey } from '../types' import { useMediaQuery } from 'react-responsive' import { useWebSocketSubscription } from '../hooks/useWebSocket' import { wsManager } from '../services/websocket' import { formatUSDC, formatNumber as formatNumberUtil } from '../utils' type PositionFilter = 'current' | 'historical' type ViewMode = 'card' | 'list' const PositionList: React.FC = () => { const navigate = useNavigate() const isMobile = useMediaQuery({ maxWidth: 768 }) const [currentPositions, setCurrentPositions] = useState([]) const [historyPositions, setHistoryPositions] = useState([]) const [accounts, setAccounts] = useState([]) const [loading, setLoading] = useState(false) const [accountsLoading, setAccountsLoading] = useState(false) const [searchKeyword, setSearchKeyword] = useState('') const [positionFilter, setPositionFilter] = useState('current') const [selectedAccountId, setSelectedAccountId] = useState(undefined) const [viewMode, setViewMode] = useState(isMobile ? 'card' : 'list') const [expandedCards, setExpandedCards] = useState>(new Set()) const [sellModalVisible, setSellModalVisible] = useState(false) const [selectedPosition, setSelectedPosition] = useState(null) const [marketPrice, setMarketPrice] = useState(null) const [orderType, setOrderType] = useState<'MARKET' | 'LIMIT'>('LIMIT') const [sellQuantity, setSellQuantity] = useState('') const [limitPrice, setLimitPrice] = useState('') const [selectedPercent, setSelectedPercent] = useState(null) // 记录选择的百分比(字符串格式) const [form] = Form.useForm() const [submitting, setSubmitting] = useState(false) const [wsConnected, setWsConnected] = useState(false) const [redeemModalVisible, setRedeemModalVisible] = useState(false) const [redeemableSummary, setRedeemableSummary] = useState(null) const [loadingRedeemableSummary, setLoadingRedeemableSummary] = useState(false) const [redeeming, setRedeeming] = useState(false) const [currentPage, setCurrentPage] = useState(1) const [pageSize, setPageSize] = useState(20) useEffect(() => { fetchAccounts() // 完全依赖 WebSocket 推送,不主动请求接口 // 连接建立后会立即收到全量数据推送 setLoading(true) // 显示加载状态,等待 WebSocket 全量推送 // 监听连接状态(WebSocket 连接在 App.tsx 中全局初始化,全局共享) const removeListener = wsManager.onConnectionChange((connected) => { setWsConnected(connected) }) // 获取当前连接状态 setWsConnected(wsManager.isConnected()) return () => { removeListener() } }, []) // 当仓位数据变化时,静默更新可赎回统计(不显示loading状态) useEffect(() => { if (currentPositions.length > 0) { fetchRedeemableSummarySilently() } }, [currentPositions, selectedAccountId]) // 当筛选条件或搜索关键词变化时,重置分页到第一页 useEffect(() => { setCurrentPage(1) }, [positionFilter, selectedAccountId, searchKeyword]) // 静默获取可赎回仓位统计(不显示loading状态) const fetchRedeemableSummarySilently = async () => { try { const response = await apiService.accounts.getRedeemableSummary({ accountId: selectedAccountId }) if (response.data.code === 0 && response.data.data) { setRedeemableSummary(response.data.data) } } catch (error: any) { console.error('获取可赎回统计失败:', error) } } // 获取可赎回仓位统计(带loading状态,用于用户主动操作) const fetchRedeemableSummary = async () => { setLoadingRedeemableSummary(true) try { const response = await apiService.accounts.getRedeemableSummary({ accountId: selectedAccountId }) if (response.data.code === 0 && response.data.data) { setRedeemableSummary(response.data.data) } } catch (error: any) { console.error('获取可赎回统计失败:', error) } finally { setLoadingRedeemableSummary(false) } } // 处理赎回按钮点击 const handleRedeemClick = async () => { setRedeemModalVisible(true) // 打开模态框时重新获取最新数据 fetchRedeemableSummary() } // 提交赎回 const handleRedeemSubmit = async () => { if (!redeemableSummary || redeemableSummary.positions.length === 0) { message.warning('没有可赎回的仓位') return } setRedeeming(true) try { const request: PositionRedeemRequest = { positions: redeemableSummary.positions.map(pos => ({ accountId: pos.accountId, marketId: pos.marketId, outcomeIndex: pos.outcomeIndex, side: pos.side })) } const response = await apiService.accounts.redeemPositions(request) if (response.data.code === 0 && response.data.data) { const transactions = response.data.data.transactions || [] const txHashes = transactions.map((tx: any) => tx.transactionHash.substring(0, 10) + '...').join(', ') message.success(`赎回成功!共 ${transactions.length} 个账户,交易哈希: ${txHashes}`) setRedeemModalVisible(false) // 刷新可赎回统计 await fetchRedeemableSummary() } else { // 检查是否是 Builder API Key 未配置的错误 if (response.data.code === 2014 || response.data.msg?.includes('Builder API Key 未配置')) { message.error({ content: response.data.msg || 'Builder API Key 未配置', duration: 5, }) // 延迟跳转,让用户看到错误消息 setTimeout(() => { navigate('/system-settings/builder-api-key') }, 1500) } else { message.error(response.data.msg || '赎回失败') } } } catch (error: any) { // 检查是否是 Builder API Key 未配置的错误 if (error.response?.data?.code === 2014 || error.message?.includes('Builder API Key 未配置')) { message.error({ content: error.response?.data?.msg || error.message || 'Builder API Key 未配置,请前往系统设置页面配置', duration: 5, }) // 延迟跳转,让用户看到错误消息 setTimeout(() => { navigate('/system-settings/builder-api-key') }, 1500) } else { message.error('赎回失败: ' + (error.message || '未知错误')) } } finally { setRedeeming(false) } } // 订阅仓位推送 const { connected: positionConnected } = useWebSocketSubscription( 'position', (message) => { handlePositionPushMessage(message) } ) // 更新连接状态(使用订阅的连接状态) useEffect(() => { setWsConnected(positionConnected) }, [positionConnected]) /** * 处理仓位推送消息 */ const handlePositionPushMessage = (message: PositionPushMessage) => { if (message.type === 'FULL') { // 全量推送:直接替换(这是首次连接时的数据,完全以推送数据为准) setCurrentPositions(message.currentPositions || []) setHistoryPositions(message.historyPositions || []) setLoading(false) console.log('收到仓位全量推送:', { current: message.currentPositions?.length || 0, history: message.historyPositions?.length || 0 }) } else if (message.type === 'INCREMENTAL') { // 增量推送:合并数据(始终以推送数据为准) setCurrentPositions(prev => mergePositions(prev, message.currentPositions || [], message.removedPositionKeys || [])) setHistoryPositions(prev => mergePositions(prev, message.historyPositions || [], message.removedPositionKeys || [])) console.log('收到仓位增量推送:', { current: message.currentPositions?.length || 0, history: message.historyPositions?.length || 0, removed: message.removedPositionKeys?.length || 0 }) } } /** * 合并仓位数据 * 新增的仓位插入到列表顶部,更新的仓位更新现有数据并保持位置,删除的仓位从列表中移除 */ const mergePositions = ( prev: AccountPosition[], updates: AccountPosition[], removedKeys: string[] ): AccountPosition[] => { // 创建现有仓位的键集合,用于快速判断是新增还是更新 const existingKeys = new Set(prev.map(pos => getPositionKey(pos))) // 区分新增和更新的仓位 const newPositions: AccountPosition[] = [] const updateMap = new Map() updates.forEach(update => { const key = getPositionKey(update) if (existingKeys.has(key)) { // 已存在的仓位,记录更新 updateMap.set(key, update) } else { // 新增的仓位,插入到顶部 newPositions.push(update) } }) // 构建结果数组 const result: AccountPosition[] = [] // 1. 先添加新增的仓位(在顶部) result.push(...newPositions) // 2. 遍历原有仓位,应用更新或保持不变 prev.forEach(pos => { const key = getPositionKey(pos) // 如果被删除,跳过 if (removedKeys.includes(key)) { return } // 如果有更新,使用新数据;否则保持原数据 if (updateMap.has(key)) { result.push(updateMap.get(key)!) } else { result.push(pos) } }) return result } const fetchAccounts = async () => { setAccountsLoading(true) try { const response = await apiService.accounts.list() if (response.data.code === 0 && response.data.data) { setAccounts(response.data.data.list || []) } else { message.error(response.data.msg || '获取账户列表失败') } } catch (error: any) { message.error(error.message || '获取账户列表失败') } finally { setAccountsLoading(false) } } // 已移除 fetchPositions 函数,完全依赖 WebSocket 推送更新数据 // 根据筛选器选择对应的仓位列表 const basePositions = useMemo(() => { return positionFilter === 'current' ? currentPositions : historyPositions }, [positionFilter, currentPositions, historyPositions]) // 本地搜索和筛选过滤 const filteredPositions = useMemo(() => { let filtered = basePositions // 1. 先按账户筛选 if (selectedAccountId !== undefined) { filtered = filtered.filter(p => p.accountId === selectedAccountId) } // 2. 最后按关键词搜索 if (searchKeyword.trim()) { const keyword = searchKeyword.trim().toLowerCase() filtered = filtered.filter(position => { // 搜索账户名 if (position.accountName?.toLowerCase().includes(keyword)) { return true } // 搜索钱包地址 if (position.walletAddress.toLowerCase().includes(keyword)) { return true } // 搜索市场标题 if (position.marketTitle?.toLowerCase().includes(keyword)) { return true } // 搜索市场slug if (position.marketSlug?.toLowerCase().includes(keyword)) { return true } // 搜索市场ID if (position.marketId.toLowerCase().includes(keyword)) { return true } // 搜索方向(YES/NO) if (position.side.toLowerCase().includes(keyword)) { return true } return false }) } return filtered }, [basePositions, searchKeyword, selectedAccountId]) // 分页后的数据 const paginatedPositions = useMemo(() => { const startIndex = (currentPage - 1) * pageSize const endIndex = startIndex + pageSize return filteredPositions.slice(startIndex, endIndex) }, [filteredPositions, currentPage, pageSize]) const getSideColor = (side: string) => { return side === 'YES' ? 'green' : 'red' } const formatNumber = (value: string | undefined, decimals: number = 2) => { if (!value) return '-' const num = parseFloat(value) if (isNaN(num)) return value return formatNumberUtil(value, decimals) } const formatPercent = (value: string | undefined) => { if (!value) return '-' const num = parseFloat(value) if (isNaN(num)) return value return `${num >= 0 ? '+' : ''}${num.toFixed(2)}%` } // 统计当前筛选后的仓位合计:开仓价值、当前价值、盈亏、已实现盈亏 const positionTotals = useMemo(() => { if (filteredPositions.length === 0) { return { totalInitialValue: 0, totalCurrentValue: 0, totalPnl: 0, totalRealizedPnl: 0 } } let totalInitialValue = 0 let totalCurrentValue = 0 let totalPnl = 0 let totalRealizedPnl = 0 filteredPositions.forEach((pos) => { const initialValue = parseFloat(pos.initialValue || '0') const currentValue = parseFloat(pos.currentValue || '0') const pnl = parseFloat(pos.pnl || '0') const realizedPnl = parseFloat(pos.realizedPnl || '0') if (!isNaN(initialValue)) { totalInitialValue += initialValue } // 当前仓位:统计持仓价值 // 历史仓位:currentValue 应该为 0(已平仓) if (!isNaN(currentValue)) { totalCurrentValue += currentValue } // 对于当前仓位: // - pnl:未实现盈亏(浮动盈亏) // - realizedPnl:已实现盈亏(部分平仓时产生) // 对于历史仓位: // - pnl:总已实现盈亏(包含部分平仓 + 完全平仓) // - realizedPnl:部分平仓的已实现盈亏(可能与 pnl 重复) if (pos.isCurrent) { // 当前仓位:未实现盈亏 + 已实现盈亏 if (!isNaN(pnl)) { totalPnl += pnl } if (!isNaN(realizedPnl)) { totalRealizedPnl += realizedPnl } } else { // 历史仓位:pnl 是总已实现盈亏,realizedPnl 可能重复,所以只统计 pnl if (!isNaN(pnl)) { totalRealizedPnl += pnl } } }) return { totalInitialValue, totalCurrentValue, totalPnl, totalRealizedPnl } }, [filteredPositions]) // 切换卡片展开/折叠状态 const toggleCard = (cardKey: string) => { setExpandedCards(prev => { const newSet = new Set(prev) if (newSet.has(cardKey)) { newSet.delete(cardKey) } else { newSet.add(cardKey) } return newSet }) } // 处理卖出按钮点击 const handleSellClick = async (position: AccountPosition) => { setSelectedPosition(position) setSellModalVisible(true) setOrderType('LIMIT') setSellQuantity('') setLimitPrice('') setSelectedPercent(null) // 重置百分比选择 form.resetFields() // 加载市场价格 try { const response = await apiService.markets.getMarketPrice({ marketId: position.marketId, outcomeIndex: position.outcomeIndex // 传递结果索引,用于确定需要查询哪个 outcome 的价格 }) if (response.data.code === 0 && response.data.data) { setMarketPrice(response.data.data) // 默认使用当前价格作为限价 if (response.data.data.currentPrice) { setLimitPrice(response.data.data.currentPrice) form.setFieldsValue({ limitPrice: response.data.data.currentPrice }) } } } catch (error: any) { message.error('获取市场价格失败: ' + (error.message || '未知错误')) } } // 处理数量快捷按钮 const handleQuantityQuickSelect = (percent: number) => { if (!selectedPosition) return // 记录选择的百分比(转为字符串,避免精度问题) setSelectedPercent(percent.toString()) // 计算显示用的数量(用于预览,使用显示数量即可) const quantity = parseFloat(selectedPosition.quantity) const sellQty = (quantity * percent / 100).toFixed(4) setSellQuantity(sellQty) form.setFieldsValue({ quantity: sellQty }) // 使用当前卖出价格计算收益 const price = getCurrentSellPrice() if (price && price !== '0') { calculatePnl(sellQty, price) } } // 计算平仓收益 const calculatePnl = (quantity: string, price: string) => { if (!selectedPosition || !quantity || !price) return { pnl: 0, percentPnl: 0 } const avgPrice = parseFloat(selectedPosition.avgPrice || '0') const sellPrice = parseFloat(price || '0') const qty = parseFloat(quantity || '0') // 验证数据有效性 if (isNaN(avgPrice) || isNaN(sellPrice) || isNaN(qty) || avgPrice <= 0 || sellPrice <= 0 || qty <= 0) { return { pnl: 0, percentPnl: 0 } } // 计算收益:收益金额 = (卖出价格 - 平均买入价格) × 卖出数量 const pnl = (sellPrice - avgPrice) * qty // 计算收益率:收益率 = (卖出价格 - 平均买入价格) / 平均买入价格 × 100% const percentPnl = ((sellPrice - avgPrice) / avgPrice) * 100 return { pnl, percentPnl } } // 获取当前卖出价格(市价或限价) const getCurrentSellPrice = (): string => { if (orderType === 'MARKET') { // 市价订单(卖出):使用当前价格 return marketPrice?.currentPrice || selectedPosition?.currentPrice || '0' } return limitPrice || '0' } // 提交卖出订单 const handleSellSubmit = async () => { if (!selectedPosition || submitting) return try { await form.validateFields() setSubmitting(true) const request: PositionSellRequest = { accountId: selectedPosition.accountId, marketId: selectedPosition.marketId, side: selectedPosition.side, outcomeIndex: selectedPosition.outcomeIndex, // 传递 outcomeIndex orderType: orderType, // 如果选择了百分比,只传递百分比,不传 quantity // 如果手动输入,只传递 quantity,不传 percent ...(selectedPercent != null ? { percent: selectedPercent } : { quantity: sellQuantity } ), price: orderType === 'LIMIT' ? limitPrice : undefined } const response = await apiService.accounts.sellPosition(request) if (response.data.code === 0) { message.success('卖出订单创建成功') setSellModalVisible(false) // 重置表单 setSellQuantity('') setLimitPrice('') setSelectedPercent(null) // 重置百分比选择 form.resetFields() // 仓位列表会通过WebSocket自动更新 } else { message.error(response.data.msg || '创建卖出订单失败') } } catch (error: any) { if (error.errorFields) { // 表单验证错误 return } message.error('创建卖出订单失败: ' + (error.message || '未知错误')) } finally { setSubmitting(false) } } // 实时计算收益(用于显示) const currentPnl = useMemo(() => { if (!selectedPosition || !sellQuantity) return { pnl: 0, percentPnl: 0 } const price = getCurrentSellPrice() if (!price || price === '0') return { pnl: 0, percentPnl: 0 } return calculatePnl(sellQuantity, price) }, [selectedPosition, sellQuantity, orderType, limitPrice, marketPrice]) // 渲染卡片视图 const renderCardView = () => { if (paginatedPositions.length === 0) { return ( ) } return ( {paginatedPositions.map((position, index) => { const pnlNum = parseFloat(position.pnl || '0') const isProfit = pnlNum >= 0 // 只有当前仓位才根据盈亏显示边框颜色 const borderColor = positionFilter === 'current' ? (isProfit ? 'rgba(82, 196, 26, 0.2)' : 'rgba(245, 34, 45, 0.2)') : 'rgba(0,0,0,0.06)' const cardKey = `${position.accountId}-${position.marketId}-${index}` const isExpanded = expandedCards.has(cardKey) // 移动端需要折叠功能,桌面端始终展开 const shouldCollapse = isMobile && !isExpanded return ( isMobile && toggleCard(cardKey)} style={{ height: '100%', borderRadius: '12px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', transition: 'all 0.3s ease', border: `1px solid ${borderColor}`, cursor: isMobile ? 'pointer' : 'default' }} bodyStyle={{ padding: '16px' }} > {/* 头部:市场图标和标题 */}
{position.marketIcon && ( {position.marketTitle { e.currentTarget.style.display = 'none' }} /> )}
{position.marketTitle ? ( (position.eventSlug || position.marketSlug) ? ( e.stopPropagation()} style={{ fontWeight: 'bold', color: '#1890ff', textDecoration: 'none', fontSize: '15px', lineHeight: '1.4', overflow: 'hidden', textOverflow: 'ellipsis', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }} > {position.marketTitle} ) : (
{position.marketTitle}
) ) : (
{position.marketId.slice(0, 16)}...
)} {position.marketSlug && (
{position.marketSlug}
)}
{/* 账户信息 */}
{position.accountName || `账户 ${position.accountId}`}
{position.walletAddress.slice(0, 6)}...{position.walletAddress.slice(-4)}
{position.side}
{/* 关键数据 */}
{/* 移动端折叠时,显示盈亏(使用简单样式) */} {shouldCollapse && positionFilter === 'current' && (
盈亏 {pnlNum >= 0 ? '+' : ''}{formatUSDC(position.pnl)} USDC
)} {/* 展开时显示所有数据 */} {!shouldCollapse && ( <>
数量 {formatNumber(position.quantity, 4)}
平均价格 {formatNumber(position.avgPrice, 4)}
开仓价值 {formatUSDC(position.initialValue)} USDC
{positionFilter === 'current' && position.currentPrice && ( <>
当前价格 {formatNumber(position.currentPrice, 4)}
当前价值 {formatUSDC(position.currentValue)} USDC
)} )} {/* 移动端展开/折叠指示器 */} {isMobile && (
{isExpanded ? ( ) : ( )}
)}
{/* 盈亏信息 - 突出显示(仅当前仓位显示,仅展开时显示) */} {positionFilter === 'current' && !shouldCollapse && (
盈亏 {pnlNum >= 0 ? '+' : ''}{formatUSDC(position.pnl)} USDC
{formatPercent(position.percentPnl)}
{position.realizedPnl && (
已实现盈亏 = 0 ? '#52c41a' : '#f5222d', fontWeight: '500' }}> {parseFloat(position.realizedPnl) >= 0 ? '+' : ''}{formatUSDC(position.realizedPnl)} USDC
)}
)} {/* 操作按钮(移动端折叠时隐藏) */} {positionFilter === 'current' && !shouldCollapse && (
{!position.redeemable && ( )}
)}
) })}
) } // 根据仓位类型动态生成列(优化后的紧凑布局) const columns = useMemo(() => { const baseColumns: any[] = [ { title: '', key: 'icon', width: 50, render: (_: any, record: AccountPosition) => { if (!record.marketIcon) return null return ( {record.marketTitle { e.currentTarget.style.display = 'none' }} /> ) }, fixed: isMobile ? ('left' as const) : undefined }, { title: '账户', dataIndex: 'accountName', key: 'accountName', render: (text: string | undefined, record: AccountPosition) => (
{text || `账户 ${record.accountId}`}
{record.walletAddress.slice(0, 6)}...{record.walletAddress.slice(-6)}
), fixed: isMobile ? ('left' as const) : undefined, width: isMobile ? 120 : 160 }, { title: '市场', dataIndex: 'marketTitle', key: 'marketTitle', render: (text: string | undefined, record: AccountPosition) => { const url = record.eventSlug || record.marketSlug ? `https://polymarket.com/event/${record.eventSlug || record.marketSlug}` : null const handleTitleClick = (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() if (url) { window.open(url, '_blank', 'noopener,noreferrer') } } return (
{text ? (
{url ? ( {text} ) : (
{text}
)}
) : (
{record.marketId.slice(0, 10)}...
)} {record.marketSlug && (
{record.marketSlug}
)}
) }, width: isMobile ? 180 : 220 }, { title: '方向', dataIndex: 'side', key: 'side', render: (side: string) => ( {side} ), width: 70 }, { title: '持仓', key: 'position', render: (_: any, record: AccountPosition) => (
{formatNumber(record.quantity, 4)}
@{formatNumber(record.avgPrice, 4)}
), align: 'right' as const, width: 100 }, { title: '开仓价值', dataIndex: 'initialValue', key: 'initialValue', render: (value: string) => ( {formatUSDC(value)} USDC ), align: 'right' as const, width: 110 }, ] // 只有当前仓位才显示当前价值/盈亏合并列 if (positionFilter === 'current') { baseColumns.push({ title: '当前价值 / 盈亏', key: 'valueAndPnl', render: (_: any, record: AccountPosition) => { const pnlNum = parseFloat(record.pnl || '0') const realizedPnl = record.realizedPnl ? parseFloat(record.realizedPnl) : null const percentRealizedPnl = record.percentRealizedPnl ? parseFloat(record.percentRealizedPnl) : null return (
{formatUSDC(record.currentValue)} USDC
= 0 ? '#3f8600' : '#cf1322', fontWeight: '500' }}> {pnlNum >= 0 ? '+' : ''}{formatUSDC(record.pnl)} ({formatPercent(record.percentPnl)})
{realizedPnl !== null && (
已实现: {realizedPnl >= 0 ? '+' : ''}{formatUSDC(record.realizedPnl)} {percentRealizedPnl !== null && ` (${formatPercent(record.percentRealizedPnl)})`}
)}
) }, align: 'right' as const, width: 160, sorter: (a: AccountPosition, b: AccountPosition) => { const valA = parseFloat(a.currentValue || '0') const valB = parseFloat(b.currentValue || '0') return valA - valB }, defaultSortOrder: 'descend' as const }) } // 只有当前仓位才显示操作列 if (positionFilter === 'current') { baseColumns.push({ title: '操作', key: 'action', render: (_: any, record: AccountPosition) => ( {!record.redeemable && ( )} ), width: 80, fixed: isMobile ? ('right' as const) : undefined }) } return baseColumns }, [positionFilter, isMobile]) // 统计当前和历史仓位数量(根据账户筛选) const filteredCurrentPositions = useMemo(() => { if (selectedAccountId === undefined) return currentPositions return currentPositions.filter(p => p.accountId === selectedAccountId) }, [currentPositions, selectedAccountId]) const filteredHistoryPositions = useMemo(() => { if (selectedAccountId === undefined) return historyPositions return historyPositions.filter(p => p.accountId === selectedAccountId) }, [historyPositions, selectedAccountId]) const currentCount = filteredCurrentPositions.length const historicalCount = filteredHistoryPositions.length return (

仓位管理

{/* WebSocket 连接状态指示器 */} {wsConnected ? '实时更新' : '连接中...'}
} value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)} allowClear style={{ width: isMobile ? '100%' : 300 }} /> {!isMobile && (
{ setPageSize(value) setCurrentPage(1) }} size="small" style={{ width: '100px' }} > 10 条/页 20 条/页 50 条/页
)} ) : ( `${record.accountId}-${record.marketId}-${index}`} loading={loading} pagination={{ current: currentPage, pageSize: pageSize, total: filteredPositions.length, showSizeChanger: true, pageSizeOptions: ['10', '20', '50'], showTotal: (total) => `共 ${total} 个仓位${searchKeyword ? `(已过滤)` : ''}`, onChange: (page, size) => { setCurrentPage(page) if (size !== pageSize) { setPageSize(size) } } }} scroll={isMobile ? { x: 1500 } : undefined} /> )} {/* 出售模态框 */} { if (!submitting) { setSellModalVisible(false) } }} onOk={handleSellSubmit} okText="确认卖出" cancelText="取消" width={isMobile ? '90%' : 600} destroyOnClose confirmLoading={submitting} maskClosable={!submitting} > {selectedPosition && (
账户: {selectedPosition.accountName || `账户 ${selectedPosition.accountId}`}
方向: {selectedPosition.side}
当前持仓: {formatNumber(selectedPosition.quantity, 4)}
平均价格: {formatNumber(selectedPosition.avgPrice, 4)}
{selectedPosition.currentPrice && (
当前价格: {formatNumber(selectedPosition.currentPrice, 4)}
)}
{ setOrderType(e.target.value) // 切换订单类型时重新计算收益 if (sellQuantity) { const price = e.target.value === 'MARKET' ? (marketPrice?.currentPrice || selectedPosition?.currentPrice || '0') : limitPrice || '0' calculatePnl(sellQuantity, price) } }} > 市价出售 限价出售 { if (!value || parseFloat(value) <= 0) { return Promise.reject('卖出数量必须大于0') } if (parseFloat(value) > parseFloat(selectedPosition.quantity)) { return Promise.reject('卖出数量不能超过持仓数量') } return Promise.resolve() } } ]} > { const newQuantity = e.target.value setSellQuantity(newQuantity) // 用户手动输入时,清除百分比选择 setSelectedPercent(null) if (newQuantity) { const price = getCurrentSellPrice() calculatePnl(newQuantity, price) } }} placeholder="请输入卖出数量" suffix={ } /> {orderType === 'LIMIT' && ( { if (!value || parseFloat(value) <= 0) { return Promise.reject('价格必须大于0') } return Promise.resolve() } } ]} > { const newPrice = e.target.value setLimitPrice(newPrice) if (sellQuantity && newPrice) { calculatePnl(sellQuantity, newPrice) } }} placeholder="请输入限价价格" /> {marketPrice?.currentPrice && (
参考价格(卖出参考): {formatNumber(marketPrice.currentPrice, 4)}
)}
)} {orderType === 'MARKET' && (
市价参考(卖出)
{marketPrice?.currentPrice ? ( <>当前价格: {formatNumber(marketPrice.currentPrice, 4)} ) : selectedPosition?.currentPrice ? ( <>当前价格: {formatNumber(selectedPosition.currentPrice, 4)} ) : ( 暂无价格数据 )}
)} {/* 预计平仓收益 */} {sellQuantity && (
= 0 ? 'rgba(82, 196, 26, 0.08)' : 'rgba(245, 34, 45, 0.08)', border: `1px solid ${currentPnl.pnl >= 0 ? 'rgba(82, 196, 26, 0.2)' : 'rgba(245, 34, 45, 0.2)'}`, borderRadius: '8px' }}>
预计平仓收益
= 0 ? '#52c41a' : '#f5222d', marginBottom: '4px' }}> {currentPnl.pnl >= 0 ? '+' : ''}{formatUSDC(currentPnl.pnl)} USDC
= 0 ? '#52c41a' : '#f5222d', fontWeight: '500' }}> {currentPnl.percentPnl >= 0 ? '+' : ''}{currentPnl.percentPnl.toFixed(2)}%
)} )}
{/* 赎回模态框 */} { if (!redeeming) { setRedeemModalVisible(false) } }} onOk={handleRedeemSubmit} okText="确认赎回" cancelText="取消" width={isMobile ? '90%' : 800} destroyOnClose confirmLoading={redeeming} maskClosable={!redeeming} > {redeemableSummary && redeemableSummary.positions.length > 0 ? (
{redeemableSummary.totalCount} 个 {formatUSDC(redeemableSummary.totalValue)} USDC {new Set(redeemableSummary.positions.map(p => p.accountId)).size} 个账户
赎回仓位列表:
`${record.marketId}-${record.outcomeIndex}-${index}`} pagination={false} size="small" scroll={{ y: 300 }} columns={[ { title: '账户', dataIndex: 'accountName', key: 'account', render: (text, record) => ( {text || `账户 ${record.accountId}`} ), width: 150 }, { title: '市场', dataIndex: 'marketTitle', key: 'marketTitle', render: (text, record) => text || record.marketId.substring(0, 10) + '...', width: 200 }, { title: '方向', dataIndex: 'side', key: 'side', render: (side) => {side}, width: 80 }, { title: '数量', dataIndex: 'quantity', key: 'quantity', align: 'right' as const, render: (value) => formatNumber(value, 4), width: 120 }, { title: '价值 (USDC)', dataIndex: 'value', key: 'value', align: 'right' as const, render: (value) => ( {formatNumber(value, 2)} ), width: 120 } ]} />
💡 提示:
• 赎回将按 1:1 比例将获胜仓位换回 USDC
• 同一市场的多个仓位将批量赎回,节省 Gas 费用
• 赎回操作需要发送链上交易,请确保账户有足够的 POL 支付 Gas
• 赎回成功后,仓位将从当前仓位列表中移除
) : (
)} ) } export default PositionList