新增:提现功能,提现记录
This commit is contained in:
parent
d377dd3523
commit
d571d1b9b0
81
docs/api/pmset.md
Normal file
81
docs/api/pmset.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# pmset.ts
|
||||||
|
|
||||||
|
**路径**:`src/api/pmset.ts`
|
||||||
|
|
||||||
|
## 功能用途
|
||||||
|
|
||||||
|
Polymarket 结算/提现相关 API:`withdrawByWallet` 用于客户端钱包提现申请,需钱包验签(SIWE)。
|
||||||
|
|
||||||
|
## 核心能力
|
||||||
|
|
||||||
|
- `withdrawByWallet`:POST /pmset/withdrawByWallet,提交提现申请,需钱包 personal_sign 验签
|
||||||
|
- `usdcToAmount`:将 USDC 显示金额转为 API 所需整数(6 位小数)
|
||||||
|
- `getSettlementRequestsList`:分页获取提现/结算请求列表(管理端),支持 status 筛选
|
||||||
|
- `getSettlementRequestsListClient`:客户端分页获取提现记录列表,支持 status 筛选(pending/success/rejected/failed)
|
||||||
|
|
||||||
|
## POST /pmset/withdrawByWallet
|
||||||
|
|
||||||
|
### 请求体(request.PmSettlementWithdrawByWallet)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| amount | number | 是 | 提现金额,6 位小数(1000000 = 1 USDC) |
|
||||||
|
| chain | string | 是 | 链标识,如 polygon、ethereum、arbitrum、optimism |
|
||||||
|
| message | string | 是 | SIWE 签名消息 |
|
||||||
|
| nonce | string | 是 | 随机 nonce |
|
||||||
|
| signature | string | 是 | 钱包签名 |
|
||||||
|
| tokenAddress | string | 是 | 出金地址(用户接收资金的地址) |
|
||||||
|
| tokenSymbol | string | 是 | 代币符号,默认 USDC |
|
||||||
|
| walletAddress | string | 是 | 钱包地址,取用户信息的 externalWalletAddress |
|
||||||
|
|
||||||
|
### 响应
|
||||||
|
|
||||||
|
`{ code, data, msg }`,`data` 为 `{ idempotencyKey?, requestNo? }`。
|
||||||
|
|
||||||
|
### 验签流程(与 Login.vue 一致)
|
||||||
|
|
||||||
|
1. 使用 `eth_requestAccounts` 获取钱包地址
|
||||||
|
2. 使用 `eth_chainId` 获取链 ID
|
||||||
|
3. 构建 SiweMessage(scheme、domain、address、statement、uri、version、chainId、nonce)
|
||||||
|
4. `personal_sign` 签名消息
|
||||||
|
5. POST 请求体包含 message、nonce、signature、walletAddress、amount、chain
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { withdrawByWallet, usdcToAmount } from '@/api/pmset'
|
||||||
|
|
||||||
|
const amount = usdcToAmount(1.5) // 1500000
|
||||||
|
const res = await withdrawByWallet(
|
||||||
|
{ amount, chain: 'polygon', message, nonce, signature, walletAddress },
|
||||||
|
{ headers: authHeaders },
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET /pmset/getPmSettlementRequestsListClient
|
||||||
|
|
||||||
|
客户端分页获取提现记录列表,需鉴权。钱包页面提现记录使用此接口。
|
||||||
|
|
||||||
|
### 响应 data 结构
|
||||||
|
|
||||||
|
`{ list, total, page, pageSize }`,list 项含:ID、CreatedAt、UpdatedAt、chain、amount、fee、status、reason、requestNo、tokenAddress。
|
||||||
|
|
||||||
|
## GET /pmset/getPmSettlementRequestsList
|
||||||
|
|
||||||
|
分页获取提现/结算请求列表(管理端),需鉴权。
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| page | number | 页码 |
|
||||||
|
| pageSize | number | 每页数量 |
|
||||||
|
| status | string | 状态筛选:pending、success、rejected、failed |
|
||||||
|
|
||||||
|
### 响应
|
||||||
|
|
||||||
|
`data` 为 `PageResult<SettlementRequestItem>`,list 项含 amount、requestNo、chain、status、createdAt、reason、payoutError 等。
|
||||||
|
|
||||||
|
## 扩展方式
|
||||||
|
|
||||||
|
- 新增其他 pmset 相关接口时,在 `src/api/pmset.ts` 中追加
|
||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 功能用途
|
## 功能用途
|
||||||
|
|
||||||
提现弹窗,支持输入金额、选择网络、选择提现目标(Connected wallet / Custom address)。
|
提现弹窗,支持输入金额、选择网络、选择提现目标(Connected wallet)。仅支持已连接钱包提现,需验签;`walletAddress` 取用户信息的 `externalWalletAddress`,`tokenAddress` 为出金地址(用户接收资金的地址)。
|
||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,8 @@
|
|||||||
|
|
||||||
- Portfolio 卡片:余额、Deposit/Withdraw 按钮
|
- Portfolio 卡片:余额、Deposit/Withdraw 按钮
|
||||||
- Profit/Loss 卡片:时间范围切换、ECharts 图表
|
- Profit/Loss 卡片:时间范围切换、ECharts 图表
|
||||||
- Tab:Positions、Open orders、History
|
- Tab:Positions、Open orders、History、Withdrawals(提现记录)
|
||||||
|
- Withdrawals:分页列表,状态筛选(全部/审核中/提现成功/审核不通过/提现失败),对接 GET /pmset/getPmSettlementRequestsListClient
|
||||||
- DepositDialog、WithdrawDialog 组件
|
- DepositDialog、WithdrawDialog 组件
|
||||||
- **401 权限错误**:取消订单等接口失败时,通过 `useAuthError().formatAuthError` 统一提示「请先登录」或「权限不足」
|
- **401 权限错误**:取消订单等接口失败时,通过 `useAuthError().formatAuthError` 统一提示「请先登录」或「权限不足」
|
||||||
|
|
||||||
|
|||||||
173
src/api/pmset.ts
Normal file
173
src/api/pmset.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { buildQuery, get, post } from './request'
|
||||||
|
import type { ApiResponse } from './types'
|
||||||
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
|
export type { PageResult }
|
||||||
|
|
||||||
|
/** USDC 6 位小数 */
|
||||||
|
const USDC_SCALE = 1_000_000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钱包提现请求体(request.PmSettlementWithdrawByWallet)
|
||||||
|
*/
|
||||||
|
export interface WithdrawByWalletRequest {
|
||||||
|
/** 提现金额,6 位小数(如 1000000 = 1 USDC) */
|
||||||
|
amount: number
|
||||||
|
/** 链标识,如 polygon、ethereum、arbitrum、optimism */
|
||||||
|
chain: string
|
||||||
|
/** SIWE 签名消息 */
|
||||||
|
message: string
|
||||||
|
/** 随机 nonce */
|
||||||
|
nonce: string
|
||||||
|
/** 钱包签名 */
|
||||||
|
signature: string
|
||||||
|
/** 出金地址(用户接收资金的地址) */
|
||||||
|
tokenAddress: string
|
||||||
|
/** 代币符号,默认 USDC */
|
||||||
|
tokenSymbol: string
|
||||||
|
/** 钱包地址 */
|
||||||
|
walletAddress: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 各链 USDC 合约地址 */
|
||||||
|
export const USDC_ADDRESS_BY_CHAIN: Record<string, string> = {
|
||||||
|
ethereum: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||||
|
polygon: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
|
||||||
|
arbitrum: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
|
||||||
|
optimism: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提现响应 data(polymarket.PmSettlementWithdrawResponse)
|
||||||
|
*/
|
||||||
|
export interface WithdrawByWalletResponseData {
|
||||||
|
idempotencyKey?: string
|
||||||
|
requestNo?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WithdrawByWalletResponse extends ApiResponse<WithdrawByWalletResponseData> {
|
||||||
|
code: number
|
||||||
|
data?: WithdrawByWalletResponseData
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /pmset/withdrawByWallet
|
||||||
|
* 客户端钱包提现申请,需钱包验签(SIWE)
|
||||||
|
* 可选传 x-token、x-user-id 用于用户上下文
|
||||||
|
*/
|
||||||
|
export async function withdrawByWallet(
|
||||||
|
data: WithdrawByWalletRequest,
|
||||||
|
config?: { headers?: Record<string, string> },
|
||||||
|
): Promise<WithdrawByWalletResponse> {
|
||||||
|
return post<WithdrawByWalletResponse>('/pmset/withdrawByWallet', data, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将 USDC 显示金额转为 API 所需整数(6 位小数) */
|
||||||
|
export function usdcToAmount(displayAmount: number): number {
|
||||||
|
return Math.round(displayAmount * USDC_SCALE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提现状态:审核中、提现成功、审核不通过、提现失败 */
|
||||||
|
export const WITHDRAW_STATUS = {
|
||||||
|
PENDING: 'pending',
|
||||||
|
SUCCESS: 'success',
|
||||||
|
REJECTED: 'rejected',
|
||||||
|
FAILED: 'failed',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/** 提现记录项(polymarket.PmSettlementRequests) */
|
||||||
|
export interface SettlementRequestItem {
|
||||||
|
ID?: number
|
||||||
|
amount?: number
|
||||||
|
auditTime?: string
|
||||||
|
auditUserId?: number
|
||||||
|
chain?: string
|
||||||
|
createdAt?: string
|
||||||
|
fee?: string
|
||||||
|
idempotencyKey?: string
|
||||||
|
netAmount?: string
|
||||||
|
payoutError?: string
|
||||||
|
payoutTime?: string
|
||||||
|
payoutTxHash?: string
|
||||||
|
reason?: string
|
||||||
|
requestNo?: string
|
||||||
|
status?: string
|
||||||
|
tokenSymbol?: string
|
||||||
|
token_address?: string
|
||||||
|
updatedAt?: string
|
||||||
|
userId?: number
|
||||||
|
walletAddress?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettlementListResponse {
|
||||||
|
code: number
|
||||||
|
data?: PageResult<SettlementRequestItem>
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSettlementListParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
status?: string
|
||||||
|
keyword?: string
|
||||||
|
requestNo?: string
|
||||||
|
tokenSymbol?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /pmset/getPmSettlementRequestsList
|
||||||
|
* 分页获取提现/结算请求列表,需鉴权
|
||||||
|
*/
|
||||||
|
export async function getSettlementRequestsList(
|
||||||
|
params: GetSettlementListParams = {},
|
||||||
|
config?: { headers?: Record<string, string> },
|
||||||
|
): Promise<SettlementListResponse> {
|
||||||
|
const { page = 1, pageSize = 10, status, keyword, requestNo, tokenSymbol } = params
|
||||||
|
const query = buildQuery({ page, pageSize, status, keyword, requestNo, tokenSymbol })
|
||||||
|
return get<SettlementListResponse>('/pmset/getPmSettlementRequestsList', query, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 客户端提现记录项(getPmSettlementRequestsListClient 返回) */
|
||||||
|
export interface SettlementRequestClientItem {
|
||||||
|
ID?: number
|
||||||
|
CreatedAt?: string
|
||||||
|
UpdatedAt?: string
|
||||||
|
chain?: string
|
||||||
|
amount?: number
|
||||||
|
fee?: string
|
||||||
|
status?: string
|
||||||
|
reason?: string
|
||||||
|
requestNo?: string
|
||||||
|
tokenAddress?: string | null
|
||||||
|
/** 兼容旧接口字段 */
|
||||||
|
createdAt?: string
|
||||||
|
walletAddress?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettlementListClientResponse {
|
||||||
|
code: number
|
||||||
|
data?: PageResult<SettlementRequestClientItem>
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /pmset/getPmSettlementRequestsListClient
|
||||||
|
* 客户端分页获取提现记录列表,需鉴权
|
||||||
|
*/
|
||||||
|
export async function getSettlementRequestsListClient(
|
||||||
|
params: GetSettlementListParams = {},
|
||||||
|
config?: { headers?: Record<string, string> },
|
||||||
|
): Promise<SettlementListClientResponse> {
|
||||||
|
const { page = 1, pageSize = 10, status, keyword, requestNo, tokenSymbol } = params
|
||||||
|
const query = buildQuery({ page, pageSize, status, keyword, requestNo, tokenSymbol })
|
||||||
|
return get<SettlementListClientResponse>('/pmset/getPmSettlementRequestsListClient', query, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将 amount(6 位小数)转为显示金额 */
|
||||||
|
export function amountToUsdcDisplay(raw: number | undefined): string {
|
||||||
|
if (raw == null || !Number.isFinite(raw)) return '0.00'
|
||||||
|
return (raw / USDC_SCALE).toFixed(2)
|
||||||
|
}
|
||||||
@ -82,7 +82,11 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="destinationType === 'address'" class="hint-msg">
|
||||||
|
{{ t('withdraw.customAddressNotSupported') || 'Custom address is not supported for this withdrawal.' }}
|
||||||
|
</div>
|
||||||
<div v-if="amountError" class="error-msg">{{ amountError }}</div>
|
<div v-if="amountError" class="error-msg">{{ amountError }}</div>
|
||||||
|
<div v-if="errorMessage" class="error-msg">{{ errorMessage }}</div>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
class="withdraw-btn"
|
class="withdraw-btn"
|
||||||
@ -105,8 +109,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { BrowserProvider } from 'ethers'
|
||||||
|
import { SiweMessage } from 'siwe'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
import { withdrawByWallet, usdcToAmount } from '@/api/pmset'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const userStore = useUserStore()
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@ -117,12 +126,13 @@ const props = withDefaults(
|
|||||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; success: [] }>()
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; success: [] }>()
|
||||||
|
|
||||||
const amount = ref('')
|
const amount = ref('')
|
||||||
const selectedNetwork = ref('ethereum')
|
const selectedNetwork = ref('polygon')
|
||||||
const destinationType = ref<'wallet' | 'address'>('wallet')
|
const destinationType = ref<'wallet' | 'address'>('wallet')
|
||||||
const customAddress = ref('')
|
const customAddress = ref('')
|
||||||
const connectedAddress = ref('')
|
const connectedAddress = ref('')
|
||||||
const connecting = ref(false)
|
const connecting = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
|
||||||
const networks = [
|
const networks = [
|
||||||
{ id: 'ethereum', label: 'Ethereum' },
|
{ id: 'ethereum', label: 'Ethereum' },
|
||||||
@ -141,9 +151,10 @@ const amountError = computed(() => {
|
|||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 仅支持已连接钱包提现(需验签);自定义地址暂不支持 */
|
||||||
const hasValidDestination = computed(() => {
|
const hasValidDestination = computed(() => {
|
||||||
if (destinationType.value === 'wallet') return !!connectedAddress.value
|
if (destinationType.value === 'wallet') return !!connectedAddress.value
|
||||||
return /^0x[a-fA-F0-9]{40}$/.test(customAddress.value.trim())
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const canSubmit = computed(
|
const canSubmit = computed(
|
||||||
@ -151,7 +162,8 @@ const canSubmit = computed(
|
|||||||
amountNum.value > 0 &&
|
amountNum.value > 0 &&
|
||||||
amountNum.value <= balanceNum.value &&
|
amountNum.value <= balanceNum.value &&
|
||||||
hasValidDestination.value &&
|
hasValidDestination.value &&
|
||||||
!amountError.value,
|
!amountError.value &&
|
||||||
|
!errorMessage.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
function shortAddress(addr: string) {
|
function shortAddress(addr: string) {
|
||||||
@ -175,27 +187,99 @@ function allowDecimal(e: KeyboardEvent) {
|
|||||||
|
|
||||||
async function connectWallet() {
|
async function connectWallet() {
|
||||||
if (!window.ethereum) {
|
if (!window.ethereum) {
|
||||||
alert(t('deposit.installMetaMask'))
|
errorMessage.value = t('deposit.installMetaMask')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
connecting.value = true
|
connecting.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
try {
|
try {
|
||||||
const accounts = (await window.ethereum.request({ method: 'eth_requestAccounts' })) as string[]
|
const accounts = (await window.ethereum.request({ method: 'eth_requestAccounts' })) as string[]
|
||||||
connectedAddress.value = accounts[0] || ''
|
connectedAddress.value = accounts[0] || ''
|
||||||
|
if (!connectedAddress.value) {
|
||||||
|
errorMessage.value = t('withdraw.connectFailed') || 'Failed to connect wallet'
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
errorMessage.value = (e as Error)?.message || 'Failed to connect wallet'
|
||||||
} finally {
|
} finally {
|
||||||
connecting.value = false
|
connecting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 钱包验签(与 Login.vue 逻辑一致) */
|
||||||
|
async function signWithWallet(walletAddress: string): Promise<{ message: string; nonce: string; signature: string }> {
|
||||||
|
if (!window.ethereum) throw new Error(t('deposit.installMetaMask'))
|
||||||
|
const chainIdRaw = await window.ethereum.request({ method: 'eth_chainId' })
|
||||||
|
const chainId = typeof chainIdRaw === 'string' ? parseInt(chainIdRaw, 16) : Number(chainIdRaw)
|
||||||
|
const scheme = window.location.protocol.slice(0, -1)
|
||||||
|
const domain = window.location.host
|
||||||
|
const origin = window.location.origin
|
||||||
|
const nonce = new Date().getTime().toString()
|
||||||
|
const statement = `Withdraw ${amountNum.value} USDC to ${networks.find((n) => n.id === selectedNetwork.value)?.label ?? selectedNetwork.value}`
|
||||||
|
|
||||||
|
const provider = new BrowserProvider(window.ethereum)
|
||||||
|
const signer = await provider.getSigner()
|
||||||
|
const siwe = new SiweMessage({
|
||||||
|
scheme,
|
||||||
|
domain,
|
||||||
|
address: signer.address,
|
||||||
|
statement,
|
||||||
|
uri: origin,
|
||||||
|
version: '1',
|
||||||
|
chainId,
|
||||||
|
nonce,
|
||||||
|
})
|
||||||
|
const message = siwe.prepareMessage()
|
||||||
|
const message1 = message.replace(/^https?:\/\//, '')
|
||||||
|
|
||||||
|
const signature = (await window.ethereum.request({
|
||||||
|
method: 'personal_sign',
|
||||||
|
params: [message1, walletAddress],
|
||||||
|
})) as string
|
||||||
|
|
||||||
|
return { message: message1, nonce, signature }
|
||||||
|
}
|
||||||
|
|
||||||
async function submitWithdraw() {
|
async function submitWithdraw() {
|
||||||
if (!canSubmit.value) return
|
if (!canSubmit.value) return
|
||||||
|
const addr = connectedAddress.value
|
||||||
|
if (!addr || !/^0x[0-9a-fA-F]{40}$/.test(addr)) {
|
||||||
|
errorMessage.value = t('withdraw.connectWalletFirst') || 'Please connect wallet first'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const walletAddr = userStore.user?.externalWalletAddress as string | undefined
|
||||||
|
if (!walletAddr || !/^0x[0-9a-fA-F]{40}$/.test(walletAddr)) {
|
||||||
|
errorMessage.value = t('withdraw.externalWalletRequired') || 'External wallet address is required'
|
||||||
|
return
|
||||||
|
}
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
try {
|
try {
|
||||||
await new Promise((r) => setTimeout(r, 800))
|
const { message, nonce, signature } = await signWithWallet(addr)
|
||||||
emit('success')
|
const headers = userStore.getAuthHeaders()
|
||||||
close()
|
const chain = selectedNetwork.value
|
||||||
|
const res = await withdrawByWallet(
|
||||||
|
{
|
||||||
|
amount: usdcToAmount(amountNum.value),
|
||||||
|
chain,
|
||||||
|
message,
|
||||||
|
nonce,
|
||||||
|
signature,
|
||||||
|
tokenAddress: addr,
|
||||||
|
tokenSymbol: 'USDC',
|
||||||
|
walletAddress: walletAddr,
|
||||||
|
},
|
||||||
|
headers ? { headers } : undefined,
|
||||||
|
)
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
emit('success')
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
errorMessage.value = res.msg || (t('error.requestFailed') ?? 'Request failed')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[submitWithdraw]', e)
|
||||||
|
errorMessage.value = (e as Error)?.message || (t('error.requestFailed') ?? 'Request failed')
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
@ -209,6 +293,7 @@ watch(
|
|||||||
destinationType.value = 'wallet'
|
destinationType.value = 'wallet'
|
||||||
customAddress.value = ''
|
customAddress.value = ''
|
||||||
connectedAddress.value = ''
|
connectedAddress.value = ''
|
||||||
|
errorMessage.value = ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -292,6 +377,12 @@ watch(
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint-msg {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.error-msg {
|
.error-msg {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
|
|||||||
@ -166,7 +166,20 @@
|
|||||||
"view": "View",
|
"view": "View",
|
||||||
"expirationLabel": "Expiration:",
|
"expirationLabel": "Expiration:",
|
||||||
"youWon": "You won ${amount}",
|
"youWon": "You won ${amount}",
|
||||||
"claim": "Claim"
|
"claim": "Claim",
|
||||||
|
"withdrawals": "Withdrawals",
|
||||||
|
"withdrawStatusAll": "All",
|
||||||
|
"withdrawStatusPending": "Under review",
|
||||||
|
"withdrawStatusSuccess": "Withdrawn",
|
||||||
|
"withdrawStatusRejected": "Rejected",
|
||||||
|
"withdrawStatusFailed": "Failed",
|
||||||
|
"withdrawAmount": "Amount",
|
||||||
|
"withdrawAddress": "Address",
|
||||||
|
"withdrawRequestNo": "Request No.",
|
||||||
|
"withdrawChain": "Chain",
|
||||||
|
"withdrawTime": "Time",
|
||||||
|
"withdrawStatus": "Status",
|
||||||
|
"noWithdrawalsFound": "No withdrawals found"
|
||||||
},
|
},
|
||||||
"deposit": {
|
"deposit": {
|
||||||
"title": "Deposit",
|
"title": "Deposit",
|
||||||
@ -204,7 +217,11 @@
|
|||||||
"addressPlaceholder": "0x...",
|
"addressPlaceholder": "0x...",
|
||||||
"amountMustBePositive": "Amount must be greater than 0",
|
"amountMustBePositive": "Amount must be greater than 0",
|
||||||
"insufficientBalance": "Insufficient balance",
|
"insufficientBalance": "Insufficient balance",
|
||||||
"close": "Close"
|
"close": "Close",
|
||||||
|
"connectFailed": "Failed to connect wallet",
|
||||||
|
"connectWalletFirst": "Please connect wallet first",
|
||||||
|
"externalWalletRequired": "External wallet address is required in user info",
|
||||||
|
"customAddressNotSupported": "Custom address is not supported for this withdrawal."
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"zh": "简体中文",
|
"zh": "简体中文",
|
||||||
|
|||||||
@ -166,7 +166,20 @@
|
|||||||
"view": "表示",
|
"view": "表示",
|
||||||
"expirationLabel": "有効期限:",
|
"expirationLabel": "有効期限:",
|
||||||
"youWon": "獲得 $${amount}",
|
"youWon": "獲得 $${amount}",
|
||||||
"claim": "受け取る"
|
"claim": "受け取る",
|
||||||
|
"withdrawals": "出金履歴",
|
||||||
|
"withdrawStatusAll": "すべて",
|
||||||
|
"withdrawStatusPending": "審査中",
|
||||||
|
"withdrawStatusSuccess": "出金成功",
|
||||||
|
"withdrawStatusRejected": "審査不通過",
|
||||||
|
"withdrawStatusFailed": "出金失敗",
|
||||||
|
"withdrawAmount": "金額",
|
||||||
|
"withdrawAddress": "出金アドレス",
|
||||||
|
"withdrawRequestNo": "申請番号",
|
||||||
|
"withdrawChain": "チェーン",
|
||||||
|
"withdrawTime": "時間",
|
||||||
|
"withdrawStatus": "状態",
|
||||||
|
"noWithdrawalsFound": "出金履歴がありません"
|
||||||
},
|
},
|
||||||
"deposit": {
|
"deposit": {
|
||||||
"title": "入金",
|
"title": "入金",
|
||||||
@ -204,7 +217,11 @@
|
|||||||
"addressPlaceholder": "0x...",
|
"addressPlaceholder": "0x...",
|
||||||
"amountMustBePositive": "金額は 0 より大きい必要があります",
|
"amountMustBePositive": "金額は 0 より大きい必要があります",
|
||||||
"insufficientBalance": "残高不足",
|
"insufficientBalance": "残高不足",
|
||||||
"close": "閉じる"
|
"close": "閉じる",
|
||||||
|
"connectFailed": "ウォレットの接続に失敗しました",
|
||||||
|
"connectWalletFirst": "まずウォレットを接続してください",
|
||||||
|
"externalWalletRequired": "ユーザー情報に外部ウォレットアドレスが必要です",
|
||||||
|
"customAddressNotSupported": "この出金ではカスタムアドレスはサポートされていません。"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"zh": "简体中文",
|
"zh": "简体中文",
|
||||||
|
|||||||
@ -166,7 +166,20 @@
|
|||||||
"view": "보기",
|
"view": "보기",
|
||||||
"expirationLabel": "만료:",
|
"expirationLabel": "만료:",
|
||||||
"youWon": "당첨 $${amount}",
|
"youWon": "당첨 $${amount}",
|
||||||
"claim": "수령"
|
"claim": "수령",
|
||||||
|
"withdrawals": "출금 내역",
|
||||||
|
"withdrawStatusAll": "전체",
|
||||||
|
"withdrawStatusPending": "검토 중",
|
||||||
|
"withdrawStatusSuccess": "출금 완료",
|
||||||
|
"withdrawStatusRejected": "승인 거부",
|
||||||
|
"withdrawStatusFailed": "출금 실패",
|
||||||
|
"withdrawAmount": "금액",
|
||||||
|
"withdrawAddress": "출금 주소",
|
||||||
|
"withdrawRequestNo": "신청 번호",
|
||||||
|
"withdrawChain": "체인",
|
||||||
|
"withdrawTime": "시간",
|
||||||
|
"withdrawStatus": "상태",
|
||||||
|
"noWithdrawalsFound": "출금 내역이 없습니다"
|
||||||
},
|
},
|
||||||
"deposit": {
|
"deposit": {
|
||||||
"title": "입금",
|
"title": "입금",
|
||||||
@ -204,7 +217,11 @@
|
|||||||
"addressPlaceholder": "0x...",
|
"addressPlaceholder": "0x...",
|
||||||
"amountMustBePositive": "금액은 0보다 커야 합니다",
|
"amountMustBePositive": "금액은 0보다 커야 합니다",
|
||||||
"insufficientBalance": "잔액 부족",
|
"insufficientBalance": "잔액 부족",
|
||||||
"close": "닫기"
|
"close": "닫기",
|
||||||
|
"connectFailed": "지갑 연결에 실패했습니다",
|
||||||
|
"connectWalletFirst": "먼저 지갑을 연결해 주세요",
|
||||||
|
"externalWalletRequired": "사용자 정보에 외부 지갑 주소가 필요합니다",
|
||||||
|
"customAddressNotSupported": "이 출금에서는 사용자 지정 주소가 지원되지 않습니다."
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"zh": "简体中文",
|
"zh": "简体中文",
|
||||||
|
|||||||
@ -166,7 +166,20 @@
|
|||||||
"view": "查看",
|
"view": "查看",
|
||||||
"expirationLabel": "到期",
|
"expirationLabel": "到期",
|
||||||
"youWon": "您赢得 ${amount}",
|
"youWon": "您赢得 ${amount}",
|
||||||
"claim": "领取"
|
"claim": "领取",
|
||||||
|
"withdrawals": "提现记录",
|
||||||
|
"withdrawStatusAll": "全部",
|
||||||
|
"withdrawStatusPending": "审核中",
|
||||||
|
"withdrawStatusSuccess": "提现成功",
|
||||||
|
"withdrawStatusRejected": "审核不通过",
|
||||||
|
"withdrawStatusFailed": "提现失败",
|
||||||
|
"withdrawAmount": "金额",
|
||||||
|
"withdrawRequestNo": "申请单号",
|
||||||
|
"withdrawChain": "链",
|
||||||
|
"withdrawTime": "时间",
|
||||||
|
"withdrawStatus": "状态",
|
||||||
|
"noWithdrawalsFound": "暂无提现记录",
|
||||||
|
"withdrawAddress": "提现地址"
|
||||||
},
|
},
|
||||||
"deposit": {
|
"deposit": {
|
||||||
"title": "入金",
|
"title": "入金",
|
||||||
@ -204,7 +217,23 @@
|
|||||||
"addressPlaceholder": "0x...",
|
"addressPlaceholder": "0x...",
|
||||||
"amountMustBePositive": "金额必须大于 0",
|
"amountMustBePositive": "金额必须大于 0",
|
||||||
"insufficientBalance": "余额不足",
|
"insufficientBalance": "余额不足",
|
||||||
"close": "关闭"
|
"close": "关闭",
|
||||||
|
"withdrawals": "提现记录",
|
||||||
|
"withdrawStatusAll": "全部",
|
||||||
|
"withdrawStatusPending": "审核中",
|
||||||
|
"withdrawStatusSuccess": "提现成功",
|
||||||
|
"withdrawStatusRejected": "审核不通过",
|
||||||
|
"withdrawStatusFailed": "提现失败",
|
||||||
|
"withdrawAmount": "金额",
|
||||||
|
"withdrawRequestNo": "申请单号",
|
||||||
|
"withdrawChain": "链",
|
||||||
|
"withdrawTime": "时间",
|
||||||
|
"withdrawStatus": "状态",
|
||||||
|
"noWithdrawalsFound": "暂无提现记录",
|
||||||
|
"connectFailed": "连接钱包失败",
|
||||||
|
"connectWalletFirst": "请先连接钱包",
|
||||||
|
"externalWalletRequired": "用户信息中缺少外部钱包地址",
|
||||||
|
"customAddressNotSupported": "暂不支持自定义地址提现。"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"zh": "简体中文",
|
"zh": "简体中文",
|
||||||
|
|||||||
@ -166,7 +166,20 @@
|
|||||||
"view": "查看",
|
"view": "查看",
|
||||||
"expirationLabel": "到期",
|
"expirationLabel": "到期",
|
||||||
"youWon": "您贏得 ${amount}",
|
"youWon": "您贏得 ${amount}",
|
||||||
"claim": "領取"
|
"claim": "領取",
|
||||||
|
"withdrawals": "提現記錄",
|
||||||
|
"withdrawStatusAll": "全部",
|
||||||
|
"withdrawStatusPending": "審核中",
|
||||||
|
"withdrawStatusSuccess": "提現成功",
|
||||||
|
"withdrawStatusRejected": "審核不通過",
|
||||||
|
"withdrawStatusFailed": "提現失敗",
|
||||||
|
"withdrawAmount": "金額",
|
||||||
|
"withdrawAddress": "提現地址",
|
||||||
|
"withdrawRequestNo": "申請單號",
|
||||||
|
"withdrawChain": "鏈",
|
||||||
|
"withdrawTime": "時間",
|
||||||
|
"withdrawStatus": "狀態",
|
||||||
|
"noWithdrawalsFound": "暫無提現記錄"
|
||||||
},
|
},
|
||||||
"deposit": {
|
"deposit": {
|
||||||
"title": "入金",
|
"title": "入金",
|
||||||
@ -204,7 +217,11 @@
|
|||||||
"addressPlaceholder": "0x...",
|
"addressPlaceholder": "0x...",
|
||||||
"amountMustBePositive": "金額必須大於 0",
|
"amountMustBePositive": "金額必須大於 0",
|
||||||
"insufficientBalance": "餘額不足",
|
"insufficientBalance": "餘額不足",
|
||||||
"close": "關閉"
|
"close": "關閉",
|
||||||
|
"connectFailed": "連接錢包失敗",
|
||||||
|
"connectWalletFirst": "請先連接錢包",
|
||||||
|
"externalWalletRequired": "用戶資訊中缺少外部錢包地址",
|
||||||
|
"customAddressNotSupported": "暫不支援自訂地址提現。"
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"zh": "繁體中文",
|
"zh": "繁體中文",
|
||||||
|
|||||||
@ -108,6 +108,7 @@
|
|||||||
<v-tab value="positions">{{ t('wallet.positions') }}</v-tab>
|
<v-tab value="positions">{{ t('wallet.positions') }}</v-tab>
|
||||||
<v-tab value="orders">{{ t('wallet.openOrders') }}</v-tab>
|
<v-tab value="orders">{{ t('wallet.openOrders') }}</v-tab>
|
||||||
<v-tab value="history">{{ t('wallet.history') }}</v-tab>
|
<v-tab value="history">{{ t('wallet.history') }}</v-tab>
|
||||||
|
<v-tab value="withdrawals">{{ t('wallet.withdrawals') }}</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@ -153,6 +154,19 @@
|
|||||||
{{ t('wallet.currentValue') }}
|
{{ t('wallet.currentValue') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</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'">
|
<template v-else-if="activeTab === 'orders'">
|
||||||
<v-btn variant="outlined" size="small" class="filter-btn">
|
<v-btn variant="outlined" size="small" class="filter-btn">
|
||||||
<v-icon size="18">mdi-filter</v-icon>
|
<v-icon size="18">mdi-filter</v-icon>
|
||||||
@ -527,6 +541,71 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</v-table>
|
</v-table>
|
||||||
</template>
|
</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">
|
<div v-if="currentListTotal > 0" class="pagination-bar">
|
||||||
<span class="pagination-info">
|
<span class="pagination-info">
|
||||||
@ -658,6 +737,12 @@ import { useAuthError } from '../composables/useAuthError'
|
|||||||
import { cancelOrder as apiCancelOrder } from '../api/order'
|
import { cancelOrder as apiCancelOrder } from '../api/order'
|
||||||
import { getOrderList, mapOrderToHistoryItem, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
|
import { getOrderList, mapOrderToHistoryItem, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
|
||||||
import { getPositionList, mapPositionToDisplayItem, claimPosition } from '../api/position'
|
import { getPositionList, mapPositionToDisplayItem, claimPosition } from '../api/position'
|
||||||
|
import {
|
||||||
|
getSettlementRequestsListClient,
|
||||||
|
amountToUsdcDisplay,
|
||||||
|
WITHDRAW_STATUS,
|
||||||
|
type SettlementRequestClientItem,
|
||||||
|
} from '../api/pmset'
|
||||||
import {
|
import {
|
||||||
MOCK_TOKEN_ID,
|
MOCK_TOKEN_ID,
|
||||||
MOCK_WALLET_POSITIONS,
|
MOCK_WALLET_POSITIONS,
|
||||||
@ -681,8 +766,16 @@ const plTimeRanges = computed(() => [
|
|||||||
{ label: t('wallet.pl1M'), value: '1M' },
|
{ label: t('wallet.pl1M'), value: '1M' },
|
||||||
{ label: t('wallet.plAll'), value: 'ALL' },
|
{ label: t('wallet.plAll'), value: 'ALL' },
|
||||||
])
|
])
|
||||||
const activeTab = ref<'positions' | 'orders' | 'history'>('positions')
|
const activeTab = ref<'positions' | 'orders' | 'history' | 'withdrawals'>('positions')
|
||||||
const search = ref('')
|
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) */
|
/** 当前展示的持仓列表(mock 或 API) */
|
||||||
const currentPositionList = computed(() =>
|
const currentPositionList = computed(() =>
|
||||||
USE_MOCK_WALLET ? positions.value : positionList.value,
|
USE_MOCK_WALLET ? positions.value : positionList.value,
|
||||||
@ -831,6 +924,11 @@ interface HistoryItem {
|
|||||||
const positions = ref<Position[]>(
|
const positions = ref<Position[]>(
|
||||||
USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] : [],
|
USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] : [],
|
||||||
)
|
)
|
||||||
|
/** 提现记录列表 */
|
||||||
|
const withdrawalsList = ref<SettlementRequestClientItem[]>([])
|
||||||
|
const withdrawalsTotal = ref(0)
|
||||||
|
const withdrawalsLoading = ref(false)
|
||||||
|
|
||||||
/** 持仓列表(API 数据,非 mock 时使用) */
|
/** 持仓列表(API 数据,非 mock 时使用) */
|
||||||
const positionList = ref<Position[]>([])
|
const positionList = ref<Position[]>([])
|
||||||
const positionTotal = ref(0)
|
const positionTotal = ref(0)
|
||||||
@ -973,6 +1071,75 @@ async function loadHistoryOrders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
function matchSearch(text: string): boolean {
|
||||||
const q = search.value.trim().toLowerCase()
|
const q = search.value.trim().toLowerCase()
|
||||||
return !q || text.toLowerCase().includes(q)
|
return !q || text.toLowerCase().includes(q)
|
||||||
@ -1023,17 +1190,22 @@ const totalPagesHistory = computed(() => {
|
|||||||
const total = USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
|
const total = USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
|
||||||
return Math.max(1, Math.ceil(total / itemsPerPage.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(() => {
|
const currentListTotal = computed(() => {
|
||||||
if (activeTab.value === 'positions')
|
if (activeTab.value === 'positions')
|
||||||
return USE_MOCK_WALLET ? filteredPositions.value.length : positionTotal.value
|
return USE_MOCK_WALLET ? filteredPositions.value.length : positionTotal.value
|
||||||
if (activeTab.value === 'orders')
|
if (activeTab.value === 'orders')
|
||||||
return USE_MOCK_WALLET ? filteredOpenOrders.value.length : openOrderTotal.value
|
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
|
return USE_MOCK_WALLET ? filteredHistory.value.length : historyTotal.value
|
||||||
})
|
})
|
||||||
const currentTotalPages = computed(() => {
|
const currentTotalPages = computed(() => {
|
||||||
if (activeTab.value === 'positions') return totalPagesPositions.value
|
if (activeTab.value === 'positions') return totalPagesPositions.value
|
||||||
if (activeTab.value === 'orders') return totalPagesOrders.value
|
if (activeTab.value === 'orders') return totalPagesOrders.value
|
||||||
|
if (activeTab.value === 'withdrawals') return totalPagesWithdrawals.value
|
||||||
return totalPagesHistory.value
|
return totalPagesHistory.value
|
||||||
})
|
})
|
||||||
const currentPageStart = computed(() =>
|
const currentPageStart = computed(() =>
|
||||||
@ -1048,11 +1220,17 @@ watch(activeTab, (tab) => {
|
|||||||
if (tab === 'positions' && !USE_MOCK_WALLET) loadPositionList()
|
if (tab === 'positions' && !USE_MOCK_WALLET) loadPositionList()
|
||||||
if (tab === 'orders' && !USE_MOCK_WALLET) loadOpenOrders()
|
if (tab === 'orders' && !USE_MOCK_WALLET) loadOpenOrders()
|
||||||
if (tab === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
if (tab === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
||||||
|
if (tab === 'withdrawals') loadWithdrawals()
|
||||||
})
|
})
|
||||||
watch([page, itemsPerPage], () => {
|
watch([page, itemsPerPage], () => {
|
||||||
if (activeTab.value === 'positions' && !USE_MOCK_WALLET) loadPositionList()
|
if (activeTab.value === 'positions' && !USE_MOCK_WALLET) loadPositionList()
|
||||||
if (activeTab.value === 'orders' && !USE_MOCK_WALLET) loadOpenOrders()
|
if (activeTab.value === 'orders' && !USE_MOCK_WALLET) loadOpenOrders()
|
||||||
if (activeTab.value === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
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], () => {
|
watch([currentListTotal, itemsPerPage], () => {
|
||||||
const maxPage = currentTotalPages.value
|
const maxPage = currentTotalPages.value
|
||||||
@ -1304,6 +1482,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
function onWithdrawSuccess() {
|
function onWithdrawSuccess() {
|
||||||
withdrawDialogOpen.value = false
|
withdrawDialogOpen.value = false
|
||||||
|
userStore.fetchUsdcBalance()
|
||||||
|
if (activeTab.value === 'withdrawals') loadWithdrawals()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAuthorizeClick() {
|
function onAuthorizeClick() {
|
||||||
@ -2017,6 +2197,82 @@ async function submitAuthorize() {
|
|||||||
font-size: 14px;
|
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 {
|
.pagination-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user