2804 lines
70 KiB
Vue
2804 lines
70 KiB
Vue
<template>
|
||
<v-container class="wallet-container">
|
||
<div class="wallet-mobile-frame">
|
||
<div class="wallet-mobile-header">{{ t('wallet.walletTitle') }}</div>
|
||
<v-card class="wallet-card portfolio-card design-tu-asset" elevation="0" rounded="lg">
|
||
<div class="card-header">
|
||
<span class="card-title">{{ t('wallet.portfolio') }}</span>
|
||
</div>
|
||
<div class="card-value">${{ portfolioBalance }}</div>
|
||
</v-card>
|
||
<div class="card-actions design-tu-actions">
|
||
<v-btn
|
||
color="default"
|
||
variant="outlined"
|
||
class="action-btn design-tu-action"
|
||
@click="depositDialogOpen = true"
|
||
>
|
||
{{ t('wallet.deposit') }}
|
||
</v-btn>
|
||
<v-btn
|
||
color="default"
|
||
variant="outlined"
|
||
class="action-btn design-tu-action"
|
||
@click="withdrawDialogOpen = true"
|
||
>
|
||
{{ t('wallet.withdraw') }}
|
||
</v-btn>
|
||
</div>
|
||
|
||
<v-card
|
||
v-if="unsettledCount > 0"
|
||
class="settlement-card design-tu-settlement"
|
||
elevation="0"
|
||
rounded="lg"
|
||
>
|
||
<div class="settlement-inner">
|
||
<div class="settlement-label">
|
||
{{ t('wallet.youWon', { amount: unsettledTotalText }) }}
|
||
<span v-if="unsettledCount > 1" class="settlement-plus-n"
|
||
>+{{ unsettledCount - 1 }}</span
|
||
>
|
||
</div>
|
||
<v-btn
|
||
color="primary"
|
||
variant="flat"
|
||
class="settlement-claim-btn design-tu-claim-btn"
|
||
:loading="claimLoading"
|
||
:disabled="claimLoading"
|
||
@click="onClaimSettlement"
|
||
>
|
||
{{ t('wallet.claim') }}
|
||
</v-btn>
|
||
</div>
|
||
</v-card>
|
||
|
||
<!-- 下方:Positions / Open orders / History -->
|
||
<div class="wallet-section">
|
||
<v-tabs v-model="activeTab" class="wallet-tabs" density="comfortable">
|
||
<v-tab value="positions">{{ t('wallet.positions') }}</v-tab>
|
||
<v-tab value="orders">{{ t('wallet.openOrders') }}</v-tab>
|
||
<v-tab value="history">{{ t('wallet.history') }}</v-tab>
|
||
<v-tab value="withdrawals">{{ t('wallet.withdrawals') }}</v-tab>
|
||
</v-tabs>
|
||
<v-card class="table-card wallet-design-card" elevation="0" rounded="lg">
|
||
<template v-if="activeTab === 'positions'">
|
||
<div class="positions-mobile-list">
|
||
<div v-if="positionLoading" class="empty-cell">{{ t('common.loading') }}</div>
|
||
<div v-else-if="filteredPositions.length === 0" class="empty-cell">
|
||
{{ t('wallet.noPositionsFound') }}
|
||
</div>
|
||
<div
|
||
v-for="pos in paginatedPositions"
|
||
:key="pos.id"
|
||
class="position-mobile-card design-pos-card"
|
||
>
|
||
<div class="design-pos-top">
|
||
<div class="design-pos-left">
|
||
<div class="position-icon" :class="pos.iconClass">
|
||
<img
|
||
v-if="pos.imageUrl"
|
||
:src="pos.imageUrl"
|
||
alt=""
|
||
class="position-icon-img"
|
||
/>
|
||
<v-icon v-else-if="pos.icon" size="20" class="position-icon-svg">{{
|
||
pos.icon
|
||
}}</v-icon>
|
||
<span v-else class="position-icon-char">{{ pos.iconChar || '•' }}</span>
|
||
</div>
|
||
<div class="design-pos-title-col">
|
||
<div class="position-mobile-title marquee-container">
|
||
<div class="marquee-track">
|
||
<span>{{ pos.market }}</span>
|
||
<span aria-hidden="true">{{ pos.market }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="design-pos-tags">
|
||
<span
|
||
v-if="pos.outcomeTag"
|
||
class="position-outcome-pill"
|
||
:class="getOutcomeClass(pos.outcomeTag, pos.outcomePillClass)"
|
||
>
|
||
{{ pos.outcomeTag }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="design-pos-right">
|
||
<div class="position-value">{{ pos.value }}</div>
|
||
<div
|
||
v-if="pos.valueChange != null"
|
||
:class="[
|
||
'position-value-change',
|
||
pos.valueChangeLoss ? 'value-loss' : 'value-gain',
|
||
]"
|
||
>
|
||
{{ pos.valueChange
|
||
}}{{ pos.valueChangePct != null ? ` (${pos.valueChangePct})` : '' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="design-pos-stats">
|
||
<div class="design-pos-stat">
|
||
<div class="design-pos-stat-label">{{ t('wallet.sharesLabel') }}</div>
|
||
<div class="design-pos-stat-value">{{ pos.shares }}</div>
|
||
</div>
|
||
<div class="design-pos-stat">
|
||
<div class="design-pos-stat-label">{{ t('wallet.avgPriceLabel') }}</div>
|
||
<div class="design-pos-stat-value">{{ parseAvgNow(pos.avgNow)[0] }}</div>
|
||
</div>
|
||
<div class="design-pos-stat">
|
||
<div class="design-pos-stat-label">{{ t('wallet.currentPriceLabel') }}</div>
|
||
<div class="design-pos-stat-value">
|
||
{{ parseAvgNow(pos.avgNow)[1] || parseAvgNow(pos.avgNow)[0] }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="activeTab === 'orders'">
|
||
<div class="orders-mobile-list">
|
||
<div v-if="openOrderLoading" class="empty-cell">{{ t('common.loading') }}</div>
|
||
<div v-else-if="filteredOpenOrders.length === 0" class="empty-cell">
|
||
{{ t('wallet.noOpenOrdersFound') }}
|
||
</div>
|
||
<div v-for="ord in paginatedOpenOrders" :key="ord.id" class="order-mobile-card design-order-card">
|
||
<div class="design-order-top">
|
||
<div class="order-mobile-icon" :class="ord.iconClass">
|
||
<span class="position-icon-char">{{ ord.iconChar || '•' }}</span>
|
||
</div>
|
||
<div class="order-mobile-main design-order-title-col">
|
||
<div class="order-mobile-title marquee-container order-title-marquee">
|
||
<div class="marquee-track">
|
||
<span>{{ getOrderDisplayTitle(ord) }}</span>
|
||
<span aria-hidden="true">{{ getOrderDisplayTitle(ord) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="design-order-tags">
|
||
<span class="order-side-pill" :class="getOrderSideClass(ord)">{{
|
||
getOrderActionLabel(ord)
|
||
}}</span>
|
||
<span class="order-outcome-pill" :class="ord.side === 'Yes' ? 'outcome-yes' : 'outcome-no'">{{
|
||
ord.side
|
||
}}</span>
|
||
</div>
|
||
</div>
|
||
<v-btn
|
||
icon
|
||
variant="text"
|
||
size="small"
|
||
class="order-cancel-icon"
|
||
:disabled="cancelOrderLoading || ord.fullyFilled"
|
||
@click.stop="cancelOrder(ord)"
|
||
>
|
||
<v-icon size="20">mdi-close</v-icon>
|
||
</v-btn>
|
||
</div>
|
||
<div class="design-order-stats">
|
||
<div class="design-order-stat">
|
||
<div class="design-order-stat-label">{{ t('wallet.openOrderPriceLabel') }}</div>
|
||
<div class="design-order-stat-value">{{ ord.price }}</div>
|
||
</div>
|
||
<div class="design-order-stat">
|
||
<div class="design-order-stat-label">{{ t('wallet.filledTotalLabel') }}</div>
|
||
<div class="design-order-stat-value">
|
||
{{ ord.filledDisplay || `${ord.filled}/${ord.total}` }}
|
||
</div>
|
||
</div>
|
||
<div class="design-order-stat">
|
||
<div class="design-order-stat-label">{{ t('wallet.orderValueLabel') }}</div>
|
||
<div class="design-order-stat-value" :class="getOrderValueClass(ord)">
|
||
{{ getOrderValueDisplay(ord) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="activeTab === 'history'">
|
||
<div class="history-mobile-list">
|
||
<div v-if="historyLoading" class="empty-cell">{{ t('common.loading') }}</div>
|
||
<div v-else-if="filteredHistory.length === 0" class="empty-cell">
|
||
{{ t('wallet.noHistoryFound') }}
|
||
</div>
|
||
<div
|
||
v-for="h in paginatedHistory"
|
||
:key="h.id"
|
||
class="history-mobile-card"
|
||
:class="isFundingHistory(h) ? 'design-funding-card' : 'design-trade-card'"
|
||
>
|
||
<template v-if="!isFundingHistory(h)">
|
||
<div class="design-history-top">
|
||
<div class="design-history-left">
|
||
<div class="history-mobile-icon" :class="h.iconClass">
|
||
<img v-if="h.imageUrl" :src="h.imageUrl" alt="" class="position-icon-img" />
|
||
<span v-else class="position-icon-char">{{ h.iconChar || '•' }}</span>
|
||
</div>
|
||
<div class="history-mobile-main">
|
||
<div class="history-mobile-title marquee-container history-title-marquee">
|
||
<div class="marquee-track">
|
||
<span>{{ h.market }}</span>
|
||
<span aria-hidden="true">{{ h.market }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="history-mobile-activity">
|
||
{{ h.timeAgo || h.activityDetail || h.activity }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<span
|
||
:class="['history-mobile-pl', h.profitLossNegative ? 'pl-loss' : 'pl-gain']"
|
||
>
|
||
{{ h.profitLoss ?? h.value }}
|
||
</span>
|
||
</div>
|
||
<div class="design-history-bottom">
|
||
<span class="order-side-pill" :class="getHistoryTagClass(h)">
|
||
{{ getHistoryTagLabel(h) }}
|
||
</span>
|
||
<span class="design-history-meta"
|
||
>{{ t('wallet.priceLabel') }}: {{ h.avgPrice || '—' }} · {{ t('wallet.sharesLabel') }}: {{
|
||
h.shares || '—'
|
||
}}</span
|
||
>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="design-funding-left">
|
||
<div class="history-mobile-icon" :class="getFundingIconClass(h)">
|
||
<img v-if="h.imageUrl" :src="h.imageUrl" alt="" class="position-icon-img" />
|
||
<span v-else class="position-icon-char">{{ getFundingIconText(h) }}</span>
|
||
</div>
|
||
<div class="history-mobile-main">
|
||
<div class="history-mobile-title marquee-container history-title-marquee">
|
||
<div class="marquee-track">
|
||
<span>{{ getFundingTitle(h) }}</span>
|
||
<span aria-hidden="true">{{ getFundingTitle(h) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="history-mobile-activity">{{ h.timeAgo || '—' }}</div>
|
||
</div>
|
||
</div>
|
||
<span
|
||
:class="[
|
||
'history-mobile-pl',
|
||
isWithdrawalHistory(h) || h.profitLossNegative ? 'pl-loss' : 'pl-gain',
|
||
]"
|
||
>
|
||
{{ h.profitLoss ?? h.value }}
|
||
</span>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="activeTab === 'withdrawals'">
|
||
<div class="withdrawals-mobile-list">
|
||
<div v-if="withdrawalsLoading" class="empty-cell">{{ t('common.loading') }}</div>
|
||
<div v-else-if="displayedWithdrawals.length === 0" class="empty-cell">
|
||
{{ t('wallet.noWithdrawalsFound') }}
|
||
</div>
|
||
<div
|
||
v-for="w in displayedWithdrawals"
|
||
:key="String(w.ID ?? w.requestNo ?? '')"
|
||
class="withdrawal-mobile-card design-withdraw-card"
|
||
>
|
||
<div class="design-withdraw-top">
|
||
<div class="design-withdraw-head">
|
||
<div class="design-withdraw-date">
|
||
{{ formatWithdrawTime(w.CreatedAt ?? w.createdAt) }}
|
||
</div>
|
||
<div class="design-withdraw-chain">{{ w.chain || '—' }}</div>
|
||
</div>
|
||
<span :class="['withdrawal-status-pill', getWithdrawStatusClass(w.status)]">
|
||
{{ getWithdrawStatusLabel(w.status) }}
|
||
</span>
|
||
</div>
|
||
<div class="design-withdraw-mid">
|
||
<div class="design-withdraw-col">
|
||
<div class="design-withdraw-label">{{ t('wallet.withdrawAmountLabel') }}</div>
|
||
<div class="design-withdraw-value">{{ getWithdrawAmountText(w) }}</div>
|
||
</div>
|
||
<div class="design-withdraw-col right">
|
||
<div class="design-withdraw-label">{{ t('wallet.feeLabel') }}</div>
|
||
<div class="design-withdraw-value">{{ getWithdrawFeeText(w) }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="design-withdraw-bottom">
|
||
<div class="design-withdraw-label">{{ t('wallet.withdrawAddressLabel') }}</div>
|
||
<div class="withdrawal-mobile-address">
|
||
{{ shortAddress(w.tokenAddress ?? w.walletAddress) }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="empty-cell">{{ t('common.noData') }}</div>
|
||
</template>
|
||
<div v-if="false && currentListTotal > 0" class="pagination-bar">
|
||
<span class="pagination-info">
|
||
{{ currentPageStart }}–{{ currentPageEnd }} of {{ currentListTotal }}
|
||
</span>
|
||
<div class="pagination-controls">
|
||
<v-select
|
||
v-model="itemsPerPage"
|
||
:items="pageSizeOptions"
|
||
density="compact"
|
||
hide-details
|
||
variant="outlined"
|
||
class="page-size-select"
|
||
@update:model-value="page = 1"
|
||
/>
|
||
<v-pagination
|
||
v-model="page"
|
||
:length="currentTotalPages"
|
||
:total-visible="5"
|
||
density="comfortable"
|
||
class="pagination"
|
||
@update:model-value="onPageChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</v-card>
|
||
</div>
|
||
</div>
|
||
|
||
<DepositDialog v-model="depositDialogOpen" :balance="portfolioBalance" />
|
||
<WithdrawDialog
|
||
v-model="withdrawDialogOpen"
|
||
:balance="portfolioBalance"
|
||
@success="onWithdrawSuccess"
|
||
/>
|
||
|
||
<!-- 授权弹窗 -->
|
||
<v-dialog
|
||
v-model="authorizeDialogOpen"
|
||
max-width="420"
|
||
persistent
|
||
transition="dialog-transition"
|
||
>
|
||
<v-card rounded="lg">
|
||
<v-card-title class="d-flex align-center">
|
||
<v-icon class="mr-2">mdi-shield-check-outline</v-icon>
|
||
{{ t('wallet.authorize') }}
|
||
</v-card-title>
|
||
<v-card-text>
|
||
{{ t('wallet.authorizeDesc') }}
|
||
</v-card-text>
|
||
<v-card-actions>
|
||
<v-spacer />
|
||
<v-btn variant="text" @click="authorizeDialogOpen = false">
|
||
{{ t('deposit.close') }}
|
||
</v-btn>
|
||
<v-btn color="primary" variant="flat" @click="submitAuthorize">
|
||
{{ t('wallet.authorize') }}
|
||
</v-btn>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
|
||
<!-- Sell position dialog -->
|
||
<v-dialog
|
||
v-model="sellDialogOpen"
|
||
max-width="440"
|
||
persistent
|
||
content-class="sell-dialog"
|
||
transition="dialog-transition"
|
||
>
|
||
<v-card v-if="sellPositionItem" class="sell-dialog-card" rounded="lg">
|
||
<div class="sell-dialog-header">
|
||
<v-btn
|
||
icon
|
||
variant="text"
|
||
size="small"
|
||
class="sell-dialog-close"
|
||
@click="closeSellDialog"
|
||
>
|
||
<v-icon>mdi-close</v-icon>
|
||
</v-btn>
|
||
</div>
|
||
<v-card-text class="sell-dialog-body">
|
||
<div class="sell-dialog-icon-wrap">
|
||
<div class="position-icon" :class="sellPositionItem.iconClass">
|
||
<img
|
||
v-if="sellPositionItem.imageUrl"
|
||
:src="sellPositionItem.imageUrl"
|
||
alt=""
|
||
class="position-icon-img"
|
||
/>
|
||
<v-icon v-else-if="sellPositionItem.icon" size="20" class="position-icon-svg">{{
|
||
sellPositionItem.icon
|
||
}}</v-icon>
|
||
<span v-else class="position-icon-char">{{ sellPositionItem.iconChar || '•' }}</span>
|
||
</div>
|
||
</div>
|
||
<h3 class="sell-dialog-title">
|
||
{{ t('wallet.sellDialogTitle', { outcome: sellPositionItem.sellOutcome || t('wallet.position') }) }}
|
||
</h3>
|
||
<p class="sell-dialog-market">{{ sellPositionItem.market }}</p>
|
||
<div class="sell-receive-box">
|
||
<div class="sell-receive-label">
|
||
<v-icon size="20" color="success">mdi-sack</v-icon>
|
||
<span>{{ t('wallet.sellDialogReceive') }}</span>
|
||
</div>
|
||
<div class="sell-receive-value">{{ sellReceiveAmount }}</div>
|
||
</div>
|
||
</v-card-text>
|
||
<v-card-actions class="sell-dialog-actions">
|
||
<v-btn color="success" variant="flat" block class="sell-redeem-btn" @click="redeemSell">
|
||
{{ t('wallet.sellDialogRedeem') }}
|
||
</v-btn>
|
||
<a href="#" class="sell-edit-link" @click.prevent="editSellOrder">{{ t('wallet.sellDialogEditOrder') }}</a>
|
||
</v-card-actions>
|
||
</v-card>
|
||
</v-dialog>
|
||
|
||
<v-snackbar v-model="showCancelError" color="error" :timeout="4000">
|
||
{{ cancelOrderError }}
|
||
</v-snackbar>
|
||
</v-container>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { useDisplay } from 'vuetify'
|
||
|
||
const { t } = useI18n()
|
||
import { createChart, AreaSeries, LineType, LastPriceAnimationMode } from 'lightweight-charts'
|
||
import { toLwcData } from '../composables/useLightweightChart'
|
||
import DepositDialog from '../components/DepositDialog.vue'
|
||
import WithdrawDialog from '../components/WithdrawDialog.vue'
|
||
import { useUserStore } from '../stores/user'
|
||
import { useLocaleStore } from '../stores/locale'
|
||
import { useAuthError } from '../composables/useAuthError'
|
||
import { cancelOrder as apiCancelOrder } from '../api/order'
|
||
import { getOrderList, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
|
||
import { getHistoryRecordListClient, getHistoryRecordList } from '../api/historyRecord'
|
||
import { getPositionList, mapPositionToDisplayItem, claimPosition } from '../api/position'
|
||
import {
|
||
getSettlementRequestsListClient,
|
||
amountToUsdcDisplay,
|
||
WITHDRAW_STATUS,
|
||
type SettlementRequestClientItem,
|
||
} from '../api/pmset'
|
||
import {
|
||
MOCK_TOKEN_ID,
|
||
MOCK_WALLET_POSITIONS,
|
||
MOCK_WALLET_ORDERS,
|
||
MOCK_WALLET_HISTORY,
|
||
} from '../api/mockData'
|
||
import { USE_MOCK_WALLET } from '../config/mock'
|
||
import { CrossChainUSDTAuth } from '../../sdk/approve'
|
||
import { useToastStore } from '../stores/toast'
|
||
|
||
const { mobile } = useDisplay()
|
||
const userStore = useUserStore()
|
||
const { formatAuthError } = useAuthError()
|
||
const localeStore = useLocaleStore()
|
||
const portfolioBalance = computed(() => userStore.balance)
|
||
const profitLoss = ref('0.00')
|
||
const plRange = ref('ALL')
|
||
const plTimeRanges = computed(() => [
|
||
{ label: t('wallet.pl1D'), value: '1D' },
|
||
{ label: t('wallet.pl1W'), value: '1W' },
|
||
{ label: t('wallet.pl1M'), value: '1M' },
|
||
{ label: t('wallet.plAll'), value: 'ALL' },
|
||
])
|
||
const activeTab = ref<'positions' | 'orders' | 'history' | 'withdrawals'>('positions')
|
||
const search = ref('')
|
||
const withdrawStatusFilter = ref<string>('')
|
||
const withdrawStatusOptions = computed(() => [
|
||
{ label: t('wallet.withdrawStatusAll'), value: '' },
|
||
{ label: t('wallet.withdrawStatusPending'), value: WITHDRAW_STATUS.PENDING },
|
||
{ label: t('wallet.withdrawStatusSuccess'), value: WITHDRAW_STATUS.SUCCESS },
|
||
{ label: t('wallet.withdrawStatusRejected'), value: WITHDRAW_STATUS.REJECTED },
|
||
{ label: t('wallet.withdrawStatusFailed'), value: WITHDRAW_STATUS.FAILED },
|
||
])
|
||
/** 当前展示的持仓列表(mock 或 API) */
|
||
const currentPositionList = computed(() => (USE_MOCK_WALLET ? positions.value : positionList.value))
|
||
/** 未结算项:从持仓列表中筛出可领取的(有 marketID+tokenID,且所属市场已关闭 market.closed=true) */
|
||
const unsettledItems = computed(() => {
|
||
const list = currentPositionList.value
|
||
return list
|
||
.filter((p) => p.marketID && p.tokenID && p.marketClosed === true)
|
||
.map((p) => {
|
||
const amount = parseFloat(String(p.value).replace(/[^0-9.-]/g, '')) || 0
|
||
return { marketID: p.marketID!, tokenID: p.tokenID!, amount }
|
||
})
|
||
})
|
||
const unsettledCount = computed(() => unsettledItems.value.length)
|
||
const unsettledTotalText = computed(() => {
|
||
const sum = unsettledItems.value.reduce((a, b) => a + b.amount, 0)
|
||
return sum.toFixed(2)
|
||
})
|
||
const claimLoading = ref(false)
|
||
const toastStore = useToastStore()
|
||
async function onClaimSettlement() {
|
||
const items = unsettledItems.value
|
||
if (items.length === 0) return
|
||
const headers = userStore.getAuthHeaders()
|
||
if (!headers) {
|
||
toastStore.show(t('trade.pleaseLogin'), 'error')
|
||
return
|
||
}
|
||
claimLoading.value = true
|
||
try {
|
||
const res = await claimPosition(
|
||
{ marketID: items.map((i) => i.marketID), tokenID: items.map((i) => i.tokenID) },
|
||
{ headers },
|
||
)
|
||
if (res.code === 0 || res.code === 200) {
|
||
toastStore.show(t('toast.claimSuccess'))
|
||
userStore.fetchUsdcBalance()
|
||
if (activeTab.value === 'positions') loadPositionList()
|
||
} else {
|
||
toastStore.show(res.msg || t('error.requestFailed'), 'error')
|
||
}
|
||
} catch (e) {
|
||
toastStore.show(formatAuthError(e, t('error.requestFailed')), 'error')
|
||
} finally {
|
||
claimLoading.value = false
|
||
}
|
||
}
|
||
const depositDialogOpen = ref(false)
|
||
const withdrawDialogOpen = ref(false)
|
||
const authorizeDialogOpen = ref(false)
|
||
const sellDialogOpen = ref(false)
|
||
const sellPositionItem = ref<Position | null>(null)
|
||
/** 移动端展开的持仓 id,null 表示全部折叠 */
|
||
const expandedPositionId = ref<string | null>(null)
|
||
/** 移动端展开的历史记录 id */
|
||
const expandedHistoryId = ref<string | null>(null)
|
||
|
||
function togglePositionExpanded(id: string) {
|
||
expandedPositionId.value = expandedPositionId.value === id ? null : id
|
||
}
|
||
|
||
function toggleHistoryExpanded(id: string) {
|
||
expandedHistoryId.value = expandedHistoryId.value === id ? null : id
|
||
}
|
||
|
||
interface Position {
|
||
id: string
|
||
market: string
|
||
icon?: string
|
||
iconChar?: string
|
||
iconClass?: string
|
||
imageUrl?: string
|
||
outcomeTag?: string
|
||
outcomePillClass?: string
|
||
shares: string
|
||
avgNow: string
|
||
bet: string
|
||
toWin: string
|
||
value: string
|
||
valueChange?: string
|
||
valueChangePct?: string
|
||
valueChangeLoss?: boolean
|
||
/** 用于 Sell 弹窗标题,如 "Down" | "Yes" | "No" */
|
||
sellOutcome?: string
|
||
/** 移动端副标题 "on Up/Down to win" 中的词 */
|
||
outcomeWord?: string
|
||
/** 市场 ID(从持仓列表来,用于领取结算) */
|
||
marketID?: string
|
||
/** Token ID(从持仓列表来,用于领取结算) */
|
||
tokenID?: string
|
||
/** 所属市场是否已关闭,marketClosed=true 表示可结算/可领取 */
|
||
marketClosed?: boolean
|
||
}
|
||
|
||
/** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */
|
||
function parseAvgNow(avgNow: string): [string, string] {
|
||
const parts = avgNow.split(' → ')
|
||
return parts.length >= 2 ? [parts[0]!.trim(), parts[1]!.trim()] : [avgNow, '']
|
||
}
|
||
interface OpenOrder {
|
||
id: string
|
||
market: string
|
||
side: 'Yes' | 'No'
|
||
outcome: string
|
||
price: string
|
||
filled: string
|
||
total: string
|
||
expiration: string
|
||
/** 移动端展示:如 "Buy Up" */
|
||
actionLabel?: string
|
||
/** 移动端展示:如 "0/5" */
|
||
filledDisplay?: string
|
||
iconChar?: string
|
||
iconClass?: string
|
||
/** 取消订单 API 用 */
|
||
orderID?: number
|
||
tokenID?: string
|
||
/** 已成交数量达到原始总数量,不可撤单 */
|
||
fullyFilled?: boolean
|
||
}
|
||
interface HistoryItem {
|
||
id: string
|
||
market: string
|
||
side: 'Yes' | 'No'
|
||
activity: string
|
||
value: string
|
||
/** 移动端:如 "Sold 1 Down at 50¢" */
|
||
activityDetail?: string
|
||
/** 移动端:盈亏展示如 "+$0.50" */
|
||
profitLoss?: string
|
||
profitLossNegative?: boolean
|
||
/** 移动端:如 "3 minutes ago" */
|
||
timeAgo?: string
|
||
avgPrice?: string
|
||
shares?: string
|
||
iconChar?: string
|
||
iconClass?: string
|
||
/** 图标 URL(来自 record.icon) */
|
||
imageUrl?: string
|
||
}
|
||
|
||
function getOutcomeClass(outcomeTag?: string, fallback?: string): string {
|
||
if (fallback) return fallback
|
||
const normalized = (outcomeTag ?? '').toLowerCase()
|
||
return normalized.includes('yes') ? 'outcome-yes' : 'outcome-no'
|
||
}
|
||
|
||
function getOrderActionLabel(ord: OpenOrder): string {
|
||
const action = (ord.actionLabel ?? '').toLowerCase()
|
||
if (action.includes('sell')) return 'SELL'
|
||
return 'BUY'
|
||
}
|
||
|
||
function getOrderSideClass(ord: OpenOrder): string {
|
||
return getOrderActionLabel(ord) === 'SELL' ? 'side-sell' : 'side-buy'
|
||
}
|
||
|
||
function getOrderValueDisplay(ord: OpenOrder): string {
|
||
return `${ord.price} × ${ord.total}`
|
||
}
|
||
|
||
function getOrderValueClass(ord: OpenOrder): string {
|
||
return getOrderActionLabel(ord) === 'BUY' ? 'order-value-primary' : 'order-value-secondary'
|
||
}
|
||
|
||
function getOrderDisplayTitle(ord: OpenOrder): string {
|
||
const title = ord.market?.trim()
|
||
if (title) return title
|
||
return ord.outcome?.trim() ? `Market · ${ord.outcome}` : 'Untitled Market'
|
||
}
|
||
|
||
function isWithdrawalHistory(h: HistoryItem): boolean {
|
||
const text = `${h.activity} ${h.activityDetail ?? ''} ${h.market}`.toLowerCase()
|
||
return text.includes('withdraw') || text.includes('提现')
|
||
}
|
||
|
||
function isFundingHistory(h: HistoryItem): boolean {
|
||
const text = `${h.activity} ${h.activityDetail ?? ''} ${h.market}`.toLowerCase()
|
||
return (
|
||
text.includes('deposit') ||
|
||
text.includes('充值') ||
|
||
text.includes('withdraw') ||
|
||
text.includes('提现')
|
||
)
|
||
}
|
||
|
||
function getFundingTitle(h: HistoryItem): string {
|
||
if (h.market?.trim()) return h.market
|
||
return isWithdrawalHistory(h) ? t('wallet.btcWithdrawHistoryLabel') : t('wallet.usdtDepositHistoryLabel')
|
||
}
|
||
|
||
function getFundingIconText(h: HistoryItem): string {
|
||
return isWithdrawalHistory(h) ? t('wallet.withdrawIconText') : t('wallet.depositIconText')
|
||
}
|
||
|
||
function getFundingIconClass(h: HistoryItem): string {
|
||
return isWithdrawalHistory(h) ? 'funding-icon-withdraw' : 'funding-icon-deposit'
|
||
}
|
||
|
||
function getHistoryTagLabel(h: HistoryItem): 'BUY' | 'SELL' {
|
||
return h.profitLossNegative ? 'BUY' : 'SELL'
|
||
}
|
||
|
||
function getHistoryTagClass(h: HistoryItem): string {
|
||
return h.profitLossNegative ? 'history-tag-buy' : 'history-tag-sell'
|
||
}
|
||
|
||
const positions = ref<Position[]>(USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] : [])
|
||
/** 提现记录列表 */
|
||
const withdrawalsList = ref<SettlementRequestClientItem[]>([])
|
||
const withdrawalsTotal = ref(0)
|
||
const withdrawalsLoading = ref(false)
|
||
const previewWithdrawals = ref<SettlementRequestClientItem[]>([
|
||
{
|
||
ID: -1,
|
||
CreatedAt: '2024-01-15T14:30:25Z',
|
||
chain: 'Polygon',
|
||
amount: 2500000000,
|
||
fee: '0.00',
|
||
status: WITHDRAW_STATUS.PENDING,
|
||
tokenAddress: '0x27fa...79ab',
|
||
tokenSymbol: 'USDT',
|
||
},
|
||
{
|
||
ID: -2,
|
||
CreatedAt: '2024-01-14T09:16:53Z',
|
||
chain: 'Bitcoin',
|
||
amount: 850000000,
|
||
fee: '0.0065',
|
||
status: WITHDRAW_STATUS.SUCCESS,
|
||
tokenAddress: 'bc1q...49x7n',
|
||
tokenSymbol: 'BTC',
|
||
},
|
||
{
|
||
ID: -3,
|
||
CreatedAt: '2024-01-12T11:26:03Z',
|
||
chain: 'Ethereum',
|
||
amount: 5260000000,
|
||
fee: '0.05',
|
||
status: WITHDRAW_STATUS.REJECTED,
|
||
tokenAddress: '0x7f42...f9d9',
|
||
tokenSymbol: 'ETH',
|
||
},
|
||
])
|
||
|
||
/** 持仓列表(API 数据,非 mock 时使用) */
|
||
const positionList = ref<Position[]>([])
|
||
const positionTotal = ref(0)
|
||
const positionLoading = ref(false)
|
||
|
||
async function loadPositionList() {
|
||
if (USE_MOCK_WALLET) return
|
||
const headers = userStore.getAuthHeaders()
|
||
if (!headers) {
|
||
positionList.value = []
|
||
positionTotal.value = 0
|
||
return
|
||
}
|
||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||
const userID = uid != null ? Number(uid) : undefined
|
||
if (!userID || !Number.isFinite(userID)) {
|
||
positionList.value = []
|
||
positionTotal.value = 0
|
||
return
|
||
}
|
||
positionLoading.value = true
|
||
try {
|
||
const res = await getPositionList(
|
||
{ page: page.value, pageSize: itemsPerPage.value, userID },
|
||
{ headers },
|
||
)
|
||
if (res.code === 0 || res.code === 200) {
|
||
const list = res.data?.list ?? []
|
||
positionList.value = list.map(mapPositionToDisplayItem)
|
||
positionTotal.value = res.data?.total ?? 0
|
||
} else {
|
||
positionList.value = []
|
||
positionTotal.value = 0
|
||
}
|
||
} catch {
|
||
positionList.value = []
|
||
positionTotal.value = 0
|
||
} finally {
|
||
positionLoading.value = false
|
||
}
|
||
}
|
||
|
||
const openOrders = ref<OpenOrder[]>(USE_MOCK_WALLET ? [...MOCK_WALLET_ORDERS] : [])
|
||
/** 未成交订单(API 数据,非 mock 时使用) */
|
||
const openOrderList = ref<OpenOrder[]>([])
|
||
const openOrderTotal = ref(0)
|
||
const openOrderLoading = ref(false)
|
||
|
||
async function loadOpenOrders() {
|
||
if (USE_MOCK_WALLET) return
|
||
const headers = userStore.getAuthHeaders()
|
||
if (!headers) {
|
||
openOrderList.value = []
|
||
openOrderTotal.value = 0
|
||
return
|
||
}
|
||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||
const userID = uid != null ? Number(uid) : undefined
|
||
if (!userID || !Number.isFinite(userID)) {
|
||
openOrderList.value = []
|
||
openOrderTotal.value = 0
|
||
return
|
||
}
|
||
openOrderLoading.value = true
|
||
try {
|
||
const res = await getOrderList(
|
||
{
|
||
page: page.value,
|
||
pageSize: itemsPerPage.value,
|
||
userID,
|
||
status: OrderStatus.Live,
|
||
},
|
||
{ headers },
|
||
)
|
||
if (res.code === 0 || res.code === 200) {
|
||
const list = res.data?.list ?? []
|
||
const openOnly = list.filter((o) => (o.status ?? 1) === OrderStatus.Live)
|
||
openOrderList.value = openOnly.map(mapOrderToOpenOrderItem)
|
||
openOrderTotal.value = openOnly.length
|
||
} else {
|
||
openOrderList.value = []
|
||
openOrderTotal.value = 0
|
||
}
|
||
} catch {
|
||
openOrderList.value = []
|
||
openOrderTotal.value = 0
|
||
} finally {
|
||
openOrderLoading.value = false
|
||
}
|
||
}
|
||
|
||
const history = ref<HistoryItem[]>(USE_MOCK_WALLET ? [...MOCK_WALLET_HISTORY] : [])
|
||
/** 订单历史(API 数据,非 mock 时使用) */
|
||
const historyList = ref<HistoryItem[]>([])
|
||
const historyTotal = ref(0)
|
||
const historyLoading = ref(false)
|
||
|
||
/** 历史记录来自 GET /hr/getHistoryRecordListClient(需鉴权,按当前用户分页) */
|
||
async function loadHistoryOrders() {
|
||
if (USE_MOCK_WALLET) return
|
||
const headers = userStore.getAuthHeaders()
|
||
if (!headers) {
|
||
historyList.value = []
|
||
historyTotal.value = 0
|
||
return
|
||
}
|
||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||
const userID = uid != null ? Number(uid) : undefined
|
||
if (!userID || !Number.isFinite(userID)) {
|
||
historyList.value = []
|
||
historyTotal.value = 0
|
||
return
|
||
}
|
||
historyLoading.value = true
|
||
try {
|
||
const res = await getHistoryRecordListClient(
|
||
{
|
||
page: page.value,
|
||
pageSize: itemsPerPage.value,
|
||
userId: userID,
|
||
},
|
||
{ headers },
|
||
)
|
||
if (res.code === 0 || res.code === 200) {
|
||
const { list, total } = getHistoryRecordList(res.data)
|
||
historyList.value = list
|
||
historyTotal.value = total
|
||
} else {
|
||
historyList.value = []
|
||
historyTotal.value = 0
|
||
}
|
||
} catch {
|
||
historyList.value = []
|
||
historyTotal.value = 0
|
||
} finally {
|
||
historyLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadWithdrawals() {
|
||
const headers = userStore.getAuthHeaders()
|
||
if (!headers) {
|
||
withdrawalsList.value = []
|
||
withdrawalsTotal.value = 0
|
||
return
|
||
}
|
||
withdrawalsLoading.value = true
|
||
try {
|
||
const res = await getSettlementRequestsListClient(
|
||
{
|
||
page: page.value,
|
||
pageSize: itemsPerPage.value,
|
||
status: withdrawStatusFilter.value || undefined,
|
||
},
|
||
{ headers },
|
||
)
|
||
if (res.code === 0 || res.code === 200) {
|
||
withdrawalsList.value = res.data?.list ?? []
|
||
withdrawalsTotal.value = res.data?.total ?? 0
|
||
} else {
|
||
withdrawalsList.value = []
|
||
withdrawalsTotal.value = 0
|
||
}
|
||
} catch {
|
||
withdrawalsList.value = []
|
||
withdrawalsTotal.value = 0
|
||
} finally {
|
||
withdrawalsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const displayedWithdrawals = computed(() =>
|
||
withdrawalsList.value.length > 0 ? withdrawalsList.value : previewWithdrawals.value,
|
||
)
|
||
|
||
function formatWithdrawAmount(amount: number | undefined): string {
|
||
return amountToUsdcDisplay(amount)
|
||
}
|
||
|
||
function shortAddress(addr: string | undefined): string {
|
||
if (!addr) return '—'
|
||
return addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr
|
||
}
|
||
|
||
function formatWithdrawTime(iso: string | undefined): string {
|
||
if (!iso) return '—'
|
||
try {
|
||
const d = new Date(iso)
|
||
return d.toLocaleString()
|
||
} catch {
|
||
return iso
|
||
}
|
||
}
|
||
|
||
function getWithdrawFeeDisplay(w: SettlementRequestClientItem): string {
|
||
const fee = (w as { fee?: number; gasFee?: number; serviceFee?: number }).fee
|
||
const gasFee = (w as { fee?: number; gasFee?: number; serviceFee?: number }).gasFee
|
||
const serviceFee = (w as { fee?: number; gasFee?: number; serviceFee?: number }).serviceFee
|
||
const value = fee ?? gasFee ?? serviceFee
|
||
return value == null ? '—' : `$${amountToUsdcDisplay(value)}`
|
||
}
|
||
|
||
function getWithdrawTokenSymbol(w: SettlementRequestClientItem): string {
|
||
const symbol = (w as { tokenSymbol?: string }).tokenSymbol
|
||
if (symbol && symbol.trim()) return symbol.toUpperCase()
|
||
const chain = (w.chain ?? '').toLowerCase()
|
||
if (chain.includes('btc') || chain.includes('bitcoin')) return 'BTC'
|
||
if (chain.includes('eth') || chain.includes('ethereum')) return 'ETH'
|
||
return 'USDT'
|
||
}
|
||
|
||
function getWithdrawAmountText(w: SettlementRequestClientItem): string {
|
||
const amount = Number(amountToUsdcDisplay(w.amount))
|
||
const amountText = Number.isFinite(amount)
|
||
? amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||
: '0.00'
|
||
return `${amountText} ${getWithdrawTokenSymbol(w)}`
|
||
}
|
||
|
||
function getWithdrawFeeText(w: SettlementRequestClientItem): string {
|
||
const feeRaw = Number(
|
||
(w as { fee?: string | number; gasFee?: string | number; serviceFee?: string | number }).fee ??
|
||
(w as { fee?: string | number; gasFee?: string | number; serviceFee?: string | number }).gasFee ??
|
||
(w as { fee?: string | number; gasFee?: string | number; serviceFee?: string | number }).serviceFee ??
|
||
0,
|
||
)
|
||
const feeText = Number.isFinite(feeRaw)
|
||
? feeRaw.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 4 })
|
||
: '0.00'
|
||
return `${feeText} ${getWithdrawTokenSymbol(w)}`
|
||
}
|
||
|
||
function getWithdrawStatusLabel(status: string | undefined): string {
|
||
const s = (status ?? '').toLowerCase()
|
||
if (s === WITHDRAW_STATUS.PENDING || s === '0' || s === 'pending')
|
||
return t('wallet.withdrawStatusPending')
|
||
if (s === WITHDRAW_STATUS.SUCCESS || s === '1' || s === 'success')
|
||
return t('wallet.withdrawStatusSuccess')
|
||
if (s === WITHDRAW_STATUS.REJECTED || s === '2' || s === 'rejected')
|
||
return t('wallet.withdrawStatusRejected')
|
||
if (s === WITHDRAW_STATUS.FAILED || s === '3' || s === 'failed')
|
||
return t('wallet.withdrawStatusFailed')
|
||
return status ?? '—'
|
||
}
|
||
|
||
function getWithdrawStatusClass(status: string | undefined): string {
|
||
const s = (status ?? '').toLowerCase()
|
||
if (s === WITHDRAW_STATUS.PENDING || s === '0' || s === 'pending') return 'status-pending'
|
||
if (s === WITHDRAW_STATUS.SUCCESS || s === '1' || s === 'success') return 'status-success'
|
||
if (s === WITHDRAW_STATUS.REJECTED || s === '2' || s === 'rejected') return 'status-rejected'
|
||
if (s === WITHDRAW_STATUS.FAILED || s === '3' || s === 'failed') return 'status-failed'
|
||
return ''
|
||
}
|
||
|
||
function matchSearch(text: string): boolean {
|
||
const q = search.value.trim().toLowerCase()
|
||
return !q || text.toLowerCase().includes(q)
|
||
}
|
||
const filteredPositions = computed(() => {
|
||
const list = USE_MOCK_WALLET ? positions.value : positionList.value
|
||
return list.filter((p) => matchSearch(p.market))
|
||
})
|
||
const filteredOpenOrders = computed(() => {
|
||
const list = USE_MOCK_WALLET ? openOrders.value : openOrderList.value
|
||
return list.filter((o) => matchSearch(o.market))
|
||
})
|
||
const filteredHistory = computed(() => {
|
||
const list = USE_MOCK_WALLET ? history.value : historyList.value
|
||
return list.filter((h) => matchSearch(h.market))
|
||
})
|
||
|
||
const page = ref(1)
|
||
const itemsPerPage = ref(10)
|
||
const pageSizeOptions = [5, 10, 25, 50]
|
||
|
||
function paginate<T>(list: T[]) {
|
||
const start = (page.value - 1) * itemsPerPage.value
|
||
return list.slice(start, start + itemsPerPage.value)
|
||
}
|
||
const paginatedPositions = computed(() => {
|
||
if (USE_MOCK_WALLET) return paginate(filteredPositions.value)
|
||
return filteredPositions.value
|
||
})
|
||
const paginatedOpenOrders = computed(() => {
|
||
if (USE_MOCK_WALLET) return paginate(filteredOpenOrders.value)
|
||
return filteredOpenOrders.value
|
||
})
|
||
const paginatedHistory = computed(() => {
|
||
if (USE_MOCK_WALLET) return paginate(filteredHistory.value)
|
||
return filteredHistory.value
|
||
})
|
||
|
||
const totalPagesPositions = computed(() => {
|
||
const total = USE_MOCK_WALLET ? filteredPositions.value.length : positionTotal.value
|
||
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
||
})
|
||
const totalPagesOrders = computed(() => {
|
||
const total = USE_MOCK_WALLET ? filteredOpenOrders.value.length : openOrderTotal.value
|
||
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
||
})
|
||
const totalPagesHistory = computed(() => {
|
||
const total = USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
|
||
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
||
})
|
||
const totalPagesWithdrawals = computed(() =>
|
||
Math.max(1, Math.ceil(withdrawalsTotal.value / itemsPerPage.value)),
|
||
)
|
||
|
||
const currentListTotal = computed(() => {
|
||
if (activeTab.value === 'positions')
|
||
return USE_MOCK_WALLET ? filteredPositions.value.length : positionTotal.value
|
||
if (activeTab.value === 'orders')
|
||
return USE_MOCK_WALLET ? filteredOpenOrders.value.length : openOrderTotal.value
|
||
if (activeTab.value === 'withdrawals') return withdrawalsTotal.value
|
||
return USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
|
||
})
|
||
const currentTotalPages = computed(() => {
|
||
if (activeTab.value === 'positions') return totalPagesPositions.value
|
||
if (activeTab.value === 'orders') return totalPagesOrders.value
|
||
if (activeTab.value === 'withdrawals') return totalPagesWithdrawals.value
|
||
return totalPagesHistory.value
|
||
})
|
||
const currentPageStart = computed(() =>
|
||
currentListTotal.value === 0 ? 0 : (page.value - 1) * itemsPerPage.value + 1,
|
||
)
|
||
const currentPageEnd = computed(() =>
|
||
Math.min(page.value * itemsPerPage.value, currentListTotal.value),
|
||
)
|
||
|
||
watch(activeTab, (tab) => {
|
||
page.value = 1
|
||
if (tab === 'positions' && !USE_MOCK_WALLET) loadPositionList()
|
||
if (tab === 'orders' && !USE_MOCK_WALLET) loadOpenOrders()
|
||
if (tab === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
||
if (tab === 'withdrawals') loadWithdrawals()
|
||
})
|
||
watch([page, itemsPerPage], () => {
|
||
if (activeTab.value === 'positions' && !USE_MOCK_WALLET) loadPositionList()
|
||
if (activeTab.value === 'orders' && !USE_MOCK_WALLET) loadOpenOrders()
|
||
if (activeTab.value === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
||
if (activeTab.value === 'withdrawals') loadWithdrawals()
|
||
})
|
||
watch(withdrawStatusFilter, () => {
|
||
page.value = 1
|
||
if (activeTab.value === 'withdrawals') loadWithdrawals()
|
||
})
|
||
watch([currentListTotal, itemsPerPage], () => {
|
||
const maxPage = currentTotalPages.value
|
||
if (page.value > maxPage) page.value = Math.max(1, maxPage)
|
||
})
|
||
|
||
function onPageChange() {
|
||
// 可选:翻页后滚动到表格顶部
|
||
}
|
||
|
||
const cancelOrderLoading = ref(false)
|
||
const cancelOrderError = ref('')
|
||
const showCancelError = ref(false)
|
||
async function cancelOrder(ord: OpenOrder) {
|
||
if (ord.fullyFilled) return
|
||
const orderID = ord.orderID ?? 5
|
||
const tokenID = ord.tokenID ?? MOCK_TOKEN_ID
|
||
const uid = userStore.user?.id ?? userStore.user?.ID
|
||
const userID = uid != null ? Number(uid) : 0
|
||
if (!Number.isFinite(userID) || userID <= 0) {
|
||
cancelOrderError.value = t('error.pleaseLogin')
|
||
showCancelError.value = true
|
||
return
|
||
}
|
||
const headers = userStore.getAuthHeaders()
|
||
if (!headers) {
|
||
cancelOrderError.value = t('error.pleaseLogin')
|
||
showCancelError.value = true
|
||
return
|
||
}
|
||
cancelOrderLoading.value = true
|
||
cancelOrderError.value = ''
|
||
try {
|
||
const res = await apiCancelOrder({ orderID, tokenID, userID }, { headers })
|
||
if (res.code === 0 || res.code === 200) {
|
||
if (USE_MOCK_WALLET) {
|
||
openOrders.value = openOrders.value.filter((o) => o.id !== ord.id)
|
||
} else {
|
||
openOrderList.value = openOrderList.value.filter((o) => o.id !== ord.id)
|
||
openOrderTotal.value = openOrderList.value.length
|
||
}
|
||
userStore.fetchUsdcBalance()
|
||
} else {
|
||
cancelOrderError.value = res.msg || t('wallet.cancelFailed')
|
||
showCancelError.value = true
|
||
}
|
||
} catch (e) {
|
||
cancelOrderError.value = formatAuthError(e, t('error.requestFailed'))
|
||
showCancelError.value = true
|
||
} finally {
|
||
cancelOrderLoading.value = false
|
||
}
|
||
}
|
||
|
||
function cancelAllOrders() {
|
||
if (USE_MOCK_WALLET) {
|
||
openOrders.value = []
|
||
} else {
|
||
openOrderList.value = []
|
||
openOrderTotal.value = 0
|
||
}
|
||
}
|
||
|
||
const sellReceiveAmount = computed(() => {
|
||
const pos = sellPositionItem.value
|
||
return pos ? pos.value : '$0.00'
|
||
})
|
||
|
||
function openSellDialog(pos: Position) {
|
||
sellPositionItem.value = pos
|
||
sellDialogOpen.value = true
|
||
}
|
||
|
||
function closeSellDialog() {
|
||
sellDialogOpen.value = false
|
||
sellPositionItem.value = null
|
||
}
|
||
|
||
function sellPosition(id: string) {
|
||
const pos = positions.value.find((p) => p.id === id)
|
||
if (pos) openSellDialog(pos)
|
||
}
|
||
|
||
function redeemSell() {
|
||
if (sellPositionItem.value) {
|
||
// TODO: 调用赎回接口,成功后从持仓移除
|
||
positions.value = positions.value.filter((p) => p.id !== sellPositionItem.value!.id)
|
||
closeSellDialog()
|
||
}
|
||
}
|
||
|
||
function editSellOrder() {
|
||
// TODO: 跳转交易页或打开限价单
|
||
closeSellDialog()
|
||
}
|
||
|
||
function sharePosition(id: string) {
|
||
// TODO: 分享或复制链接
|
||
}
|
||
|
||
function closeLosses() {
|
||
// TODO: 关闭亏损仓位或筛选
|
||
}
|
||
|
||
function viewHistory(id: string) {
|
||
// TODO: 跳转交易详情或订单详情
|
||
}
|
||
|
||
function shareHistory(id: string) {
|
||
// TODO: 分享或复制链接
|
||
}
|
||
|
||
const plChartRef = ref<HTMLElement | null>(null)
|
||
let plChartInstance: ReturnType<typeof createChart> | null = null
|
||
let plChartSeries: ReturnType<ReturnType<typeof createChart>['addSeries']> | null = null
|
||
|
||
/**
|
||
* 资产变化折线图数据 [timestamp_ms, pnl]
|
||
* 暂无接口时返回真实格式的时间序列,数值均为 0;有接口后改为从 API 拉取并在此处做分时过滤
|
||
*/
|
||
function getPlChartData(range: string): [number, number][] {
|
||
const now = Date.now()
|
||
let stepMs: number
|
||
let count: number
|
||
switch (range) {
|
||
case '1D':
|
||
stepMs = 60 * 60 * 1000
|
||
count = 24
|
||
break
|
||
case '1W':
|
||
stepMs = 24 * 60 * 60 * 1000
|
||
count = 7
|
||
break
|
||
case '1M':
|
||
stepMs = 24 * 60 * 60 * 1000
|
||
count = 30
|
||
break
|
||
case 'ALL':
|
||
default:
|
||
stepMs = 24 * 60 * 60 * 1000
|
||
count = 30
|
||
break
|
||
}
|
||
const data: [number, number][] = []
|
||
for (let i = count; i >= 0; i--) {
|
||
const t = now - i * stepMs
|
||
data.push([t, 0])
|
||
}
|
||
return data
|
||
}
|
||
|
||
const plChartData = ref<[number, number][]>([])
|
||
|
||
function updatePlChart() {
|
||
plChartData.value = getPlChartData(plRange.value)
|
||
const last = plChartData.value[plChartData.value.length - 1]
|
||
if (last != null) profitLoss.value = last[1].toFixed(2)
|
||
if (plChartSeries) plChartSeries.setData(toLwcData(plChartData.value))
|
||
}
|
||
|
||
function initPlChart() {
|
||
if (!plChartRef.value) return
|
||
plChartData.value = getPlChartData(plRange.value)
|
||
const last = plChartData.value[plChartData.value.length - 1]
|
||
if (last != null) profitLoss.value = last[1].toFixed(2)
|
||
const el = plChartRef.value
|
||
plChartInstance = createChart(el, {
|
||
width: el.clientWidth,
|
||
height: 160,
|
||
layout: {
|
||
background: { color: '#ffffff' },
|
||
textColor: '#9ca3af',
|
||
fontFamily: 'inherit',
|
||
fontSize: 9,
|
||
attributionLogo: false,
|
||
},
|
||
grid: { vertLines: { visible: false }, horzLines: { color: '#f3f4f6' } },
|
||
rightPriceScale: { borderVisible: false },
|
||
timeScale: { borderVisible: false },
|
||
crosshair: { vertLine: { labelVisible: false }, horzLine: { labelVisible: true } },
|
||
})
|
||
const lastVal = last != null ? last[1] : 0
|
||
const color = lastVal >= 0 ? '#059669' : '#dc2626'
|
||
plChartSeries = plChartInstance.addSeries(AreaSeries, {
|
||
topColor: color + '40',
|
||
bottomColor: color + '08',
|
||
lineColor: color,
|
||
lineWidth: 2,
|
||
lineType: LineType.Curved,
|
||
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
|
||
priceFormat: { type: 'price', precision: 2 },
|
||
})
|
||
plChartSeries.setData(toLwcData(plChartData.value))
|
||
}
|
||
|
||
const handleResize = () => {
|
||
if (plChartInstance && plChartRef.value) plChartInstance.resize(plChartRef.value.clientWidth, 160)
|
||
}
|
||
|
||
watch(plRange, () => updatePlChart())
|
||
|
||
// 监听语言切换,语言变化时重新加载数据
|
||
watch(
|
||
() => localeStore.currentLocale,
|
||
() => {
|
||
loadPositionList()
|
||
loadOpenOrders()
|
||
},
|
||
)
|
||
|
||
onMounted(() => {
|
||
if (!USE_MOCK_WALLET && activeTab.value === 'positions') loadPositionList()
|
||
nextTick(() => {
|
||
initPlChart()
|
||
})
|
||
window.addEventListener('resize', handleResize)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
window.removeEventListener('resize', handleResize)
|
||
plChartInstance?.remove()
|
||
plChartInstance = null
|
||
plChartSeries = null
|
||
})
|
||
|
||
function onWithdrawSuccess() {
|
||
withdrawDialogOpen.value = false
|
||
userStore.fetchUsdcBalance()
|
||
if (activeTab.value === 'withdrawals') loadWithdrawals()
|
||
}
|
||
|
||
function onAuthorizeClick() {
|
||
authorizeDialogOpen.value = true
|
||
}
|
||
|
||
async function submitAuthorize() {
|
||
// TODO: 对接 USDC 授权接口(approve CLOB 合约)
|
||
// authorizeDialogOpen.value = false
|
||
await CrossChainUSDTAuth.authorizeUSDT('eth', '0x024b7270Ee9c0Fc0de2E00a979d146255E0e9C00', '100')
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.wallet-container {
|
||
width: 100%;
|
||
max-width: 100%;
|
||
margin: 0 auto;
|
||
padding: 16px;
|
||
background: #fcfcfc;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.wallet-mobile-frame {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.wallet-mobile-header {
|
||
color: #111827;
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.wallet-cards {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.wallet-card {
|
||
padding: 16px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 16px;
|
||
height: auto;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.design-tu-asset {
|
||
background: #5b5bd6;
|
||
border: 0;
|
||
}
|
||
|
||
.design-tu-asset .card-title,
|
||
.design-tu-asset .card-value {
|
||
color: #fff;
|
||
}
|
||
|
||
.design-tu-asset .card-title {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.design-tu-actions {
|
||
gap: 10px;
|
||
}
|
||
|
||
.design-tu-action {
|
||
flex: 1;
|
||
height: 72px;
|
||
border-radius: 12px;
|
||
border: 1px solid #e5e7eb;
|
||
background: #fff;
|
||
color: #111827;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.design-tu-settlement {
|
||
padding: 12px 14px;
|
||
border-radius: 16px;
|
||
background: #fff;
|
||
}
|
||
|
||
.design-tu-claim-btn {
|
||
min-width: 0;
|
||
height: 30px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
padding: 0 12px;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #111827;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.title-icon {
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.balance-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
color: #059669;
|
||
background-color: #d1fae5;
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.card-value {
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.card-value-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.card-value-row .card-value {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.info-icon {
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.card-timeframe {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.card-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
.action-btn {
|
||
text-transform: none;
|
||
}
|
||
|
||
.pl-tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
}
|
||
|
||
.pl-tab {
|
||
min-width: 40px;
|
||
text-transform: none;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.pl-chart {
|
||
height: 120px;
|
||
width: 100%;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* 未结算汇总:单条 item,多条 +N,领取按钮无图标 */
|
||
.wallet-settlement-row {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.settlement-card {
|
||
border: 1px solid #e5e7eb;
|
||
padding: 12px 14px;
|
||
}
|
||
|
||
.settlement-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
.settlement-label {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
.settlement-plus-n {
|
||
display: inline-block;
|
||
margin-left: 6px;
|
||
padding: 2px 8px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #6b7280;
|
||
background: #f3f4f6;
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.settlement-claim-btn {
|
||
text-transform: none;
|
||
font-weight: 600;
|
||
min-width: 64px;
|
||
}
|
||
|
||
.wallet-section {
|
||
margin-top: 0;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 16px;
|
||
background: #fff;
|
||
padding: 16px;
|
||
}
|
||
|
||
.wallet-tabs {
|
||
margin-bottom: 12px;
|
||
min-height: 30px;
|
||
}
|
||
|
||
.wallet-tabs :deep(.v-slide-group__content) {
|
||
gap: 8px;
|
||
}
|
||
|
||
.wallet-tabs :deep(.v-tab) {
|
||
min-width: auto;
|
||
height: 30px;
|
||
padding: 0 12px;
|
||
border-radius: 999px;
|
||
text-transform: none;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
background: #f8fafc;
|
||
color: #6b7280;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.wallet-tabs :deep(.v-tab--selected) {
|
||
background: #5b5bd6;
|
||
color: #fff;
|
||
border-color: #5b5bd6;
|
||
}
|
||
|
||
.wallet-tabs :deep(.v-tab__slider) {
|
||
display: none;
|
||
}
|
||
|
||
.toolbar {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.search-field {
|
||
max-width: 280px;
|
||
}
|
||
|
||
.search-field :deep(.v-field) {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.filter-btn {
|
||
text-transform: none;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.table-card {
|
||
border: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.wallet-design-card {
|
||
border: 0;
|
||
box-shadow: none;
|
||
padding: 0;
|
||
}
|
||
|
||
.design-pos-card,
|
||
.design-order-card,
|
||
.design-trade-card,
|
||
.design-funding-card,
|
||
.design-withdraw-card {
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 16px;
|
||
padding: 14px;
|
||
background: #fff;
|
||
}
|
||
|
||
.positions-mobile-list .position-mobile-card.design-pos-card,
|
||
.orders-mobile-list .order-mobile-card.design-order-card,
|
||
.history-mobile-list .history-mobile-card.design-trade-card,
|
||
.history-mobile-list .history-mobile-card.design-funding-card,
|
||
.withdrawals-mobile-list .withdrawal-mobile-card.design-withdraw-card {
|
||
padding: 14px;
|
||
margin-bottom: 12px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
background: #fff;
|
||
cursor: default;
|
||
transition: none;
|
||
}
|
||
|
||
.positions-mobile-list .position-mobile-card.design-pos-card:last-child,
|
||
.orders-mobile-list .order-mobile-card.design-order-card:last-child,
|
||
.history-mobile-list .history-mobile-card:last-child,
|
||
.withdrawals-mobile-list .withdrawal-mobile-card:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.positions-mobile-list .position-mobile-card.design-pos-card:hover,
|
||
.orders-mobile-list .order-mobile-card.design-order-card:hover,
|
||
.history-mobile-list .history-mobile-card.design-trade-card:hover,
|
||
.history-mobile-list .history-mobile-card.design-funding-card:hover,
|
||
.withdrawals-mobile-list .withdrawal-mobile-card.design-withdraw-card:hover {
|
||
background: #fff;
|
||
}
|
||
|
||
.design-pos-top,
|
||
.design-order-top,
|
||
.design-history-top,
|
||
.design-withdraw-top,
|
||
.design-withdraw-mid {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
}
|
||
|
||
.design-pos-left,
|
||
.design-order-left,
|
||
.design-history-left,
|
||
.design-funding-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.design-order-top {
|
||
display: grid;
|
||
grid-template-columns: 44px minmax(0, 1fr) 32px;
|
||
column-gap: 12px;
|
||
align-items: start;
|
||
}
|
||
|
||
.design-pos-title-col,
|
||
.design-order-tags,
|
||
.design-pos-tags,
|
||
.design-history-bottom,
|
||
.design-withdraw-tags {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.design-order-title-col {
|
||
grid-column: 2;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.design-pos-right {
|
||
text-align: right;
|
||
flex-shrink: 0;
|
||
max-width: 45%;
|
||
}
|
||
|
||
.design-pos-card .position-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.design-pos-card .position-mobile-title {
|
||
margin-bottom: 0;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.design-pos-card .design-pos-title-col {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
width: 0;
|
||
}
|
||
|
||
.marquee-container {
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
width: 100%;
|
||
}
|
||
|
||
.marquee-track {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
min-width: max-content;
|
||
gap: 40px;
|
||
animation: wallet-title-marquee 10s linear infinite;
|
||
}
|
||
|
||
.marquee-track > span {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
@keyframes wallet-title-marquee {
|
||
0% {
|
||
transform: translateX(0);
|
||
}
|
||
100% {
|
||
transform: translateX(calc(-50% - 20px));
|
||
}
|
||
}
|
||
|
||
.design-pos-card .position-value {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.design-pos-card .position-value-change {
|
||
font-size: 13px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.order-title-marquee {
|
||
width: 100%;
|
||
}
|
||
|
||
.order-title-marquee .marquee-track {
|
||
animation-duration: 9s;
|
||
}
|
||
|
||
.history-title-marquee {
|
||
width: 100%;
|
||
}
|
||
|
||
.history-title-marquee .marquee-track {
|
||
animation-duration: 9s;
|
||
}
|
||
|
||
.design-order-card .design-order-tags {
|
||
flex-wrap: nowrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.design-order-card .design-order-stats {
|
||
margin-top: 8px;
|
||
padding-top: 4px;
|
||
width: 100%;
|
||
}
|
||
|
||
.order-value-primary {
|
||
color: #5b5bd6;
|
||
}
|
||
|
||
.order-value-secondary {
|
||
color: #6b7280;
|
||
}
|
||
|
||
.design-pos-stats,
|
||
.design-order-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
margin-top: 4px;
|
||
padding-top: 4px;
|
||
}
|
||
|
||
.design-pos-stat,
|
||
.design-order-stat,
|
||
.design-withdraw-col {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.design-pos-stat-label,
|
||
.design-order-stat-label,
|
||
.design-withdraw-label {
|
||
font-size: 11px;
|
||
color: #9ca3af;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.design-pos-stat-value,
|
||
.design-order-stat-value,
|
||
.design-withdraw-value {
|
||
font-size: 13px;
|
||
color: #111827;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.order-side-pill,
|
||
.order-outcome-pill,
|
||
.withdraw-chain-pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.order-side-pill {
|
||
height: 22px;
|
||
border-radius: 6px;
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.order-outcome-pill {
|
||
height: 20px;
|
||
border-radius: 999px;
|
||
padding: 0 8px;
|
||
}
|
||
|
||
.withdraw-chain-pill {
|
||
height: 22px;
|
||
border-radius: 8px;
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.side-buy {
|
||
color: #2563eb;
|
||
border: 1.5px solid #2563eb;
|
||
background: transparent;
|
||
}
|
||
|
||
.side-sell {
|
||
color: #dc2626;
|
||
border: 1.5px solid #dc2626;
|
||
background: transparent;
|
||
}
|
||
|
||
.outcome-yes {
|
||
color: #16a34a;
|
||
background: #dcfce7;
|
||
}
|
||
|
||
.outcome-no {
|
||
color: #dc2626;
|
||
background: #fee2e2;
|
||
}
|
||
|
||
.design-history-bottom {
|
||
justify-content: space-between;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.history-mobile-card.design-trade-card,
|
||
.history-mobile-card.design-funding-card {
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 16px;
|
||
padding: 14px;
|
||
background: #fff;
|
||
}
|
||
|
||
.history-mobile-card.design-trade-card .history-mobile-icon,
|
||
.history-mobile-card.design-funding-card .history-mobile-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.history-mobile-card.design-trade-card .history-mobile-title,
|
||
.history-mobile-card.design-funding-card .history-mobile-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.history-mobile-card.design-trade-card .history-mobile-activity,
|
||
.history-mobile-card.design-funding-card .history-mobile-activity {
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.history-mobile-card.design-trade-card .history-mobile-pl {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.history-mobile-card.design-funding-card .history-mobile-pl {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.history-mobile-card.design-funding-card {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.history-tag-sell {
|
||
color: #2563eb;
|
||
border: 1.5px solid #2563eb;
|
||
background: transparent;
|
||
}
|
||
|
||
.history-tag-buy {
|
||
color: #dc2626;
|
||
border: 1.5px solid #dc2626;
|
||
background: transparent;
|
||
}
|
||
|
||
.funding-icon-deposit {
|
||
background: #dcfce7;
|
||
color: #16a34a;
|
||
}
|
||
|
||
.funding-icon-withdraw {
|
||
background: #fee2e2;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.design-history-meta {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.design-funding-card {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.withdraw-chain-pill {
|
||
background: #f8fafc;
|
||
color: #334155;
|
||
}
|
||
|
||
.design-withdraw-date {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
.design-withdraw-mid {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.design-withdraw-col.right {
|
||
text-align: right;
|
||
}
|
||
|
||
.design-withdraw-bottom {
|
||
margin-top: 8px;
|
||
padding-top: 6px;
|
||
}
|
||
|
||
.positions-table,
|
||
.wallet-table {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.wallet-table th,
|
||
.positions-table th {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #6b7280;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.02em;
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.th-icon {
|
||
vertical-align: middle;
|
||
margin-left: 2px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.positions-table td,
|
||
.wallet-table td {
|
||
padding: 12px 16px;
|
||
color: #374151;
|
||
}
|
||
|
||
.cell-market {
|
||
max-width: 320px;
|
||
word-break: break-word;
|
||
}
|
||
|
||
/* 持仓行:市场图标 + 标题 + 标签 + 份额 */
|
||
.position-market-cell {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
}
|
||
|
||
.position-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
color: #fff;
|
||
font-weight: 700;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.position-icon-btc {
|
||
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
|
||
}
|
||
|
||
.position-icon-eth {
|
||
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
|
||
}
|
||
|
||
.position-icon-svg {
|
||
color: inherit;
|
||
}
|
||
|
||
.position-icon-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
border-radius: inherit;
|
||
}
|
||
|
||
.position-market-info {
|
||
min-width: 0;
|
||
}
|
||
|
||
.position-market-title {
|
||
display: block;
|
||
font-weight: 500;
|
||
color: #111827;
|
||
margin-bottom: 6px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.position-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.position-outcome-pill {
|
||
font-size: 12px;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.pill-down {
|
||
background-color: #fee2e2;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.pill-yes {
|
||
background-color: #dcfce7;
|
||
color: #059669;
|
||
}
|
||
|
||
.position-shares {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.cell-avg-now {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.cell-value {
|
||
vertical-align: top;
|
||
}
|
||
|
||
.position-value {
|
||
font-weight: 500;
|
||
color: #111827;
|
||
}
|
||
|
||
.position-value-change {
|
||
font-size: 12px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.value-loss {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.value-gain {
|
||
color: #059669;
|
||
}
|
||
|
||
.cell-actions {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.position-row .cell-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 4px;
|
||
}
|
||
|
||
.position-sell-btn {
|
||
text-transform: none;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.position-share-btn {
|
||
color: #6b7280;
|
||
}
|
||
|
||
/* 移动端持仓可折叠列表 */
|
||
.positions-mobile-list {
|
||
padding: 0;
|
||
}
|
||
|
||
.positions-mobile-list .empty-cell {
|
||
padding: 48px 16px;
|
||
text-align: center;
|
||
color: #6b7280;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.position-mobile-card {
|
||
border-bottom: 1px solid #f3f4f6;
|
||
padding: 14px 16px;
|
||
cursor: pointer;
|
||
transition: background-color 0.15s ease;
|
||
}
|
||
|
||
.position-mobile-card:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.position-mobile-card:hover {
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.position-mobile-card.expanded {
|
||
background-color: #f9fafb;
|
||
}
|
||
|
||
.position-mobile-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
}
|
||
|
||
.position-mobile-card .position-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.position-mobile-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.position-mobile-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #111827;
|
||
line-height: 1.35;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.position-mobile-sub {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.position-mobile-right {
|
||
text-align: right;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.position-mobile-right .position-value {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
.position-mobile-right .position-value-change {
|
||
font-size: 12px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.position-mobile-expanded {
|
||
margin-top: 14px;
|
||
padding-top: 14px;
|
||
border-top: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.position-mobile-avg-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.avg-now-label {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.avg-now-values {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.avg-now-arrow {
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.position-mobile-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.position-mobile-actions .position-sell-btn {
|
||
text-transform: none;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 移动端挂单列表 */
|
||
.orders-mobile-list {
|
||
padding: 0;
|
||
}
|
||
|
||
.orders-mobile-list .empty-cell {
|
||
padding: 48px 16px;
|
||
text-align: center;
|
||
color: #6b7280;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.order-mobile-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.order-mobile-card.design-order-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 10px;
|
||
}
|
||
|
||
.order-mobile-card:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.order-mobile-icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
color: #fff;
|
||
font-weight: 700;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.order-mobile-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.order-mobile-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
line-height: 1.35;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.order-mobile-action {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.order-mobile-price {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.order-mobile-right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 4px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.order-cancel-icon {
|
||
grid-column: 3;
|
||
justify-self: end;
|
||
align-self: start;
|
||
width: 32px;
|
||
height: 32px;
|
||
min-width: 32px;
|
||
border-radius: 999px;
|
||
border: 1px solid #e5e7eb;
|
||
background: #f8fafc;
|
||
color: #6b7280;
|
||
padding: 0;
|
||
}
|
||
|
||
.order-mobile-filled {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #111827;
|
||
}
|
||
|
||
.order-mobile-expiry {
|
||
font-size: 11px;
|
||
color: #9ca3af;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-btn-cancel {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.filter-btn-cancel .v-icon {
|
||
color: inherit;
|
||
}
|
||
|
||
/* 移动端历史列表 */
|
||
.history-mobile-list {
|
||
padding: 0;
|
||
}
|
||
|
||
.history-mobile-list .empty-cell {
|
||
padding: 48px 16px;
|
||
text-align: center;
|
||
color: #6b7280;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.history-mobile-card {
|
||
border-bottom: 1px solid #f3f4f6;
|
||
padding: 14px 16px;
|
||
cursor: pointer;
|
||
transition: background-color 0.15s ease;
|
||
}
|
||
|
||
.history-mobile-card:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.history-mobile-card:hover {
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.history-mobile-card.expanded {
|
||
background-color: #f9fafb;
|
||
}
|
||
|
||
.history-mobile-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
}
|
||
|
||
.history-mobile-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
color: #fff;
|
||
font-weight: 700;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.history-mobile-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.history-mobile-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #111827;
|
||
line-height: 1.35;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.history-mobile-activity {
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.history-mobile-right {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 2px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.history-mobile-pl {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.pl-gain {
|
||
color: #059669;
|
||
}
|
||
|
||
.pl-loss {
|
||
color: #dc2626;
|
||
}
|
||
|
||
.history-mobile-time {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.history-chevron {
|
||
color: #9ca3af;
|
||
flex-shrink: 0;
|
||
align-self: center;
|
||
}
|
||
|
||
.history-mobile-expanded {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.history-mobile-detail-row {
|
||
display: flex;
|
||
gap: 16px;
|
||
font-size: 13px;
|
||
color: #6b7280;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.history-detail-label {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.history-mobile-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.history-view-btn {
|
||
text-transform: none;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.filter-btn-close-losses {
|
||
color: #6b7280;
|
||
}
|
||
|
||
.side-yes {
|
||
color: #059669;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.side-no {
|
||
color: #dc2626;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.empty-cell {
|
||
text-align: center;
|
||
color: #6b7280;
|
||
padding: 48px 16px !important;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 提现记录 */
|
||
.withdrawals-mobile-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
}
|
||
.withdrawal-mobile-card {
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
background: #f9fafb;
|
||
border: 1px solid #e5e7eb;
|
||
}
|
||
.withdrawal-mobile-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
.withdrawal-mobile-main {
|
||
min-width: 0;
|
||
}
|
||
.withdrawal-mobile-amount {
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
color: #111827;
|
||
}
|
||
.withdrawal-mobile-meta {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
margin-top: 4px;
|
||
}
|
||
.withdrawal-mobile-reason {
|
||
font-size: 12px;
|
||
color: #dc2626;
|
||
margin-top: 8px;
|
||
}
|
||
.withdrawal-status-pill {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
flex-shrink: 0;
|
||
}
|
||
.withdrawal-status-pill.status-pending {
|
||
background: #fef3c7;
|
||
color: #b45309;
|
||
}
|
||
.withdrawal-status-pill.status-success {
|
||
background: #dcfce7;
|
||
color: #166534;
|
||
}
|
||
.withdrawal-status-pill.status-rejected {
|
||
background: #fee2e2;
|
||
color: #991b1b;
|
||
}
|
||
.withdrawal-status-pill.status-failed {
|
||
background: #fee2e2;
|
||
color: #991b1b;
|
||
}
|
||
.withdrawal-reason {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
margin-top: 4px;
|
||
}
|
||
.withdrawal-mobile-address {
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
font-family: monospace;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* 1:1 overrides for UjOKn withdraw cards */
|
||
.withdrawal-mobile-card.design-withdraw-card {
|
||
background: #fff;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 16px;
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.withdrawal-mobile-card.design-withdraw-card .design-withdraw-top {
|
||
align-items: flex-start;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.withdrawal-mobile-card.design-withdraw-card .design-withdraw-head {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.withdrawal-mobile-card.design-withdraw-card .design-withdraw-date {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.withdrawal-mobile-card.design-withdraw-card .design-withdraw-chain {
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.withdrawal-mobile-card.design-withdraw-card .design-withdraw-mid {
|
||
margin-top: 4px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.withdrawal-mobile-card.design-withdraw-card .design-withdraw-value {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
.withdrawal-mobile-card.design-withdraw-card .design-withdraw-label {
|
||
font-size: 10px;
|
||
font-weight: 500;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.withdrawal-mobile-card.design-withdraw-card .withdrawal-mobile-address {
|
||
margin-top: 0;
|
||
font-family: Inter, sans-serif;
|
||
font-size: 13px;
|
||
color: #111827;
|
||
}
|
||
|
||
.withdrawal-mobile-card.design-withdraw-card .withdrawal-status-pill {
|
||
height: 26px;
|
||
padding: 0 12px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.wallet-design-card .withdrawals-mobile-list {
|
||
padding: 0;
|
||
gap: 10px;
|
||
}
|
||
.cell-address {
|
||
font-family: monospace;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.pagination-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
border-top: 1px solid #e5e7eb;
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.pagination-info {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.pagination-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.page-size-select {
|
||
min-width: 88px;
|
||
width: 88px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.page-size-select :deep(.v-field) {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.page-size-select :deep(.v-field__input) {
|
||
min-width: 2ch;
|
||
}
|
||
|
||
.pagination :deep(.v-pagination__item),
|
||
.pagination :deep(.v-pagination__prev),
|
||
.pagination :deep(.v-pagination__next) {
|
||
min-width: 32px;
|
||
height: 32px;
|
||
}
|
||
|
||
/* Sell position dialog */
|
||
.sell-dialog-card {
|
||
padding: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sell-dialog-header {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 12px 16px 0;
|
||
}
|
||
|
||
.sell-dialog-close {
|
||
color: #6b7280;
|
||
}
|
||
|
||
.sell-dialog-body {
|
||
padding: 0 24px 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
.sell-dialog-icon-wrap {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.sell-dialog-body .position-icon {
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.sell-dialog-title {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
margin: 0 0 8px;
|
||
}
|
||
|
||
.sell-dialog-market {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
line-height: 1.4;
|
||
margin: 0 0 20px;
|
||
}
|
||
|
||
.sell-receive-box {
|
||
background-color: #dcfce7;
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
text-align: left;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.sell-receive-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #166534;
|
||
}
|
||
|
||
.sell-receive-value {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #166534;
|
||
}
|
||
|
||
.sell-dialog-actions {
|
||
flex-direction: column;
|
||
padding: 8px 24px 24px;
|
||
gap: 12px;
|
||
}
|
||
|
||
.sell-redeem-btn {
|
||
text-transform: none;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sell-edit-link {
|
||
font-size: 14px;
|
||
color: #2563eb;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.sell-edit-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
</style>
|