新增:持仓数据接口对接
This commit is contained in:
parent
38d70e3e56
commit
3e329c307e
@ -16,19 +16,38 @@ description: Interprets the XTrader API from the Swagger 2.0 spec at https://api
|
|||||||
### 强制动作序列(必须依次执行)
|
### 强制动作序列(必须依次执行)
|
||||||
|
|
||||||
1. **收到对接请求** → 立即用 `mcp_web_fetch` 或 `curl` 获取 `https://api.xtrader.vip/swagger/doc.json`
|
1. **收到对接请求** → 立即用 `mcp_web_fetch` 或 `curl` 获取 `https://api.xtrader.vip/swagger/doc.json`
|
||||||
2. **第一步** → 解析 `paths["<path>"]["<method>"]` 与 `definitions`,**在对话中输出**:
|
2. **检查接口是否存在** → 若 `paths["<path>"]` 或 `paths["<path>"]["<method>"]` **不存在**,则:
|
||||||
|
- ❌ **禁止**自行猜测或虚构数据结构并自动对接
|
||||||
|
- ✅ **必须**在对话中明确告知用户「该接口在 doc.json 中不存在」
|
||||||
|
- ✅ **必须**向用户询问:
|
||||||
|
- 是否仍要对接?若对接,请提供**请求参数**与**响应数据结构**(或示例 JSON)
|
||||||
|
- 用户可选择**不对接**,则流程终止
|
||||||
|
- 仅在用户明确提供数据结构或确认对接后,才可进入第二步
|
||||||
|
3. **第一步**(接口存在时)→ 解析 `paths["<path>"]["<method>"]` 与 `definitions`,**在对话中输出**:
|
||||||
- 请求参数表(Query/Body/鉴权)
|
- 请求参数表(Query/Body/鉴权)
|
||||||
- 响应参数表(200 schema)
|
- 响应参数表(200 schema)
|
||||||
- data 的 `$ref` 对应 definitions 的**完整字段表**
|
- data 的 `$ref` 对应 definitions 的**完整字段表**
|
||||||
- 输出后**明确标注「第一步完成」**
|
- 输出后**明确标注「第一步完成」**
|
||||||
3. **第二步** → 在 `src/api/` 中根据第一步表格定义 TypeScript 类型,**不得在第一步完成前执行**
|
4. **第二步** → 在 `src/api/` 中根据第一步表格(或用户提供的数据结构)定义 TypeScript 类型,**不得在第一步完成前执行**
|
||||||
4. **第三步** → 实现请求函数并集成到页面,**不得在第二步完成前执行**
|
5. **第三步** → 实现请求函数并集成到页面,**不得在第二步完成前执行**
|
||||||
|
|
||||||
### 禁止行为
|
### 禁止行为
|
||||||
|
|
||||||
- ❌ 在对话中输出第一步结果**之前**写任何 `src/api/` 或 `src/views/` 业务代码
|
- ❌ 在对话中输出第一步结果**之前**写任何 `src/api/` 或 `src/views/` 业务代码
|
||||||
- ❌ 跳过第一步直接定义类型或实现请求
|
- ❌ 跳过第一步直接定义类型或实现请求
|
||||||
- ❌ 合并步骤(如边输出边写代码)
|
- ❌ 合并步骤(如边输出边写代码)
|
||||||
|
- ❌ **接口文档不存在时**:自行猜测数据结构并自动对接;必须向用户询问后再决定是否对接
|
||||||
|
|
||||||
|
### 接口文档不存在时的处理流程
|
||||||
|
|
||||||
|
当 `paths["<path>"]` 在 doc.json 中**不存在**时:
|
||||||
|
|
||||||
|
1. **立即停止**:不写任何对接代码,不猜测数据结构
|
||||||
|
2. **明确告知**:在对话中说明「该接口在 Swagger doc.json 中不存在」
|
||||||
|
3. **向用户询问**:
|
||||||
|
- 是否仍要对接?若对接,请提供请求参数与响应数据结构(或示例 JSON)
|
||||||
|
- 用户可选择**不对接**,则流程终止
|
||||||
|
4. **仅在用户明确提供**后,才继续执行第二步、第三步
|
||||||
|
|
||||||
### 第一步输出模板(必须包含)
|
### 第一步输出模板(必须包含)
|
||||||
|
|
||||||
@ -163,6 +182,7 @@ Swagger UI 页面(如 [PmEvent findPmEvent](https://api.xtrader.vip/swagger/in
|
|||||||
|
|
||||||
## 简要检查清单
|
## 简要检查清单
|
||||||
|
|
||||||
|
- [ ] **接口存在性**:若 doc.json 中无该 path,已向用户询问数据结构,未擅自猜测对接
|
||||||
- [ ] **按规范顺序**:已先列出请求参数与响应参数,再建 Model,最后集成到页面
|
- [ ] **按规范顺序**:已先列出请求参数与响应参数,再建 Model,最后集成到页面
|
||||||
- [ ] 规范 URL 使用 `https://api.xtrader.vip/swagger/doc.json`,或本地缓存与之一致
|
- [ ] 规范 URL 使用 `https://api.xtrader.vip/swagger/doc.json`,或本地缓存与之一致
|
||||||
- [ ] 请求 path、method、query/body 与 `paths` 一致
|
- [ ] 请求 path、method、query/body 与 `paths` 一致
|
||||||
|
|||||||
@ -38,12 +38,21 @@ export interface OrderListResponse {
|
|||||||
msg: string
|
msg: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 订单状态:1=未成交(Live),2=已成交,0=已取消等 */
|
||||||
|
export const OrderStatus = {
|
||||||
|
Live: 1,
|
||||||
|
Matched: 2,
|
||||||
|
Cancelled: 0,
|
||||||
|
} as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /clob/order/getOrderList 请求参数
|
* GET /clob/order/getOrderList 请求参数
|
||||||
*/
|
*/
|
||||||
export interface GetOrderListParams {
|
export interface GetOrderListParams {
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
|
/** 订单状态筛选:1=未成交 */
|
||||||
|
status?: number
|
||||||
startCreatedAt?: string
|
startCreatedAt?: string
|
||||||
endCreatedAt?: string
|
endCreatedAt?: string
|
||||||
marketID?: string
|
marketID?: string
|
||||||
@ -60,8 +69,10 @@ export async function getOrderList(
|
|||||||
params: GetOrderListParams = {},
|
params: GetOrderListParams = {},
|
||||||
config?: { headers?: Record<string, string> },
|
config?: { headers?: Record<string, string> },
|
||||||
): Promise<OrderListResponse> {
|
): Promise<OrderListResponse> {
|
||||||
const { page = 1, pageSize = 10, startCreatedAt, endCreatedAt, marketID, tokenID, userID } = params
|
const { page = 1, pageSize = 10, status, startCreatedAt, endCreatedAt, marketID, tokenID, userID } =
|
||||||
|
params
|
||||||
const query: Record<string, string | number | undefined> = { page, pageSize }
|
const query: Record<string, string | number | undefined> = { page, pageSize }
|
||||||
|
if (status != null && Number.isFinite(status)) query.status = status
|
||||||
if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt
|
if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt
|
||||||
if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt
|
if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt
|
||||||
if (marketID != null && marketID !== '') query.marketID = marketID
|
if (marketID != null && marketID !== '') query.marketID = marketID
|
||||||
@ -136,3 +147,59 @@ export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem
|
|||||||
shares: String(size),
|
shares: String(size),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 钱包 Open Orders 展示项(与 Wallet.vue OpenOrder 一致) */
|
||||||
|
export interface OpenOrderDisplayItem {
|
||||||
|
id: string
|
||||||
|
market: string
|
||||||
|
side: 'Yes' | 'No'
|
||||||
|
outcome: string
|
||||||
|
price: string
|
||||||
|
filled: string
|
||||||
|
total: string
|
||||||
|
expiration: string
|
||||||
|
actionLabel?: string
|
||||||
|
filledDisplay?: string
|
||||||
|
orderID?: number
|
||||||
|
tokenID?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OrderType GTC=0 表示 Until Cancelled */
|
||||||
|
const OrderType = { GTC: 0, GTD: 1 } as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 ClobOrderItem 映射为钱包 Open Orders 展示项(未成交订单)
|
||||||
|
* price 为整数(已乘 10000)
|
||||||
|
*/
|
||||||
|
export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayItem {
|
||||||
|
const id = String(order.ID ?? '')
|
||||||
|
const market = order.market ?? ''
|
||||||
|
const sideNum = order.side ?? Side.Buy
|
||||||
|
const side = sideNum === Side.Sell ? 'No' : 'Yes'
|
||||||
|
const outcome = order.outcome || (side === 'Yes' ? 'Yes' : 'No')
|
||||||
|
const priceBps = order.price ?? 0
|
||||||
|
const priceCents = Math.round(priceBps / 100)
|
||||||
|
const price = `${priceCents}¢`
|
||||||
|
const originalSize = order.originalSize ?? 0
|
||||||
|
const sizeMatched = order.sizeMatched ?? 0
|
||||||
|
const filled = `${sizeMatched}/${originalSize}`
|
||||||
|
const totalUsd = (priceBps / 10000) * originalSize
|
||||||
|
const total = `$${totalUsd.toFixed(2)}`
|
||||||
|
const expiration =
|
||||||
|
order.orderType === OrderType.GTC ? 'Until Cancelled' : order.expiration?.toString() ?? ''
|
||||||
|
const actionLabel = sideNum === Side.Buy ? `Buy ${outcome}` : `Sell ${outcome}`
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
market,
|
||||||
|
side,
|
||||||
|
outcome,
|
||||||
|
price,
|
||||||
|
filled,
|
||||||
|
total,
|
||||||
|
expiration,
|
||||||
|
actionLabel,
|
||||||
|
filledDisplay: filled,
|
||||||
|
orderID: order.ID,
|
||||||
|
tokenID: order.assetID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
122
src/api/position.ts
Normal file
122
src/api/position.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { get } from './request'
|
||||||
|
|
||||||
|
/** 分页结果 */
|
||||||
|
export interface PageResult<T> {
|
||||||
|
list: T[]
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 持仓项(与 doc.json definitions["model.ClobPosition"] 对齐)
|
||||||
|
* GET /clob/position/getPositionList 列表项
|
||||||
|
*/
|
||||||
|
export interface ClobPositionItem {
|
||||||
|
ID?: number
|
||||||
|
available?: number
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
marketID?: string
|
||||||
|
side?: number
|
||||||
|
size?: number
|
||||||
|
tokenId?: string
|
||||||
|
userID?: number
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 持仓列表响应 */
|
||||||
|
export interface PositionListResponse {
|
||||||
|
code: number
|
||||||
|
data: PageResult<ClobPositionItem>
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /clob/position/getPositionList 请求参数
|
||||||
|
*/
|
||||||
|
export interface GetPositionListParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
startCreatedAt?: string
|
||||||
|
endCreatedAt?: string
|
||||||
|
marketID?: string
|
||||||
|
tokenID?: string
|
||||||
|
userID?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取持仓列表
|
||||||
|
* GET /clob/position/getPositionList
|
||||||
|
* 需鉴权:x-token、x-user-id
|
||||||
|
*/
|
||||||
|
export async function getPositionList(
|
||||||
|
params: GetPositionListParams = {},
|
||||||
|
config?: { headers?: Record<string, string> },
|
||||||
|
): Promise<PositionListResponse> {
|
||||||
|
const { page = 1, pageSize = 10, startCreatedAt, endCreatedAt, marketID, tokenID, userID } = params
|
||||||
|
const query: Record<string, string | number | undefined> = { page, pageSize }
|
||||||
|
if (startCreatedAt != null && startCreatedAt !== '') query.startCreatedAt = startCreatedAt
|
||||||
|
if (endCreatedAt != null && endCreatedAt !== '') query.endCreatedAt = endCreatedAt
|
||||||
|
if (marketID != null && marketID !== '') query.marketID = marketID
|
||||||
|
if (tokenID != null && tokenID !== '') query.tokenID = tokenID
|
||||||
|
if (userID != null && Number.isFinite(userID)) query.userID = userID
|
||||||
|
return get<PositionListResponse>('/clob/position/getPositionList', query, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 钱包 Positions 展示项(与 Wallet.vue Position 一致) */
|
||||||
|
export interface PositionDisplayItem {
|
||||||
|
id: string
|
||||||
|
market: string
|
||||||
|
shares: string
|
||||||
|
avgNow: string
|
||||||
|
bet: string
|
||||||
|
toWin: string
|
||||||
|
value: string
|
||||||
|
valueChange?: string
|
||||||
|
valueChangePct?: string
|
||||||
|
valueChangeLoss?: boolean
|
||||||
|
sellOutcome?: string
|
||||||
|
outcomeWord?: string
|
||||||
|
iconChar?: string
|
||||||
|
iconClass?: string
|
||||||
|
outcomeTag?: string
|
||||||
|
outcomePillClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Side: Buy=1, Sell=2 */
|
||||||
|
const Side = { Buy: 1, Sell: 2 } as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 ClobPositionItem 映射为钱包 Position 展示项
|
||||||
|
* model.ClobPosition 仅有 size、available、marketID、side,无价格字段,部分展示用占位
|
||||||
|
*/
|
||||||
|
export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplayItem {
|
||||||
|
const id = String(pos.ID ?? '')
|
||||||
|
const market = pos.marketID ?? ''
|
||||||
|
const size = pos.size ?? pos.available ?? 0
|
||||||
|
const shares = `${size} shares`
|
||||||
|
const sideNum = pos.side ?? Side.Buy
|
||||||
|
const outcomeWord = sideNum === Side.Sell ? 'No' : 'Yes'
|
||||||
|
const pillClass = sideNum === Side.Sell ? 'pill-down' : 'pill-yes'
|
||||||
|
const sellOutcome = outcomeWord
|
||||||
|
const valueUsd = size
|
||||||
|
const value = `$${valueUsd.toFixed(2)}`
|
||||||
|
const bet = value
|
||||||
|
const toWin = `$${valueUsd.toFixed(2)}`
|
||||||
|
const avgNow = '—'
|
||||||
|
const outcomeTag = `${outcomeWord} —`
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
market,
|
||||||
|
shares,
|
||||||
|
avgNow,
|
||||||
|
bet,
|
||||||
|
toWin,
|
||||||
|
value,
|
||||||
|
sellOutcome,
|
||||||
|
outcomeWord,
|
||||||
|
outcomeTag,
|
||||||
|
outcomePillClass: pillClass,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,11 +3,49 @@ import { get } from './request'
|
|||||||
const USDC_DECIMALS = 1_000_000
|
const USDC_DECIMALS = 1_000_000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getUserInfo 返回的 data(definitions system.SysUser)
|
* getUserInfo 返回的 data 结构(2026 更新)
|
||||||
* doc.json definitions["system.SysUser"] 完整字段
|
* data 包含 balance、orders、positions、userInfo
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** 余额项(data.balance) */
|
||||||
|
export interface UserInfoBalance {
|
||||||
|
ID?: number
|
||||||
|
CreatedAt?: string
|
||||||
|
UpdatedAt?: string
|
||||||
|
userID?: number
|
||||||
|
marketID?: string
|
||||||
|
tokenType?: string
|
||||||
|
tokenID?: string
|
||||||
|
amount?: string
|
||||||
|
available?: string
|
||||||
|
locked?: string
|
||||||
|
version?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 订单项(data.orders[]) */
|
||||||
|
export interface UserInfoOrder {
|
||||||
|
ID?: number
|
||||||
|
CreatedAt?: string
|
||||||
|
UpdatedAt?: string
|
||||||
|
userID?: number
|
||||||
|
market?: string
|
||||||
|
status?: number
|
||||||
|
assetID?: string
|
||||||
|
side?: number
|
||||||
|
price?: number
|
||||||
|
originalSize?: number
|
||||||
|
sizeMatched?: number
|
||||||
|
outcome?: string
|
||||||
|
expiration?: number
|
||||||
|
orderType?: number
|
||||||
|
feeRateBps?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户信息(data.userInfo) */
|
||||||
export interface UserInfoData {
|
export interface UserInfoData {
|
||||||
ID?: number
|
ID?: number
|
||||||
|
CreatedAt?: string
|
||||||
|
UpdatedAt?: string
|
||||||
userName?: string
|
userName?: string
|
||||||
nickName?: string
|
nickName?: string
|
||||||
headerImg?: string
|
headerImg?: string
|
||||||
@ -15,8 +53,6 @@ export interface UserInfoData {
|
|||||||
authorityId?: number
|
authorityId?: number
|
||||||
authority?: unknown
|
authority?: unknown
|
||||||
authorities?: unknown[]
|
authorities?: unknown[]
|
||||||
createdAt?: string
|
|
||||||
updatedAt?: string
|
|
||||||
email?: string
|
email?: string
|
||||||
phone?: string
|
phone?: string
|
||||||
enable?: number
|
enable?: number
|
||||||
@ -26,12 +62,20 @@ export interface UserInfoData {
|
|||||||
promotionCode?: string
|
promotionCode?: string
|
||||||
puserId?: number
|
puserId?: number
|
||||||
remark?: string
|
remark?: string
|
||||||
originSetting?: Record<string, unknown>
|
originSetting?: Record<string, unknown> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** getUserInfo 完整 data */
|
||||||
|
export interface GetUserInfoData {
|
||||||
|
balance?: UserInfoBalance
|
||||||
|
orders?: UserInfoOrder[]
|
||||||
|
positions?: unknown[]
|
||||||
|
userInfo?: UserInfoData
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetUserInfoResponse {
|
export interface GetUserInfoResponse {
|
||||||
code: number
|
code: number
|
||||||
data?: UserInfoData
|
data?: GetUserInfoData
|
||||||
msg?: string
|
msg?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -70,6 +70,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
reconnectInterval: 2000,
|
reconnectInterval: 2000,
|
||||||
})
|
})
|
||||||
sdk.onBalanceUpdate((data: BalanceData) => {
|
sdk.onBalanceUpdate((data: BalanceData) => {
|
||||||
|
if ((data.tokenType ?? '').toUpperCase() !== 'USDC') return
|
||||||
const avail = data.available ?? data.amount
|
const avail = data.available ?? data.amount
|
||||||
if (avail != null) {
|
if (avail != null) {
|
||||||
balance.value = formatUsdcBalance(String(avail))
|
balance.value = formatUsdcBalance(String(avail))
|
||||||
@ -151,36 +152,38 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 请求用户信息(需已登录),更新 store 中的 user */
|
/** 请求用户信息(需已登录),更新 store 中的 user 与 balance */
|
||||||
async function fetchUserInfo() {
|
async function fetchUserInfo() {
|
||||||
const headers = getAuthHeaders()
|
const headers = getAuthHeaders()
|
||||||
if (!headers) return
|
if (!headers) return
|
||||||
try {
|
try {
|
||||||
const res = await getUserInfo(headers)
|
const res = await getUserInfo(headers)
|
||||||
const data = res.data as Record<string, unknown> | undefined
|
const data = res.data as Record<string, unknown> | undefined
|
||||||
// 接口返回 data.userInfo 或 data.user,取实际用户对象;若仍含 userInfo 则再取一层
|
if (res.code !== 0 && res.code !== 200) return
|
||||||
let u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown>
|
// 更新余额:data.balance.available
|
||||||
if (u?.userInfo && (u.ID == null && u.id == null)) {
|
const bal = data?.balance as { available?: string } | undefined
|
||||||
u = u.userInfo as Record<string, unknown>
|
if (bal?.available != null) {
|
||||||
}
|
balance.value = formatUsdcBalance(String(bal.available))
|
||||||
if ((res.code === 0 || res.code === 200) && u) {
|
|
||||||
const rawId = u.ID ?? u.id
|
|
||||||
const numId =
|
|
||||||
typeof rawId === 'number'
|
|
||||||
? rawId
|
|
||||||
: rawId != null
|
|
||||||
? parseInt(String(rawId), 10)
|
|
||||||
: undefined
|
|
||||||
user.value = {
|
|
||||||
...u,
|
|
||||||
userName: (u.userName ?? u.username) as string | undefined,
|
|
||||||
nickName: (u.nickName ?? u.nickname) as string | undefined,
|
|
||||||
headerImg: (u.headerImg ?? u.avatar ?? u.avatarUrl) as string | undefined,
|
|
||||||
id: (rawId ?? numId) as number | string | undefined,
|
|
||||||
ID: Number.isFinite(numId) ? numId : undefined,
|
|
||||||
} as UserInfo
|
|
||||||
if (token.value && user.value) saveToStorage(token.value, user.value)
|
|
||||||
}
|
}
|
||||||
|
// 更新用户信息:data.userInfo
|
||||||
|
const u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown> | undefined
|
||||||
|
if (!u) return
|
||||||
|
const rawId = u.ID ?? u.id
|
||||||
|
const numId =
|
||||||
|
typeof rawId === 'number'
|
||||||
|
? rawId
|
||||||
|
: rawId != null
|
||||||
|
? parseInt(String(rawId), 10)
|
||||||
|
: undefined
|
||||||
|
user.value = {
|
||||||
|
...u,
|
||||||
|
userName: (u.userName ?? u.username) as string | undefined,
|
||||||
|
nickName: (u.nickName ?? u.nickname) as string | undefined,
|
||||||
|
headerImg: (u.headerImg ?? u.avatar ?? u.avatarUrl) as string | undefined,
|
||||||
|
id: (rawId ?? numId) as number | string | undefined,
|
||||||
|
ID: Number.isFinite(numId) ? numId : undefined,
|
||||||
|
} as UserInfo
|
||||||
|
if (token.value && user.value) saveToStorage(token.value, user.value)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[fetchUserInfo] 请求失败:', e)
|
console.error('[fetchUserInfo] 请求失败:', e)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -154,7 +154,10 @@
|
|||||||
<template v-if="activeTab === 'positions'">
|
<template v-if="activeTab === 'positions'">
|
||||||
<!-- 移动端:可折叠列表 -->
|
<!-- 移动端:可折叠列表 -->
|
||||||
<div v-if="mobile" class="positions-mobile-list">
|
<div v-if="mobile" class="positions-mobile-list">
|
||||||
<template v-if="filteredPositions.length === 0">
|
<template v-if="positionLoading">
|
||||||
|
<div class="empty-cell">{{ t('common.loading') }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="filteredPositions.length === 0">
|
||||||
<div class="empty-cell">{{ t('wallet.noPositionsFound') }}</div>
|
<div class="empty-cell">{{ t('wallet.noPositionsFound') }}</div>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<div
|
||||||
@ -256,7 +259,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="filteredPositions.length === 0">
|
<tr v-if="positionLoading">
|
||||||
|
<td colspan="6" class="empty-cell">{{ t('common.loading') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else-if="filteredPositions.length === 0">
|
||||||
<td colspan="6" class="empty-cell">{{ t('wallet.noPositionsFound') }}</td>
|
<td colspan="6" class="empty-cell">{{ t('wallet.noPositionsFound') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="pos in paginatedPositions" :key="pos.id" class="position-row">
|
<tr v-for="pos in paginatedPositions" :key="pos.id" class="position-row">
|
||||||
@ -325,7 +331,10 @@
|
|||||||
<template v-else-if="activeTab === 'orders'">
|
<template v-else-if="activeTab === 'orders'">
|
||||||
<!-- 移动端:挂单卡片列表 -->
|
<!-- 移动端:挂单卡片列表 -->
|
||||||
<div v-if="mobile" class="orders-mobile-list">
|
<div v-if="mobile" class="orders-mobile-list">
|
||||||
<template v-if="filteredOpenOrders.length === 0">
|
<template v-if="openOrderLoading">
|
||||||
|
<div class="empty-cell">{{ t('common.loading') }}</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="filteredOpenOrders.length === 0">
|
||||||
<div class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</div>
|
<div class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-for="ord in paginatedOpenOrders" :key="ord.id" class="order-mobile-card">
|
<div v-for="ord in paginatedOpenOrders" :key="ord.id" class="order-mobile-card">
|
||||||
@ -374,7 +383,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="filteredOpenOrders.length === 0">
|
<tr v-if="openOrderLoading">
|
||||||
|
<td colspan="8" class="empty-cell">{{ t('common.loading') }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-else-if="filteredOpenOrders.length === 0">
|
||||||
<td colspan="8" class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</td>
|
<td colspan="8" class="empty-cell">{{ t('wallet.noOpenOrdersFound') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="ord in paginatedOpenOrders" :key="ord.id">
|
<tr v-for="ord in paginatedOpenOrders" :key="ord.id">
|
||||||
@ -615,7 +627,8 @@ import DepositDialog from '../components/DepositDialog.vue'
|
|||||||
import WithdrawDialog from '../components/WithdrawDialog.vue'
|
import WithdrawDialog from '../components/WithdrawDialog.vue'
|
||||||
import { useUserStore } from '../stores/user'
|
import { useUserStore } from '../stores/user'
|
||||||
import { pmCancelOrder } from '../api/market'
|
import { pmCancelOrder } from '../api/market'
|
||||||
import { getOrderList, mapOrderToHistoryItem } from '../api/order'
|
import { getOrderList, mapOrderToHistoryItem, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
|
||||||
|
import { getPositionList, mapPositionToDisplayItem } from '../api/position'
|
||||||
import {
|
import {
|
||||||
MOCK_TOKEN_ID,
|
MOCK_TOKEN_ID,
|
||||||
MOCK_WALLET_POSITIONS,
|
MOCK_WALLET_POSITIONS,
|
||||||
@ -724,9 +737,99 @@ interface HistoryItem {
|
|||||||
const positions = ref<Position[]>(
|
const positions = ref<Position[]>(
|
||||||
USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] : [],
|
USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] : [],
|
||||||
)
|
)
|
||||||
|
/** 持仓列表(API 数据,非 mock 时使用) */
|
||||||
|
const positionList = ref<Position[]>([])
|
||||||
|
const positionTotal = ref(0)
|
||||||
|
const positionLoading = ref(false)
|
||||||
|
|
||||||
|
async function loadPositionList() {
|
||||||
|
if (USE_MOCK_WALLET) return
|
||||||
|
const headers = userStore.getAuthHeaders()
|
||||||
|
if (!headers) {
|
||||||
|
positionList.value = []
|
||||||
|
positionTotal.value = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||||
|
const userID = uid != null ? Number(uid) : undefined
|
||||||
|
if (!userID || !Number.isFinite(userID)) {
|
||||||
|
positionList.value = []
|
||||||
|
positionTotal.value = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
positionLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getPositionList(
|
||||||
|
{ page: page.value, pageSize: itemsPerPage.value, userID },
|
||||||
|
{ headers },
|
||||||
|
)
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
const list = res.data?.list ?? []
|
||||||
|
positionList.value = list.map(mapPositionToDisplayItem)
|
||||||
|
positionTotal.value = res.data?.total ?? 0
|
||||||
|
} else {
|
||||||
|
positionList.value = []
|
||||||
|
positionTotal.value = 0
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
positionList.value = []
|
||||||
|
positionTotal.value = 0
|
||||||
|
} finally {
|
||||||
|
positionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const openOrders = ref<OpenOrder[]>(
|
const openOrders = ref<OpenOrder[]>(
|
||||||
USE_MOCK_WALLET ? [...MOCK_WALLET_ORDERS] : [],
|
USE_MOCK_WALLET ? [...MOCK_WALLET_ORDERS] : [],
|
||||||
)
|
)
|
||||||
|
/** 未成交订单(API 数据,非 mock 时使用) */
|
||||||
|
const openOrderList = ref<OpenOrder[]>([])
|
||||||
|
const openOrderTotal = ref(0)
|
||||||
|
const openOrderLoading = ref(false)
|
||||||
|
|
||||||
|
async function loadOpenOrders() {
|
||||||
|
if (USE_MOCK_WALLET) return
|
||||||
|
const headers = userStore.getAuthHeaders()
|
||||||
|
if (!headers) {
|
||||||
|
openOrderList.value = []
|
||||||
|
openOrderTotal.value = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const uid = userStore.user?.id ?? userStore.user?.ID
|
||||||
|
const userID = uid != null ? Number(uid) : undefined
|
||||||
|
if (!userID || !Number.isFinite(userID)) {
|
||||||
|
openOrderList.value = []
|
||||||
|
openOrderTotal.value = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
openOrderLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getOrderList(
|
||||||
|
{
|
||||||
|
page: page.value,
|
||||||
|
pageSize: itemsPerPage.value,
|
||||||
|
userID,
|
||||||
|
status: OrderStatus.Live,
|
||||||
|
},
|
||||||
|
{ headers },
|
||||||
|
)
|
||||||
|
if (res.code === 0 || res.code === 200) {
|
||||||
|
const list = res.data?.list ?? []
|
||||||
|
const openOnly = list.filter((o) => (o.status ?? 1) === OrderStatus.Live)
|
||||||
|
openOrderList.value = openOnly.map(mapOrderToOpenOrderItem)
|
||||||
|
openOrderTotal.value = openOnly.length
|
||||||
|
} else {
|
||||||
|
openOrderList.value = []
|
||||||
|
openOrderTotal.value = 0
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
openOrderList.value = []
|
||||||
|
openOrderTotal.value = 0
|
||||||
|
} finally {
|
||||||
|
openOrderLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const history = ref<HistoryItem[]>(
|
const history = ref<HistoryItem[]>(
|
||||||
USE_MOCK_WALLET ? [...MOCK_WALLET_HISTORY] : [],
|
USE_MOCK_WALLET ? [...MOCK_WALLET_HISTORY] : [],
|
||||||
)
|
)
|
||||||
@ -780,8 +883,14 @@ 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)
|
||||||
}
|
}
|
||||||
const filteredPositions = computed(() => positions.value.filter((p) => matchSearch(p.market)))
|
const filteredPositions = computed(() => {
|
||||||
const filteredOpenOrders = computed(() => openOrders.value.filter((o) => matchSearch(o.market)))
|
const list = USE_MOCK_WALLET ? positions.value : positionList.value
|
||||||
|
return list.filter((p) => matchSearch(p.market))
|
||||||
|
})
|
||||||
|
const filteredOpenOrders = computed(() => {
|
||||||
|
const list = USE_MOCK_WALLET ? openOrders.value : openOrderList.value
|
||||||
|
return list.filter((o) => matchSearch(o.market))
|
||||||
|
})
|
||||||
const filteredHistory = computed(() => {
|
const filteredHistory = computed(() => {
|
||||||
const list = USE_MOCK_WALLET ? history.value : historyList.value
|
const list = USE_MOCK_WALLET ? history.value : historyList.value
|
||||||
return list.filter((h) => matchSearch(h.market))
|
return list.filter((h) => matchSearch(h.market))
|
||||||
@ -795,27 +904,37 @@ function paginate<T>(list: T[]) {
|
|||||||
const start = (page.value - 1) * itemsPerPage.value
|
const start = (page.value - 1) * itemsPerPage.value
|
||||||
return list.slice(start, start + itemsPerPage.value)
|
return list.slice(start, start + itemsPerPage.value)
|
||||||
}
|
}
|
||||||
const paginatedPositions = computed(() => paginate(filteredPositions.value))
|
const paginatedPositions = computed(() => {
|
||||||
const paginatedOpenOrders = computed(() => paginate(filteredOpenOrders.value))
|
if (USE_MOCK_WALLET) return paginate(filteredPositions.value)
|
||||||
|
return filteredPositions.value
|
||||||
|
})
|
||||||
|
const paginatedOpenOrders = computed(() => {
|
||||||
|
if (USE_MOCK_WALLET) return paginate(filteredOpenOrders.value)
|
||||||
|
return filteredOpenOrders.value
|
||||||
|
})
|
||||||
const paginatedHistory = computed(() => {
|
const paginatedHistory = computed(() => {
|
||||||
if (USE_MOCK_WALLET) return paginate(filteredHistory.value)
|
if (USE_MOCK_WALLET) return paginate(filteredHistory.value)
|
||||||
return filteredHistory.value
|
return filteredHistory.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalPagesPositions = computed(() =>
|
const totalPagesPositions = computed(() => {
|
||||||
Math.max(1, Math.ceil(filteredPositions.value.length / itemsPerPage.value)),
|
const total = USE_MOCK_WALLET ? filteredPositions.value.length : positionTotal.value
|
||||||
)
|
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
||||||
const totalPagesOrders = computed(() =>
|
})
|
||||||
Math.max(1, Math.ceil(filteredOpenOrders.value.length / itemsPerPage.value)),
|
const totalPagesOrders = computed(() => {
|
||||||
)
|
const total = USE_MOCK_WALLET ? filteredOpenOrders.value.length : openOrderTotal.value
|
||||||
|
return Math.max(1, Math.ceil(total / itemsPerPage.value))
|
||||||
|
})
|
||||||
const totalPagesHistory = computed(() => {
|
const 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 currentListTotal = computed(() => {
|
const currentListTotal = computed(() => {
|
||||||
if (activeTab.value === 'positions') return filteredPositions.value.length
|
if (activeTab.value === 'positions')
|
||||||
if (activeTab.value === 'orders') return filteredOpenOrders.value.length
|
return USE_MOCK_WALLET ? filteredPositions.value.length : positionTotal.value
|
||||||
|
if (activeTab.value === 'orders')
|
||||||
|
return USE_MOCK_WALLET ? filteredOpenOrders.value.length : openOrderTotal.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(() => {
|
||||||
@ -832,9 +951,13 @@ const currentPageEnd = computed(() =>
|
|||||||
|
|
||||||
watch(activeTab, (tab) => {
|
watch(activeTab, (tab) => {
|
||||||
page.value = 1
|
page.value = 1
|
||||||
|
if (tab === 'positions' && !USE_MOCK_WALLET) loadPositionList()
|
||||||
|
if (tab === 'orders' && !USE_MOCK_WALLET) loadOpenOrders()
|
||||||
if (tab === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
if (tab === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
||||||
})
|
})
|
||||||
watch([page, itemsPerPage], () => {
|
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 === 'history' && !USE_MOCK_WALLET) loadHistoryOrders()
|
||||||
})
|
})
|
||||||
watch([currentListTotal, itemsPerPage], () => {
|
watch([currentListTotal, itemsPerPage], () => {
|
||||||
@ -870,7 +993,12 @@ async function cancelOrder(ord: OpenOrder) {
|
|||||||
try {
|
try {
|
||||||
const res = await pmCancelOrder({ orderID, tokenID, userID }, { headers })
|
const res = await pmCancelOrder({ orderID, tokenID, userID }, { headers })
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
openOrders.value = openOrders.value.filter((o) => o.id !== ord.id)
|
if (USE_MOCK_WALLET) {
|
||||||
|
openOrders.value = openOrders.value.filter((o) => o.id !== ord.id)
|
||||||
|
} else {
|
||||||
|
openOrderList.value = openOrderList.value.filter((o) => o.id !== ord.id)
|
||||||
|
openOrderTotal.value = openOrderList.value.length
|
||||||
|
}
|
||||||
userStore.fetchUsdcBalance()
|
userStore.fetchUsdcBalance()
|
||||||
} else {
|
} else {
|
||||||
cancelOrderError.value = res.msg || '取消失败'
|
cancelOrderError.value = res.msg || '取消失败'
|
||||||
@ -885,7 +1013,12 @@ async function cancelOrder(ord: OpenOrder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cancelAllOrders() {
|
function cancelAllOrders() {
|
||||||
openOrders.value = []
|
if (USE_MOCK_WALLET) {
|
||||||
|
openOrders.value = []
|
||||||
|
} else {
|
||||||
|
openOrderList.value = []
|
||||||
|
openOrderTotal.value = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sellReceiveAmount = computed(() => {
|
const sellReceiveAmount = computed(() => {
|
||||||
@ -1052,6 +1185,7 @@ const handleResize = () => plChartInstance?.resize()
|
|||||||
watch(plRange, () => updatePlChart())
|
watch(plRange, () => updatePlChart())
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (!USE_MOCK_WALLET && activeTab.value === 'positions') loadPositionList()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initPlChart()
|
initPlChart()
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user