Compare commits

..

No commits in common. "062e370bea96e341b3508797379faefbf41523f2" and "99037b536e8734af776fb2dd9d05c51ae02e9f74" have entirely different histories.

21 changed files with 190 additions and 2098 deletions

View File

@ -9,7 +9,7 @@
"name": "Profile Screen", "name": "Profile Screen",
"clip": true, "clip": true,
"width": 402, "width": 402,
"height": 980, "height": 648,
"fill": "$--bg-page", "fill": "$--bg-page",
"layout": "vertical", "layout": "vertical",
"gap": 16, "gap": 16,
@ -94,8 +94,8 @@
"type": "text", "type": "text",
"id": "VLEgU", "id": "VLEgU",
"name": "tagText", "name": "tagText",
"fill": "#a16207", "fill": "$--primary",
"content": "尊享交易权益", "content": "VIP Trader",
"fontFamily": "Inter", "fontFamily": "Inter",
"fontSize": 12, "fontSize": 12,
"fontWeight": "600" "fontWeight": "600"
@ -134,97 +134,6 @@
] ]
} }
] ]
},
{
"type": "frame",
"id": "BQsV9",
"name": "VIP Entry",
"width": "fill_container",
"fill": "#fff7e6",
"cornerRadius": "$--radius-md",
"stroke": {
"align": "inside",
"thickness": 1.5,
"fill": "#e6a817"
},
"gap": 10,
"padding": [
12,
14
],
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "frame",
"id": "AsI1v",
"name": "vipLeft",
"gap": 10,
"alignItems": "center",
"children": [
{
"type": "text",
"id": "tklXN",
"name": "vipTitle",
"fill": "#78350f",
"content": "会员中心",
"fontFamily": "Inter",
"fontSize": 15,
"fontWeight": "600"
}
]
},
{
"type": "frame",
"id": "RjQ0D",
"name": "vipRight",
"gap": 8,
"alignItems": "center",
"children": [
{
"type": "frame",
"id": "sKfYz",
"name": "levelCapsule",
"width": 30,
"height": 22,
"fill": {
"type": "color",
"color": "#c9970a",
"enabled": false
},
"cornerRadius": "$--radius-pill",
"padding": [
8,
14
],
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "N7wG9",
"name": "levelCapsuleText",
"fill": "#c9970a",
"content": "vip3",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
}
]
},
{
"type": "text",
"id": "6K4W4",
"name": "entryChevron",
"fill": "#ca8a04",
"content": "",
"fontFamily": "Inter",
"fontSize": 18,
"fontWeight": "normal"
}
]
}
]
} }
] ]
}, },
@ -6461,387 +6370,6 @@
] ]
} }
] ]
},
{
"type": "frame",
"id": "lvTGn",
"x": 3820,
"y": 0,
"name": "会员中心 Screen",
"clip": true,
"width": 402,
"height": 1180,
"fill": "$--bg-page",
"layout": "vertical",
"gap": 16,
"padding": 16,
"children": [
{
"type": "frame",
"id": "RjQ1F",
"name": "mcHeader",
"width": "fill_container",
"gap": 12,
"alignItems": "center",
"children": [
{
"type": "text",
"id": "EqeEY",
"name": "mcBack",
"fill": "$--text-primary",
"content": "←",
"fontFamily": "Inter",
"fontSize": 22
},
{
"type": "text",
"id": "3ZEsD",
"name": "mcTitle",
"fill": "$--text-primary",
"content": "会员中心",
"fontFamily": "Inter",
"fontSize": 20,
"fontWeight": "700"
}
]
},
{
"type": "frame",
"id": "bX2qC",
"name": "currentLevelCard",
"width": "fill_container",
"fill": "#fff7e6",
"cornerRadius": "$--radius-lg",
"stroke": {
"align": "inside",
"thickness": 1.5,
"fill": "#e6a817"
},
"layout": "vertical",
"gap": 12,
"padding": 20,
"children": [
{
"type": "frame",
"id": "8zw2G",
"name": "heroRow",
"width": "fill_container",
"gap": 10,
"justifyContent": "space_between",
"alignItems": "center",
"children": [
{
"type": "frame",
"id": "h9i8v",
"name": "heroLeft",
"width": "fill_container",
"gap": 8,
"alignItems": "center",
"children": [
{
"type": "text",
"id": "LRQkX",
"name": "heroLbl2",
"fill": "#78350f",
"content": "当前等级",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "500"
},
{
"type": "text",
"id": "Gn048",
"name": "heroLvl",
"fill": "#c9970a",
"content": "VIP 3",
"fontFamily": "Inter",
"fontSize": 16,
"fontWeight": "700"
}
]
},
{
"type": "frame",
"id": "K46YD",
"name": "btnRecharge",
"height": 36,
"fill": "#c9970a",
"cornerRadius": 8,
"padding": [
8,
14
],
"justifyContent": "center",
"alignItems": "center",
"children": [
{
"type": "text",
"id": "zdalb",
"name": "btx",
"fill": "#fffef5",
"content": "去充值",
"fontFamily": "Inter",
"fontSize": 13,
"fontWeight": "600"
}
]
}
]
},
{
"type": "text",
"id": "xINU0",
"name": "heroHint",
"fill": "#a16207",
"content": "距离 vip4 还需累计交易量达到 $500,000",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "500"
}
]
},
{
"type": "frame",
"id": "lEW94",
"name": "mcLevelBlock",
"width": "fill_container",
"layout": "vertical",
"gap": 14,
"children": [
{
"type": "text",
"id": "dgWjb",
"name": "mcExplainTitle",
"fill": "$--text-secondary",
"content": "等级说明",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "600"
},
{
"type": "frame",
"id": "KPIS6",
"name": "mcExplain",
"width": "fill_container",
"layout": "vertical",
"gap": 8,
"children": [
{
"type": "frame",
"id": "RYpWx",
"name": "exRow0",
"width": "fill_container",
"fill": "$--bg-page",
"stroke": {
"align": "inside",
"thickness": 0,
"fill": "$--border-color"
},
"gap": 10,
"justifyContent": "space_between",
"children": [
{
"type": "text",
"id": "HbZIh",
"name": "ex0l",
"fill": "$--text-secondary",
"content": "完成注册即可。",
"lineHeight": 1.45,
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "2T1e2",
"name": "ex0r",
"fill": "$--text-primary",
"textGrowth": "fixed-width",
"width": 52,
"content": "VIP 0",
"textAlign": "right",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "700"
}
]
},
{
"type": "frame",
"id": "brfky",
"name": "exRow1",
"width": "fill_container",
"fill": "$--bg-page",
"stroke": {
"align": "inside",
"thickness": 0,
"fill": "$--border-color"
},
"gap": 10,
"justifyContent": "space_between",
"children": [
{
"type": "text",
"id": "5Q49A",
"name": "r2l",
"fill": "$--text-secondary",
"content": "累计充值 ≥ $10,000 USDC。",
"lineHeight": 1.45,
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "oSw0l",
"name": "r2r",
"fill": "$--text-primary",
"textGrowth": "fixed-width",
"width": 52,
"content": "VIP 1",
"textAlign": "right",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "700"
}
]
},
{
"type": "frame",
"id": "wEUKd",
"name": "exRow2",
"width": "fill_container",
"fill": "$--bg-page",
"stroke": {
"align": "inside",
"thickness": 0,
"fill": "$--border-color"
},
"gap": 10,
"justifyContent": "space_between",
"children": [
{
"type": "text",
"id": "F8h9p",
"name": "r3l",
"fill": "$--text-secondary",
"content": "累计充值 ≥ $50,000 USDC。",
"lineHeight": 1.45,
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "zqbDv",
"name": "r3r",
"fill": "$--text-primary",
"textGrowth": "fixed-width",
"width": 52,
"content": "VIP 2",
"textAlign": "right",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "700"
}
]
},
{
"type": "frame",
"id": "YacFs",
"name": "exRow3",
"width": "fill_container",
"fill": "$--bg-page",
"stroke": {
"align": "inside",
"thickness": 0,
"fill": "$--border-color"
},
"gap": 10,
"justifyContent": "space_between",
"children": [
{
"type": "text",
"id": "UElGL",
"name": "r4l",
"fill": "#78350f",
"content": "累计充值 ≥ $200,000 USDC。",
"lineHeight": 1.45,
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "9HZuo",
"name": "r4r",
"fill": "#c9970a",
"textGrowth": "fixed-width",
"width": 52,
"content": "VIP 3",
"textAlign": "right",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "700"
}
]
},
{
"type": "frame",
"id": "aKf3Y",
"name": "exRow4",
"width": "fill_container",
"fill": "$--bg-page",
"stroke": {
"align": "inside",
"thickness": 0,
"fill": "$--border-color"
},
"gap": 10,
"justifyContent": "space_between",
"children": [
{
"type": "text",
"id": "tIUh6",
"name": "r5l",
"fill": "$--text-secondary",
"content": "累计充值 ≥ $500,000 USDC\n或邀请有效用户 ≥ 50 人。",
"lineHeight": 1.45,
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "normal"
},
{
"type": "text",
"id": "Z1nPr",
"name": "r5r",
"fill": "$--text-primary",
"textGrowth": "fixed-width",
"width": 52,
"content": "VIP 4",
"textAlign": "right",
"fontFamily": "Inter",
"fontSize": 12,
"fontWeight": "700"
}
]
}
]
}
]
},
{
"type": "text",
"id": "83n3g",
"name": "mcFoot",
"fill": "$--text-secondary",
"content": "* 统计数据与门槛以后台规则为准(示意数据,非最终规则)。",
"lineHeight": 1.35,
"fontFamily": "Inter",
"fontSize": 11,
"fontWeight": "normal"
}
]
} }
], ],
"variables": { "variables": {

View File

@ -215,60 +215,6 @@ export async function findPmEvent(
return get<PmEventDetailResponse>('/PmEvent/findPmEvent', query, config) return get<PmEventDetailResponse>('/PmEvent/findPmEvent', query, config)
} }
/** 从嵌套对象读取事件 ID兼容多种后端字段名 */
export function readTradeRouteEventId(obj: unknown): string | undefined {
if (!obj || typeof obj !== 'object') return undefined
const r = obj as Record<string, unknown>
const keys = ['eventID', 'EventID', 'eventId', 'pmEventID', 'PmEventID', 'PMEventID', 'pmEventId']
for (const k of keys) {
const v = r[k]
if (v != null && String(v).trim() !== '') return String(v).trim()
}
return undefined
}
/** 从嵌套对象读取事件 slug不含通用 slug避免与市场 slug 混淆) */
export function readTradeRouteEventSlug(obj: unknown): string | undefined {
if (!obj || typeof obj !== 'object') return undefined
const r = obj as Record<string, unknown>
const v = r.eventSlug ?? r.event_slug
if (v != null && String(v).trim() !== '') return String(v).trim()
return undefined
}
export interface TradeDetailRouteInput {
/** 事件数字 ID与首页 MarketCard :id 一致),优先用于路径参数 */
eventId?: string | number | null
/** 事件 slug无 eventId 时用作路径;路径为数字 ID 时可作为 query.slug 传给 findPmEvent */
eventSlug?: string | null
/** 市场行 ID对应详情页 query.marketId */
marketId?: string | null
/** 详情页标题回显 query.title */
title?: string | null
}
/**
* MarketCardEventMarkets trade-detail router.push
* `/trade-detail/:id`id ID slug
*/
export function buildTradeDetailPushOptions(
input: TradeDetailRouteInput,
): { name: 'trade-detail'; params: { id: string }; query: Record<string, string> } | null {
const eid =
input.eventId != null && String(input.eventId).trim() !== '' ? String(input.eventId).trim() : ''
const slugOnly = input.eventSlug?.trim() || ''
if (!eid && !slugOnly) return null
const pathId = eid || slugOnly
const numId = parseInt(eid, 10)
const isNumericEventPath =
eid !== '' && Number.isFinite(numId) && String(numId) === eid && numId >= 1
const query: Record<string, string> = {}
if (input.title?.trim()) query.title = input.title.trim()
if (input.marketId?.trim()) query.marketId = input.marketId.trim()
if (isNumericEventPath && slugOnly) query.slug = slugOnly
return { name: 'trade-detail', params: { id: pathId }, query }
}
/** 多选项卡片中单个选项(用于左右滑动切换) */ /** 多选项卡片中单个选项(用于左右滑动切换) */
export interface EventCardOutcome { export interface EventCardOutcome {
title: string title: string

View File

@ -3,22 +3,10 @@
* Wallet.vue Tab * Wallet.vue Tab
*/ */
import { readTradeRouteEventId, readTradeRouteEventSlug } from './event'
import { buildQuery, get } from './request' import { buildQuery, get } from './request'
import { BASE_URL } from './request' import { BASE_URL } from './request'
import type { PageResult } from './types' import type { PageResult } from './types'
/** 历史记录 type与后端常量一致 */
export const HISTORY_RECORD_TYPE = {
TRADE: 'TRADE',
SPLIT: 'SPLIT',
MERGE: 'MERGE',
REDEEM: 'REDEEM',
REWARD: 'REWARD',
CONVERSION: 'CONVERSION',
MAKER_REBATE: 'MAKER_REBATE',
} as const
/** 单条历史记录(与 doc.json definitions["polymarket.HistoryRecord"] 对齐) */ /** 单条历史记录(与 doc.json definitions["polymarket.HistoryRecord"] 对齐) */
export interface HistoryRecordItem { export interface HistoryRecordItem {
ID?: number ID?: number
@ -48,10 +36,6 @@ export interface HistoryRecordItem {
UpdatedAt?: string UpdatedAt?: string
/** 金额USDC用于充值等类型 */ /** 金额USDC用于充值等类型 */
usdcSize?: number usdcSize?: number
eventID?: number
eventId?: number
marketID?: string | number
marketId?: string | number
} }
/** GET /hr/getHistoryRecordPublic 请求参数 */ /** GET /hr/getHistoryRecordPublic 请求参数 */
@ -152,16 +136,6 @@ export interface HistoryDisplayItem {
iconClass?: string iconClass?: string
/** 图标 URL来自 record.icon用于展示 */ /** 图标 URL来自 record.icon用于展示 */
imageUrl?: string imageUrl?: string
/** 接口 typeTRADE、SPLIT、MERGE 等 */
recordType?: string
/** TRADE 时 API 的 sidebuy / sell小写用于标签 */
tradeSideRaw?: string
/** 跳转详情:事件 ID */
tradeEventId?: string
/** 跳转详情:事件 slug */
tradeEventSlug?: string
/** 跳转详情 query.marketId */
detailMarketId?: string
} }
function formatTimeAgo(dateStr: string | undefined, timestamp?: number): string { function formatTimeAgo(dateStr: string | undefined, timestamp?: number): string {
@ -192,38 +166,23 @@ function toFullIconUrl(icon: string | undefined): string | undefined {
*/ */
export function mapHistoryRecordToDisplayItem(record: HistoryRecordItem): HistoryDisplayItem { export function mapHistoryRecordToDisplayItem(record: HistoryRecordItem): HistoryDisplayItem {
const id = String(record.ID ?? '') const id = String(record.ID ?? '')
const market = record.name ?? record.title ?? record.eventSlug ?? '' const market = record.title ?? record.name ?? record.eventSlug ?? ''
const outcome = (record.outcome ?? 'Yes').toString() const outcome = (record.outcome ?? record.side ?? 'Yes').toString()
const side = outcome === 'No' || outcome === 'Down' ? 'No' : 'Yes' const side = outcome === 'No' || outcome === 'Down' ? 'No' : 'Yes'
const recordType = (record.type ?? HISTORY_RECORD_TYPE.TRADE).toString().toUpperCase() const typeLabel = record.type ?? 'Trade'
const tradeSideRaw = (record.side ?? '').toLowerCase() const activity = `${typeLabel} ${outcome}`.trim()
const activity = (record.title ?? `${recordType} · ${outcome}`).trim()
const usdcSize = record.usdcSize ?? 0 const usdcSize = record.usdcSize ?? 0
const price = record.price ?? 0 const price = record.price ?? 0
const size = record.size ?? 0 const size = record.size ?? 0
const valueUsd = usdcSize !== 0 ? usdcSize : price * size const valueUsd = usdcSize !== 0 ? usdcSize : price * size
const value = `$${Math.abs(valueUsd).toFixed(2)}` const value = `$${Math.abs(valueUsd).toFixed(2)}`
const priceCents = Math.round(price * 100) const priceCents = Math.round(price * 100)
const activityDetail = const activityDetail = size > 0 ? `Sold ${Math.floor(size)} ${outcome} at ${priceCents}¢` : value
size > 0
? recordType === HISTORY_RECORD_TYPE.TRADE
? `${tradeSideRaw === 'sell' ? 'Sold' : 'Bought'} ${Math.floor(size)} ${outcome} at ${priceCents}¢`
: `Sold ${Math.floor(size)} ${outcome} at ${priceCents}¢`
: value
const timeAgo = formatTimeAgo( const timeAgo = formatTimeAgo(
record.UpdatedAt ?? record.updatedAt ?? record.CreatedAt ?? record.createdAt, record.UpdatedAt ?? record.updatedAt ?? record.CreatedAt ?? record.createdAt,
record.timestamp, record.timestamp,
) )
const imageUrl = toFullIconUrl(record.icon) const imageUrl = toFullIconUrl(record.icon)
const tradeEventId = readTradeRouteEventId(record)
const tradeEventSlug =
readTradeRouteEventSlug(record) ||
record.eventSlug?.trim() ||
record.slug?.trim() ||
undefined
const rawMk = record.marketID ?? record.marketId
const detailMarketId =
rawMk != null && String(rawMk).trim() !== '' ? String(rawMk).trim() : undefined
return { return {
id, id,
market, market,
@ -237,11 +196,6 @@ export function mapHistoryRecordToDisplayItem(record: HistoryRecordItem): Histor
avgPrice: priceCents ? `${priceCents}¢` : undefined, avgPrice: priceCents ? `${priceCents}¢` : undefined,
shares: size > 0 ? String(Math.floor(size)) : undefined, shares: size > 0 ? String(Math.floor(size)) : undefined,
imageUrl, imageUrl,
recordType,
tradeSideRaw: recordType === HISTORY_RECORD_TYPE.TRADE ? tradeSideRaw : undefined,
tradeEventId: tradeEventId || undefined,
tradeEventSlug: tradeEventSlug || undefined,
detailMarketId,
} }
} }

View File

@ -19,68 +19,8 @@ export interface MockOrderBookRow {
shares: number shares: number
} }
/** random integer in [min, max] inclusive */ /** 生成随机订单簿数据,每次进入页面不同;保证卖单最低价 > 买单最高价 */
function randomInt(min: number, max: number): number { export function generateRandomOrderBook(): {
return min + Math.floor(Math.random() * (max - min + 1))
}
export interface GenerateRandomOrderBookOptions {
/** 最优买价(美分);与卖盘对齐后恒满足 lowestAsk = highestBid + 1 */
highestBid?: number
/** 最优卖价(美分);与买盘对齐后恒满足 highestBid = lowestAsk - 1 */
lowestAsk?: number
/**
* outcomePrices[0] probability×100
* = 11¢
*/
outcomePriceAnchorCents?: number
/**
* `both` `asks` `bids`
* 25% /50%
*/
sides?: 'both' | 'asks' | 'bids'
}
function buildMockAsks(lowest: number, count: number, r: () => number): MockOrderBookRow[] {
return Array.from({ length: count }, (_, i) => ({
price: lowest + i,
shares: Math.round((500 + r() * 3000) * 10) / 10,
}))
}
function buildMockBids(highest: number, count: number, r: () => number): MockOrderBookRow[] {
const rows: MockOrderBookRow[] = []
for (let i = 0; i < count; i++) {
const p = highest - i
if (p < 1) break
rows.push({
price: p,
shares: Math.round((200 + r() * 3000) * 10) / 10,
})
}
return rows
}
/** 触摸价:最优卖一 la最优买一 = la 11¢ 价差) */
function touchFromLowestAsk(lowestAsk: number): { highestBid: number; lowestAsk: number } {
let la = Math.floor(lowestAsk)
la = Math.min(99, Math.max(2, la))
return { highestBid: la - 1, lowestAsk: la }
}
function touchFromHighestBid(highestBid: number): { highestBid: number; lowestAsk: number } {
let hb = Math.floor(highestBid)
hb = Math.min(98, Math.max(1, hb))
return { highestBid: hb, lowestAsk: hb + 1 }
}
/**
* 簿
* - ** = 1** 1¢
* - `outcomePriceAnchorCents`outcomePrices[0]
* - 210
*/
export function generateRandomOrderBook(options?: GenerateRandomOrderBookOptions): {
asks: MockOrderBookRow[] asks: MockOrderBookRow[]
bids: MockOrderBookRow[] bids: MockOrderBookRow[]
lastPrice: number lastPrice: number
@ -88,63 +28,24 @@ export function generateRandomOrderBook(options?: GenerateRandomOrderBookOptions
} { } {
const r = () => Math.random() const r = () => Math.random()
const sideRoll = r() // 买单最高价 2535卖单最低价 = 买单最高价 + 1 + spread保证有价差
const sides: 'both' | 'asks' | 'bids' = const highestBid = Math.floor(25 + r() * 11)
options?.sides ?? const spread = Math.max(1, Math.floor(1 + r() * 3))
(sideRoll < 0.25 ? 'asks' : sideRoll < 0.5 ? 'bids' : 'both') const lowestAsk = highestBid + spread
const nAsks = randomInt(2, 10) const askPrices = Array.from({ length: 9 }, (_, i) => lowestAsk + i)
const nBids = randomInt(2, 10) const asks: MockOrderBookRow[] = askPrices.map((p) => ({
price: p,
shares: Math.round((500 + r() * 3000) * 10) / 10,
}))
let highestBid = 0 const bidPrices = Array.from({ length: 12 }, (_, i) => highestBid - i)
let lowestAsk = 0 const bids: MockOrderBookRow[] = bidPrices.map((p) => ({
let asks: MockOrderBookRow[] = [] price: Math.max(1, p),
let bids: MockOrderBookRow[] = [] shares: Math.round((200 + r() * 3000) * 10) / 10,
let lastPrice = 0 }))
const spread = 1
if (sides === 'both') { const lastPrice = Math.floor((highestBid + lowestAsk) / 2)
const anchor = options?.outcomePriceAnchorCents
const hbOpt = options?.highestBid
const laOpt = options?.lowestAsk
if (anchor != null && Number.isFinite(anchor) && anchor > 0) {
;({ highestBid, lowestAsk } = touchFromLowestAsk(Math.round(anchor)))
} else if (laOpt != null && Number.isFinite(laOpt)) {
;({ highestBid, lowestAsk } = touchFromLowestAsk(laOpt))
} else if (hbOpt != null && Number.isFinite(hbOpt)) {
;({ highestBid, lowestAsk } = touchFromHighestBid(hbOpt))
} else {
lowestAsk = randomInt(2, 99)
highestBid = lowestAsk - 1
}
asks = buildMockAsks(lowestAsk, nAsks, r)
bids = buildMockBids(highestBid, nBids, r)
lastPrice = Math.round((highestBid + lowestAsk) / 2)
} else if (sides === 'asks') {
if (options?.highestBid != null && Number.isFinite(options.highestBid)) {
;({ highestBid, lowestAsk } = touchFromHighestBid(options.highestBid))
} else if (options?.lowestAsk != null && Number.isFinite(options.lowestAsk)) {
;({ highestBid, lowestAsk } = touchFromLowestAsk(options.lowestAsk))
} else {
;({ highestBid, lowestAsk } = touchFromLowestAsk(randomInt(2, 99)))
}
asks = buildMockAsks(lowestAsk, nAsks, r)
bids = []
lastPrice = Math.round((highestBid + lowestAsk) / 2)
} else {
if (options?.lowestAsk != null && Number.isFinite(options.lowestAsk)) {
;({ highestBid, lowestAsk } = touchFromLowestAsk(options.lowestAsk))
} else if (options?.highestBid != null && Number.isFinite(options.highestBid)) {
;({ highestBid, lowestAsk } = touchFromHighestBid(options.highestBid))
} else {
;({ highestBid, lowestAsk } = touchFromHighestBid(randomInt(1, 98)))
}
bids = buildMockBids(highestBid, nBids, r)
asks = []
lastPrice = Math.round((highestBid + lowestAsk) / 2)
}
return { asks, bids, lastPrice, spread } return { asks, bids, lastPrice, spread }
} }
@ -206,8 +107,6 @@ export interface MockPosition {
marketID?: string marketID?: string
tokenID?: string tokenID?: string
needClaim?: boolean needClaim?: boolean
tradeEventId?: string
tradeEventSlug?: string
} }
export interface MockOpenOrder { export interface MockOpenOrder {
@ -225,9 +124,6 @@ export interface MockOpenOrder {
iconClass?: string iconClass?: string
orderID?: number orderID?: number
tokenID?: string tokenID?: string
tradeEventId?: string
tradeEventSlug?: string
detailMarketId?: string
} }
export interface MockHistoryItem { export interface MockHistoryItem {
@ -244,9 +140,6 @@ export interface MockHistoryItem {
shares?: string shares?: string
iconChar?: string iconChar?: string
iconClass?: string iconClass?: string
tradeEventId?: string
tradeEventSlug?: string
detailMarketId?: string
} }
export const MOCK_WALLET_POSITIONS: MockPosition[] = [ export const MOCK_WALLET_POSITIONS: MockPosition[] = [
@ -270,8 +163,6 @@ export const MOCK_WALLET_POSITIONS: MockPosition[] = [
marketID: 'mock-market-1', marketID: 'mock-market-1',
tokenID: MOCK_TOKEN_ID, tokenID: MOCK_TOKEN_ID,
needClaim: true, needClaim: true,
tradeEventId: '1',
tradeEventSlug: 'mock-wallet-event',
}, },
{ {
id: 'p2', id: 'p2',
@ -327,9 +218,6 @@ export const MOCK_WALLET_ORDERS: MockOpenOrder[] = [
iconClass: 'position-icon-btc', iconClass: 'position-icon-btc',
orderID: 5, orderID: 5,
tokenID: MOCK_TOKEN_ID, tokenID: MOCK_TOKEN_ID,
tradeEventId: '1',
tradeEventSlug: 'mock-wallet-event',
detailMarketId: 'mock-market-1',
}, },
{ {
id: 'o2', id: 'o2',
@ -361,8 +249,6 @@ export const MOCK_WALLET_HISTORY: MockHistoryItem[] = [
{ {
id: 'h1', id: 'h1',
market: 'Bitcoin Up or Down - February 9, 3:00AM-3:15AM ET', market: 'Bitcoin Up or Down - February 9, 3:00AM-3:15AM ET',
tradeEventId: '1',
tradeEventSlug: 'mock-wallet-event',
side: 'No', side: 'No',
activity: 'Sell No', activity: 'Sell No',
activityDetail: 'Sold 1 Down at 50¢', activityDetail: 'Sold 1 Down at 50¢',

View File

@ -1,20 +1,8 @@
import { readTradeRouteEventId, readTradeRouteEventSlug } from './event' import { buildQuery, get, post } from './request'
import { BASE_URL, buildQuery, get, post } from './request'
import type { ApiResponse, PageResult } from './types' import type { ApiResponse, PageResult } from './types'
export type { PageResult } export type { PageResult }
/** 订单列表嵌套的市场信息getOrderList 响应内 pmMarket */
export interface PmMarketEmbed {
ID?: number
question?: string
slug?: string
image?: string
icon?: string
conditionId?: string
outcomes?: string[]
}
/** /**
* doc.json definitions["model.ClobOrder"] * doc.json definitions["model.ClobOrder"]
* GET /clob/order/getOrderList * GET /clob/order/getOrderList
@ -35,34 +23,9 @@ export interface ClobOrderItem {
sizeMatched?: number sizeMatched?: number
status?: number status?: number
userID?: number userID?: number
pmMarket?: PmMarketEmbed
[key: string]: unknown [key: string]: unknown
} }
/** 展示用标题:优先 pmMarket.question否则回退 market多为 ID */
export function resolveClobOrderMarketTitle(order: ClobOrderItem): string {
const q = order.pmMarket?.question?.trim()
if (q) return q
const m = order.market?.toString().trim()
return m ?? ''
}
function toFullIconUrl(icon: string | undefined): string | undefined {
if (!icon?.trim()) return undefined
const s = icon.trim()
if (s.startsWith('http://') || s.startsWith('https://')) return s
const base = BASE_URL?.replace(/\/$/, '') ?? ''
return `${base}/${s.replace(/^\//, '')}`
}
/** 市场图:优先 pmMarket.image其次 pmMarket.icon */
export function resolveClobOrderMarketImageUrl(order: ClobOrderItem): string | undefined {
const pm = order.pmMarket
if (!pm) return undefined
const raw = (pm.image ?? pm.icon ?? '').trim()
return raw ? toFullIconUrl(raw) : undefined
}
/** 订单列表响应 */ /** 订单列表响应 */
export interface OrderListResponse { export interface OrderListResponse {
code: number code: number
@ -151,8 +114,6 @@ export interface HistoryDisplayItem {
shares?: string shares?: string
iconChar?: string iconChar?: string
iconClass?: string iconClass?: string
/** 来自订单 pmMarket.image / icon */
imageUrl?: string
} }
/** Side: Buy=1, Sell=2 */ /** Side: Buy=1, Sell=2 */
@ -179,7 +140,7 @@ function formatTimeAgo(createdAt: string | undefined): string {
*/ */
export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem { export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem {
const id = String(order.ID ?? '') const id = String(order.ID ?? '')
const market = resolveClobOrderMarketTitle(order) || (order.market ?? '') const market = order.market ?? ''
const outcome = order.outcome ?? 'Yes' const outcome = order.outcome ?? 'Yes'
const sideNum = order.side ?? Side.Buy const sideNum = order.side ?? Side.Buy
const sideLabel = sideNum === Side.Sell ? 'Sell' : 'Buy' const sideLabel = sideNum === Side.Sell ? 'Sell' : 'Buy'
@ -194,7 +155,6 @@ export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem
const activityDetail = `${verb} ${Math.floor(size)} ${outcome} at ${priceCents}¢` const activityDetail = `${verb} ${Math.floor(size)} ${outcome} at ${priceCents}¢`
const avgPrice = `${priceCents}¢` const avgPrice = `${priceCents}¢`
const timeAgo = formatTimeAgo(order.createdAt) const timeAgo = formatTimeAgo(order.createdAt)
const imageUrl = resolveClobOrderMarketImageUrl(order)
return { return {
id, id,
market, market,
@ -207,7 +167,6 @@ export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem
timeAgo, timeAgo,
avgPrice, avgPrice,
shares: String(Math.floor(size)), shares: String(Math.floor(size)),
imageUrl,
} }
} }
@ -227,14 +186,6 @@ export interface OpenOrderDisplayItem {
tokenID?: string tokenID?: string
/** 已成交数量达到原始总数量,不可撤单 */ /** 已成交数量达到原始总数量,不可撤单 */
fullyFilled?: boolean fullyFilled?: boolean
/** 来自 pmMarket.image / icon */
imageUrl?: string
/** 跳转详情:事件 ID */
tradeEventId?: string
/** 跳转详情:事件 slug */
tradeEventSlug?: string
/** 跳转详情 query.marketIdpmMarket.ID */
detailMarketId?: string
} }
/** OrderType GTC=0 表示 Until Cancelled */ /** OrderType GTC=0 表示 Until Cancelled */
@ -246,7 +197,7 @@ const OrderType = { GTC: 0, GTD: 1 } as const
*/ */
export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayItem { export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayItem {
const id = String(order.ID ?? '') const id = String(order.ID ?? '')
const market = resolveClobOrderMarketTitle(order) || (order.market ?? '') const market = order.market ?? ''
const sideNum = order.side ?? Side.Buy const sideNum = order.side ?? Side.Buy
const side = sideNum === Side.Sell ? 'No' : 'Yes' const side = sideNum === Side.Sell ? 'No' : 'Yes'
const outcome = order.outcome || (side === 'Yes' ? 'Yes' : 'No') const outcome = order.outcome || (side === 'Yes' ? 'Yes' : 'No')
@ -264,13 +215,6 @@ export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayI
order.orderType === OrderType.GTC ? 'Until Cancelled' : order.expiration?.toString() ?? '' order.orderType === OrderType.GTC ? 'Until Cancelled' : order.expiration?.toString() ?? ''
const actionLabel = sideNum === Side.Buy ? `Buy ${outcome}` : `Sell ${outcome}` const actionLabel = sideNum === Side.Buy ? `Buy ${outcome}` : `Sell ${outcome}`
const fullyFilled = originalSize <= 0 || sizeMatched >= originalSize const fullyFilled = originalSize <= 0 || sizeMatched >= originalSize
const imageUrl = resolveClobOrderMarketImageUrl(order)
const pm = order.pmMarket
const tradeEventId = readTradeRouteEventId(pm) ?? readTradeRouteEventId(order)
const slugExplicit = readTradeRouteEventSlug(pm) ?? readTradeRouteEventSlug(order)
const slugFromPm = pm?.slug?.trim()
const tradeEventSlug = slugExplicit || (!tradeEventId && slugFromPm ? slugFromPm : undefined)
const detailMarketId = pm?.ID != null ? String(pm.ID) : undefined
return { return {
id, id,
market, market,
@ -285,9 +229,5 @@ export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayI
orderID: order.ID, orderID: order.ID,
tokenID: order.assetID, tokenID: order.assetID,
fullyFilled, fullyFilled,
imageUrl,
tradeEventId: tradeEventId || undefined,
tradeEventSlug: tradeEventSlug || undefined,
detailMarketId,
} }
} }

View File

@ -1,4 +1,3 @@
import { readTradeRouteEventId, readTradeRouteEventSlug } from './event'
import { buildQuery, get, post } from './request' import { buildQuery, get, post } from './request'
import type { ApiResponse } from './types' import type { ApiResponse } from './types'
import type { PageResult } from './types' import type { PageResult } from './types'
@ -167,10 +166,6 @@ export interface PositionDisplayItem {
tokenID?: string tokenID?: string
/** 所属市场是否已关闭market.closed=true 表示可结算/可领取 */ /** 所属市场是否已关闭market.closed=true 表示可结算/可领取 */
marketClosed?: boolean marketClosed?: boolean
/** 跳转市场详情:事件 ID路径优先 */
tradeEventId?: string
/** 跳转市场详情:事件 slug */
tradeEventSlug?: string
} }
/** /**
@ -238,11 +233,6 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
const tokenID = pos.tokenId ?? (pos as { tokenID?: string }).tokenID ?? '' const tokenID = pos.tokenId ?? (pos as { tokenID?: string }).tokenID ?? ''
const imageUrl = (pos.market?.image ?? pos.market?.icon) as string | undefined const imageUrl = (pos.market?.image ?? pos.market?.icon) as string | undefined
const marketClosed = pos.market?.closed === true const marketClosed = pos.market?.closed === true
const tradeEventId = readTradeRouteEventId(pos.market) ?? readTradeRouteEventId(pos)
const slugExplicit = readTradeRouteEventSlug(pos.market) ?? readTradeRouteEventSlug(pos)
const slugFromMarket = pos.market?.slug?.trim()
const tradeEventSlug =
slugExplicit || (!tradeEventId && slugFromMarket ? slugFromMarket : undefined)
return { return {
id, id,
@ -266,7 +256,5 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
marketID: marketID || undefined, marketID: marketID || undefined,
tokenID: tokenID || undefined, tokenID: tokenID || undefined,
marketClosed, marketClosed,
tradeEventId: tradeEventId || undefined,
tradeEventSlug: tradeEventSlug || undefined,
} }
} }

View File

@ -1,18 +0,0 @@
import { get } from './request'
import type { ApiResponse } from './types'
/** GET /VipLevel/getPmVipLevelPublic — 手续费等级(无需鉴权) */
export interface PmVipLevelItem {
levelName: string
takerFeeRate: number
makerFeeRate: number
needDeposit: number
}
export interface PmVipLevelPublicData {
list: PmVipLevelItem[]
}
export async function getPmVipLevelPublic(): Promise<ApiResponse<PmVipLevelPublicData>> {
return get<ApiResponse<PmVipLevelPublicData>>('/VipLevel/getPmVipLevelPublic')
}

View File

@ -1,103 +0,0 @@
<template>
<div
ref="rootRef"
:class="['marquee-root', titleClass, { 'marquee-root--animate': overflow, 'marquee-root--fast': fast }]"
>
<div class="marquee-track">
<span>{{ text }}</span>
<span v-if="overflow" aria-hidden="true">{{ text }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
const props = withDefaults(
defineProps<{
/** 展示的标题文案 */
text: string
/** 附加到根节点的类名(如 position-mobile-title、order-mobile-title */
titleClass?: string
/** true 时使用较短动画时长(与订单/历史原样式一致) */
fast?: boolean
}>(),
{ titleClass: '', fast: false },
)
const rootRef = ref<HTMLElement | null>(null)
const overflow = ref(false)
let resizeObserver: ResizeObserver | null = null
async function measure() {
await nextTick()
const root = rootRef.value
if (!root) {
overflow.value = false
return
}
const track = root.querySelector('.marquee-track')
if (!(track instanceof HTMLElement)) {
overflow.value = false
return
}
overflow.value = track.scrollWidth > root.clientWidth + 1
}
onMounted(() => {
measure()
resizeObserver = new ResizeObserver(() => measure())
if (rootRef.value) resizeObserver.observe(rootRef.value)
})
onBeforeUnmount(() => {
resizeObserver?.disconnect()
resizeObserver = null
})
watch(
() => props.text,
() => measure(),
)
watch(overflow, () => {
void nextTick(() => measure())
})
</script>
<style scoped>
.marquee-root {
overflow: hidden;
white-space: nowrap;
width: 100%;
}
.marquee-track {
display: inline-flex;
align-items: center;
min-width: max-content;
gap: 0;
}
.marquee-root--animate .marquee-track {
gap: 40px;
animation: marquee-title-scroll 10s linear infinite;
}
.marquee-root--animate.marquee-root--fast .marquee-track {
animation-duration: 9s;
}
.marquee-track > span {
white-space: nowrap;
}
@keyframes marquee-title-scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-50% - 20px));
}
}
</style>

View File

@ -91,7 +91,7 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import HorizontalProgressBar from './HorizontalProgressBar.vue' import HorizontalProgressBar from './HorizontalProgressBar.vue'
import { generateRandomOrderBook, type MockOrderBookRow } from '../api/mockData' import { generateRandomOrderBook } from '../api/mockData'
import { USE_MOCK_ORDER_BOOK } from '../config/mock' import { USE_MOCK_ORDER_BOOK } from '../config/mock'
const { t } = useI18n() const { t } = useI18n()
@ -126,15 +126,6 @@ const props = withDefaults(
connected?: boolean connected?: boolean
yesLabel?: string yesLabel?: string
noLabel?: string noLabel?: string
/** Yes 簿真实买单最高价(美分),单侧真实数据时用于生成另一侧模拟盘口 */
anchorBestBidYesCents?: number
anchorLowestAskYesCents?: number
anchorBestBidNoCents?: number
anchorLowestAskNoCents?: number
/** 无真实盘口时Yes 侧用 outcomePrices[0] 换算的美分锚定最优卖一(买一=卖一1 */
outcomePriceAnchorYesCents?: number
/** 无真实盘口时No 侧锚定(常为 outcomePrices[1]),由父组件传入 */
outcomePriceAnchorNoCents?: number
}>(), }>(),
{ {
asksYes: () => [], asksYes: () => [],
@ -153,179 +144,17 @@ const props = withDefaults(
connected: false, connected: false,
yesLabel: 'Yes', yesLabel: 'Yes',
noLabel: 'No', noLabel: 'No',
anchorBestBidYesCents: 0,
anchorLowestAskYesCents: 0,
anchorBestBidNoCents: 0,
anchorLowestAskNoCents: 0,
outcomePriceAnchorYesCents: 0,
outcomePriceAnchorNoCents: 0,
}, },
) )
// Stateup = Yes down = No // Stateup = Yes down = No
const activeTrade = ref('up') const activeTrade = ref('up')
/** 一侧真实、另一侧由 mock 填充时的模拟档位与展示的 last/spread */ const initMock = generateRandomOrderBook()
type HybridSupplement = { const internalAsks = ref<OrderBookRow[]>([...initMock.asks])
mockAsks: OrderBookRow[] const internalBids = ref<OrderBookRow[]>([...initMock.bids])
mockBids: OrderBookRow[] const internalLastPrice = ref(initMock.lastPrice)
lastPrice: number const internalSpread = ref(initMock.spread)
spread: number
}
const hybridSupplementYes = ref<HybridSupplement | null>(null)
const hybridSupplementNo = ref<HybridSupplement | null>(null)
/** 双侧皆无真实数据时的完整模拟簿Yes / No 各自一份) */
const fullMockYes = ref<HybridSupplement | null>(null)
const fullMockNo = ref<HybridSupplement | null>(null)
function mockRowsToOrderBookRows(rows: MockOrderBookRow[]): OrderBookRow[] {
return rows.map((r) => ({ price: r.price, shares: r.shares }))
}
function bestBidCentsFromRows(rows: OrderBookRow[] | undefined): number {
if (!rows?.length) return 0
return Math.max(...rows.map((b) => b.price))
}
function bestAskCentsFromRows(rows: OrderBookRow[] | undefined): number {
if (!rows?.length) return 0
return Math.min(...rows.map((a) => a.price))
}
function supplementFromGen(gen: ReturnType<typeof generateRandomOrderBook>): HybridSupplement {
return {
mockAsks: mockRowsToOrderBookRows(gen.asks),
mockBids: mockRowsToOrderBookRows(gen.bids),
lastPrice: gen.lastPrice,
spread: gen.spread,
}
}
function syncOrderBookMocks() {
if (!USE_MOCK_ORDER_BOOK || props.loading) {
hybridSupplementYes.value = null
hybridSupplementNo.value = null
fullMockYes.value = null
fullMockNo.value = null
return
}
const runSide = (
nAsks: number,
nBids: number,
anchorBidProp: number,
anchorAskProp: number,
bidsRows: OrderBookRow[] | undefined,
asksRows: OrderBookRow[] | undefined,
setRef: (v: HybridSupplement | null) => void,
) => {
if (nAsks > 0 && nBids > 0) {
setRef(null)
return
}
if (nAsks === 0 && nBids === 0) {
setRef(null)
return
}
const hb =
anchorBidProp > 0 ? Math.floor(anchorBidProp) : bestBidCentsFromRows(bidsRows)
const la =
anchorAskProp > 0 ? Math.floor(anchorAskProp) : bestAskCentsFromRows(asksRows)
if (nAsks === 0 && nBids > 0 && hb > 0) {
const gen = generateRandomOrderBook({ highestBid: hb, sides: 'asks' })
setRef({
mockAsks: mockRowsToOrderBookRows(gen.asks),
mockBids: [],
lastPrice: gen.lastPrice,
spread: gen.spread,
})
return
}
if (nBids === 0 && nAsks > 0 && la > 0) {
const gen = generateRandomOrderBook({ lowestAsk: la, sides: 'bids' })
setRef({
mockAsks: [],
mockBids: mockRowsToOrderBookRows(gen.bids),
lastPrice: gen.lastPrice,
spread: gen.spread,
})
return
}
setRef(null)
}
runSide(
props.asksYes?.length ?? 0,
props.bidsYes?.length ?? 0,
props.anchorBestBidYesCents ?? 0,
props.anchorLowestAskYesCents ?? 0,
props.bidsYes,
props.asksYes,
(v) => {
hybridSupplementYes.value = v
},
)
runSide(
props.asksNo?.length ?? 0,
props.bidsNo?.length ?? 0,
props.anchorBestBidNoCents ?? 0,
props.anchorLowestAskNoCents ?? 0,
props.bidsNo,
props.asksNo,
(v) => {
hybridSupplementNo.value = v
},
)
const ay = props.asksYes?.length ?? 0
const by = props.bidsYes?.length ?? 0
if (ay === 0 && by === 0) {
const anchorY = props.outcomePriceAnchorYesCents ?? 0
fullMockYes.value = supplementFromGen(
generateRandomOrderBook({
sides: 'both',
outcomePriceAnchorCents: anchorY > 0 ? anchorY : undefined,
}),
)
} else {
fullMockYes.value = null
}
const an = props.asksNo?.length ?? 0
const bn = props.bidsNo?.length ?? 0
if (an === 0 && bn === 0) {
const anchorN = props.outcomePriceAnchorNoCents ?? 0
fullMockNo.value = supplementFromGen(
generateRandomOrderBook({
sides: 'both',
outcomePriceAnchorCents: anchorN > 0 ? anchorN : undefined,
}),
)
} else {
fullMockNo.value = null
}
}
watch(
() =>
[
props.loading,
props.asksYes?.length ?? 0,
props.bidsYes?.length ?? 0,
props.asksNo?.length ?? 0,
props.bidsNo?.length ?? 0,
props.anchorBestBidYesCents ?? 0,
props.anchorLowestAskYesCents ?? 0,
props.anchorBestBidNoCents ?? 0,
props.anchorLowestAskNoCents ?? 0,
props.outcomePriceAnchorYesCents ?? 0,
props.outcomePriceAnchorNoCents ?? 0,
] as const,
() => syncOrderBookMocks(),
{ immediate: true },
)
// activeTrade tab loading // activeTrade tab loading
const asks = computed(() => { const asks = computed(() => {
@ -333,34 +162,22 @@ const asks = computed(() => {
const isYes = activeTrade.value === 'up' const isYes = activeTrade.value === 'up'
const fromYes = isYes ? props.asksYes : props.asksNo const fromYes = isYes ? props.asksYes : props.asksNo
const fromLegacy = props.asks const fromLegacy = props.asks
const hybrid = isYes ? hybridSupplementYes.value : hybridSupplementNo.value
const hasYesNo = (fromYes?.length ?? 0) > 0 const hasYesNo = (fromYes?.length ?? 0) > 0
const hasLegacy = (fromLegacy?.length ?? 0) > 0 const hasLegacy = (fromLegacy?.length ?? 0) > 0
if (hasYesNo) return fromYes ?? [] if (hasYesNo) return fromYes ?? []
if (hasLegacy) return fromLegacy ?? [] if (hasLegacy) return fromLegacy ?? []
if (USE_MOCK_ORDER_BOOK && hybrid && hybrid.mockAsks.length > 0) return hybrid.mockAsks return USE_MOCK_ORDER_BOOK ? internalAsks.value : []
if (USE_MOCK_ORDER_BOOK) {
const full = isYes ? fullMockYes.value : fullMockNo.value
if (full?.mockAsks.length) return full.mockAsks
}
return []
}) })
const bids = computed(() => { const bids = computed(() => {
if (props.loading) return [] if (props.loading) return []
const isYes = activeTrade.value === 'up' const isYes = activeTrade.value === 'up'
const fromYes = isYes ? props.bidsYes : props.bidsNo const fromYes = isYes ? props.bidsYes : props.bidsNo
const fromLegacy = props.bids const fromLegacy = props.bids
const hybrid = isYes ? hybridSupplementYes.value : hybridSupplementNo.value
const hasYesNo = (fromYes?.length ?? 0) > 0 const hasYesNo = (fromYes?.length ?? 0) > 0
const hasLegacy = (fromLegacy?.length ?? 0) > 0 const hasLegacy = (fromLegacy?.length ?? 0) > 0
if (hasYesNo) return fromYes ?? [] if (hasYesNo) return fromYes ?? []
if (hasLegacy) return fromLegacy ?? [] if (hasLegacy) return fromLegacy ?? []
if (USE_MOCK_ORDER_BOOK && hybrid && hybrid.mockBids.length > 0) return hybrid.mockBids return USE_MOCK_ORDER_BOOK ? internalBids.value : []
if (USE_MOCK_ORDER_BOOK) {
const full = isYes ? fullMockYes.value : fullMockNo.value
if (full?.mockBids.length) return full.mockBids
}
return []
}) })
const displayLastPrice = computed(() => { const displayLastPrice = computed(() => {
if (props.loading) return '—' if (props.loading) return '—'
@ -368,13 +185,7 @@ const displayLastPrice = computed(() => {
const fromYesNo = isYes ? props.lastPriceYes : props.lastPriceNo const fromYesNo = isYes ? props.lastPriceYes : props.lastPriceNo
if (fromYesNo != null) return fromYesNo if (fromYesNo != null) return fromYesNo
if (props.lastPrice != null) return props.lastPrice if (props.lastPrice != null) return props.lastPrice
if (USE_MOCK_ORDER_BOOK) { return USE_MOCK_ORDER_BOOK ? internalLastPrice.value : 0
const h = isYes ? hybridSupplementYes.value : hybridSupplementNo.value
if (h) return h.lastPrice
const full = isYes ? fullMockYes.value : fullMockNo.value
if (full) return full.lastPrice
}
return 0
}) })
const displaySpread = computed(() => { const displaySpread = computed(() => {
if (props.loading) return '—' if (props.loading) return '—'
@ -382,13 +193,7 @@ const displaySpread = computed(() => {
const fromYesNo = isYes ? props.spreadYes : props.spreadNo const fromYesNo = isYes ? props.spreadYes : props.spreadNo
if (fromYesNo != null) return fromYesNo if (fromYesNo != null) return fromYesNo
if (props.spread != null) return props.spread if (props.spread != null) return props.spread
if (USE_MOCK_ORDER_BOOK) { return USE_MOCK_ORDER_BOOK ? internalSpread.value : 0
const h = isYes ? hybridSupplementYes.value : hybridSupplementNo.value
if (h) return h.spread
const full = isYes ? fullMockYes.value : fullMockNo.value
if (full) return full.spread
}
return 0
}) })
// mock mock // mock mock
@ -407,22 +212,24 @@ watch(
mockInterval = undefined mockInterval = undefined
} else if (!hasRealData && USE_MOCK_ORDER_BOOK && !mockInterval) { } else if (!hasRealData && USE_MOCK_ORDER_BOOK && !mockInterval) {
mockInterval = setInterval(() => { mockInterval = setInterval(() => {
const jitterSide = (book: HybridSupplement | null) => { const randomAskIndex = Math.floor(Math.random() * internalAsks.value.length)
if (!book) return book const askItem = internalAsks.value[randomAskIndex]
if (!book.mockAsks.length && !book.mockBids.length) return book if (askItem) {
const jitterRow = (row: OrderBookRow): OrderBookRow => ({ internalAsks.value[randomAskIndex] = {
...row, price: askItem.price,
shares: Math.max(0, row.shares + Math.floor(Math.random() * 100) - 50), shares: Math.max(0, askItem.shares + Math.floor(Math.random() * 100) - 50),
})
return {
mockAsks: book.mockAsks.map(jitterRow),
mockBids: book.mockBids.map(jitterRow),
lastPrice: Math.max(1, Math.min(99, book.lastPrice + Math.floor(Math.random() * 3) - 1)),
spread: 1,
} }
} }
fullMockYes.value = jitterSide(fullMockYes.value) const randomBidIndex = Math.floor(Math.random() * internalBids.value.length)
fullMockNo.value = jitterSide(fullMockNo.value) const bidItem = internalBids.value[randomBidIndex]
if (bidItem) {
internalBids.value[randomBidIndex] = {
price: bidItem.price,
shares: Math.max(0, bidItem.shares + Math.floor(Math.random() * 100) - 50),
}
}
internalLastPrice.value = Math.max(1, Math.min(99, internalLastPrice.value + Math.floor(Math.random() * 3) - 1))
internalSpread.value = Math.max(1, internalSpread.value + Math.floor(Math.random() * 2) - 1)
}, 2000) }, 2000)
} }
}, },

View File

@ -1494,7 +1494,6 @@ function indexOfAllowedPrice(v: number): number {
export interface TradeMarketPayload { export interface TradeMarketPayload {
marketId?: string marketId?: string
/** 优先来自订单簿卖一01为 0 时可配合 outcomePrices 解析 */
yesPrice: number yesPrice: number
noPrice: number noPrice: number
title?: string title?: string
@ -1502,8 +1501,6 @@ export interface TradeMarketPayload {
clobTokenIds?: string[] clobTokenIds?: string[]
/** 选项展示文案,如 ["Yes","No"] 或 ["Up","Down"],用于 Buy Yes/No 按钮文字 */ /** 选项展示文案,如 ["Yes","No"] 或 ["Up","Down"],用于 Buy Yes/No 按钮文字 */
outcomes?: string[] outcomes?: string[]
/** yesPrice/noPrice 均为 0 时用于回退展示与限价(与 API outcomePrices 同源) */
outcomePrices?: Array<string | number>
/** 订单簿 Yes 买单最高价(美分),市价卖出时用于计算将收到金额 */ /** 订单簿 Yes 买单最高价(美分),市价卖出时用于计算将收到金额 */
bestBidYesCents?: number bestBidYesCents?: number
/** 订单簿 No 买单最高价(美分),市价卖出时用于计算将收到金额 */ /** 订单簿 No 买单最高价(美分),市价卖出时用于计算将收到金额 */
@ -1636,41 +1633,8 @@ async function submitSplit() {
defineExpose({ openMergeDialog, openSplitDialog }) defineExpose({ openMergeDialog, openSplitDialog })
/** outcomePrices 单项 → 美元单价 01兼容概率小数或 1100 美分写法) */ const yesPriceCents = computed(() => (props.market ? Math.round(props.market.yesPrice * 100) : 19))
function outcomeRawToDollars(raw: string | number | undefined): number { const noPriceCents = computed(() => (props.market ? Math.round(props.market.noPrice * 100) : 82))
if (raw == null || (typeof raw === 'string' && raw.trim() === '')) return 0
const p = parseFloat(String(raw))
if (!Number.isFinite(p)) return 0
if (p > 1 && p <= 100) return Math.min(1, p / 100)
return Math.min(1, Math.max(0, p))
}
/** 展示/限价:优先 market.yesPrice来自订单簿卖一否则 outcomePrices[0] */
const yesPriceDollarsEffective = computed(() => {
const m = props.market
if (!m) return 0.19
if (Math.round(m.yesPrice * 100) > 0) return m.yesPrice
const d = outcomeRawToDollars(m.outcomePrices?.[0])
return d > 0 ? d : (m.yesPrice > 0 ? m.yesPrice : 0.19)
})
const noPriceDollarsEffective = computed(() => {
const m = props.market
if (!m) return 0.82
if (Math.round(m.noPrice * 100) > 0) return m.noPrice
const raw1 = m.outcomePrices?.[1]
if (raw1 != null && String(raw1).trim() !== '') {
const d = outcomeRawToDollars(raw1)
if (d > 0) return d
}
const y = yesPriceDollarsEffective.value
if (y > 0 && y <= 1) return Math.min(1, Math.max(0, 1 - y))
const d0 = outcomeRawToDollars(m.outcomePrices?.[0])
return d0 > 0 ? d0 : (m.noPrice > 0 ? m.noPrice : 0.82)
})
const yesPriceCents = computed(() => Math.round(yesPriceDollarsEffective.value * 100))
const noPriceCents = computed(() => Math.round(noPriceDollarsEffective.value * 100))
const yesLabel = computed(() => props.market?.outcomes?.[0] ?? 'Yes') const yesLabel = computed(() => props.market?.outcomes?.[0] ?? 'Yes')
const noLabel = computed(() => props.market?.outcomes?.[1] ?? 'No') const noLabel = computed(() => props.market?.outcomes?.[1] ?? 'No')
@ -1802,7 +1766,9 @@ const avgPriceCents = computed(() => {
const toWinValue = computed(() => { const toWinValue = computed(() => {
if (isMarketMode.value) { if (isMarketMode.value) {
const price = const price =
selectedOption.value === 'yes' ? yesPriceDollarsEffective.value : noPriceDollarsEffective.value selectedOption.value === 'yes'
? (props.market?.yesPrice ?? 0.5)
: (props.market?.noPrice ?? 0.5)
const sharesFromAmount = price > 0 ? amount.value / price : 0 const sharesFromAmount = price > 0 ? amount.value / price : 0
return trimTrailingZeros(sharesFromAmount.toFixed(2)) return trimTrailingZeros(sharesFromAmount.toFixed(2))
} }
@ -1865,9 +1831,9 @@ function clampLimitPrice(v: number): number {
/** 根据当前 props.market 与 selectedOption 同步 limitPrice组件显示或 market 更新时调用) */ /** 根据当前 props.market 与 selectedOption 同步 limitPrice组件显示或 market 更新时调用) */
function syncLimitPriceFromMarket() { function syncLimitPriceFromMarket() {
limitPrice.value = clampLimitPrice( const yesP = props.market?.yesPrice ?? 0.19
selectedOption.value === 'yes' ? yesPriceDollarsEffective.value : noPriceDollarsEffective.value, const noP = props.market?.noPrice ?? 0.82
) limitPrice.value = clampLimitPrice(selectedOption.value === 'yes' ? yesP : noP)
} }
onMounted(() => { onMounted(() => {
@ -1937,9 +1903,9 @@ watch(limitType, () => saveTradePrefs(), { flush: 'post' })
// Methods // Methods
const handleOptionChange = (option: 'yes' | 'no') => { const handleOptionChange = (option: 'yes' | 'no') => {
selectedOption.value = option selectedOption.value = option
limitPrice.value = clampLimitPrice( const yesP = props.market?.yesPrice ?? 0.19
option === 'yes' ? yesPriceDollarsEffective.value : noPriceDollarsEffective.value, const noP = props.market?.noPrice ?? 0.82
) limitPrice.value = clampLimitPrice(option === 'yes' ? yesP : noP)
emit('optionChange', option) emit('optionChange', option)
// Set max shares when option changes in sell mode // Set max shares when option changes in sell mode

View File

@ -221,14 +221,6 @@
"noPositionsFound": "No positions found.", "noPositionsFound": "No positions found.",
"noOpenOrdersFound": "No open orders found.", "noOpenOrdersFound": "No open orders found.",
"noHistoryFound": "You haven't traded any polymarkets yet", "noHistoryFound": "You haven't traded any polymarkets yet",
"historySideBuy": "BUY",
"historySideSell": "SELL",
"historyTypeSplit": "SPLIT",
"historyTypeMerge": "MERGE",
"historyTypeRedeem": "REDEEM",
"historyTypeReward": "REWARD",
"historyTypeConversion": "CONVERSION",
"historyTypeMakerRebate": "MAKER REBATE",
"market": "Market", "market": "Market",
"avgNow": "AVG → NOW", "avgNow": "AVG → NOW",
"bet": "BET", "bet": "BET",
@ -316,27 +308,7 @@
"nameSaved": "Username updated", "nameSaved": "Username updated",
"changeAvatar": "Change avatar", "changeAvatar": "Change avatar",
"avatarUploadSuccess": "Avatar updated", "avatarUploadSuccess": "Avatar updated",
"avatarUploadFailed": "Failed to upload avatar", "avatarUploadFailed": "Failed to upload avatar"
"memberCenter": "Member Center",
"depositCoin": "Deposit",
"withdrawCoin": "Withdraw"
},
"memberCenter": {
"title": "Member Center",
"currentLevel": "Current level",
"vipLabel": "VIP {n}",
"goRecharge": "Top up",
"explainTitle": "Levels",
"hintNeedMoreDeposit": "{amount} more USDC needed to reach the next level.",
"footnote": "* Thresholds and statistics follow backend rules.",
"tier0": "Complete registration.",
"tierNeedDeposit": "Cumulative deposits ≥ {amount} USDC.",
"feesLine": "Taker {taker} · Maker {maker}",
"loadError": "Failed to load VIP tiers. Please try again.",
"tier1": "Cumulative deposits ≥ $10,000 USDC.",
"tier2": "Cumulative deposits ≥ $50,000 USDC.",
"tier3": "Cumulative deposits ≥ $200,000 USDC.",
"tier4": "Cumulative deposits ≥ $500,000 USDC;"
}, },
"deposit": { "deposit": {
"title": "Deposit", "title": "Deposit",

View File

@ -221,14 +221,6 @@
"noPositionsFound": "ポジションがありません", "noPositionsFound": "ポジションがありません",
"noOpenOrdersFound": "未約定注文がありません", "noOpenOrdersFound": "未約定注文がありません",
"noHistoryFound": "まだ取引履歴がありません", "noHistoryFound": "まだ取引履歴がありません",
"historySideBuy": "買い",
"historySideSell": "売り",
"historyTypeSplit": "スプリット",
"historyTypeMerge": "マージ",
"historyTypeRedeem": "償還",
"historyTypeReward": "リワード",
"historyTypeConversion": "コンバージョン",
"historyTypeMakerRebate": "メイカー還元",
"market": "市場", "market": "市場",
"avgNow": "平均→現在", "avgNow": "平均→現在",
"bet": "ベット", "bet": "ベット",
@ -316,27 +308,7 @@
"nameSaved": "ユーザー名を更新しました", "nameSaved": "ユーザー名を更新しました",
"changeAvatar": "アバターを変更", "changeAvatar": "アバターを変更",
"avatarUploadSuccess": "アバターを更新しました", "avatarUploadSuccess": "アバターを更新しました",
"avatarUploadFailed": "アバターのアップロードに失敗しました", "avatarUploadFailed": "アバターのアップロードに失敗しました"
"memberCenter": "メンバー",
"depositCoin": "入金",
"withdrawCoin": "出金"
},
"memberCenter": {
"title": "メンバーセンター",
"currentLevel": "現在のレベル",
"vipLabel": "VIP {n}",
"goRecharge": "チャージへ",
"explainTitle": "レベル説明",
"hintNeedMoreDeposit": "次のレベルまであと {amount} USDC の入金が必要です。",
"footnote": "* 数値と条件はバックエンドの規則に従います。",
"tier0": "登録で達成。",
"tierNeedDeposit": "累計入金 ≥ {amount} USDC。",
"feesLine": "テイカー {taker} · メイカー {maker}",
"loadError": "レベル設定の読み込みに失敗しました",
"tier1": "累計入金 ≥ $10,000 USDC。",
"tier2": "累計入金 ≥ $50,000 USDC。",
"tier3": "累計入金 ≥ $200,000 USDC。",
"tier4": "累計入金 ≥ $500,000 USDC。"
}, },
"deposit": { "deposit": {
"title": "入金", "title": "入金",

View File

@ -221,14 +221,6 @@
"noPositionsFound": "포지션이 없습니다", "noPositionsFound": "포지션이 없습니다",
"noOpenOrdersFound": "미체결 주문이 없습니다", "noOpenOrdersFound": "미체결 주문이 없습니다",
"noHistoryFound": "아직 거래 내역이 없습니다", "noHistoryFound": "아직 거래 내역이 없습니다",
"historySideBuy": "매수",
"historySideSell": "매도",
"historyTypeSplit": "스플릿",
"historyTypeMerge": "머지",
"historyTypeRedeem": "상환",
"historyTypeReward": "리워드",
"historyTypeConversion": "전환",
"historyTypeMakerRebate": "메이커 리베이트",
"market": "시장", "market": "시장",
"avgNow": "평균→현재", "avgNow": "평균→현재",
"bet": "베팅", "bet": "베팅",
@ -316,27 +308,7 @@
"nameSaved": "사용자 이름이 업데이트되었습니다", "nameSaved": "사용자 이름이 업데이트되었습니다",
"changeAvatar": "아바타 변경", "changeAvatar": "아바타 변경",
"avatarUploadSuccess": "아바타가 업데이트되었습니다", "avatarUploadSuccess": "아바타가 업데이트되었습니다",
"avatarUploadFailed": "아바타 업로드에 실패했습니다", "avatarUploadFailed": "아바타 업로드에 실패했습니다"
"memberCenter": "멤버십",
"depositCoin": "입금",
"withdrawCoin": "출금"
},
"memberCenter": {
"title": "멤버 센터",
"currentLevel": "현재 등급",
"vipLabel": "VIP {n}",
"goRecharge": "충전하기",
"explainTitle": "등급 안내",
"hintNeedMoreDeposit": "다음 등급까지 {amount} USDC를 더 입금해야 합니다.",
"footnote": "* 통계와 기준은 백엔드 규칙을 따릅니다.",
"tier0": "가입 완료 시.",
"tierNeedDeposit": "누적 입금 ≥ {amount} USDC.",
"feesLine": "테이커 {taker} · 메이커 {maker}",
"loadError": "등급 정보를 불러오지 못했습니다",
"tier1": "누적 입금 ≥ $10,000 USDC.",
"tier2": "누적 입금 ≥ $50,000 USDC.",
"tier3": "누적 입금 ≥ $200,000 USDC.",
"tier4": "누적 입금 ≥ $500,000 USDC ."
}, },
"deposit": { "deposit": {
"title": "입금", "title": "입금",

View File

@ -221,14 +221,6 @@
"noPositionsFound": "暂无持仓", "noPositionsFound": "暂无持仓",
"noOpenOrdersFound": "暂无未成交订单", "noOpenOrdersFound": "暂无未成交订单",
"noHistoryFound": "您还未进行过任何交易", "noHistoryFound": "您还未进行过任何交易",
"historySideBuy": "买入",
"historySideSell": "卖出",
"historyTypeSplit": "拆分",
"historyTypeMerge": "合并",
"historyTypeRedeem": "赎回",
"historyTypeReward": "奖励",
"historyTypeConversion": "转换",
"historyTypeMakerRebate": "做市返佣",
"market": "市场", "market": "市场",
"avgNow": "均价→现价", "avgNow": "均价→现价",
"bet": "投注", "bet": "投注",
@ -316,27 +308,7 @@
"nameSaved": "用户名已更新", "nameSaved": "用户名已更新",
"changeAvatar": "更换头像", "changeAvatar": "更换头像",
"avatarUploadSuccess": "头像已更新", "avatarUploadSuccess": "头像已更新",
"avatarUploadFailed": "头像上传失败", "avatarUploadFailed": "头像上传失败"
"memberCenter": "会员中心",
"depositCoin": "充币",
"withdrawCoin": "提币"
},
"memberCenter": {
"title": "会员中心",
"currentLevel": "当前等级",
"vipLabel": "VIP {n}",
"goRecharge": "去充值",
"explainTitle": "等级说明",
"hintNeedMoreDeposit": "距离下一等级还需要充值 {amount} USDC。",
"footnote": "* 统计数据与门槛以后台规则为准。",
"tier0": "完成注册即可。",
"tierNeedDeposit": "累计充值 ≥ {amount} USDC。",
"feesLine": "吃单 {taker} · 挂单 {maker}",
"loadError": "等级配置加载失败,请稍后重试",
"tier1": "累计充值 ≥ $10,000 USDC。",
"tier2": "累计充值 ≥ $50,000 USDC。",
"tier3": "累计充值 ≥ $200,000 USDC。",
"tier4": "累计充值 ≥ $500,000 USDC"
}, },
"deposit": { "deposit": {
"title": "入金", "title": "入金",

View File

@ -221,14 +221,6 @@
"noPositionsFound": "暫無持倉", "noPositionsFound": "暫無持倉",
"noOpenOrdersFound": "暫無未成交訂單", "noOpenOrdersFound": "暫無未成交訂單",
"noHistoryFound": "您還未進行過任何交易", "noHistoryFound": "您還未進行過任何交易",
"historySideBuy": "買入",
"historySideSell": "賣出",
"historyTypeSplit": "拆分",
"historyTypeMerge": "合併",
"historyTypeRedeem": "贖回",
"historyTypeReward": "獎勵",
"historyTypeConversion": "轉換",
"historyTypeMakerRebate": "做市返佣",
"market": "市場", "market": "市場",
"avgNow": "均價→現價", "avgNow": "均價→現價",
"bet": "投注", "bet": "投注",
@ -316,27 +308,7 @@
"nameSaved": "使用者名稱已更新", "nameSaved": "使用者名稱已更新",
"changeAvatar": "更換頭像", "changeAvatar": "更換頭像",
"avatarUploadSuccess": "頭像已更新", "avatarUploadSuccess": "頭像已更新",
"avatarUploadFailed": "頭像上傳失敗", "avatarUploadFailed": "頭像上傳失敗"
"memberCenter": "會員中心",
"depositCoin": "充幣",
"withdrawCoin": "提幣"
},
"memberCenter": {
"title": "會員中心",
"currentLevel": "目前等級",
"vipLabel": "VIP {n}",
"goRecharge": "去充值",
"explainTitle": "等級說明",
"hintNeedMoreDeposit": "距離下一等級還需要充值 {amount} USDC。",
"footnote": "* 統計數據與門檻以後台規則為準。",
"tier0": "完成註冊即可。",
"tierNeedDeposit": "累計充值 ≥ {amount} USDC。",
"feesLine": "吃單 {taker} · 掛單 {maker}",
"loadError": "等級配置載入失敗,請稍後再試",
"tier1": "累計充值 ≥ $10,000 USDC。",
"tier2": "累計充值 ≥ $50,000 USDC。",
"tier3": "累計充值 ≥ $200,000 USDC。",
"tier4": "累計充值 ≥ $500,000 USDC"
}, },
"deposit": { "deposit": {
"title": "入金", "title": "入金",

View File

@ -7,7 +7,6 @@ import EventMarkets from '../views/EventMarkets.vue'
import Wallet from '../views/Wallet.vue' import Wallet from '../views/Wallet.vue'
import Search from '../views/Search.vue' import Search from '../views/Search.vue'
import Profile from '../views/Profile.vue' import Profile from '../views/Profile.vue'
import MemberCenter from '../views/MemberCenter.vue'
import ApiKey from '../views/ApiKey.vue' import ApiKey from '../views/ApiKey.vue'
const router = createRouter({ const router = createRouter({
@ -53,11 +52,6 @@ const router = createRouter({
name: 'profile', name: 'profile',
component: Profile, component: Profile,
}, },
{
path: '/member-center',
name: 'member-center',
component: MemberCenter,
},
{ {
path: '/api-key', path: '/api-key',
name: 'api-key', name: 'api-key',

View File

@ -281,7 +281,6 @@ const tradeMarketPayload = computed(() => {
title: m.question, title: m.question,
clobTokenIds: m.clobTokenIds, clobTokenIds: m.clobTokenIds,
outcomes: m.outcomes, outcomes: m.outcomes,
outcomePrices: m.outcomePrices,
} }
}) })
@ -416,7 +415,7 @@ function setChartSeries(seriesArr: ChartSeriesItem[]) {
const series = chartInstance!.addSeries(LineSeries, { const series = chartInstance!.addSeries(LineSeries, {
color, color,
lineWidth: 2, lineWidth: 2,
lineType: LineType.Simple, lineType: LineType.Curved,
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate, lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
crosshairMarkerVisible: true, crosshairMarkerVisible: true,
lastValueVisible: true, lastValueVisible: true,

View File

@ -1,404 +0,0 @@
<template>
<div class="member-center-page">
<div class="member-screen">
<header class="mc-header">
<h1 class="mc-title">{{ t('memberCenter.title') }}</h1>
</header>
<section class="hero-card">
<div class="hero-row">
<div class="hero-left">
<span class="hero-lbl">{{ t('memberCenter.currentLevel') }}</span>
<span class="hero-lvl">{{ t('memberCenter.vipLabel', { n: displayLevel }) }}</span>
</div>
<button type="button" class="btn-recharge" @click="goWallet">
{{ t('memberCenter.goRecharge') }}
</button>
</div>
<p v-if="nextHint" class="hero-hint">{{ nextHint }}</p>
</section>
<section v-if="vipLoading" class="mc-status">{{ t('common.loading') }}</section>
<section v-else-if="vipLoadError" class="mc-status mc-status--error">
{{ vipLoadError }}
</section>
<section v-else class="explain-block">
<h2 class="explain-title">{{ t('memberCenter.explainTitle') }}</h2>
<ul class="explain-list">
<li
v-for="row in tierRows"
:key="row.levelKey"
class="explain-row"
:class="{ 'explain-row--current': row.levelNum === displayLevel }"
>
<div class="explain-main">
<p class="explain-desc">{{ row.condition }}</p>
<p v-if="row.fees" class="explain-fees">{{ row.fees }}</p>
</div>
<span class="explain-vip">{{ row.label }}</span>
</li>
</ul>
</section>
<p class="mc-foot">{{ t('memberCenter.footnote') }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/stores/user'
import { getPmVipLevelPublic, type PmVipLevelItem } from '@/api/vipLevel'
const router = useRouter()
const { t } = useI18n()
const userStore = useUserStore()
const vipLevels = ref<PmVipLevelItem[]>([])
const vipLoading = ref(false)
const vipLoadError = ref<string | null>(null)
onMounted(() => {
loadVipLevels()
if (userStore.isLoggedIn) userStore.fetchUserInfo()
})
async function loadVipLevels() {
vipLoading.value = true
vipLoadError.value = null
try {
const res = await getPmVipLevelPublic()
if (res.code !== 0 && res.code !== 200) {
vipLoadError.value = res.msg || t('memberCenter.loadError')
return
}
vipLevels.value = res.data?.list ?? []
} catch {
vipLoadError.value = t('memberCenter.loadError')
} finally {
vipLoading.value = false
}
}
const rawUser = computed(() => (userStore.user ?? {}) as Record<string, unknown>)
function resolveVipLevel(user: Record<string, unknown>): number {
const keys = ['vipLevel', 'memberLevel', 'vip', 'level']
for (const k of keys) {
const v = user[k]
if (typeof v === 'number' && Number.isFinite(v)) {
return Math.max(0, Math.floor(v))
}
if (typeof v === 'string' && v.trim()) {
const n = parseInt(v.replace(/\D/g, ''), 10)
if (!Number.isNaN(n)) return Math.max(0, n)
}
}
return 0
}
/**
* 与订单价格 bps 一致费率整数 / 10000 为比例展示为百分比 数值 / 100 + '%'
*/
function formatTradingFee(rate: number): string {
if (!Number.isFinite(rate)) return '--'
return `${(rate / 10000).toFixed(2)}%`
}
/** 与接口 needDeposit / accumulated 同一套口径:大额按 USDC 6 位小数,否则按美元数值 */
function depositRawToUsd(raw: number): number {
if (!Number.isFinite(raw) || raw <= 0) return 0
return raw >= 1_000_000_000 ? raw / 1_000_000 : raw
}
function parseUserAccumulatedUsd(user: Record<string, unknown>): number {
const v = user.accumulated ?? user.Accumulated
if (typeof v === 'number' && Number.isFinite(v)) return depositRawToUsd(v)
if (typeof v === 'string' && v.trim()) {
const n = parseFloat(v.replace(/,/g, ''))
return Number.isFinite(n) ? depositRawToUsd(n) : 0
}
return 0
}
function formatNeedDepositUsd(n: number): string {
const dollars = depositRawToUsd(n)
if (dollars <= 0) return ''
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
maximumFractionDigits: Number.isInteger(dollars) && dollars >= 100 ? 0 : 2,
}).format(dollars)
}
function formatUsdcAmountPlain(dollars: number): string {
if (!Number.isFinite(dollars) || dollars <= 0) return ''
return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(dollars)
}
const sortedLevels = computed(() =>
[...vipLevels.value].sort(
(a, b) => parseInt(String(a.levelName), 10) - parseInt(String(b.levelName), 10),
),
)
const displayLevel = computed(() => resolveVipLevel(rawUser.value))
const nextHint = computed(() => {
const list = sortedLevels.value
if (!list.length) return ''
const lv = displayLevel.value
const idx = list.findIndex((x) => parseInt(String(x.levelName), 10) === lv)
if (idx < 0 || idx >= list.length - 1) return ''
const nextTier = list[idx + 1]
if (!nextTier) return ''
const nextNeedUsd = depositRawToUsd(nextTier.needDeposit)
if (nextNeedUsd <= 0) return ''
const accumulatedUsd = parseUserAccumulatedUsd(rawUser.value)
const remainUsd = Math.max(0, nextNeedUsd - accumulatedUsd)
const amount = formatUsdcAmountPlain(remainUsd)
if (!amount) return ''
return t('memberCenter.hintNeedMoreDeposit', { amount })
})
const FALLBACK_MAX = 4
const tierRows = computed(() => {
const list = sortedLevels.value
if (list.length) {
return list.map((item) => {
const n = parseInt(String(item.levelName), 10)
const levelNum = Number.isNaN(n) ? 0 : n
const condition =
!item.needDeposit || item.needDeposit <= 0
? t('memberCenter.tier0')
: t('memberCenter.tierNeedDeposit', { amount: formatNeedDepositUsd(item.needDeposit) })
const fees = t('memberCenter.feesLine', {
taker: formatTradingFee(item.takerFeeRate),
maker: formatTradingFee(item.makerFeeRate),
})
return {
levelKey: String(item.levelName),
levelNum,
condition,
fees,
label: t('memberCenter.vipLabel', { n: levelNum }),
}
})
}
return Array.from({ length: FALLBACK_MAX + 1 }, (_, i) => ({
levelKey: `fallback-${i}`,
levelNum: i,
condition: t(`memberCenter.tier${i}`),
fees: '',
label: t('memberCenter.vipLabel', { n: i }),
}))
})
function goWallet() {
router.push('/wallet')
}
</script>
<style scoped>
.member-center-page {
min-height: 100vh;
background: #ffffff;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 0;
}
.member-screen {
width: 100%;
max-width: 100%;
min-height: 0;
background: #fcfcfc;
font-family: Inter, sans-serif;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
box-sizing: border-box;
}
.mc-header {
display: flex;
align-items: center;
gap: 12px;
}
.mc-back {
border: 0;
background: transparent;
padding: 0;
font-size: 22px;
line-height: 1;
color: #111827;
cursor: pointer;
}
.mc-title {
margin: 0;
font-size: 20px;
font-weight: 700;
color: #111827;
line-height: 1.2;
}
.mc-status {
margin: 0;
font-size: 13px;
color: #6b7280;
padding: 8px 0;
}
.mc-status--error {
color: #b45309;
}
.hero-card {
background: #fff7e6;
border: 1.5px solid #e6a817;
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
box-sizing: border-box;
}
.hero-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.hero-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
.hero-lbl {
color: #78350f;
font-size: 13px;
font-weight: 500;
}
.hero-lvl {
color: #c9970a;
font-size: 20px;
font-weight: bold;
}
.btn-recharge {
flex-shrink: 0;
height: 36px;
padding: 0 14px;
border: 0;
border-radius: 8px;
background: #c9970a;
color: #fffef5;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.hero-hint {
margin: 0;
color: #a16207;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
}
.explain-block {
display: flex;
flex-direction: column;
gap: 14px;
}
.explain-title {
margin: 0;
font-size: 16px;
font-weight: 700;
color: #333333;
line-height: 1.2;
}
.explain-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.explain-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.explain-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.explain-desc {
margin: 0;
font-size: 12px;
line-height: 1.45;
color: #6b7280;
}
.explain-fees {
margin: 0;
font-size: 11px;
line-height: 1.35;
color: #9ca3af;
}
.explain-vip {
flex-shrink: 0;
min-width: 52px;
text-align: right;
font-size: 12px;
font-weight: 700;
color: #111827;
}
.explain-row--current .explain-desc {
color: #78350f;
font-weight: bold;
}
.explain-row--current .explain-fees {
color: #a16207;
font-weight: 600;
}
.explain-row--current .explain-vip {
color: #c9970a;
font-weight: bold;
}
.mc-foot {
margin: 0;
font-size: 11px;
line-height: 1.35;
color: #6b7280;
}
</style>

View File

@ -4,7 +4,12 @@
<section class="card profile-card"> <section class="card profile-card">
<div class="top-row"> <div class="top-row">
<label class="avatar-wrap" :aria-label="t('profile.changeAvatar')"> <label class="avatar-wrap" :aria-label="t('profile.changeAvatar')">
<input type="file" accept="image/*" class="avatar-input" @change="onAvatarFileChange" /> <input
type="file"
accept="image/*"
class="avatar-input"
@change="onAvatarFileChange"
/>
<div class="avatar"> <div class="avatar">
<v-progress-circular <v-progress-circular
v-if="avatarUploading" v-if="avatarUploading"
@ -27,54 +32,27 @@
<div class="acc-text">{{ t('profile.uidLabel', { uid: userIdText }) }}</div> <div class="acc-text">{{ t('profile.uidLabel', { uid: userIdText }) }}</div>
<div class="tag-text">{{ userTag }}</div> <div class="tag-text">{{ userTag }}</div>
</div> </div>
<button class="edit-btn" type="button" @click="onEditProfile"> <button class="edit-btn" type="button" @click="onEditProfile">{{ t('profile.edit') }}</button>
{{ t('profile.edit') }}
</button>
</div> </div>
<button type="button" class="vip-entry" @click="goMemberCenter">
<span class="vip-entry-title">{{ t('profile.memberCenter') }}</span>
<span class="vip-entry-right">
<span class="vip-pill">{{ t('memberCenter.vipLabel', { n: vipLevel }) }}</span>
<span class="vip-chev" aria-hidden="true"></span>
</span>
</button>
</section> </section>
<section class="card wallet-card"> <section class="card wallet-card">
<div class="wallet-head"> <div class="wallet-head">
<span class="wallet-title">{{ t('profile.walletOverview') }}</span> <span class="wallet-title">{{ t('profile.walletOverview') }}</span>
<button class="wallet-link" type="button" @click="goWallet"> <button class="wallet-link" type="button" @click="goWallet">{{ t('profile.walletDetail') }} &gt;</button>
{{ t('profile.walletDetail') }} &gt;
</button>
</div> </div>
<div class="wallet-balance">${{ totalBalance }}</div> <div class="wallet-balance">${{ totalBalance }}</div>
<div class="wallet-sub"> <div class="wallet-sub">
{{ t('profile.walletSub', { available: availableBalance, frozen: frozenBalance }) }} {{ t('profile.walletSub', { available: availableBalance, frozen: frozenBalance }) }}
</div> </div>
<div class="wallet-actions">
<button type="button" class="wallet-action-primary" @click="goWallet">
{{ t('profile.depositCoin') }}
</button>
<button type="button" class="wallet-action-secondary" @click="goWallet">
{{ t('profile.withdrawCoin') }}
</button>
</div>
</section> </section>
<section class="card menu-card"> <section class="card menu-card">
<div class="menu-title">{{ t('profile.accountSettings') }}</div> <div class="menu-title">{{ t('profile.accountSettings') }}</div>
<button <button v-for="item in settingItems" :key="item.label" class="menu-item" type="button" @click="goSetting(item)">
v-for="item in settingItems"
:key="item.label"
class="menu-item"
type="button"
@click="goSetting(item)"
>
<span>{{ item.label }}</span> <span>{{ item.label }}</span>
<span v-if="item.action === 'locale'" class="menu-locale">{{ currentLocaleLabel }}</span> <span v-if="item.action === 'locale'" class="menu-locale">{{ currentLocaleLabel }}</span>
<span v-else-if="item.action === 'wallet'" class="menu-locale">{{ <span v-else-if="item.action === 'wallet'" class="menu-locale">{{ walletAddressShort }}</span>
walletAddressShort
}}</span>
<span v-else class="menu-arrow">&gt;</span> <span v-else class="menu-arrow">&gt;</span>
</button> </button>
</section> </section>
@ -105,12 +83,7 @@
<div class="wallet-dialog-title">{{ t('profile.currentWalletAddress') }}</div> <div class="wallet-dialog-title">{{ t('profile.currentWalletAddress') }}</div>
<div class="wallet-dialog-address">{{ walletAddressText }}</div> <div class="wallet-dialog-address">{{ walletAddressText }}</div>
<div class="wallet-dialog-actions"> <div class="wallet-dialog-actions">
<button <button class="wallet-copy-btn" type="button" :disabled="!walletAddress" @click="copyWalletAddress">
class="wallet-copy-btn"
type="button"
:disabled="!walletAddress"
@click="copyWalletAddress"
>
{{ t('profile.copyAddress') }} {{ t('profile.copyAddress') }}
</button> </button>
</div> </div>
@ -131,12 +104,7 @@
persistent-hint persistent-hint
/> />
<div class="name-dialog-actions"> <div class="name-dialog-actions">
<button <button class="name-dialog-cancel-btn" type="button" :disabled="isSaving" @click="closeEditNameDialog">
class="name-dialog-cancel-btn"
type="button"
:disabled="isSaving"
@click="closeEditNameDialog"
>
{{ t('profile.cancel') }} {{ t('profile.cancel') }}
</button> </button>
<button class="name-dialog-save-btn" type="button" :disabled="isSaving" @click="saveName"> <button class="name-dialog-save-btn" type="button" :disabled="isSaving" @click="saveName">
@ -214,9 +182,7 @@ const walletAddress = computed(() => {
return '' return ''
}) })
const walletAddressText = computed( const walletAddressText = computed(() => walletAddress.value || t('profile.walletAddressUnavailable'))
() => walletAddress.value || t('profile.walletAddressUnavailable'),
)
const walletAddressShort = computed(() => { const walletAddressShort = computed(() => {
const value = walletAddress.value const value = walletAddress.value
if (!value) return t('profile.unbound') if (!value) return t('profile.unbound')
@ -305,28 +271,6 @@ function goWallet() {
router.push('/wallet') router.push('/wallet')
} }
function goMemberCenter() {
router.push('/member-center')
}
function resolveVipLevel(user: Record<string, unknown> | null): number {
if (!user) return 0
const keys = ['vipLevel', 'memberLevel', 'vip', 'level']
for (const k of keys) {
const v = user[k]
if (typeof v === 'number' && Number.isFinite(v)) {
return Math.min(4, Math.max(0, Math.floor(v)))
}
if (typeof v === 'string' && v.trim()) {
const n = parseInt(v.replace(/\D/g, ''), 10)
if (!Number.isNaN(n)) return Math.min(4, Math.max(0, n))
}
}
return 0
}
const vipLevel = computed(() => resolveVipLevel(rawUser.value as Record<string, unknown> | null))
function validateUserName(name: string): string | null { function validateUserName(name: string): string | null {
const v = name.trim() const v = name.trim()
if (!v) return t('profile.nameRequired') if (!v) return t('profile.nameRequired')
@ -591,49 +535,6 @@ onMounted(() => {
cursor: pointer; cursor: pointer;
} }
.vip-entry {
width: 100%;
margin-top: 14px;
padding: 12px 14px;
border-radius: 12px;
border: 1.5px solid #e6a817;
background: #fff7e6;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
cursor: pointer;
box-sizing: border-box;
}
.vip-entry-title {
color: #78350f;
font-size: 15px;
font-weight: 600;
}
.vip-entry-right {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.vip-pill {
padding: 2px 14px;
color: #c9970a;
font-size: 16px;
font-weight: 700;
line-height: 1.2;
}
.vip-chev {
color: #ca8a04;
font-size: 18px;
font-weight: 400;
line-height: 1;
}
.wallet-card { .wallet-card {
padding: 16px; padding: 16px;
display: flex; display: flex;
@ -683,28 +584,25 @@ onMounted(() => {
gap: 10px; gap: 10px;
} }
.wallet-action-primary { .action-btn {
flex: 1; flex: 1;
height: 40px; height: 40px;
border: 0;
border-radius: 12px; border-radius: 12px;
background: #5b5bd6;
color: #ffffff;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
} }
.wallet-action-secondary { .action-primary {
flex: 1; border: 0;
height: 40px; background: #5b5bd6;
border-radius: 12px; color: #ffffff;
}
.action-secondary {
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
background: #fcfcfc; background: #fcfcfc;
color: #111827; color: #111827;
font-size: 13px;
font-weight: 600;
cursor: pointer;
} }
.menu-card { .menu-card {

View File

@ -187,12 +187,6 @@
:bids-yes="orderBookBidsYes" :bids-yes="orderBookBidsYes"
:asks-no="orderBookAsksNo" :asks-no="orderBookAsksNo"
:bids-no="orderBookBidsNo" :bids-no="orderBookBidsNo"
:anchor-best-bid-yes="orderBookBestBidYesCents"
:anchor-lowest-ask-yes="orderBookLowestAskYesCents"
:anchor-best-bid-no="orderBookBestBidNoCents"
:anchor-lowest-ask-no="orderBookLowestAskNoCents"
:outcome-price-anchor-yes-cents="orderBookOutcomeAnchorYesCents"
:outcome-price-anchor-no-cents="orderBookOutcomeAnchorNoCents"
:last-price-yes="clobLastPriceYes" :last-price-yes="clobLastPriceYes"
:last-price-no="clobLastPriceNo" :last-price-no="clobLastPriceNo"
:spread-yes="clobSpreadYes" :spread-yes="clobSpreadYes"
@ -621,32 +615,6 @@ const orderBookBestBidNoCents = computed(() => {
if (!bids.length) return 0 if (!bids.length) return 0
return Math.max(...bids.map((b) => b.price)) return Math.max(...bids.map((b) => b.price))
}) })
/** outcomePrices 单项 → 订单簿锚价(美分):概率 01 或已是美分 1100 */
function outcomePriceToAnchorCents(raw: string | number | undefined): number {
if (raw == null || (typeof raw === 'string' && raw.trim() === '')) return 0
const p = parseFloat(String(raw))
if (!Number.isFinite(p)) return 0
if (p > 1 && p <= 100) return Math.min(99, Math.max(1, Math.round(p)))
return Math.min(99, Math.max(0, Math.round(p * 100)))
}
/** 无真实盘口时模拟 Yes 簿:锚定 outcomePrices[0] 为最优卖一 */
const orderBookOutcomeAnchorYesCents = computed(() =>
outcomePriceToAnchorCents(currentMarket.value?.outcomePrices?.[0]),
)
/** 无真实盘口时模拟 No 簿:优先 outcomePrices[1],否则与 Yes 互补100Yes¢ */
const orderBookOutcomeAnchorNoCents = computed(() => {
const m = currentMarket.value
const raw1 = m?.outcomePrices?.[1]
if (raw1 != null && String(raw1).trim() !== '') {
return outcomePriceToAnchorCents(raw1)
}
const y = orderBookOutcomeAnchorYesCents.value
if (y > 0) return Math.min(99, Math.max(2, 100 - y))
return outcomePriceToAnchorCents(m?.outcomePrices?.[0])
})
const clobLastPriceYes = computed(() => clobLastPriceByToken.value[0]) const clobLastPriceYes = computed(() => clobLastPriceByToken.value[0])
const clobLastPriceNo = computed(() => clobLastPriceByToken.value[1]) const clobLastPriceNo = computed(() => clobLastPriceByToken.value[1])
const clobSpreadYes = computed(() => clobSpreadByToken.value[0]) const clobSpreadYes = computed(() => clobSpreadByToken.value[0])
@ -790,36 +758,14 @@ function disconnectClob() {
clobLoading.value = false clobLoading.value = false
} }
/** /** 传给 TradeComponent 的 market供 Split 调用 /PmMarket/splityesPrice/noPrice 取订单簿卖单最低价,无数据时为 0 */
* 传给 TradeComponentyesPrice/noPrice 优先取对应 token 订单簿 **卖单最低价**美元 01
* 取不到再用当前市场的 outcomePrices
*/
const tradeMarketPayload = computed(() => { const tradeMarketPayload = computed(() => {
const m = currentMarket.value const m = currentMarket.value
const yC = orderBookLowestAskYesCents.value const yesPrice = orderBookLowestAskYesCents.value / 100
const nC = orderBookLowestAskNoCents.value const noPrice = orderBookLowestAskNoCents.value / 100
let yesPrice = yC > 0 ? yC / 100 : 0
let noPrice = nC > 0 ? nC / 100 : 0
const bestBidYesCents = orderBookBestBidYesCents.value const bestBidYesCents = orderBookBestBidYesCents.value
const bestBidNoCents = orderBookBestBidNoCents.value const bestBidNoCents = orderBookBestBidNoCents.value
if (m) { if (m) {
if (yesPrice <= 0) {
const c = outcomePriceToAnchorCents(m.outcomePrices?.[0])
if (c > 0) yesPrice = c / 100
}
if (noPrice <= 0) {
const raw1 = m.outcomePrices?.[1]
if (raw1 != null && String(raw1).trim() !== '') {
const c = outcomePriceToAnchorCents(raw1)
if (c > 0) noPrice = c / 100
} else if (yesPrice > 0) {
noPrice = Math.min(1, Math.max(0, 1 - yesPrice))
} else {
const c = outcomePriceToAnchorCents(m.outcomePrices?.[0])
if (c > 0) noPrice = c / 100
}
}
return { return {
marketId: getMarketId(m), marketId: getMarketId(m),
yesPrice, yesPrice,
@ -827,7 +773,6 @@ const tradeMarketPayload = computed(() => {
title: m.question, title: m.question,
clobTokenIds: m.clobTokenIds, clobTokenIds: m.clobTokenIds,
outcomes: m.outcomes, outcomes: m.outcomes,
outcomePrices: m.outcomePrices,
bestBidYesCents, bestBidYesCents,
bestBidNoCents, bestBidNoCents,
} }
@ -1207,7 +1152,7 @@ function ensureChartSeries() {
chartSeries = chartInstance.addSeries(LineSeries, { chartSeries = chartInstance.addSeries(LineSeries, {
color: lineColor, color: lineColor,
lineWidth: 2, lineWidth: 2,
lineType: LineType.Simple, lineType: LineType.Curved,
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate, lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
crosshairMarkerVisible: true, crosshairMarkerVisible: true,
lastValueVisible: true, lastValueVisible: true,

View File

@ -75,12 +75,6 @@
v-for="pos in paginatedPositions" v-for="pos in paginatedPositions"
:key="pos.id" :key="pos.id"
class="position-mobile-card design-pos-card" class="position-mobile-card design-pos-card"
:class="{ 'wallet-row--nav': canOpenTradeDetail(pos) }"
:role="canOpenTradeDetail(pos) ? 'button' : undefined"
:tabindex="canOpenTradeDetail(pos) ? 0 : -1"
@click="onPositionRowClick(pos)"
@keydown.enter.prevent="canOpenTradeDetail(pos) && onPositionRowClick(pos)"
@keydown.space.prevent="canOpenTradeDetail(pos) && onPositionRowClick(pos)"
> >
<div class="design-pos-top"> <div class="design-pos-top">
<div class="design-pos-left"> <div class="design-pos-left">
@ -97,7 +91,12 @@
<span v-else class="position-icon-char">{{ pos.iconChar || '•' }}</span> <span v-else class="position-icon-char">{{ pos.iconChar || '•' }}</span>
</div> </div>
<div class="design-pos-title-col"> <div class="design-pos-title-col">
<MarqueeTitle :text="pos.market" title-class="position-mobile-title" /> <div class="position-mobile-title marquee-container">
<div class="marquee-track">
<span>{{ pos.market }}</span>
<span aria-hidden="true">{{ pos.market }}</span>
</div>
</div>
<div class="design-pos-tags"> <div class="design-pos-tags">
<span <span
v-if="pos.outcomeTag" v-if="pos.outcomeTag"
@ -151,33 +150,18 @@
<div v-else-if="filteredOpenOrders.length === 0" class="empty-cell"> <div v-else-if="filteredOpenOrders.length === 0" class="empty-cell">
{{ t('wallet.noOpenOrdersFound') }} {{ t('wallet.noOpenOrdersFound') }}
</div> </div>
<div <div v-for="ord in paginatedOpenOrders" :key="ord.id" class="order-mobile-card design-order-card">
v-for="ord in paginatedOpenOrders"
:key="ord.id"
class="order-mobile-card design-order-card"
:class="{ 'wallet-row--nav': canOpenTradeDetail(ord) }"
:role="canOpenTradeDetail(ord) ? 'button' : undefined"
:tabindex="canOpenTradeDetail(ord) ? 0 : -1"
@click="onOpenOrderRowClick(ord)"
@keydown.enter.prevent="canOpenTradeDetail(ord) && onOpenOrderRowClick(ord)"
@keydown.space.prevent="canOpenTradeDetail(ord) && onOpenOrderRowClick(ord)"
>
<div class="design-order-top"> <div class="design-order-top">
<div class="order-mobile-icon" :class="ord.iconClass"> <div class="order-mobile-icon" :class="ord.iconClass">
<img <span class="position-icon-char">{{ ord.iconChar || '•' }}</span>
v-if="ord.imageUrl"
:src="ord.imageUrl"
alt=""
class="position-icon-img"
/>
<span v-else class="position-icon-char">{{ ord.iconChar || '•' }}</span>
</div> </div>
<div class="order-mobile-main design-order-title-col"> <div class="order-mobile-main design-order-title-col">
<MarqueeTitle <div class="order-mobile-title marquee-container order-title-marquee">
:text="getOrderDisplayTitle(ord)" <div class="marquee-track">
title-class="order-mobile-title" <span>{{ getOrderDisplayTitle(ord) }}</span>
fast <span aria-hidden="true">{{ getOrderDisplayTitle(ord) }}</span>
/> </div>
</div>
<div class="design-order-tags"> <div class="design-order-tags">
<span class="order-side-pill" :class="getOrderSideClass(ord)">{{ <span class="order-side-pill" :class="getOrderSideClass(ord)">{{
getOrderActionLabel(ord) getOrderActionLabel(ord)
@ -232,15 +216,7 @@
v-for="h in paginatedHistory" v-for="h in paginatedHistory"
:key="h.id" :key="h.id"
class="history-mobile-card" class="history-mobile-card"
:class="[ :class="isFundingHistory(h) ? 'design-funding-card' : 'design-trade-card'"
isFundingHistory(h) ? 'design-funding-card' : 'design-trade-card',
{ 'wallet-row--nav': historyRowCanOpenDetail(h) },
]"
:role="historyRowCanOpenDetail(h) ? 'button' : undefined"
:tabindex="historyRowCanOpenDetail(h) ? 0 : undefined"
@click="onHistoryRowClick(h)"
@keydown.enter.prevent="onHistoryRowClick(h)"
@keydown.space.prevent="onHistoryRowClick(h)"
> >
<template v-if="!isFundingHistory(h)"> <template v-if="!isFundingHistory(h)">
<div class="design-history-top"> <div class="design-history-top">
@ -250,7 +226,12 @@
<span v-else class="position-icon-char">{{ h.iconChar || '•' }}</span> <span v-else class="position-icon-char">{{ h.iconChar || '•' }}</span>
</div> </div>
<div class="history-mobile-main"> <div class="history-mobile-main">
<MarqueeTitle :text="h.market" title-class="history-mobile-title" fast /> <div class="history-mobile-title marquee-container history-title-marquee">
<div class="marquee-track">
<span>{{ h.market }}</span>
<span aria-hidden="true">{{ h.market }}</span>
</div>
</div>
<div class="history-mobile-activity"> <div class="history-mobile-activity">
{{ h.timeAgo || h.activityDetail || h.activity }} {{ h.timeAgo || h.activityDetail || h.activity }}
</div> </div>
@ -280,7 +261,12 @@
<span v-else class="position-icon-char">{{ getFundingIconText(h) }}</span> <span v-else class="position-icon-char">{{ getFundingIconText(h) }}</span>
</div> </div>
<div class="history-mobile-main"> <div class="history-mobile-main">
<MarqueeTitle :text="getFundingTitle(h)" title-class="history-mobile-title" fast /> <div class="history-mobile-title marquee-container history-title-marquee">
<div class="marquee-track">
<span>{{ getFundingTitle(h) }}</span>
<span aria-hidden="true">{{ getFundingTitle(h) }}</span>
</div>
</div>
<div class="history-mobile-activity">{{ h.timeAgo || '—' }}</div> <div class="history-mobile-activity">{{ h.timeAgo || '—' }}</div>
</div> </div>
</div> </div>
@ -343,7 +329,7 @@
<template v-else> <template v-else>
<div class="empty-cell">{{ t('common.noData') }}</div> <div class="empty-cell">{{ t('common.noData') }}</div>
</template> </template>
<div v-if="currentListTotal > 0" class="pagination-bar"> <div v-if="false && currentListTotal > 0" class="pagination-bar">
<span class="pagination-info"> <span class="pagination-info">
{{ currentPageStart }}{{ currentPageEnd }} of {{ currentListTotal }} {{ currentPageStart }}{{ currentPageEnd }} of {{ currentListTotal }}
</span> </span>
@ -470,7 +456,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue' import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
const { t } = useI18n() const { t } = useI18n()
@ -478,19 +463,13 @@ import { createChart, AreaSeries, LineType, LastPriceAnimationMode } from 'light
import { toLwcData } from '../composables/useLightweightChart' import { toLwcData } from '../composables/useLightweightChart'
import DepositDialog from '../components/DepositDialog.vue' import DepositDialog from '../components/DepositDialog.vue'
import WithdrawDialog from '../components/WithdrawDialog.vue' import WithdrawDialog from '../components/WithdrawDialog.vue'
import MarqueeTitle from '../components/MarqueeTitle.vue'
import { useUserStore } from '../stores/user' import { useUserStore } from '../stores/user'
import { useLocaleStore } from '../stores/locale' import { useLocaleStore } from '../stores/locale'
import { useAuthError } from '../composables/useAuthError' import { useAuthError } from '../composables/useAuthError'
import { cancelOrder as apiCancelOrder } from '../api/order' import { cancelOrder as apiCancelOrder } from '../api/order'
import { getOrderList, mapOrderToOpenOrderItem, OrderStatus } from '../api/order' import { getOrderList, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
import { import { getHistoryRecordListClient, getHistoryRecordList } from '../api/historyRecord'
getHistoryRecordListClient,
getHistoryRecordList,
HISTORY_RECORD_TYPE,
} from '../api/historyRecord'
import { getPositionList, mapPositionToDisplayItem, claimPosition } from '../api/position' import { getPositionList, mapPositionToDisplayItem, claimPosition } from '../api/position'
import { buildTradeDetailPushOptions } from '../api/event'
import { import {
getSettlementRequestsListClient, getSettlementRequestsListClient,
amountToUsdcDisplay, amountToUsdcDisplay,
@ -507,7 +486,6 @@ import { USE_MOCK_WALLET } from '../config/mock'
import { CrossChainUSDTAuth } from '../../sdk/approve' import { CrossChainUSDTAuth } from '../../sdk/approve'
import { useToastStore } from '../stores/toast' import { useToastStore } from '../stores/toast'
const router = useRouter()
const { mobile } = useDisplay() const { mobile } = useDisplay()
const userStore = useUserStore() const userStore = useUserStore()
const { formatAuthError } = useAuthError() const { formatAuthError } = useAuthError()
@ -622,10 +600,6 @@ interface Position {
tokenID?: string tokenID?: string
/** 所属市场是否已关闭marketClosed=true 表示可结算/可领取 */ /** 所属市场是否已关闭marketClosed=true 表示可结算/可领取 */
marketClosed?: boolean marketClosed?: boolean
/** 跳转市场详情:事件 ID路径 */
tradeEventId?: string
/** 跳转市场详情:事件 slug */
tradeEventSlug?: string
} }
/** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */ /** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */
@ -653,11 +627,6 @@ interface OpenOrder {
tokenID?: string tokenID?: string
/** 已成交数量达到原始总数量,不可撤单 */ /** 已成交数量达到原始总数量,不可撤单 */
fullyFilled?: boolean fullyFilled?: boolean
/** 来自订单 pmMarket.image / icon */
imageUrl?: string
tradeEventId?: string
tradeEventSlug?: string
detailMarketId?: string
} }
interface HistoryItem { interface HistoryItem {
id: string id: string
@ -678,71 +647,6 @@ interface HistoryItem {
iconClass?: string iconClass?: string
/** 图标 URL来自 record.icon */ /** 图标 URL来自 record.icon */
imageUrl?: string imageUrl?: string
/** API typeTRADE、SPLIT… */
recordType?: string
/** TRADE 时 buy / sell */
tradeSideRaw?: string
tradeEventId?: string
tradeEventSlug?: string
detailMarketId?: string
}
function canOpenTradeDetail(opts: {
tradeEventId?: string
tradeEventSlug?: string
}): boolean {
return !!(opts.tradeEventId?.trim() || opts.tradeEventSlug?.trim())
}
function openTradeDetailFromWallet(input: {
tradeEventId?: string
tradeEventSlug?: string
marketId?: string
title?: string
}) {
const loc = buildTradeDetailPushOptions({
eventId: input.tradeEventId,
eventSlug: input.tradeEventSlug,
marketId: input.marketId,
title: input.title,
})
if (loc) router.push(loc)
}
function onPositionRowClick(pos: Position) {
if (!canOpenTradeDetail(pos)) return
openTradeDetailFromWallet({
tradeEventId: pos.tradeEventId,
tradeEventSlug: pos.tradeEventSlug,
marketId: pos.marketID,
title: pos.market,
})
}
function onOpenOrderRowClick(ord: OpenOrder) {
if (!canOpenTradeDetail(ord)) return
openTradeDetailFromWallet({
tradeEventId: ord.tradeEventId,
tradeEventSlug: ord.tradeEventSlug,
marketId: ord.detailMarketId,
title: ord.market,
})
}
function historyRowCanOpenDetail(h: HistoryItem): boolean {
if (isFundingHistory(h)) return false
return canOpenTradeDetail(h)
}
function onHistoryRowClick(h: HistoryItem) {
if (isFundingHistory(h)) return
if (!canOpenTradeDetail(h)) return
openTradeDetailFromWallet({
tradeEventId: h.tradeEventId,
tradeEventSlug: h.tradeEventSlug,
marketId: h.detailMarketId,
title: h.market,
})
} }
function getOutcomeClass(outcomeTag?: string, fallback?: string): string { function getOutcomeClass(outcomeTag?: string, fallback?: string): string {
@ -803,37 +707,13 @@ function getFundingIconClass(h: HistoryItem): string {
return isWithdrawalHistory(h) ? 'funding-icon-withdraw' : 'funding-icon-deposit' return isWithdrawalHistory(h) ? 'funding-icon-withdraw' : 'funding-icon-deposit'
} }
function getHistoryTagLabel(h: HistoryItem): string { function getHistoryTagLabel(h: HistoryItem): 'BUY' | 'SELL' {
if (!h.recordType) { return h.profitLossNegative ? 'BUY' : 'SELL'
return h.profitLossNegative ? t('wallet.historySideBuy') : t('wallet.historySideSell')
}
const type = h.recordType.toUpperCase()
if (type === HISTORY_RECORD_TYPE.TRADE) {
const s = (h.tradeSideRaw ?? '').toLowerCase()
return s === 'sell' ? t('wallet.historySideSell') : t('wallet.historySideBuy')
}
const typeLabels: Record<string, string> = {
[HISTORY_RECORD_TYPE.SPLIT]: t('wallet.historyTypeSplit'),
[HISTORY_RECORD_TYPE.MERGE]: t('wallet.historyTypeMerge'),
[HISTORY_RECORD_TYPE.REDEEM]: t('wallet.historyTypeRedeem'),
[HISTORY_RECORD_TYPE.REWARD]: t('wallet.historyTypeReward'),
[HISTORY_RECORD_TYPE.CONVERSION]: t('wallet.historyTypeConversion'),
[HISTORY_RECORD_TYPE.MAKER_REBATE]: t('wallet.historyTypeMakerRebate'),
}
return typeLabels[type] ?? type
} }
function getHistoryTagClass(h: HistoryItem): string { function getHistoryTagClass(h: HistoryItem): string {
if (!h.recordType) {
return h.profitLossNegative ? 'history-tag-buy' : 'history-tag-sell' return h.profitLossNegative ? 'history-tag-buy' : 'history-tag-sell'
} }
const type = h.recordType.toUpperCase()
if (type === HISTORY_RECORD_TYPE.TRADE) {
const s = (h.tradeSideRaw ?? '').toLowerCase()
return s === 'sell' ? 'history-tag-sell' : 'history-tag-buy'
}
return 'history-tag-type'
}
const positions = ref<Position[]>(USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] : []) const positions = ref<Position[]>(USE_MOCK_WALLET ? [...MOCK_WALLET_POSITIONS] : [])
/** 提现记录列表 */ /** 提现记录列表 */
@ -918,9 +798,7 @@ async function loadOpenOrders() {
const list = res.data?.list ?? [] const list = res.data?.list ?? []
const openOnly = list.filter((o) => (o.status ?? 1) === OrderStatus.Live) const openOnly = list.filter((o) => (o.status ?? 1) === OrderStatus.Live)
openOrderList.value = openOnly.map(mapOrderToOpenOrderItem) openOrderList.value = openOnly.map(mapOrderToOpenOrderItem)
const apiTotal = res.data?.total openOrderTotal.value = openOnly.length
openOrderTotal.value =
typeof apiTotal === 'number' && Number.isFinite(apiTotal) ? apiTotal : openOnly.length
} else { } else {
openOrderList.value = [] openOrderList.value = []
openOrderTotal.value = 0 openOrderTotal.value = 0
@ -1375,7 +1253,7 @@ function initPlChart() {
bottomColor: color + '08', bottomColor: color + '08',
lineColor: color, lineColor: color,
lineWidth: 2, lineWidth: 2,
lineType: LineType.Simple, lineType: LineType.Curved,
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate, lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
priceFormat: { type: 'price', precision: 2 }, priceFormat: { type: 'price', precision: 2 },
}) })
@ -1644,15 +1522,6 @@ async function submitAuthorize() {
padding: 16px; padding: 16px;
} }
.wallet-row--nav {
cursor: pointer;
}
.wallet-row--nav:focus-visible {
outline: 2px solid rgb(var(--v-theme-primary));
outline-offset: 2px;
}
.wallet-tabs { .wallet-tabs {
margin-bottom: 12px; margin-bottom: 12px;
min-height: 30px; min-height: 30px;
@ -1820,7 +1689,7 @@ async function submitAuthorize() {
border-radius: 10px; border-radius: 10px;
} }
.design-pos-card :deep(.position-mobile-title) { .design-pos-card .position-mobile-title {
margin-bottom: 0; margin-bottom: 0;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
@ -1832,6 +1701,33 @@ async function submitAuthorize() {
width: 0; width: 0;
} }
.marquee-container {
overflow: hidden;
white-space: nowrap;
width: 100%;
}
.marquee-track {
display: inline-flex;
align-items: center;
min-width: max-content;
gap: 40px;
animation: wallet-title-marquee 10s linear infinite;
}
.marquee-track > span {
white-space: nowrap;
}
@keyframes wallet-title-marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-50% - 20px));
}
}
.design-pos-card .position-value { .design-pos-card .position-value {
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
@ -1842,6 +1738,22 @@ async function submitAuthorize() {
margin-top: 2px; margin-top: 2px;
} }
.order-title-marquee {
width: 100%;
}
.order-title-marquee .marquee-track {
animation-duration: 9s;
}
.history-title-marquee {
width: 100%;
}
.history-title-marquee .marquee-track {
animation-duration: 9s;
}
.design-order-card .design-order-tags { .design-order-card .design-order-tags {
flex-wrap: nowrap; flex-wrap: nowrap;
gap: 6px; gap: 6px;
@ -1966,8 +1878,8 @@ async function submitAuthorize() {
border-radius: 10px; border-radius: 10px;
} }
.history-mobile-card.design-trade-card :deep(.history-mobile-title), .history-mobile-card.design-trade-card .history-mobile-title,
.history-mobile-card.design-funding-card :deep(.history-mobile-title) { .history-mobile-card.design-funding-card .history-mobile-title {
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
margin-bottom: 0; margin-bottom: 0;
@ -2009,12 +1921,6 @@ async function submitAuthorize() {
background: transparent; background: transparent;
} }
.history-tag-type {
color: #6d28d9;
border: 1.5px solid #7c3aed;
background: rgba(124, 58, 237, 0.06);
}
.funding-icon-deposit { .funding-icon-deposit {
background: #dcfce7; background: #dcfce7;
color: #16a34a; color: #16a34a;
@ -2268,7 +2174,7 @@ async function submitAuthorize() {
min-width: 0; min-width: 0;
} }
.design-pos-title-col :deep(.position-mobile-title) { .position-mobile-title {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #111827; color: #111827;
@ -2387,7 +2293,7 @@ async function submitAuthorize() {
min-width: 0; min-width: 0;
} }
.order-mobile-main :deep(.order-mobile-title) { .order-mobile-title {
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: #111827; color: #111827;
@ -2503,7 +2409,7 @@ async function submitAuthorize() {
min-width: 0; min-width: 0;
} }
.history-mobile-main :deep(.history-mobile-title) { .history-mobile-title {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #111827; color: #111827;