Compare commits
No commits in common. "d571d1b9b09c872d67fc76e8677c953fe195d40c" and "45a4b2c00f039f74e7e5652897e744151419a09d" have entirely different histories.
d571d1b9b0
...
45a4b2c00f
@ -1,81 +0,0 @@
|
||||
# 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,62 +4,12 @@
|
||||
|
||||
## 功能用途
|
||||
|
||||
持仓相关 API:分页获取持仓列表,以及将 `ClobPositionItem` 映射为钱包展示项。`PageResult` 来自 `@/api/types`,使用 `buildQuery` 构建请求参数。接口定义以 Swagger doc.json 为准。
|
||||
持仓相关 API:分页获取持仓列表,以及将 `ClobPositionItem` 映射为钱包展示项。`PageResult` 来自 `@/api/types`,使用 `buildQuery` 构建请求参数。
|
||||
|
||||
## 核心能力
|
||||
|
||||
- `getPositionList`:分页获取持仓列表(需鉴权 x-token、x-user-id);返回项含 `needClaim`、`market`(内嵌市场 question、outcomes、outcomePrices)
|
||||
- `mapPositionToDisplayItem`:将接口项转为展示结构;`market` 优先用 `market.question`,否则用 marketID;`avgNow` 有 `market.outcomePrices` 时展示「AVG → NOW」格式;`iconChar`/`iconClass`/`imageUrl` 用于展示图标(market.image 优先)
|
||||
|
||||
## GET /clob/position/getPositionList
|
||||
|
||||
### 请求参数(Query)
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| page | number | 否 | 页码 |
|
||||
| pageSize | number | 否 | 每页数量 |
|
||||
| startCreatedAt | string | 否 | 开始时间 |
|
||||
| endCreatedAt | string | 否 | 结束时间 |
|
||||
| marketID | string | 否 | 市场ID |
|
||||
| tokenID | string | 否 | Token ID |
|
||||
| userID | number | 否 | 用户ID |
|
||||
|
||||
### 响应
|
||||
|
||||
`{ code, data, msg }`,`data` 为 `PageResult<ClobPositionItem>`。
|
||||
|
||||
### ClobPositionItem(实际返回结构)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| ID | number | 主键 |
|
||||
| userID | number | 用户ID |
|
||||
| marketID | number \| string | 市场ID |
|
||||
| tokenId | string | Token ID |
|
||||
| size | string | 份额(6 位小数) |
|
||||
| available | string | 可用份额 |
|
||||
| lock | string | 锁单数量 |
|
||||
| cost | string | 成本 |
|
||||
| outcome | string | 方向 |
|
||||
| version | number | 版本号 |
|
||||
| needClaim | boolean | 是否待领取结算 |
|
||||
| market | ClobPositionMarket | 内嵌市场详情(question、outcomes、outcomePrices 等) |
|
||||
| createdAt | string | 创建时间 |
|
||||
| updatedAt | string | 更新时间 |
|
||||
|
||||
### ClobPositionMarket(market 字段)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| ID | number | 市场ID |
|
||||
| question | string | 市场问题 |
|
||||
| slug | string | 市场 slug |
|
||||
| image | string | 市场图片 URL |
|
||||
| icon | string | 市场图标 URL |
|
||||
| outcomes | string[] | 选项(如 ["Up", "Down"]) |
|
||||
| outcomePrices | string[] \| number[] | 各选项当前价格 |
|
||||
| clobTokenIds | string[] | CLOB Token ID 列表 |
|
||||
- `getPositionList`:分页获取持仓列表(需鉴权)
|
||||
- `mapPositionToDisplayItem`:将接口项转为展示结构(含 locked、availableSharesNum、outcome 等);`outcome` 保留 API 原始值(如 "Up"/"Down"、"Yes"/"No"),供 TradeDetail 与市场 outcomes 匹配
|
||||
|
||||
## 使用方式
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
## 功能用途
|
||||
|
||||
提现弹窗,支持输入金额、选择网络、选择提现目标(Connected wallet)。仅支持已连接钱包提现,需验签;`walletAddress` 取用户信息的 `externalWalletAddress`,`tokenAddress` 为出金地址(用户接收资金的地址)。
|
||||
提现弹窗,支持输入金额、选择网络、选择提现目标(Connected wallet / Custom address)。
|
||||
|
||||
## Props
|
||||
|
||||
|
||||
@ -11,8 +11,7 @@
|
||||
|
||||
- Portfolio 卡片:余额、Deposit/Withdraw 按钮
|
||||
- Profit/Loss 卡片:时间范围切换、ECharts 图表
|
||||
- Tab:Positions、Open orders、History、Withdrawals(提现记录)
|
||||
- Withdrawals:分页列表,状态筛选(全部/审核中/提现成功/审核不通过/提现失败),对接 GET /pmset/getPmSettlementRequestsListClient
|
||||
- Tab:Positions、Open orders、History
|
||||
- DepositDialog、WithdrawDialog 组件
|
||||
- **401 权限错误**:取消订单等接口失败时,通过 `useAuthError().formatAuthError` 统一提示「请先登录」或「权限不足」
|
||||
|
||||
|
||||
102
src/api/chart.ts
102
src/api/chart.ts
@ -1,102 +0,0 @@
|
||||
/**
|
||||
* 市场概率/价格历史图表 API 类型定义
|
||||
* 用于 TradeDetail.vue 折线图接入真实接口时的请求与响应约定
|
||||
*/
|
||||
|
||||
import type { ApiResponse } from './types'
|
||||
|
||||
/** 图表使用的基础数据格式:[时间戳(ms), 概率值(0-100)][] */
|
||||
export type ChartDataPoint = [number, number]
|
||||
|
||||
/**
|
||||
* 时间范围,与前端 Time Range 按钮一致
|
||||
* - 1H / 6H / 1D / 1W / 1M:对应时间窗口
|
||||
* - ALL:全部历史
|
||||
*/
|
||||
export type ChartTimeRange = '1H' | '6H' | '1D' | '1W' | '1M' | 'ALL'
|
||||
|
||||
/** 请求参数:按市场 + 时间范围拉取历史 */
|
||||
export interface ChartHistoryParams {
|
||||
/** 市场 ID(或 tokenId / conditionId,以实际后端为准) */
|
||||
marketID: string
|
||||
/** 时间范围 */
|
||||
range: ChartTimeRange
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条历史点(接口可任选一种格式,由 normalizer 统一转成 ChartDataPoint)
|
||||
* 推荐:timestamp 毫秒 + value 为 0–100 的概率
|
||||
*/
|
||||
export interface ChartHistoryItem {
|
||||
/** 时间:Unix 毫秒时间戳 */
|
||||
timestamp: number
|
||||
/** 概率/价格:0–100 表示百分比,或 0–1 表示小数(normalizer 会乘 100) */
|
||||
value: number
|
||||
}
|
||||
|
||||
/** 可选:接口用秒级时间戳 + 小数概率 */
|
||||
export interface ChartHistoryItemAlt {
|
||||
/** 时间:Unix 秒级时间戳 */
|
||||
t?: number
|
||||
/** 概率:0–1 小数 */
|
||||
probability?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 接口响应体建议结构
|
||||
* 后端可返回 data 为 ChartHistoryItem[] 或 ChartHistoryItemAlt[] 或 ChartDataPoint[]
|
||||
*/
|
||||
export interface ChartHistoryResponse {
|
||||
code?: number
|
||||
data?: ChartHistoryItem[] | ChartHistoryItemAlt[] | ChartDataPoint[]
|
||||
msg?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 将接口返回的 data 转成 ECharts 使用的 [timestamp, value][]
|
||||
* - 支持 { timestamp, value }(value 0–100 或 0–1)
|
||||
* - 支持 { t, probability }(t 为秒则乘 1000,probability 0–1 乘 100)
|
||||
* - 已是 [number, number][] 则按需只做排序
|
||||
*/
|
||||
export function normalizeChartData(
|
||||
raw: ChartHistoryResponse['data']
|
||||
): ChartDataPoint[] {
|
||||
if (!raw || !Array.isArray(raw)) return []
|
||||
|
||||
const out: ChartDataPoint[] = []
|
||||
|
||||
for (const item of raw) {
|
||||
if (Array.isArray(item) && item.length >= 2 && typeof item[0] === 'number' && typeof item[1] === 'number') {
|
||||
out.push([item[0], item[1]])
|
||||
continue
|
||||
}
|
||||
const obj = item as Record<string, unknown>
|
||||
if (typeof obj.timestamp === 'number' && typeof obj.value === 'number') {
|
||||
const v = (obj.value as number) <= 1 ? (obj.value as number) * 100 : (obj.value as number)
|
||||
out.push([obj.timestamp as number, v])
|
||||
continue
|
||||
}
|
||||
if (typeof obj.t === 'number' && typeof obj.probability === 'number') {
|
||||
const tMs = (obj.t as number) < 1e12 ? (obj.t as number) * 1000 : (obj.t as number)
|
||||
out.push([tMs, (obj.probability as number) * 100])
|
||||
}
|
||||
}
|
||||
|
||||
out.sort((a, b) => a[0] - b[0])
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例:请求市场历史数据(需根据实际后端路径实现)
|
||||
* GET /api/market/{marketID}/chart?range=1D
|
||||
* 或 POST /api/chart/history body: { marketID, range }
|
||||
*/
|
||||
export async function fetchChartHistory(
|
||||
_params: ChartHistoryParams,
|
||||
_config?: { headers?: Record<string, string> }
|
||||
): Promise<ApiResponse<ChartHistoryItem[]>> {
|
||||
// TODO: 替换为真实 request.get/post 调用
|
||||
// const res = await get<ChartHistoryResponse>(`/market/${params.marketID}/chart`, { params: { range: params.range } })
|
||||
// return { ...res, data: normalizeChartData(res.data) }
|
||||
return { code: 0, data: [], msg: '' }
|
||||
}
|
||||
@ -70,10 +70,6 @@ export interface MockPosition {
|
||||
valueChangeLoss?: boolean
|
||||
sellOutcome?: string
|
||||
outcomeWord?: string
|
||||
/** 用于领取结算(mock 时可选,便于展示未结算行) */
|
||||
marketID?: string
|
||||
tokenID?: string
|
||||
needClaim?: boolean
|
||||
}
|
||||
|
||||
export interface MockOpenOrder {
|
||||
@ -127,9 +123,6 @@ export const MOCK_WALLET_POSITIONS: MockPosition[] = [
|
||||
valueChangeLoss: true,
|
||||
sellOutcome: 'Down',
|
||||
outcomeWord: 'Down',
|
||||
marketID: 'mock-market-1',
|
||||
tokenID: MOCK_TOKEN_ID,
|
||||
needClaim: true,
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
|
||||
173
src/api/pmset.ts
173
src/api/pmset.ts
@ -1,173 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,34 +1,16 @@
|
||||
import { buildQuery, get, post } from './request'
|
||||
import type { ApiResponse } from './types'
|
||||
import { buildQuery, get } from './request'
|
||||
import type { PageResult } from './types'
|
||||
|
||||
export type { PageResult }
|
||||
|
||||
/**
|
||||
* 持仓项内嵌的市场信息(getPositionList 返回的 market 字段)
|
||||
*/
|
||||
export interface ClobPositionMarket {
|
||||
ID?: number
|
||||
question?: string
|
||||
slug?: string
|
||||
image?: string
|
||||
icon?: string
|
||||
outcomes?: string[]
|
||||
outcomePrices?: string[] | number[]
|
||||
clobTokenIds?: string[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* 持仓项(与 getPositionList 实际返回对齐)
|
||||
* size、available、cost、lock 为字符串,6 位小数(除以 1000000 得实际值)
|
||||
* marketID 可能为 number 或 string;market 为内嵌市场详情
|
||||
* 持仓项(与 /clob/position/getPositionList 实际返回对齐)
|
||||
* size、available、cost 为字符串,6 位小数(除以 1000000 得实际值)
|
||||
*/
|
||||
export interface ClobPositionItem {
|
||||
ID?: number
|
||||
userID?: number
|
||||
/** 市场 ID,可能为 number 或 string */
|
||||
marketID?: number | string
|
||||
marketID?: string
|
||||
tokenId?: string
|
||||
/** 份额,字符串,6 位小数 */
|
||||
size?: string
|
||||
@ -38,16 +20,9 @@ export interface ClobPositionItem {
|
||||
lock?: string
|
||||
/** 成本,字符串,6 位小数 */
|
||||
cost?: string
|
||||
/** 方向:Yes | No 或 Up | Down 等 */
|
||||
/** 方向:Yes | No */
|
||||
outcome?: string
|
||||
version?: number
|
||||
/** 是否待领取结算 */
|
||||
needClaim?: boolean
|
||||
/** 内嵌市场详情(问题、outcomes、outcomePrices 等) */
|
||||
market?: ClobPositionMarket
|
||||
/** 创建时间 */
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
CreatedAt?: string
|
||||
UpdatedAt?: string
|
||||
[key: string]: unknown
|
||||
@ -104,37 +79,11 @@ export async function getPositionList(
|
||||
return get<PositionListResponse>('/clob/position/getPositionList', query, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取结算请求体(与 doc.json definitions.request.PositionClaimRequest 一致)
|
||||
* POST /clob/position/claimPosition
|
||||
*/
|
||||
export interface ClaimPositionRequest {
|
||||
/** 市场 ID 列表,与 tokenID 一一对应 */
|
||||
marketID: string[]
|
||||
/** Token ID 列表(如 CLOB tokenId),与 marketID 一一对应 */
|
||||
tokenID: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取结算(已结束市场的赢利持仓)
|
||||
* POST /clob/position/claimPosition
|
||||
* Body: { marketID: string[], tokenID: string[] }
|
||||
* 需鉴权:x-token、x-user-id
|
||||
*/
|
||||
export async function claimPosition(
|
||||
data: ClaimPositionRequest,
|
||||
config?: { headers?: Record<string, string> },
|
||||
): Promise<ApiResponse> {
|
||||
return post<ApiResponse>('/clob/position/claimPosition', data, config)
|
||||
}
|
||||
|
||||
/** 钱包 Positions 展示项(与 Wallet.vue Position 一致) */
|
||||
export interface PositionDisplayItem {
|
||||
id: string
|
||||
/** 市场标题(优先 market.question,否则 marketID) */
|
||||
market: string
|
||||
shares: string
|
||||
/** AVG • NOW 格式,如 "50¢ → 39¢";有 market.outcomePrices 时展示当前价 */
|
||||
avgNow: string
|
||||
bet: string
|
||||
toWin: string
|
||||
@ -156,43 +105,15 @@ export interface PositionDisplayItem {
|
||||
availableSharesNum?: number
|
||||
/** 原始 outcome 字段(API 返回,如 "Up"/"Down" 或 "Yes"/"No"),用于与市场 outcomes 匹配 */
|
||||
outcome?: string
|
||||
/** 市场图片 URL(market.image),有则优先展示 */
|
||||
imageUrl?: string
|
||||
/** 市场 ID(用于 claimPosition),统一为 string */
|
||||
marketID?: string
|
||||
/** Token ID(用于 claimPosition,API 返回 tokenId) */
|
||||
tokenID?: string
|
||||
/** 是否待领取/未结算(needClaim 为 true 时显示领取按钮) */
|
||||
needClaim?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 market.outcomes 和 outcome 找到对应 outcomePrices 的索引
|
||||
*/
|
||||
function getOutcomePriceIndex(
|
||||
outcomes: string[] | undefined,
|
||||
outcome: string,
|
||||
): number {
|
||||
if (!outcomes?.length) return 0
|
||||
const idx = outcomes.findIndex((o) => o === outcome)
|
||||
return idx >= 0 ? idx : 0
|
||||
}
|
||||
|
||||
/** 根据 outcome 返回图标字符 */
|
||||
function getOutcomeIconChar(outcome: string): string {
|
||||
const o = outcome?.trim() || ''
|
||||
if (o === 'Up' || o === 'Yes' || o === 'Above') return '↑'
|
||||
if (o === 'Down' || o === 'No' || o === 'Below') return '↓'
|
||||
return o.charAt(0)?.toUpperCase() || '•'
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 ClobPositionItem 映射为钱包 Position 展示项
|
||||
* size、available、cost 为 6 位小数字符串;market 有 question 时优先展示问题
|
||||
* size、available、cost 为 6 位小数字符串
|
||||
*/
|
||||
export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplayItem {
|
||||
const id = String(pos.ID ?? '')
|
||||
const market = pos.market?.question ?? String(pos.marketID ?? pos.market?.ID ?? '')
|
||||
const market = pos.marketID ?? ''
|
||||
const sizeRaw = parsePosNum(pos.size ?? pos.available)
|
||||
const availableRaw = parsePosNum(pos.available)
|
||||
const costRaw = parsePosNum(pos.cost)
|
||||
@ -210,32 +131,11 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
|
||||
const outcomeTag = `${outcome} —`
|
||||
const locked = lockRaw > 0
|
||||
const lockedSharesNum = lockRaw > 0 ? lockRaw / SCALE : undefined
|
||||
|
||||
// AVG:成本/份额;NOW:market.outcomePrices 中对应 outcome 的当前价
|
||||
let avgNow = '—'
|
||||
if (size > 0 && costUsd > 0) {
|
||||
const avgCents = Math.round((costUsd / size) * 100)
|
||||
const avgStr = `${avgCents}¢`
|
||||
const prices = pos.market?.outcomePrices
|
||||
if (prices?.length) {
|
||||
const idx = getOutcomePriceIndex(pos.market?.outcomes, outcome)
|
||||
const nowVal = typeof prices[idx] === 'string' ? parseFloat(prices[idx] as string) : Number(prices[idx])
|
||||
const nowCents = Number.isFinite(nowVal) ? Math.round(nowVal * 100) : null
|
||||
avgNow = nowCents != null ? `${avgStr} → ${nowCents}¢` : avgStr
|
||||
} else {
|
||||
avgNow = avgStr
|
||||
}
|
||||
}
|
||||
|
||||
const marketID = String(pos.marketID ?? pos.market?.ID ?? '')
|
||||
const tokenID = pos.tokenId ?? (pos as { tokenID?: string }).tokenID ?? ''
|
||||
const imageUrl = (pos.market?.image ?? pos.market?.icon) as string | undefined
|
||||
|
||||
return {
|
||||
id,
|
||||
market,
|
||||
shares,
|
||||
avgNow,
|
||||
avgNow: '—',
|
||||
bet,
|
||||
toWin,
|
||||
value,
|
||||
@ -244,14 +144,8 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
|
||||
outcomeTag,
|
||||
outcomePillClass: pillClass,
|
||||
outcome,
|
||||
iconChar: getOutcomeIconChar(outcome),
|
||||
iconClass: pillClass,
|
||||
imageUrl: imageUrl || undefined,
|
||||
locked,
|
||||
lockedSharesNum,
|
||||
availableSharesNum: availableNum >= 0 ? availableNum : undefined,
|
||||
marketID: marketID || undefined,
|
||||
tokenID: tokenID || undefined,
|
||||
needClaim: pos.needClaim,
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,11 +82,7 @@
|
||||
/>
|
||||
</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="errorMessage" class="error-msg">{{ errorMessage }}</div>
|
||||
|
||||
<v-btn
|
||||
class="withdraw-btn"
|
||||
@ -109,13 +105,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
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 userStore = useUserStore()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: boolean
|
||||
@ -126,13 +117,12 @@ const props = withDefaults(
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; success: [] }>()
|
||||
|
||||
const amount = ref('')
|
||||
const selectedNetwork = ref('polygon')
|
||||
const selectedNetwork = ref('ethereum')
|
||||
const destinationType = ref<'wallet' | 'address'>('wallet')
|
||||
const customAddress = ref('')
|
||||
const connectedAddress = ref('')
|
||||
const connecting = ref(false)
|
||||
const submitting = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const networks = [
|
||||
{ id: 'ethereum', label: 'Ethereum' },
|
||||
@ -151,10 +141,9 @@ const amountError = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
/** 仅支持已连接钱包提现(需验签);自定义地址暂不支持 */
|
||||
const hasValidDestination = computed(() => {
|
||||
if (destinationType.value === 'wallet') return !!connectedAddress.value
|
||||
return false
|
||||
return /^0x[a-fA-F0-9]{40}$/.test(customAddress.value.trim())
|
||||
})
|
||||
|
||||
const canSubmit = computed(
|
||||
@ -162,8 +151,7 @@ const canSubmit = computed(
|
||||
amountNum.value > 0 &&
|
||||
amountNum.value <= balanceNum.value &&
|
||||
hasValidDestination.value &&
|
||||
!amountError.value &&
|
||||
!errorMessage.value,
|
||||
!amountError.value,
|
||||
)
|
||||
|
||||
function shortAddress(addr: string) {
|
||||
@ -187,99 +175,27 @@ function allowDecimal(e: KeyboardEvent) {
|
||||
|
||||
async function connectWallet() {
|
||||
if (!window.ethereum) {
|
||||
errorMessage.value = t('deposit.installMetaMask')
|
||||
alert(t('deposit.installMetaMask'))
|
||||
return
|
||||
}
|
||||
connecting.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const accounts = (await window.ethereum.request({ method: 'eth_requestAccounts' })) as string[]
|
||||
connectedAddress.value = accounts[0] || ''
|
||||
if (!connectedAddress.value) {
|
||||
errorMessage.value = t('withdraw.connectFailed') || 'Failed to connect wallet'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
errorMessage.value = (e as Error)?.message || 'Failed to connect wallet'
|
||||
} finally {
|
||||
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() {
|
||||
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
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const { message, nonce, signature } = await signWithWallet(addr)
|
||||
const headers = userStore.getAuthHeaders()
|
||||
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) {
|
||||
await new Promise((r) => setTimeout(r, 800))
|
||||
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 {
|
||||
submitting.value = false
|
||||
}
|
||||
@ -293,7 +209,6 @@ watch(
|
||||
destinationType.value = 'wallet'
|
||||
customAddress.value = ''
|
||||
connectedAddress.value = ''
|
||||
errorMessage.value = ''
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -377,12 +292,6 @@ watch(
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.hint-msg {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
font-size: 0.8rem;
|
||||
color: #dc2626;
|
||||
|
||||
@ -17,8 +17,7 @@
|
||||
"toast": {
|
||||
"orderSuccess": "Order placed successfully",
|
||||
"splitSuccess": "Split successful",
|
||||
"mergeSuccess": "Merge successful",
|
||||
"claimSuccess": "Claim successful"
|
||||
"mergeSuccess": "Merge successful"
|
||||
},
|
||||
"trade": {
|
||||
"buy": "Buy",
|
||||
@ -164,22 +163,7 @@
|
||||
"expiration": "EXPIRATION",
|
||||
"activity": "ACTIVITY",
|
||||
"view": "View",
|
||||
"expirationLabel": "Expiration:",
|
||||
"youWon": "You won ${amount}",
|
||||
"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"
|
||||
"expirationLabel": "Expiration:"
|
||||
},
|
||||
"deposit": {
|
||||
"title": "Deposit",
|
||||
@ -217,11 +201,7 @@
|
||||
"addressPlaceholder": "0x...",
|
||||
"amountMustBePositive": "Amount must be greater than 0",
|
||||
"insufficientBalance": "Insufficient balance",
|
||||
"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."
|
||||
"close": "Close"
|
||||
},
|
||||
"locale": {
|
||||
"zh": "简体中文",
|
||||
|
||||
@ -17,8 +17,7 @@
|
||||
"toast": {
|
||||
"orderSuccess": "注文が完了しました",
|
||||
"splitSuccess": "スプリット成功",
|
||||
"mergeSuccess": "マージ成功",
|
||||
"claimSuccess": "受け取り完了"
|
||||
"mergeSuccess": "マージ成功"
|
||||
},
|
||||
"trade": {
|
||||
"buy": "買う",
|
||||
@ -164,22 +163,7 @@
|
||||
"expiration": "有効期限",
|
||||
"activity": "アクティビティ",
|
||||
"view": "表示",
|
||||
"expirationLabel": "有効期限:",
|
||||
"youWon": "獲得 $${amount}",
|
||||
"claim": "受け取る",
|
||||
"withdrawals": "出金履歴",
|
||||
"withdrawStatusAll": "すべて",
|
||||
"withdrawStatusPending": "審査中",
|
||||
"withdrawStatusSuccess": "出金成功",
|
||||
"withdrawStatusRejected": "審査不通過",
|
||||
"withdrawStatusFailed": "出金失敗",
|
||||
"withdrawAmount": "金額",
|
||||
"withdrawAddress": "出金アドレス",
|
||||
"withdrawRequestNo": "申請番号",
|
||||
"withdrawChain": "チェーン",
|
||||
"withdrawTime": "時間",
|
||||
"withdrawStatus": "状態",
|
||||
"noWithdrawalsFound": "出金履歴がありません"
|
||||
"expirationLabel": "有効期限:"
|
||||
},
|
||||
"deposit": {
|
||||
"title": "入金",
|
||||
@ -217,11 +201,7 @@
|
||||
"addressPlaceholder": "0x...",
|
||||
"amountMustBePositive": "金額は 0 より大きい必要があります",
|
||||
"insufficientBalance": "残高不足",
|
||||
"close": "閉じる",
|
||||
"connectFailed": "ウォレットの接続に失敗しました",
|
||||
"connectWalletFirst": "まずウォレットを接続してください",
|
||||
"externalWalletRequired": "ユーザー情報に外部ウォレットアドレスが必要です",
|
||||
"customAddressNotSupported": "この出金ではカスタムアドレスはサポートされていません。"
|
||||
"close": "閉じる"
|
||||
},
|
||||
"locale": {
|
||||
"zh": "简体中文",
|
||||
|
||||
@ -17,8 +17,7 @@
|
||||
"toast": {
|
||||
"orderSuccess": "주문이 완료되었습니다",
|
||||
"splitSuccess": "분할 완료",
|
||||
"mergeSuccess": "병합 완료",
|
||||
"claimSuccess": "수령 완료"
|
||||
"mergeSuccess": "병합 완료"
|
||||
},
|
||||
"trade": {
|
||||
"buy": "매수",
|
||||
@ -164,22 +163,7 @@
|
||||
"expiration": "만료",
|
||||
"activity": "활동",
|
||||
"view": "보기",
|
||||
"expirationLabel": "만료:",
|
||||
"youWon": "당첨 $${amount}",
|
||||
"claim": "수령",
|
||||
"withdrawals": "출금 내역",
|
||||
"withdrawStatusAll": "전체",
|
||||
"withdrawStatusPending": "검토 중",
|
||||
"withdrawStatusSuccess": "출금 완료",
|
||||
"withdrawStatusRejected": "승인 거부",
|
||||
"withdrawStatusFailed": "출금 실패",
|
||||
"withdrawAmount": "금액",
|
||||
"withdrawAddress": "출금 주소",
|
||||
"withdrawRequestNo": "신청 번호",
|
||||
"withdrawChain": "체인",
|
||||
"withdrawTime": "시간",
|
||||
"withdrawStatus": "상태",
|
||||
"noWithdrawalsFound": "출금 내역이 없습니다"
|
||||
"expirationLabel": "만료:"
|
||||
},
|
||||
"deposit": {
|
||||
"title": "입금",
|
||||
@ -217,11 +201,7 @@
|
||||
"addressPlaceholder": "0x...",
|
||||
"amountMustBePositive": "금액은 0보다 커야 합니다",
|
||||
"insufficientBalance": "잔액 부족",
|
||||
"close": "닫기",
|
||||
"connectFailed": "지갑 연결에 실패했습니다",
|
||||
"connectWalletFirst": "먼저 지갑을 연결해 주세요",
|
||||
"externalWalletRequired": "사용자 정보에 외부 지갑 주소가 필요합니다",
|
||||
"customAddressNotSupported": "이 출금에서는 사용자 지정 주소가 지원되지 않습니다."
|
||||
"close": "닫기"
|
||||
},
|
||||
"locale": {
|
||||
"zh": "简体中文",
|
||||
|
||||
@ -17,8 +17,7 @@
|
||||
"toast": {
|
||||
"orderSuccess": "下单成功",
|
||||
"splitSuccess": "拆分成功",
|
||||
"mergeSuccess": "合并成功",
|
||||
"claimSuccess": "领取成功"
|
||||
"mergeSuccess": "合并成功"
|
||||
},
|
||||
"trade": {
|
||||
"buy": "买入",
|
||||
@ -164,22 +163,7 @@
|
||||
"expiration": "到期",
|
||||
"activity": "活动",
|
||||
"view": "查看",
|
||||
"expirationLabel": "到期",
|
||||
"youWon": "您赢得 ${amount}",
|
||||
"claim": "领取",
|
||||
"withdrawals": "提现记录",
|
||||
"withdrawStatusAll": "全部",
|
||||
"withdrawStatusPending": "审核中",
|
||||
"withdrawStatusSuccess": "提现成功",
|
||||
"withdrawStatusRejected": "审核不通过",
|
||||
"withdrawStatusFailed": "提现失败",
|
||||
"withdrawAmount": "金额",
|
||||
"withdrawRequestNo": "申请单号",
|
||||
"withdrawChain": "链",
|
||||
"withdrawTime": "时间",
|
||||
"withdrawStatus": "状态",
|
||||
"noWithdrawalsFound": "暂无提现记录",
|
||||
"withdrawAddress": "提现地址"
|
||||
"expirationLabel": "到期"
|
||||
},
|
||||
"deposit": {
|
||||
"title": "入金",
|
||||
@ -217,23 +201,7 @@
|
||||
"addressPlaceholder": "0x...",
|
||||
"amountMustBePositive": "金额必须大于 0",
|
||||
"insufficientBalance": "余额不足",
|
||||
"close": "关闭",
|
||||
"withdrawals": "提现记录",
|
||||
"withdrawStatusAll": "全部",
|
||||
"withdrawStatusPending": "审核中",
|
||||
"withdrawStatusSuccess": "提现成功",
|
||||
"withdrawStatusRejected": "审核不通过",
|
||||
"withdrawStatusFailed": "提现失败",
|
||||
"withdrawAmount": "金额",
|
||||
"withdrawRequestNo": "申请单号",
|
||||
"withdrawChain": "链",
|
||||
"withdrawTime": "时间",
|
||||
"withdrawStatus": "状态",
|
||||
"noWithdrawalsFound": "暂无提现记录",
|
||||
"connectFailed": "连接钱包失败",
|
||||
"connectWalletFirst": "请先连接钱包",
|
||||
"externalWalletRequired": "用户信息中缺少外部钱包地址",
|
||||
"customAddressNotSupported": "暂不支持自定义地址提现。"
|
||||
"close": "关闭"
|
||||
},
|
||||
"locale": {
|
||||
"zh": "简体中文",
|
||||
|
||||
@ -17,8 +17,7 @@
|
||||
"toast": {
|
||||
"orderSuccess": "下單成功",
|
||||
"splitSuccess": "拆分成功",
|
||||
"mergeSuccess": "合併成功",
|
||||
"claimSuccess": "領取成功"
|
||||
"mergeSuccess": "合併成功"
|
||||
},
|
||||
"trade": {
|
||||
"buy": "買入",
|
||||
@ -164,22 +163,7 @@
|
||||
"expiration": "到期",
|
||||
"activity": "活動",
|
||||
"view": "查看",
|
||||
"expirationLabel": "到期",
|
||||
"youWon": "您贏得 ${amount}",
|
||||
"claim": "領取",
|
||||
"withdrawals": "提現記錄",
|
||||
"withdrawStatusAll": "全部",
|
||||
"withdrawStatusPending": "審核中",
|
||||
"withdrawStatusSuccess": "提現成功",
|
||||
"withdrawStatusRejected": "審核不通過",
|
||||
"withdrawStatusFailed": "提現失敗",
|
||||
"withdrawAmount": "金額",
|
||||
"withdrawAddress": "提現地址",
|
||||
"withdrawRequestNo": "申請單號",
|
||||
"withdrawChain": "鏈",
|
||||
"withdrawTime": "時間",
|
||||
"withdrawStatus": "狀態",
|
||||
"noWithdrawalsFound": "暫無提現記錄"
|
||||
"expirationLabel": "到期"
|
||||
},
|
||||
"deposit": {
|
||||
"title": "入金",
|
||||
@ -217,11 +201,7 @@
|
||||
"addressPlaceholder": "0x...",
|
||||
"amountMustBePositive": "金額必須大於 0",
|
||||
"insufficientBalance": "餘額不足",
|
||||
"close": "關閉",
|
||||
"connectFailed": "連接錢包失敗",
|
||||
"connectWalletFirst": "請先連接錢包",
|
||||
"externalWalletRequired": "用戶資訊中缺少外部錢包地址",
|
||||
"customAddressNotSupported": "暫不支援自訂地址提現。"
|
||||
"close": "關閉"
|
||||
},
|
||||
"locale": {
|
||||
"zh": "繁體中文",
|
||||
|
||||
@ -62,13 +62,6 @@
|
||||
:key="pos.id"
|
||||
class="position-row-item"
|
||||
>
|
||||
<div class="position-row-header">
|
||||
<div class="position-row-icon" :class="pos.iconClass">
|
||||
<img v-if="pos.imageUrl" :src="pos.imageUrl" alt="" class="position-row-icon-img" />
|
||||
<span v-else class="position-row-icon-char">{{ pos.iconChar || '•' }}</span>
|
||||
</div>
|
||||
<span class="position-row-title">{{ pos.market }}</span>
|
||||
</div>
|
||||
<div class="position-row-main">
|
||||
<span :class="['position-outcome-pill', pos.outcomePillClass]">{{ pos.outcomeTag }}</span>
|
||||
<span v-if="pos.locked" class="position-lock-badge">
|
||||
@ -362,12 +355,6 @@ import {
|
||||
type OpenOrderDisplayItem,
|
||||
} from '../api/order'
|
||||
import { cancelOrder as apiCancelOrder } from '../api/order'
|
||||
import {
|
||||
normalizeChartData,
|
||||
fetchChartHistory,
|
||||
type ChartDataPoint,
|
||||
type ChartTimeRange,
|
||||
} from '../api/chart'
|
||||
|
||||
const { t } = useI18n()
|
||||
import {
|
||||
@ -1175,8 +1162,8 @@ const timeRanges = [
|
||||
{ label: 'ALL', value: 'ALL' },
|
||||
]
|
||||
|
||||
// 图表数据格式:[时间戳(ms), 概率(0-100)][]。接口约定见 src/api/chart.ts(ChartHistoryParams / ChartHistoryItem / normalizeChartData)
|
||||
function generateData(range: string): ChartDataPoint[] {
|
||||
// 时间轴数据 [timestamp, value][],按粒度生成
|
||||
function generateData(range: string): [number, number][] {
|
||||
const now = Date.now()
|
||||
const data: [number, number][] = []
|
||||
let stepMs: number
|
||||
@ -1351,15 +1338,6 @@ function initChart() {
|
||||
chartInstance.setOption(buildOption(data.value, w))
|
||||
}
|
||||
|
||||
/** 从接口拉取图表数据(接入时在 updateChartData 中调用并赋给 data.value) */
|
||||
async function loadChartFromApi(marketId: string): Promise<ChartDataPoint[]> {
|
||||
const res = await fetchChartHistory(
|
||||
{ marketID: marketId, range: selectedTimeRange.value as ChartTimeRange }
|
||||
)
|
||||
return normalizeChartData(res.data ?? [])
|
||||
}
|
||||
|
||||
// 使用接口时:在 updateChartData 内先 await loadChartFromApi(marketId),再 setOption;暂无接口时用 generateData
|
||||
function updateChartData() {
|
||||
data.value = generateData(selectedTimeRange.value)
|
||||
const w = chartContainerRef.value?.clientWidth
|
||||
@ -1989,54 +1967,6 @@ onUnmounted(() => {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.position-row-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.position-row-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.position-row-icon.pill-yes {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
}
|
||||
|
||||
.position-row-icon.pill-down {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
}
|
||||
|
||||
.position-row-icon-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.position-row-icon-char {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.position-row-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
line-height: 1.3;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.position-row-main,
|
||||
.order-row-main {
|
||||
display: flex;
|
||||
|
||||
@ -78,37 +78,12 @@
|
||||
</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
|
||||
@ -154,19 +129,6 @@
|
||||
{{ 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>
|
||||
@ -207,9 +169,7 @@
|
||||
>
|
||||
<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>
|
||||
<span class="position-icon-char">{{ pos.iconChar }}</span>
|
||||
</div>
|
||||
<div class="position-mobile-main">
|
||||
<div class="position-mobile-title">{{ pos.market }}</div>
|
||||
@ -309,9 +269,10 @@
|
||||
<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>
|
||||
<v-icon v-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>
|
||||
@ -541,71 +502,6 @@
|
||||
</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">
|
||||
@ -691,9 +587,7 @@
|
||||
<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>
|
||||
<span class="position-icon-char">{{ sellPositionItem.iconChar }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="sell-dialog-title">Sell {{ sellPositionItem.sellOutcome || 'Position' }}</h3>
|
||||
@ -736,13 +630,7 @@ import { useLocaleStore } from '../stores/locale'
|
||||
import { useAuthError } from '../composables/useAuthError'
|
||||
import { cancelOrder as apiCancelOrder } from '../api/order'
|
||||
import { getOrderList, mapOrderToHistoryItem, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
|
||||
import { getPositionList, mapPositionToDisplayItem, claimPosition } from '../api/position'
|
||||
import {
|
||||
getSettlementRequestsListClient,
|
||||
amountToUsdcDisplay,
|
||||
WITHDRAW_STATUS,
|
||||
type SettlementRequestClientItem,
|
||||
} from '../api/pmset'
|
||||
import { getPositionList, mapPositionToDisplayItem } from '../api/position'
|
||||
import {
|
||||
MOCK_TOKEN_ID,
|
||||
MOCK_WALLET_POSITIONS,
|
||||
@ -751,7 +639,6 @@ import {
|
||||
} 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()
|
||||
@ -766,69 +653,8 @@ const plTimeRanges = computed(() => [
|
||||
{ label: t('wallet.pl1M'), value: '1M' },
|
||||
{ label: t('wallet.plAll'), value: 'ALL' },
|
||||
])
|
||||
const activeTab = ref<'positions' | 'orders' | 'history' | 'withdrawals'>('positions')
|
||||
const activeTab = ref<'positions' | 'orders' | 'history'>('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;若后端有 needClaim 则仅 needClaim 为 true) */
|
||||
const unsettledItems = computed(() => {
|
||||
const list = currentPositionList.value
|
||||
return list
|
||||
.filter(
|
||||
(p) =>
|
||||
p.marketID &&
|
||||
p.tokenID &&
|
||||
(p.needClaim === undefined || p.needClaim === 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)
|
||||
@ -853,7 +679,6 @@ interface Position {
|
||||
icon?: string
|
||||
iconChar?: string
|
||||
iconClass?: string
|
||||
imageUrl?: string
|
||||
outcomeTag?: string
|
||||
outcomePillClass?: string
|
||||
shares: string
|
||||
@ -868,12 +693,6 @@ interface Position {
|
||||
sellOutcome?: string
|
||||
/** 移动端副标题 "on Up/Down to win" 中的词 */
|
||||
outcomeWord?: string
|
||||
/** 市场 ID(从持仓列表来,用于领取结算) */
|
||||
marketID?: string
|
||||
/** Token ID(从持仓列表来,用于领取结算) */
|
||||
tokenID?: string
|
||||
/** 是否待领取/未结算(后端可选,无则按有 marketID+tokenID 视为可领取) */
|
||||
needClaim?: boolean
|
||||
}
|
||||
|
||||
/** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */
|
||||
@ -924,11 +743,6 @@ interface HistoryItem {
|
||||
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)
|
||||
@ -1071,75 +885,6 @@ 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 {
|
||||
const q = search.value.trim().toLowerCase()
|
||||
return !q || text.toLowerCase().includes(q)
|
||||
@ -1190,22 +935,17 @@ 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(() =>
|
||||
@ -1220,17 +960,11 @@ watch(activeTab, (tab) => {
|
||||
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
|
||||
@ -1482,8 +1216,6 @@ onUnmounted(() => {
|
||||
|
||||
function onWithdrawSuccess() {
|
||||
withdrawDialogOpen.value = false
|
||||
userStore.fetchUsdcBalance()
|
||||
if (activeTab.value === 'withdrawals') loadWithdrawals()
|
||||
}
|
||||
|
||||
function onAuthorizeClick() {
|
||||
@ -1600,47 +1332,6 @@ async function submitAuthorize() {
|
||||
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;
|
||||
}
|
||||
@ -1739,13 +1430,6 @@ async function submitAuthorize() {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.position-icon-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.position-market-info {
|
||||
min-width: 0;
|
||||
}
|
||||
@ -2197,82 +1881,6 @@ async function submitAuthorize() {
|
||||
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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user