2415 lines
67 KiB
Vue
2415 lines
67 KiB
Vue
<template>
|
||
<v-container class="wallet-container">
|
||
<!-- 顶部:Portfolio + Profit/Loss 卡片 -->
|
||
<v-row class="wallet-cards">
|
||
<v-col cols="12" md="6">
|
||
<v-card class="wallet-card portfolio-card" elevation="0" rounded="lg">
|
||
<div class="card-header">
|
||
<span class="card-title">
|
||
{{ t('wallet.portfolio') }}
|
||
<v-icon size="16" class="title-icon">mdi-eye-off-outline</v-icon>
|
||
</span>
|
||
<div class="balance-badge">
|
||
<v-icon size="14">mdi-sack</v-icon>
|
||
<span>${{ portfolioBalance }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="card-value">${{ portfolioBalance }}</div>
|
||
<div class="card-timeframe">{{ t('wallet.today') }}</div>
|
||
<div class="card-actions">
|
||
<v-btn
|
||
color="primary"
|
||
variant="flat"
|
||
class="action-btn"
|
||
prepend-icon="mdi-arrow-down"
|
||
@click="depositDialogOpen = true"
|
||
>
|
||
{{ t('wallet.deposit') }}
|
||
</v-btn>
|
||
<v-btn
|
||
variant="outlined"
|
||
color="grey"
|
||
class="action-btn"
|
||
prepend-icon="mdi-arrow-up"
|
||
@click="withdrawDialogOpen = true"
|
||
>
|
||
{{ t('wallet.withdraw') }}
|
||
</v-btn>
|
||
<!-- <v-btn
|
||
variant="outlined"
|
||
color="grey"
|
||
class="action-btn"
|
||
prepend-icon="mdi-shield-check-outline"
|
||
@click="onAuthorizeClick"
|
||
>
|
||
{{ t('wallet.authorize') }}
|
||
</v-btn> -->
|
||
</div>
|
||
</v-card>
|
||
</v-col>
|
||
<v-col cols="12" md="6">
|
||
<v-card class="wallet-card pl-card" elevation="0" rounded="lg">
|
||
<div class="card-header">
|
||
<span class="card-title">
|
||
<v-icon size="16" color="success">mdi-triangle-small-up</v-icon>
|
||
{{ t('wallet.profitLoss') }}
|
||
</span>
|
||
<div class="pl-tabs">
|
||
<v-btn
|
||
v-for="tr in plTimeRanges"
|
||
:key="tr.value"
|
||
:variant="plRange === tr.value ? 'flat' : 'text'"
|
||
:color="plRange === tr.value ? 'primary' : undefined"
|
||
size="small"
|
||
class="pl-tab"
|
||
@click="plRange = tr.value"
|
||
>
|
||
{{ tr.label }}
|
||
</v-btn>
|
||
</div>
|
||
</div>
|
||
<div class="card-value-row">
|
||
<span class="card-value">${{ profitLoss }}</span>
|
||
<v-icon size="18" class="info-icon">mdi-information-outline</v-icon>
|
||
</div>
|
||
<div class="card-timeframe">{{ t('wallet.allTime') }}</div>
|
||
<div ref="plChartRef" class="pl-chart"></div>
|
||
</v-card>
|
||
</v-col>
|
||
</v-row>
|
||
|
||
<!-- 未结算汇总:单条展示,多条用 +N,结算按钮不显示图标 -->
|
||
<v-row v-if="unsettledCount > 0" class="wallet-settlement-row">
|
||
<v-col cols="12">
|
||
<v-card class="settlement-card" 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"
|
||
:loading="claimLoading"
|
||
:disabled="claimLoading"
|
||
@click="onClaimSettlement"
|
||
>
|
||
{{ t('wallet.claim') }}
|
||
</v-btn>
|
||
</div>
|
||
</v-card>
|
||
</v-col>
|
||
</v-row>
|
||
|
||
<!-- 下方: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>
|
||
<div class="toolbar">
|
||
<v-text-field
|
||
v-model="search"
|
||
:placeholder="t('wallet.searchPlaceholder')"
|
||
density="compact"
|
||
hide-details
|
||
variant="outlined"
|
||
rounded
|
||
class="search-field"
|
||
prepend-inner-icon="mdi-magnify"
|
||
/>
|
||
<template v-if="activeTab === 'history'">
|
||
<v-btn
|
||
v-if="mobile"
|
||
variant="outlined"
|
||
size="small"
|
||
class="filter-btn filter-btn-close-losses"
|
||
@click="closeLosses"
|
||
>
|
||
<v-icon size="18">mdi-delete-outline</v-icon>
|
||
{{ t('wallet.closeLosses') }}
|
||
</v-btn>
|
||
<v-btn variant="outlined" size="small" class="filter-btn">
|
||
<v-icon size="18">mdi-filter</v-icon>
|
||
{{ t('wallet.all') }}
|
||
</v-btn>
|
||
<v-btn variant="outlined" size="small" class="filter-btn">
|
||
<v-icon size="18">mdi-sort</v-icon>
|
||
{{ t('wallet.newest') }}
|
||
</v-btn>
|
||
<v-btn variant="outlined" size="small" class="filter-btn" icon>
|
||
<v-icon size="18">mdi-calendar</v-icon>
|
||
</v-btn>
|
||
<v-btn variant="outlined" size="small" class="filter-btn">
|
||
<v-icon size="18">mdi-download</v-icon>
|
||
{{ t('wallet.export') }}
|
||
</v-btn>
|
||
</template>
|
||
<template v-else-if="activeTab === 'positions'">
|
||
<v-btn variant="outlined" size="small" class="filter-btn">
|
||
<v-icon size="18">mdi-filter</v-icon>
|
||
{{ t('wallet.currentValue') }}
|
||
</v-btn>
|
||
</template>
|
||
<template v-else-if="activeTab === 'withdrawals'">
|
||
<v-btn
|
||
v-for="opt in withdrawStatusOptions"
|
||
:key="opt.value"
|
||
:variant="withdrawStatusFilter === opt.value ? 'flat' : 'outlined'"
|
||
:color="withdrawStatusFilter === opt.value ? 'primary' : undefined"
|
||
size="small"
|
||
class="filter-btn"
|
||
@click="withdrawStatusFilter = opt.value"
|
||
>
|
||
{{ opt.label }}
|
||
</v-btn>
|
||
</template>
|
||
<template v-else-if="activeTab === 'orders'">
|
||
<v-btn variant="outlined" size="small" class="filter-btn">
|
||
<v-icon size="18">mdi-filter</v-icon>
|
||
{{ t('wallet.market') }}
|
||
</v-btn>
|
||
<v-btn
|
||
variant="outlined"
|
||
size="small"
|
||
class="filter-btn filter-btn-cancel"
|
||
@click="cancelAllOrders"
|
||
>
|
||
<v-icon size="18">mdi-close</v-icon>
|
||
{{ t('wallet.cancelAll') }}
|
||
</v-btn>
|
||
</template>
|
||
<v-btn v-else variant="outlined" size="small" class="filter-btn">
|
||
<v-icon size="18">mdi-filter</v-icon>
|
||
{{ t('wallet.market') }}
|
||
</v-btn>
|
||
</div>
|
||
<v-card class="table-card" elevation="0" rounded="lg">
|
||
<!-- 持仓:桌面端表格 / 移动端可折叠列表 -->
|
||
<template v-if="activeTab === 'positions'">
|
||
<!-- 移动端:可折叠列表 -->
|
||
<div v-if="mobile" class="positions-mobile-list">
|
||
<template v-if="positionLoading">
|
||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
||
</template>
|
||
<template v-else-if="filteredPositions.length === 0">
|
||
<div class="empty-cell">{{ t('wallet.noPositionsFound') }}</div>
|
||
</template>
|
||
<div
|
||
v-for="pos in paginatedPositions"
|
||
:key="pos.id"
|
||
class="position-mobile-card"
|
||
:class="{ expanded: expandedPositionId === pos.id }"
|
||
@click="togglePositionExpanded(pos.id)"
|
||
>
|
||
<div class="position-mobile-row">
|
||
<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="position-mobile-main">
|
||
<div class="position-mobile-title">{{ pos.market }}</div>
|
||
<div class="position-mobile-sub">
|
||
{{ pos.bet }} on {{ pos.outcomeWord || pos.sellOutcome || 'Position' }} to win
|
||
{{ pos.toWin }}
|
||
</div>
|
||
</div>
|
||
<div class="position-mobile-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>
|
||
<!-- 展开内容:AVG • NOW + 操作按钮 -->
|
||
<div
|
||
v-show="expandedPositionId === pos.id"
|
||
class="position-mobile-expanded"
|
||
@click.stop
|
||
>
|
||
<div class="position-mobile-avg-row">
|
||
<span class="avg-now-label">AVG • NOW</span>
|
||
<span class="avg-now-values">
|
||
<template v-if="parseAvgNow(pos.avgNow)[1]">
|
||
{{ parseAvgNow(pos.avgNow)[0] }}
|
||
<v-icon
|
||
v-if="pos.valueChangeLoss"
|
||
size="14"
|
||
color="error"
|
||
class="avg-now-arrow"
|
||
>mdi-chevron-down</v-icon
|
||
>
|
||
<v-icon v-else size="14" color="success" class="avg-now-arrow"
|
||
>mdi-chevron-up</v-icon
|
||
>
|
||
{{ parseAvgNow(pos.avgNow)[1] }}
|
||
</template>
|
||
<template v-else>{{ pos.avgNow }}</template>
|
||
</span>
|
||
</div>
|
||
<div class="position-mobile-actions">
|
||
<v-btn
|
||
color="primary"
|
||
variant="flat"
|
||
size="small"
|
||
class="position-sell-btn"
|
||
@click="sellPosition(pos.id)"
|
||
>{{ t('trade.sell') }}</v-btn
|
||
>
|
||
<v-btn
|
||
icon
|
||
variant="text"
|
||
size="small"
|
||
class="position-share-btn"
|
||
@click="sharePosition(pos.id)"
|
||
>
|
||
<v-icon size="18">mdi-share-variant</v-icon>
|
||
</v-btn>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 桌面端:表格 -->
|
||
<v-table v-else class="wallet-table positions-table-full">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-left">{{ t('wallet.market') }}</th>
|
||
<th class="text-left">
|
||
{{ t('wallet.avgNow') }}
|
||
<v-icon size="14" class="th-icon">mdi-information-outline</v-icon>
|
||
</th>
|
||
<th class="text-left">{{ t('wallet.bet') }}</th>
|
||
<th class="text-left">{{ t('wallet.toWin') }}</th>
|
||
<th class="text-left">
|
||
{{ t('wallet.value') }}
|
||
<v-icon size="14" class="th-icon">mdi-chevron-down</v-icon>
|
||
</th>
|
||
<th class="text-right">{{ t('wallet.action') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-if="positionLoading">
|
||
<td colspan="6" class="empty-cell">{{ t('common.loading') }}</td>
|
||
</tr>
|
||
<tr v-else-if="filteredPositions.length === 0">
|
||
<td colspan="6" class="empty-cell">{{ t('wallet.noPositionsFound') }}</td>
|
||
</tr>
|
||
<tr v-for="pos in paginatedPositions" :key="pos.id" class="position-row">
|
||
<td class="cell-market">
|
||
<div class="position-market-cell">
|
||
<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="position-market-info">
|
||
<span class="position-market-title">{{ pos.market }}</span>
|
||
<div class="position-meta">
|
||
<span
|
||
v-if="pos.outcomeTag"
|
||
class="position-outcome-pill"
|
||
:class="pos.outcomePillClass"
|
||
>{{ pos.outcomeTag }}</span
|
||
>
|
||
<span class="position-shares">{{ pos.shares }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td class="cell-avg-now">{{ pos.avgNow }}</td>
|
||
<td>{{ pos.bet }}</td>
|
||
<td>{{ pos.toWin }}</td>
|
||
<td class="cell-value">
|
||
<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>
|
||
</td>
|
||
<td class="text-right cell-actions">
|
||
<v-btn
|
||
color="primary"
|
||
variant="flat"
|
||
size="small"
|
||
class="position-sell-btn"
|
||
@click="sellPosition(pos.id)"
|
||
>{{ t('trade.sell') }}</v-btn
|
||
>
|
||
<v-btn
|
||
icon
|
||
variant="text"
|
||
size="small"
|
||
class="position-share-btn"
|
||
@click="sharePosition(pos.id)"
|
||
>
|
||
<v-icon size="18">mdi-share-variant</v-icon>
|
||
</v-btn>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</v-table>
|
||
</template>
|
||
<!-- 未成交:移动端卡片列表 / 桌面端表格 -->
|
||
<template v-else-if="activeTab === 'orders'">
|
||
<!-- 移动端:挂单卡片列表 -->
|
||
<div v-if="mobile" class="orders-mobile-list">
|
||
<template v-if="openOrderLoading">
|
||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
||
</template>
|
||
<template v-else-if="filteredOpenOrders.length === 0">
|
||
<div class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</div>
|
||
</template>
|
||
<div v-for="ord in paginatedOpenOrders" :key="ord.id" class="order-mobile-card">
|
||
<div class="order-mobile-icon" :class="ord.iconClass">
|
||
<span class="position-icon-char">{{ ord.iconChar }}</span>
|
||
</div>
|
||
<div class="order-mobile-main">
|
||
<div class="order-mobile-title">{{ ord.market }}</div>
|
||
<div
|
||
class="order-mobile-action"
|
||
:class="ord.side === 'Yes' ? 'side-yes' : 'side-no'"
|
||
>
|
||
{{ ord.actionLabel || `Buy ${ord.outcome}` }}
|
||
</div>
|
||
<div class="order-mobile-price">{{ ord.price }} • {{ ord.total }}</div>
|
||
</div>
|
||
<div class="order-mobile-right">
|
||
<v-btn
|
||
icon
|
||
variant="text"
|
||
size="small"
|
||
class="order-cancel-icon"
|
||
color="error"
|
||
:disabled="cancelOrderLoading || ord.fullyFilled"
|
||
@click.stop="cancelOrder(ord)"
|
||
>
|
||
<v-icon size="20">mdi-close</v-icon>
|
||
</v-btn>
|
||
<div class="order-mobile-filled">{{ ord.filledDisplay || ord.filled }}</div>
|
||
<div class="order-mobile-expiry">{{ t('wallet.expirationLabel') }} {{ ord.expiration }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 桌面端:表格 -->
|
||
<v-table v-else class="wallet-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-left">{{ t('wallet.market') }}</th>
|
||
<th class="text-left">{{ t('wallet.side') }}</th>
|
||
<th class="text-left">{{ t('wallet.outcome') }}</th>
|
||
<th class="text-left">{{ t('wallet.price') }}</th>
|
||
<th class="text-left">{{ t('wallet.filled') }}</th>
|
||
<th class="text-left">{{ t('wallet.total') }}</th>
|
||
<th class="text-left">{{ t('wallet.expiration') }}</th>
|
||
<th class="text-right">{{ t('wallet.action') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-if="openOrderLoading">
|
||
<td colspan="8" class="empty-cell">{{ t('common.loading') }}</td>
|
||
</tr>
|
||
<tr v-else-if="filteredOpenOrders.length === 0">
|
||
<td colspan="8" class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</td>
|
||
</tr>
|
||
<tr v-for="ord in paginatedOpenOrders" :key="ord.id">
|
||
<td class="cell-market">{{ ord.market }}</td>
|
||
<td>
|
||
<span :class="ord.side === 'Yes' ? 'side-yes' : 'side-no'">{{ ord.side }}</span>
|
||
</td>
|
||
<td>{{ ord.outcome }}</td>
|
||
<td>{{ ord.price }}</td>
|
||
<td>{{ ord.filled }}</td>
|
||
<td>{{ ord.total }}</td>
|
||
<td>{{ ord.expiration }}</td>
|
||
<td class="text-right">
|
||
<v-btn
|
||
variant="text"
|
||
size="small"
|
||
color="error"
|
||
:disabled="cancelOrderLoading || ord.fullyFilled"
|
||
@click="cancelOrder(ord)"
|
||
>Cancel</v-btn
|
||
>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</v-table>
|
||
</template>
|
||
<!-- 历史记录:移动端可展开列表 / 桌面端表格 -->
|
||
<template v-else-if="activeTab === 'history'">
|
||
<!-- 移动端:历史卡片列表 -->
|
||
<div v-if="mobile" class="history-mobile-list">
|
||
<template v-if="historyLoading">
|
||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
||
</template>
|
||
<template v-else-if="filteredHistory.length === 0">
|
||
<div class="empty-cell">{{ t('wallet.noHistoryFound') }}</div>
|
||
</template>
|
||
<div
|
||
v-for="h in paginatedHistory"
|
||
:key="h.id"
|
||
class="history-mobile-card"
|
||
:class="{ expanded: expandedHistoryId === h.id }"
|
||
@click="toggleHistoryExpanded(h.id)"
|
||
>
|
||
<div class="history-mobile-row">
|
||
<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">{{ h.market }}</div>
|
||
<div class="history-mobile-activity">{{ h.activityDetail || h.activity }}</div>
|
||
</div>
|
||
<div class="history-mobile-right">
|
||
<span
|
||
:class="['history-mobile-pl', h.profitLossNegative ? 'pl-loss' : 'pl-gain']"
|
||
>
|
||
{{ h.profitLoss ?? h.value }}
|
||
</span>
|
||
<div class="history-mobile-time">{{ h.timeAgo || '' }}</div>
|
||
</div>
|
||
<v-icon size="20" class="history-chevron">
|
||
{{ expandedHistoryId === h.id ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
|
||
</v-icon>
|
||
</div>
|
||
<div v-show="expandedHistoryId === h.id" class="history-mobile-expanded" @click.stop>
|
||
<div class="history-mobile-detail-row">
|
||
<span v-if="h.avgPrice" class="history-detail-label">AVG {{ h.avgPrice }}</span>
|
||
<span v-if="h.shares" class="history-detail-label">SHARES {{ h.shares }}</span>
|
||
</div>
|
||
<div class="history-mobile-actions">
|
||
<v-btn
|
||
variant="outlined"
|
||
size="small"
|
||
class="history-view-btn"
|
||
@click="viewHistory(h.id)"
|
||
>{{ t('wallet.view') }}</v-btn
|
||
>
|
||
<v-btn
|
||
icon
|
||
variant="text"
|
||
size="small"
|
||
class="position-share-btn"
|
||
@click="shareHistory(h.id)"
|
||
>
|
||
<v-icon size="18">mdi-open-in-new</v-icon>
|
||
</v-btn>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 桌面端:表格 -->
|
||
<v-table v-else class="wallet-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-left">{{ t('wallet.activity') }}</th>
|
||
<th class="text-left">{{ t('wallet.market') }}</th>
|
||
<th class="text-left">{{ t('wallet.value') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-if="historyLoading">
|
||
<td colspan="3" class="empty-cell">{{ t('common.loading') }}</td>
|
||
</tr>
|
||
<tr v-else-if="filteredHistory.length === 0">
|
||
<td colspan="3" class="empty-cell">{{ t('wallet.noHistoryFound') }}</td>
|
||
</tr>
|
||
<tr v-for="h in paginatedHistory" :key="h.id">
|
||
<td>
|
||
<span :class="h.side === 'Yes' ? 'side-yes' : 'side-no'">{{ h.activity }}</span>
|
||
</td>
|
||
<td class="cell-market">{{ h.market }}</td>
|
||
<td>{{ h.value }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</v-table>
|
||
</template>
|
||
<!-- 提现记录:分页列表 -->
|
||
<template v-else-if="activeTab === 'withdrawals'">
|
||
<div v-if="mobile" class="withdrawals-mobile-list">
|
||
<template v-if="withdrawalsLoading">
|
||
<div class="empty-cell">{{ t('common.loading') }}</div>
|
||
</template>
|
||
<template v-else-if="withdrawalsList.length === 0">
|
||
<div class="empty-cell">{{ t('wallet.noWithdrawalsFound') }}</div>
|
||
</template>
|
||
<div
|
||
v-for="w in withdrawalsList"
|
||
:key="String(w.ID ?? w.requestNo ?? '')"
|
||
class="withdrawal-mobile-card"
|
||
>
|
||
<div class="withdrawal-mobile-row">
|
||
<div class="withdrawal-mobile-main">
|
||
<div class="withdrawal-mobile-amount">${{ formatWithdrawAmount(w.amount) }}</div>
|
||
<div class="withdrawal-mobile-meta">
|
||
{{ w.chain || '—' }} · {{ formatWithdrawTime(w.CreatedAt ?? w.createdAt) }}
|
||
</div>
|
||
<div class="withdrawal-mobile-address">{{ shortAddress(w.tokenAddress ?? w.walletAddress) }}</div>
|
||
</div>
|
||
<span :class="['withdrawal-status-pill', getWithdrawStatusClass(w.status)]">
|
||
{{ getWithdrawStatusLabel(w.status) }}
|
||
</span>
|
||
</div>
|
||
<div v-if="w.reason" class="withdrawal-mobile-reason">
|
||
{{ w.reason }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<v-table v-else class="wallet-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="text-left">{{ t('wallet.withdrawAmount') }}</th>
|
||
<th class="text-left">{{ t('wallet.withdrawStatus') }}</th>
|
||
<th class="text-left">{{ t('wallet.withdrawAddress') }}</th>
|
||
<th class="text-left">{{ t('wallet.withdrawChain') }}</th>
|
||
<th class="text-left">{{ t('wallet.withdrawTime') }}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-if="withdrawalsLoading">
|
||
<td colspan="5" class="empty-cell">{{ t('common.loading') }}</td>
|
||
</tr>
|
||
<tr v-else-if="withdrawalsList.length === 0">
|
||
<td colspan="5" class="empty-cell">{{ t('wallet.noWithdrawalsFound') }}</td>
|
||
</tr>
|
||
<tr v-for="w in withdrawalsList" :key="String(w.ID ?? w.requestNo ?? '')">
|
||
<td>${{ formatWithdrawAmount(w.amount) }}</td>
|
||
<td>
|
||
<span :class="['withdrawal-status-pill', getWithdrawStatusClass(w.status)]">
|
||
{{ getWithdrawStatusLabel(w.status) }}
|
||
</span>
|
||
<div v-if="w.reason" class="withdrawal-reason">
|
||
{{ w.reason }}
|
||
</div>
|
||
</td>
|
||
<td class="cell-address">{{ shortAddress(w.tokenAddress ?? w.walletAddress) }}</td>
|
||
<td>{{ w.chain || '—' }}</td>
|
||
<td>{{ formatWithdrawTime(w.CreatedAt ?? w.createdAt) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</v-table>
|
||
</template>
|
||
<!-- 分页 -->
|
||
<div v-if="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>
|
||
|
||
<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">Sell {{ sellPositionItem.sellOutcome || '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>Receive</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">
|
||
Redeem
|
||
</v-btn>
|
||
<a href="#" class="sell-edit-link" @click.prevent="editSellOrder">Edit order</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 * as echarts from 'echarts'
|
||
import type { ECharts } from 'echarts'
|
||
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
|
||
}
|
||
|
||
const positions = ref<Position[]>(
|
||
USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] : [],
|
||
)
|
||
/** 提现记录列表 */
|
||
const withdrawalsList = ref<SettlementRequestClientItem[]>([])
|
||
const withdrawalsTotal = ref(0)
|
||
const withdrawalsLoading = ref(false)
|
||
|
||
/** 持仓列表(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
|
||
}
|
||
}
|
||
|
||
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 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 || '取消失败'
|
||
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: ECharts | 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
|
||
}
|
||
|
||
function buildPlChartOption(chartData: [number, number][]) {
|
||
const lastPoint = chartData[chartData.length - 1]
|
||
const lastVal = lastPoint != null ? lastPoint[1] : 0
|
||
const lineColor = lastVal >= 0 ? '#059669' : '#dc2626'
|
||
return {
|
||
animation: false,
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: (params: unknown) => {
|
||
const p = (Array.isArray(params) ? params[0] : params) as {
|
||
name: string | number
|
||
value: unknown
|
||
}
|
||
const date = new Date(p.name as number)
|
||
const val = Array.isArray(p.value) ? (p.value as number[])[1] : p.value
|
||
const sign = Number(val) >= 0 ? '+' : ''
|
||
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}<br/>${sign}$${Number(val).toFixed(2)}`
|
||
},
|
||
axisPointer: { animation: false },
|
||
},
|
||
grid: { left: 8, right: 8, top: 8, bottom: 24, containLabel: false },
|
||
xAxis: {
|
||
type: 'time',
|
||
axisLine: { show: false },
|
||
axisTick: { show: false },
|
||
axisLabel: { color: '#9ca3af', fontSize: 10 },
|
||
splitLine: { show: false },
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
position: 'right',
|
||
axisLine: { show: false },
|
||
axisTick: { show: false },
|
||
axisLabel: { color: '#9ca3af', fontSize: 10, formatter: (v: number) => `$${v}` },
|
||
splitLine: { lineStyle: { type: 'dashed', color: '#e5e7eb' } },
|
||
},
|
||
series: [
|
||
{
|
||
type: 'line',
|
||
data: chartData,
|
||
smooth: true,
|
||
showSymbol: false,
|
||
lineStyle: { width: 2, color: lineColor },
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: lineColor + '40' },
|
||
{ offset: 1, color: lineColor + '08' },
|
||
]),
|
||
},
|
||
},
|
||
],
|
||
}
|
||
}
|
||
|
||
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 (plChartInstance)
|
||
plChartInstance.setOption(buildPlChartOption(plChartData.value), { replaceMerge: ['series'] })
|
||
}
|
||
|
||
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)
|
||
plChartInstance = echarts.init(plChartRef.value)
|
||
plChartInstance.setOption(buildPlChartOption(plChartData.value))
|
||
}
|
||
|
||
const handleResize = () => plChartInstance?.resize()
|
||
|
||
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?.dispose()
|
||
plChartInstance = 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 {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 24px 16px;
|
||
}
|
||
|
||
.wallet-cards {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.wallet-card {
|
||
padding: 20px;
|
||
border: 1px solid #e5e7eb;
|
||
height: 100%;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
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: 28px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.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: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.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 16px;
|
||
}
|
||
|
||
.settlement-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.settlement-label {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
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: 500;
|
||
min-width: 88px;
|
||
}
|
||
|
||
.wallet-section {
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.wallet-tabs {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.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: 1px solid #e5e7eb;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.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:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.order-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;
|
||
}
|
||
|
||
.order-mobile-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.order-mobile-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
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 {
|
||
min-width: 32px;
|
||
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;
|
||
}
|
||
.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>
|