diff --git a/docs/api/position.md b/docs/api/position.md index fc1eb1f..7238776 100644 --- a/docs/api/position.md +++ b/docs/api/position.md @@ -4,12 +4,62 @@ ## 功能用途 -持仓相关 API:分页获取持仓列表,以及将 `ClobPositionItem` 映射为钱包展示项。`PageResult` 来自 `@/api/types`,使用 `buildQuery` 构建请求参数。 +持仓相关 API:分页获取持仓列表,以及将 `ClobPositionItem` 映射为钱包展示项。`PageResult` 来自 `@/api/types`,使用 `buildQuery` 构建请求参数。接口定义以 Swagger doc.json 为准。 ## 核心能力 -- `getPositionList`:分页获取持仓列表(需鉴权) -- `mapPositionToDisplayItem`:将接口项转为展示结构(含 locked、availableSharesNum、outcome 等);`outcome` 保留 API 原始值(如 "Up"/"Down"、"Yes"/"No"),供 TradeDetail 与市场 outcomes 匹配 +- `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(实际返回结构) + +| 字段 | 类型 | 说明 | +|------|------|------| +| 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 列表 | ## 使用方式 diff --git a/src/api/chart.ts b/src/api/chart.ts new file mode 100644 index 0000000..355472f --- /dev/null +++ b/src/api/chart.ts @@ -0,0 +1,102 @@ +/** + * 市场概率/价格历史图表 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 + 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 } +): Promise> { + // TODO: 替换为真实 request.get/post 调用 + // const res = await get(`/market/${params.marketID}/chart`, { params: { range: params.range } }) + // return { ...res, data: normalizeChartData(res.data) } + return { code: 0, data: [], msg: '' } +} diff --git a/src/api/mockData.ts b/src/api/mockData.ts index 65e5d0d..e394463 100644 --- a/src/api/mockData.ts +++ b/src/api/mockData.ts @@ -70,6 +70,10 @@ export interface MockPosition { valueChangeLoss?: boolean sellOutcome?: string outcomeWord?: string + /** 用于领取结算(mock 时可选,便于展示未结算行) */ + marketID?: string + tokenID?: string + needClaim?: boolean } export interface MockOpenOrder { @@ -123,6 +127,9 @@ export const MOCK_WALLET_POSITIONS: MockPosition[] = [ valueChangeLoss: true, sellOutcome: 'Down', outcomeWord: 'Down', + marketID: 'mock-market-1', + tokenID: MOCK_TOKEN_ID, + needClaim: true, }, { id: 'p2', diff --git a/src/api/position.ts b/src/api/position.ts index 84f68e7..a9315ce 100644 --- a/src/api/position.ts +++ b/src/api/position.ts @@ -1,16 +1,34 @@ -import { buildQuery, get } from './request' +import { buildQuery, get, post } from './request' +import type { ApiResponse } from './types' import type { PageResult } from './types' export type { PageResult } /** - * 持仓项(与 /clob/position/getPositionList 实际返回对齐) - * size、available、cost 为字符串,6 位小数(除以 1000000 得实际值) + * 持仓项内嵌的市场信息(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 为内嵌市场详情 */ export interface ClobPositionItem { ID?: number userID?: number - marketID?: string + /** 市场 ID,可能为 number 或 string */ + marketID?: number | string tokenId?: string /** 份额,字符串,6 位小数 */ size?: string @@ -20,9 +38,16 @@ export interface ClobPositionItem { lock?: string /** 成本,字符串,6 位小数 */ cost?: string - /** 方向:Yes | No */ + /** 方向:Yes | No 或 Up | Down 等 */ outcome?: string version?: number + /** 是否待领取结算 */ + needClaim?: boolean + /** 内嵌市场详情(问题、outcomes、outcomePrices 等) */ + market?: ClobPositionMarket + /** 创建时间 */ + createdAt?: string + updatedAt?: string CreatedAt?: string UpdatedAt?: string [key: string]: unknown @@ -79,11 +104,37 @@ export async function getPositionList( return get('/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 }, +): Promise { + return post('/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 @@ -105,15 +156,43 @@ 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 位小数字符串 + * size、available、cost 为 6 位小数字符串;market 有 question 时优先展示问题 */ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplayItem { const id = String(pos.ID ?? '') - const market = pos.marketID ?? '' + const market = pos.market?.question ?? String(pos.marketID ?? pos.market?.ID ?? '') const sizeRaw = parsePosNum(pos.size ?? pos.available) const availableRaw = parsePosNum(pos.available) const costRaw = parsePosNum(pos.cost) @@ -131,11 +210,32 @@ 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, @@ -144,8 +244,14 @@ 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, } } diff --git a/src/locales/en.json b/src/locales/en.json index e7f28e5..7a98b3c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -17,7 +17,8 @@ "toast": { "orderSuccess": "Order placed successfully", "splitSuccess": "Split successful", - "mergeSuccess": "Merge successful" + "mergeSuccess": "Merge successful", + "claimSuccess": "Claim successful" }, "trade": { "buy": "Buy", @@ -163,7 +164,9 @@ "expiration": "EXPIRATION", "activity": "ACTIVITY", "view": "View", - "expirationLabel": "Expiration:" + "expirationLabel": "Expiration:", + "youWon": "You won ${amount}", + "claim": "Claim" }, "deposit": { "title": "Deposit", diff --git a/src/locales/ja.json b/src/locales/ja.json index d04b66f..8f359d7 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -17,7 +17,8 @@ "toast": { "orderSuccess": "注文が完了しました", "splitSuccess": "スプリット成功", - "mergeSuccess": "マージ成功" + "mergeSuccess": "マージ成功", + "claimSuccess": "受け取り完了" }, "trade": { "buy": "買う", @@ -163,7 +164,9 @@ "expiration": "有効期限", "activity": "アクティビティ", "view": "表示", - "expirationLabel": "有効期限:" + "expirationLabel": "有効期限:", + "youWon": "獲得 $${amount}", + "claim": "受け取る" }, "deposit": { "title": "入金", diff --git a/src/locales/ko.json b/src/locales/ko.json index e16dc59..fd85aa5 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -17,7 +17,8 @@ "toast": { "orderSuccess": "주문이 완료되었습니다", "splitSuccess": "분할 완료", - "mergeSuccess": "병합 완료" + "mergeSuccess": "병합 완료", + "claimSuccess": "수령 완료" }, "trade": { "buy": "매수", @@ -163,7 +164,9 @@ "expiration": "만료", "activity": "활동", "view": "보기", - "expirationLabel": "만료:" + "expirationLabel": "만료:", + "youWon": "당첨 $${amount}", + "claim": "수령" }, "deposit": { "title": "입금", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index bd5e24e..7ea6934 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -17,7 +17,8 @@ "toast": { "orderSuccess": "下单成功", "splitSuccess": "拆分成功", - "mergeSuccess": "合并成功" + "mergeSuccess": "合并成功", + "claimSuccess": "领取成功" }, "trade": { "buy": "买入", @@ -163,7 +164,9 @@ "expiration": "到期", "activity": "活动", "view": "查看", - "expirationLabel": "到期" + "expirationLabel": "到期", + "youWon": "您赢得 ${amount}", + "claim": "领取" }, "deposit": { "title": "入金", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index f060014..28b27d2 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -17,7 +17,8 @@ "toast": { "orderSuccess": "下單成功", "splitSuccess": "拆分成功", - "mergeSuccess": "合併成功" + "mergeSuccess": "合併成功", + "claimSuccess": "領取成功" }, "trade": { "buy": "買入", @@ -163,7 +164,9 @@ "expiration": "到期", "activity": "活動", "view": "查看", - "expirationLabel": "到期" + "expirationLabel": "到期", + "youWon": "您贏得 ${amount}", + "claim": "領取" }, "deposit": { "title": "入金", diff --git a/src/views/TradeDetail.vue b/src/views/TradeDetail.vue index 5576379..bb0b2e0 100644 --- a/src/views/TradeDetail.vue +++ b/src/views/TradeDetail.vue @@ -62,6 +62,13 @@ :key="pos.id" class="position-row-item" > +
+
+ + {{ pos.iconChar || '•' }} +
+ {{ pos.market }} +
{{ pos.outcomeTag }} @@ -355,6 +362,12 @@ 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 { @@ -1162,8 +1175,8 @@ const timeRanges = [ { label: 'ALL', value: 'ALL' }, ] -// 时间轴数据 [timestamp, value][],按粒度生成 -function generateData(range: string): [number, number][] { +// 图表数据格式:[时间戳(ms), 概率(0-100)][]。接口约定见 src/api/chart.ts(ChartHistoryParams / ChartHistoryItem / normalizeChartData) +function generateData(range: string): ChartDataPoint[] { const now = Date.now() const data: [number, number][] = [] let stepMs: number @@ -1338,6 +1351,15 @@ function initChart() { chartInstance.setOption(buildOption(data.value, w)) } +/** 从接口拉取图表数据(接入时在 updateChartData 中调用并赋给 data.value) */ +async function loadChartFromApi(marketId: string): Promise { + 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 @@ -1967,6 +1989,54 @@ 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; diff --git a/src/views/Wallet.vue b/src/views/Wallet.vue index a31be82..6c6216d 100644 --- a/src/views/Wallet.vue +++ b/src/views/Wallet.vue @@ -78,6 +78,30 @@ + + + + +
+
+ {{ t('wallet.youWon', { amount: unsettledTotalText }) }} + +{{ unsettledCount - 1 }} +
+ + {{ t('wallet.claim') }} + +
+
+
+
+
@@ -169,7 +193,9 @@ >
- {{ pos.iconChar }} + + {{ pos.icon }} + {{ pos.iconChar || '•' }}
{{ pos.market }}
@@ -269,10 +295,9 @@
- {{ - pos.icon - }} - {{ pos.iconChar }} + + {{ pos.icon }} + {{ pos.iconChar || '•' }}
{{ pos.market }} @@ -587,7 +612,9 @@
- {{ sellPositionItem.iconChar }} + + {{ sellPositionItem.icon }} + {{ sellPositionItem.iconChar || '•' }}

Sell {{ sellPositionItem.sellOutcome || 'Position' }}

@@ -630,7 +657,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 } from '../api/position' +import { getPositionList, mapPositionToDisplayItem, claimPosition } from '../api/position' import { MOCK_TOKEN_ID, MOCK_WALLET_POSITIONS, @@ -639,6 +666,7 @@ 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() @@ -655,6 +683,59 @@ const plTimeRanges = computed(() => [ ]) const activeTab = ref<'positions' | 'orders' | 'history'>('positions') const search = ref('') +/** 当前展示的持仓列表(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) @@ -679,6 +760,7 @@ interface Position { icon?: string iconChar?: string iconClass?: string + imageUrl?: string outcomeTag?: string outcomePillClass?: string shares: string @@ -693,6 +775,12 @@ 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] */ @@ -1332,6 +1420,47 @@ 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; } @@ -1430,6 +1559,13 @@ async function submitAuthorize() { color: inherit; } +.position-icon-img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: inherit; +} + .position-market-info { min-width: 0; }