Compare commits
2 Commits
99037b536e
...
062e370bea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
062e370bea | ||
|
|
4927953dc9 |
@ -9,7 +9,7 @@
|
||||
"name": "Profile Screen",
|
||||
"clip": true,
|
||||
"width": 402,
|
||||
"height": 648,
|
||||
"height": 980,
|
||||
"fill": "$--bg-page",
|
||||
"layout": "vertical",
|
||||
"gap": 16,
|
||||
@ -94,8 +94,8 @@
|
||||
"type": "text",
|
||||
"id": "VLEgU",
|
||||
"name": "tagText",
|
||||
"fill": "$--primary",
|
||||
"content": "VIP Trader",
|
||||
"fill": "#a16207",
|
||||
"content": "尊享交易权益",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "600"
|
||||
@ -134,6 +134,97 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -6370,6 +6461,387 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
|
||||
@ -215,6 +215,60 @@ export async function findPmEvent(
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造与 MarketCard、EventMarkets → 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 {
|
||||
title: string
|
||||
|
||||
@ -3,10 +3,22 @@
|
||||
* 用于 Wallet.vue 历史 Tab 数据
|
||||
*/
|
||||
|
||||
import { readTradeRouteEventId, readTradeRouteEventSlug } from './event'
|
||||
import { buildQuery, get } from './request'
|
||||
import { BASE_URL } from './request'
|
||||
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"] 对齐) */
|
||||
export interface HistoryRecordItem {
|
||||
ID?: number
|
||||
@ -36,6 +48,10 @@ export interface HistoryRecordItem {
|
||||
UpdatedAt?: string
|
||||
/** 金额(USDC),用于充值等类型 */
|
||||
usdcSize?: number
|
||||
eventID?: number
|
||||
eventId?: number
|
||||
marketID?: string | number
|
||||
marketId?: string | number
|
||||
}
|
||||
|
||||
/** GET /hr/getHistoryRecordPublic 请求参数 */
|
||||
@ -136,6 +152,16 @@ export interface HistoryDisplayItem {
|
||||
iconClass?: string
|
||||
/** 图标 URL(来自 record.icon,用于展示) */
|
||||
imageUrl?: string
|
||||
/** 接口 type:TRADE、SPLIT、MERGE 等 */
|
||||
recordType?: string
|
||||
/** TRADE 时 API 的 side:buy / sell(小写,用于标签) */
|
||||
tradeSideRaw?: string
|
||||
/** 跳转详情:事件 ID */
|
||||
tradeEventId?: string
|
||||
/** 跳转详情:事件 slug */
|
||||
tradeEventSlug?: string
|
||||
/** 跳转详情 query.marketId */
|
||||
detailMarketId?: string
|
||||
}
|
||||
|
||||
function formatTimeAgo(dateStr: string | undefined, timestamp?: number): string {
|
||||
@ -166,23 +192,38 @@ function toFullIconUrl(icon: string | undefined): string | undefined {
|
||||
*/
|
||||
export function mapHistoryRecordToDisplayItem(record: HistoryRecordItem): HistoryDisplayItem {
|
||||
const id = String(record.ID ?? '')
|
||||
const market = record.title ?? record.name ?? record.eventSlug ?? ''
|
||||
const outcome = (record.outcome ?? record.side ?? 'Yes').toString()
|
||||
const market = record.name ?? record.title ?? record.eventSlug ?? ''
|
||||
const outcome = (record.outcome ?? 'Yes').toString()
|
||||
const side = outcome === 'No' || outcome === 'Down' ? 'No' : 'Yes'
|
||||
const typeLabel = record.type ?? 'Trade'
|
||||
const activity = `${typeLabel} ${outcome}`.trim()
|
||||
const recordType = (record.type ?? HISTORY_RECORD_TYPE.TRADE).toString().toUpperCase()
|
||||
const tradeSideRaw = (record.side ?? '').toLowerCase()
|
||||
const activity = (record.title ?? `${recordType} · ${outcome}`).trim()
|
||||
const usdcSize = record.usdcSize ?? 0
|
||||
const price = record.price ?? 0
|
||||
const size = record.size ?? 0
|
||||
const valueUsd = usdcSize !== 0 ? usdcSize : price * size
|
||||
const value = `$${Math.abs(valueUsd).toFixed(2)}`
|
||||
const priceCents = Math.round(price * 100)
|
||||
const activityDetail = size > 0 ? `Sold ${Math.floor(size)} ${outcome} at ${priceCents}¢` : value
|
||||
const activityDetail =
|
||||
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(
|
||||
record.UpdatedAt ?? record.updatedAt ?? record.CreatedAt ?? record.createdAt,
|
||||
record.timestamp,
|
||||
)
|
||||
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 {
|
||||
id,
|
||||
market,
|
||||
@ -196,6 +237,11 @@ export function mapHistoryRecordToDisplayItem(record: HistoryRecordItem): Histor
|
||||
avgPrice: priceCents ? `${priceCents}¢` : undefined,
|
||||
shares: size > 0 ? String(Math.floor(size)) : undefined,
|
||||
imageUrl,
|
||||
recordType,
|
||||
tradeSideRaw: recordType === HISTORY_RECORD_TYPE.TRADE ? tradeSideRaw : undefined,
|
||||
tradeEventId: tradeEventId || undefined,
|
||||
tradeEventSlug: tradeEventSlug || undefined,
|
||||
detailMarketId,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,8 +19,68 @@ export interface MockOrderBookRow {
|
||||
shares: number
|
||||
}
|
||||
|
||||
/** 生成随机订单簿数据,每次进入页面不同;保证卖单最低价 > 买单最高价 */
|
||||
export function generateRandomOrderBook(): {
|
||||
/** random integer in [min, max] inclusive */
|
||||
function randomInt(min: number, max: number): number {
|
||||
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),确定最优卖单价;
|
||||
* 最优买单价 = 卖一 − 1(1¢ 价差)。
|
||||
*/
|
||||
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 − 1(1¢ 价差) */
|
||||
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] 换算的美分,锚定最优卖一。
|
||||
* - 可仅卖单或仅买单;条数各侧 2~10 随机。
|
||||
*/
|
||||
export function generateRandomOrderBook(options?: GenerateRandomOrderBookOptions): {
|
||||
asks: MockOrderBookRow[]
|
||||
bids: MockOrderBookRow[]
|
||||
lastPrice: number
|
||||
@ -28,24 +88,63 @@ export function generateRandomOrderBook(): {
|
||||
} {
|
||||
const r = () => Math.random()
|
||||
|
||||
// 买单最高价 25–35,卖单最低价 = 买单最高价 + 1 + spread,保证有价差
|
||||
const highestBid = Math.floor(25 + r() * 11)
|
||||
const spread = Math.max(1, Math.floor(1 + r() * 3))
|
||||
const lowestAsk = highestBid + spread
|
||||
const sideRoll = r()
|
||||
const sides: 'both' | 'asks' | 'bids' =
|
||||
options?.sides ??
|
||||
(sideRoll < 0.25 ? 'asks' : sideRoll < 0.5 ? 'bids' : 'both')
|
||||
|
||||
const askPrices = Array.from({ length: 9 }, (_, i) => lowestAsk + i)
|
||||
const asks: MockOrderBookRow[] = askPrices.map((p) => ({
|
||||
price: p,
|
||||
shares: Math.round((500 + r() * 3000) * 10) / 10,
|
||||
}))
|
||||
const nAsks = randomInt(2, 10)
|
||||
const nBids = randomInt(2, 10)
|
||||
|
||||
const bidPrices = Array.from({ length: 12 }, (_, i) => highestBid - i)
|
||||
const bids: MockOrderBookRow[] = bidPrices.map((p) => ({
|
||||
price: Math.max(1, p),
|
||||
shares: Math.round((200 + r() * 3000) * 10) / 10,
|
||||
}))
|
||||
let highestBid = 0
|
||||
let lowestAsk = 0
|
||||
let asks: MockOrderBookRow[] = []
|
||||
let bids: MockOrderBookRow[] = []
|
||||
let lastPrice = 0
|
||||
const spread = 1
|
||||
|
||||
const lastPrice = Math.floor((highestBid + lowestAsk) / 2)
|
||||
if (sides === 'both') {
|
||||
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 }
|
||||
}
|
||||
@ -107,6 +206,8 @@ export interface MockPosition {
|
||||
marketID?: string
|
||||
tokenID?: string
|
||||
needClaim?: boolean
|
||||
tradeEventId?: string
|
||||
tradeEventSlug?: string
|
||||
}
|
||||
|
||||
export interface MockOpenOrder {
|
||||
@ -124,6 +225,9 @@ export interface MockOpenOrder {
|
||||
iconClass?: string
|
||||
orderID?: number
|
||||
tokenID?: string
|
||||
tradeEventId?: string
|
||||
tradeEventSlug?: string
|
||||
detailMarketId?: string
|
||||
}
|
||||
|
||||
export interface MockHistoryItem {
|
||||
@ -140,6 +244,9 @@ export interface MockHistoryItem {
|
||||
shares?: string
|
||||
iconChar?: string
|
||||
iconClass?: string
|
||||
tradeEventId?: string
|
||||
tradeEventSlug?: string
|
||||
detailMarketId?: string
|
||||
}
|
||||
|
||||
export const MOCK_WALLET_POSITIONS: MockPosition[] = [
|
||||
@ -163,6 +270,8 @@ export const MOCK_WALLET_POSITIONS: MockPosition[] = [
|
||||
marketID: 'mock-market-1',
|
||||
tokenID: MOCK_TOKEN_ID,
|
||||
needClaim: true,
|
||||
tradeEventId: '1',
|
||||
tradeEventSlug: 'mock-wallet-event',
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
@ -218,6 +327,9 @@ export const MOCK_WALLET_ORDERS: MockOpenOrder[] = [
|
||||
iconClass: 'position-icon-btc',
|
||||
orderID: 5,
|
||||
tokenID: MOCK_TOKEN_ID,
|
||||
tradeEventId: '1',
|
||||
tradeEventSlug: 'mock-wallet-event',
|
||||
detailMarketId: 'mock-market-1',
|
||||
},
|
||||
{
|
||||
id: 'o2',
|
||||
@ -249,6 +361,8 @@ export const MOCK_WALLET_HISTORY: MockHistoryItem[] = [
|
||||
{
|
||||
id: 'h1',
|
||||
market: 'Bitcoin Up or Down - February 9, 3:00AM-3:15AM ET',
|
||||
tradeEventId: '1',
|
||||
tradeEventSlug: 'mock-wallet-event',
|
||||
side: 'No',
|
||||
activity: 'Sell No',
|
||||
activityDetail: 'Sold 1 Down at 50¢',
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
import { buildQuery, get, post } from './request'
|
||||
import { readTradeRouteEventId, readTradeRouteEventSlug } from './event'
|
||||
import { BASE_URL, buildQuery, get, post } from './request'
|
||||
import type { ApiResponse, PageResult } from './types'
|
||||
|
||||
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"] 对齐)
|
||||
* GET /clob/order/getOrderList 列表项
|
||||
@ -23,9 +35,34 @@ export interface ClobOrderItem {
|
||||
sizeMatched?: number
|
||||
status?: number
|
||||
userID?: number
|
||||
pmMarket?: PmMarketEmbed
|
||||
[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 {
|
||||
code: number
|
||||
@ -114,6 +151,8 @@ export interface HistoryDisplayItem {
|
||||
shares?: string
|
||||
iconChar?: string
|
||||
iconClass?: string
|
||||
/** 来自订单 pmMarket.image / icon */
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
/** Side: Buy=1, Sell=2 */
|
||||
@ -140,7 +179,7 @@ function formatTimeAgo(createdAt: string | undefined): string {
|
||||
*/
|
||||
export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem {
|
||||
const id = String(order.ID ?? '')
|
||||
const market = order.market ?? ''
|
||||
const market = resolveClobOrderMarketTitle(order) || (order.market ?? '')
|
||||
const outcome = order.outcome ?? 'Yes'
|
||||
const sideNum = order.side ?? Side.Buy
|
||||
const sideLabel = sideNum === Side.Sell ? 'Sell' : 'Buy'
|
||||
@ -155,6 +194,7 @@ export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem
|
||||
const activityDetail = `${verb} ${Math.floor(size)} ${outcome} at ${priceCents}¢`
|
||||
const avgPrice = `${priceCents}¢`
|
||||
const timeAgo = formatTimeAgo(order.createdAt)
|
||||
const imageUrl = resolveClobOrderMarketImageUrl(order)
|
||||
return {
|
||||
id,
|
||||
market,
|
||||
@ -167,6 +207,7 @@ export function mapOrderToHistoryItem(order: ClobOrderItem): HistoryDisplayItem
|
||||
timeAgo,
|
||||
avgPrice,
|
||||
shares: String(Math.floor(size)),
|
||||
imageUrl,
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,6 +227,14 @@ export interface OpenOrderDisplayItem {
|
||||
tokenID?: string
|
||||
/** 已成交数量达到原始总数量,不可撤单 */
|
||||
fullyFilled?: boolean
|
||||
/** 来自 pmMarket.image / icon */
|
||||
imageUrl?: string
|
||||
/** 跳转详情:事件 ID */
|
||||
tradeEventId?: string
|
||||
/** 跳转详情:事件 slug */
|
||||
tradeEventSlug?: string
|
||||
/** 跳转详情 query.marketId(pmMarket.ID) */
|
||||
detailMarketId?: string
|
||||
}
|
||||
|
||||
/** OrderType GTC=0 表示 Until Cancelled */
|
||||
@ -197,7 +246,7 @@ const OrderType = { GTC: 0, GTD: 1 } as const
|
||||
*/
|
||||
export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayItem {
|
||||
const id = String(order.ID ?? '')
|
||||
const market = order.market ?? ''
|
||||
const market = resolveClobOrderMarketTitle(order) || (order.market ?? '')
|
||||
const sideNum = order.side ?? Side.Buy
|
||||
const side = sideNum === Side.Sell ? 'No' : 'Yes'
|
||||
const outcome = order.outcome || (side === 'Yes' ? 'Yes' : 'No')
|
||||
@ -215,6 +264,13 @@ export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayI
|
||||
order.orderType === OrderType.GTC ? 'Until Cancelled' : order.expiration?.toString() ?? ''
|
||||
const actionLabel = sideNum === Side.Buy ? `Buy ${outcome}` : `Sell ${outcome}`
|
||||
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 {
|
||||
id,
|
||||
market,
|
||||
@ -229,5 +285,9 @@ export function mapOrderToOpenOrderItem(order: ClobOrderItem): OpenOrderDisplayI
|
||||
orderID: order.ID,
|
||||
tokenID: order.assetID,
|
||||
fullyFilled,
|
||||
imageUrl,
|
||||
tradeEventId: tradeEventId || undefined,
|
||||
tradeEventSlug: tradeEventSlug || undefined,
|
||||
detailMarketId,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { readTradeRouteEventId, readTradeRouteEventSlug } from './event'
|
||||
import { buildQuery, get, post } from './request'
|
||||
import type { ApiResponse } from './types'
|
||||
import type { PageResult } from './types'
|
||||
@ -166,6 +167,10 @@ export interface PositionDisplayItem {
|
||||
tokenID?: string
|
||||
/** 所属市场是否已关闭,market.closed=true 表示可结算/可领取 */
|
||||
marketClosed?: boolean
|
||||
/** 跳转市场详情:事件 ID(路径优先) */
|
||||
tradeEventId?: string
|
||||
/** 跳转市场详情:事件 slug */
|
||||
tradeEventSlug?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -233,6 +238,11 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
|
||||
const tokenID = pos.tokenId ?? (pos as { tokenID?: string }).tokenID ?? ''
|
||||
const imageUrl = (pos.market?.image ?? pos.market?.icon) as string | undefined
|
||||
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 {
|
||||
id,
|
||||
@ -256,5 +266,7 @@ export function mapPositionToDisplayItem(pos: ClobPositionItem): PositionDisplay
|
||||
marketID: marketID || undefined,
|
||||
tokenID: tokenID || undefined,
|
||||
marketClosed,
|
||||
tradeEventId: tradeEventId || undefined,
|
||||
tradeEventSlug: tradeEventSlug || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
18
src/api/vipLevel.ts
Normal file
18
src/api/vipLevel.ts
Normal file
@ -0,0 +1,18 @@
|
||||
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')
|
||||
}
|
||||
103
src/components/MarqueeTitle.vue
Normal file
103
src/components/MarqueeTitle.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<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>
|
||||
@ -91,7 +91,7 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import HorizontalProgressBar from './HorizontalProgressBar.vue'
|
||||
import { generateRandomOrderBook } from '../api/mockData'
|
||||
import { generateRandomOrderBook, type MockOrderBookRow } from '../api/mockData'
|
||||
import { USE_MOCK_ORDER_BOOK } from '../config/mock'
|
||||
|
||||
const { t } = useI18n()
|
||||
@ -126,6 +126,15 @@ const props = withDefaults(
|
||||
connected?: boolean
|
||||
yesLabel?: string
|
||||
noLabel?: string
|
||||
/** Yes 簿真实买单最高价(美分),单侧真实数据时用于生成另一侧模拟盘口 */
|
||||
anchorBestBidYesCents?: number
|
||||
anchorLowestAskYesCents?: number
|
||||
anchorBestBidNoCents?: number
|
||||
anchorLowestAskNoCents?: number
|
||||
/** 无真实盘口时:Yes 侧用 outcomePrices[0] 换算的美分锚定最优卖一(买一=卖一−1) */
|
||||
outcomePriceAnchorYesCents?: number
|
||||
/** 无真实盘口时:No 侧锚定(常为 outcomePrices[1]),由父组件传入 */
|
||||
outcomePriceAnchorNoCents?: number
|
||||
}>(),
|
||||
{
|
||||
asksYes: () => [],
|
||||
@ -144,17 +153,179 @@ const props = withDefaults(
|
||||
connected: false,
|
||||
yesLabel: 'Yes',
|
||||
noLabel: 'No',
|
||||
anchorBestBidYesCents: 0,
|
||||
anchorLowestAskYesCents: 0,
|
||||
anchorBestBidNoCents: 0,
|
||||
anchorLowestAskNoCents: 0,
|
||||
outcomePriceAnchorYesCents: 0,
|
||||
outcomePriceAnchorNoCents: 0,
|
||||
},
|
||||
)
|
||||
|
||||
// State:up = Yes 交易,down = No 交易
|
||||
const activeTrade = ref('up')
|
||||
|
||||
const initMock = generateRandomOrderBook()
|
||||
const internalAsks = ref<OrderBookRow[]>([...initMock.asks])
|
||||
const internalBids = ref<OrderBookRow[]>([...initMock.bids])
|
||||
const internalLastPrice = ref(initMock.lastPrice)
|
||||
const internalSpread = ref(initMock.spread)
|
||||
/** 一侧真实、另一侧由 mock 填充时的模拟档位与展示的 last/spread */
|
||||
type HybridSupplement = {
|
||||
mockAsks: OrderBookRow[]
|
||||
mockBids: OrderBookRow[]
|
||||
lastPrice: number
|
||||
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 时不显示模拟数据
|
||||
const asks = computed(() => {
|
||||
@ -162,22 +333,34 @@ const asks = computed(() => {
|
||||
const isYes = activeTrade.value === 'up'
|
||||
const fromYes = isYes ? props.asksYes : props.asksNo
|
||||
const fromLegacy = props.asks
|
||||
const hybrid = isYes ? hybridSupplementYes.value : hybridSupplementNo.value
|
||||
const hasYesNo = (fromYes?.length ?? 0) > 0
|
||||
const hasLegacy = (fromLegacy?.length ?? 0) > 0
|
||||
if (hasYesNo) return fromYes ?? []
|
||||
if (hasLegacy) return fromLegacy ?? []
|
||||
return USE_MOCK_ORDER_BOOK ? internalAsks.value : []
|
||||
if (USE_MOCK_ORDER_BOOK && hybrid && hybrid.mockAsks.length > 0) return hybrid.mockAsks
|
||||
if (USE_MOCK_ORDER_BOOK) {
|
||||
const full = isYes ? fullMockYes.value : fullMockNo.value
|
||||
if (full?.mockAsks.length) return full.mockAsks
|
||||
}
|
||||
return []
|
||||
})
|
||||
const bids = computed(() => {
|
||||
if (props.loading) return []
|
||||
const isYes = activeTrade.value === 'up'
|
||||
const fromYes = isYes ? props.bidsYes : props.bidsNo
|
||||
const fromLegacy = props.bids
|
||||
const hybrid = isYes ? hybridSupplementYes.value : hybridSupplementNo.value
|
||||
const hasYesNo = (fromYes?.length ?? 0) > 0
|
||||
const hasLegacy = (fromLegacy?.length ?? 0) > 0
|
||||
if (hasYesNo) return fromYes ?? []
|
||||
if (hasLegacy) return fromLegacy ?? []
|
||||
return USE_MOCK_ORDER_BOOK ? internalBids.value : []
|
||||
if (USE_MOCK_ORDER_BOOK && hybrid && hybrid.mockBids.length > 0) return hybrid.mockBids
|
||||
if (USE_MOCK_ORDER_BOOK) {
|
||||
const full = isYes ? fullMockYes.value : fullMockNo.value
|
||||
if (full?.mockBids.length) return full.mockBids
|
||||
}
|
||||
return []
|
||||
})
|
||||
const displayLastPrice = computed(() => {
|
||||
if (props.loading) return '—'
|
||||
@ -185,7 +368,13 @@ const displayLastPrice = computed(() => {
|
||||
const fromYesNo = isYes ? props.lastPriceYes : props.lastPriceNo
|
||||
if (fromYesNo != null) return fromYesNo
|
||||
if (props.lastPrice != null) return props.lastPrice
|
||||
return USE_MOCK_ORDER_BOOK ? internalLastPrice.value : 0
|
||||
if (USE_MOCK_ORDER_BOOK) {
|
||||
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(() => {
|
||||
if (props.loading) return '—'
|
||||
@ -193,7 +382,13 @@ const displaySpread = computed(() => {
|
||||
const fromYesNo = isYes ? props.spreadYes : props.spreadNo
|
||||
if (fromYesNo != null) return fromYesNo
|
||||
if (props.spread != null) return props.spread
|
||||
return USE_MOCK_ORDER_BOOK ? internalSpread.value : 0
|
||||
if (USE_MOCK_ORDER_BOOK) {
|
||||
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 更新
|
||||
@ -212,24 +407,22 @@ watch(
|
||||
mockInterval = undefined
|
||||
} else if (!hasRealData && USE_MOCK_ORDER_BOOK && !mockInterval) {
|
||||
mockInterval = setInterval(() => {
|
||||
const randomAskIndex = Math.floor(Math.random() * internalAsks.value.length)
|
||||
const askItem = internalAsks.value[randomAskIndex]
|
||||
if (askItem) {
|
||||
internalAsks.value[randomAskIndex] = {
|
||||
price: askItem.price,
|
||||
shares: Math.max(0, askItem.shares + Math.floor(Math.random() * 100) - 50),
|
||||
const jitterSide = (book: HybridSupplement | null) => {
|
||||
if (!book) return book
|
||||
if (!book.mockAsks.length && !book.mockBids.length) return book
|
||||
const jitterRow = (row: OrderBookRow): OrderBookRow => ({
|
||||
...row,
|
||||
shares: Math.max(0, row.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,
|
||||
}
|
||||
}
|
||||
const randomBidIndex = Math.floor(Math.random() * internalBids.value.length)
|
||||
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)
|
||||
fullMockYes.value = jitterSide(fullMockYes.value)
|
||||
fullMockNo.value = jitterSide(fullMockNo.value)
|
||||
}, 2000)
|
||||
}
|
||||
},
|
||||
|
||||
@ -1494,6 +1494,7 @@ function indexOfAllowedPrice(v: number): number {
|
||||
|
||||
export interface TradeMarketPayload {
|
||||
marketId?: string
|
||||
/** 优先来自订单簿卖一(0–1);为 0 时可配合 outcomePrices 解析 */
|
||||
yesPrice: number
|
||||
noPrice: number
|
||||
title?: string
|
||||
@ -1501,6 +1502,8 @@ export interface TradeMarketPayload {
|
||||
clobTokenIds?: string[]
|
||||
/** 选项展示文案,如 ["Yes","No"] 或 ["Up","Down"],用于 Buy Yes/No 按钮文字 */
|
||||
outcomes?: string[]
|
||||
/** yesPrice/noPrice 均为 0 时用于回退展示与限价(与 API outcomePrices 同源) */
|
||||
outcomePrices?: Array<string | number>
|
||||
/** 订单簿 Yes 买单最高价(美分),市价卖出时用于计算将收到金额 */
|
||||
bestBidYesCents?: number
|
||||
/** 订单簿 No 买单最高价(美分),市价卖出时用于计算将收到金额 */
|
||||
@ -1633,8 +1636,41 @@ async function submitSplit() {
|
||||
|
||||
defineExpose({ openMergeDialog, openSplitDialog })
|
||||
|
||||
const yesPriceCents = computed(() => (props.market ? Math.round(props.market.yesPrice * 100) : 19))
|
||||
const noPriceCents = computed(() => (props.market ? Math.round(props.market.noPrice * 100) : 82))
|
||||
/** outcomePrices 单项 → 美元单价 0–1(兼容概率小数或 1–100 美分写法) */
|
||||
function outcomeRawToDollars(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(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 noLabel = computed(() => props.market?.outcomes?.[1] ?? 'No')
|
||||
|
||||
@ -1766,9 +1802,7 @@ const avgPriceCents = computed(() => {
|
||||
const toWinValue = computed(() => {
|
||||
if (isMarketMode.value) {
|
||||
const price =
|
||||
selectedOption.value === 'yes'
|
||||
? (props.market?.yesPrice ?? 0.5)
|
||||
: (props.market?.noPrice ?? 0.5)
|
||||
selectedOption.value === 'yes' ? yesPriceDollarsEffective.value : noPriceDollarsEffective.value
|
||||
const sharesFromAmount = price > 0 ? amount.value / price : 0
|
||||
return trimTrailingZeros(sharesFromAmount.toFixed(2))
|
||||
}
|
||||
@ -1831,9 +1865,9 @@ function clampLimitPrice(v: number): number {
|
||||
|
||||
/** 根据当前 props.market 与 selectedOption 同步 limitPrice(组件显示或 market 更新时调用) */
|
||||
function syncLimitPriceFromMarket() {
|
||||
const yesP = props.market?.yesPrice ?? 0.19
|
||||
const noP = props.market?.noPrice ?? 0.82
|
||||
limitPrice.value = clampLimitPrice(selectedOption.value === 'yes' ? yesP : noP)
|
||||
limitPrice.value = clampLimitPrice(
|
||||
selectedOption.value === 'yes' ? yesPriceDollarsEffective.value : noPriceDollarsEffective.value,
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@ -1903,9 +1937,9 @@ watch(limitType, () => saveTradePrefs(), { flush: 'post' })
|
||||
// Methods
|
||||
const handleOptionChange = (option: 'yes' | 'no') => {
|
||||
selectedOption.value = option
|
||||
const yesP = props.market?.yesPrice ?? 0.19
|
||||
const noP = props.market?.noPrice ?? 0.82
|
||||
limitPrice.value = clampLimitPrice(option === 'yes' ? yesP : noP)
|
||||
limitPrice.value = clampLimitPrice(
|
||||
option === 'yes' ? yesPriceDollarsEffective.value : noPriceDollarsEffective.value,
|
||||
)
|
||||
emit('optionChange', option)
|
||||
|
||||
// Set max shares when option changes in sell mode
|
||||
|
||||
@ -221,6 +221,14 @@
|
||||
"noPositionsFound": "No positions found.",
|
||||
"noOpenOrdersFound": "No open orders found.",
|
||||
"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",
|
||||
"avgNow": "AVG → NOW",
|
||||
"bet": "BET",
|
||||
@ -308,7 +316,27 @@
|
||||
"nameSaved": "Username updated",
|
||||
"changeAvatar": "Change avatar",
|
||||
"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": {
|
||||
"title": "Deposit",
|
||||
|
||||
@ -221,6 +221,14 @@
|
||||
"noPositionsFound": "ポジションがありません",
|
||||
"noOpenOrdersFound": "未約定注文がありません",
|
||||
"noHistoryFound": "まだ取引履歴がありません",
|
||||
"historySideBuy": "買い",
|
||||
"historySideSell": "売り",
|
||||
"historyTypeSplit": "スプリット",
|
||||
"historyTypeMerge": "マージ",
|
||||
"historyTypeRedeem": "償還",
|
||||
"historyTypeReward": "リワード",
|
||||
"historyTypeConversion": "コンバージョン",
|
||||
"historyTypeMakerRebate": "メイカー還元",
|
||||
"market": "市場",
|
||||
"avgNow": "平均→現在",
|
||||
"bet": "ベット",
|
||||
@ -308,7 +316,27 @@
|
||||
"nameSaved": "ユーザー名を更新しました",
|
||||
"changeAvatar": "アバターを変更",
|
||||
"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": {
|
||||
"title": "入金",
|
||||
|
||||
@ -221,6 +221,14 @@
|
||||
"noPositionsFound": "포지션이 없습니다",
|
||||
"noOpenOrdersFound": "미체결 주문이 없습니다",
|
||||
"noHistoryFound": "아직 거래 내역이 없습니다",
|
||||
"historySideBuy": "매수",
|
||||
"historySideSell": "매도",
|
||||
"historyTypeSplit": "스플릿",
|
||||
"historyTypeMerge": "머지",
|
||||
"historyTypeRedeem": "상환",
|
||||
"historyTypeReward": "리워드",
|
||||
"historyTypeConversion": "전환",
|
||||
"historyTypeMakerRebate": "메이커 리베이트",
|
||||
"market": "시장",
|
||||
"avgNow": "평균→현재",
|
||||
"bet": "베팅",
|
||||
@ -308,7 +316,27 @@
|
||||
"nameSaved": "사용자 이름이 업데이트되었습니다",
|
||||
"changeAvatar": "아바타 변경",
|
||||
"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": {
|
||||
"title": "입금",
|
||||
|
||||
@ -221,6 +221,14 @@
|
||||
"noPositionsFound": "暂无持仓",
|
||||
"noOpenOrdersFound": "暂无未成交订单",
|
||||
"noHistoryFound": "您还未进行过任何交易",
|
||||
"historySideBuy": "买入",
|
||||
"historySideSell": "卖出",
|
||||
"historyTypeSplit": "拆分",
|
||||
"historyTypeMerge": "合并",
|
||||
"historyTypeRedeem": "赎回",
|
||||
"historyTypeReward": "奖励",
|
||||
"historyTypeConversion": "转换",
|
||||
"historyTypeMakerRebate": "做市返佣",
|
||||
"market": "市场",
|
||||
"avgNow": "均价→现价",
|
||||
"bet": "投注",
|
||||
@ -308,7 +316,27 @@
|
||||
"nameSaved": "用户名已更新",
|
||||
"changeAvatar": "更换头像",
|
||||
"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": {
|
||||
"title": "入金",
|
||||
|
||||
@ -221,6 +221,14 @@
|
||||
"noPositionsFound": "暫無持倉",
|
||||
"noOpenOrdersFound": "暫無未成交訂單",
|
||||
"noHistoryFound": "您還未進行過任何交易",
|
||||
"historySideBuy": "買入",
|
||||
"historySideSell": "賣出",
|
||||
"historyTypeSplit": "拆分",
|
||||
"historyTypeMerge": "合併",
|
||||
"historyTypeRedeem": "贖回",
|
||||
"historyTypeReward": "獎勵",
|
||||
"historyTypeConversion": "轉換",
|
||||
"historyTypeMakerRebate": "做市返佣",
|
||||
"market": "市場",
|
||||
"avgNow": "均價→現價",
|
||||
"bet": "投注",
|
||||
@ -308,7 +316,27 @@
|
||||
"nameSaved": "使用者名稱已更新",
|
||||
"changeAvatar": "更換頭像",
|
||||
"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": {
|
||||
"title": "入金",
|
||||
|
||||
@ -7,6 +7,7 @@ import EventMarkets from '../views/EventMarkets.vue'
|
||||
import Wallet from '../views/Wallet.vue'
|
||||
import Search from '../views/Search.vue'
|
||||
import Profile from '../views/Profile.vue'
|
||||
import MemberCenter from '../views/MemberCenter.vue'
|
||||
import ApiKey from '../views/ApiKey.vue'
|
||||
|
||||
const router = createRouter({
|
||||
@ -52,6 +53,11 @@ const router = createRouter({
|
||||
name: 'profile',
|
||||
component: Profile,
|
||||
},
|
||||
{
|
||||
path: '/member-center',
|
||||
name: 'member-center',
|
||||
component: MemberCenter,
|
||||
},
|
||||
{
|
||||
path: '/api-key',
|
||||
name: 'api-key',
|
||||
|
||||
@ -281,6 +281,7 @@ const tradeMarketPayload = computed(() => {
|
||||
title: m.question,
|
||||
clobTokenIds: m.clobTokenIds,
|
||||
outcomes: m.outcomes,
|
||||
outcomePrices: m.outcomePrices,
|
||||
}
|
||||
})
|
||||
|
||||
@ -415,7 +416,7 @@ function setChartSeries(seriesArr: ChartSeriesItem[]) {
|
||||
const series = chartInstance!.addSeries(LineSeries, {
|
||||
color,
|
||||
lineWidth: 2,
|
||||
lineType: LineType.Curved,
|
||||
lineType: LineType.Simple,
|
||||
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
|
||||
crosshairMarkerVisible: true,
|
||||
lastValueVisible: true,
|
||||
|
||||
404
src/views/MemberCenter.vue
Normal file
404
src/views/MemberCenter.vue
Normal file
@ -0,0 +1,404 @@
|
||||
<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>
|
||||
@ -4,12 +4,7 @@
|
||||
<section class="card profile-card">
|
||||
<div class="top-row">
|
||||
<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">
|
||||
<v-progress-circular
|
||||
v-if="avatarUploading"
|
||||
@ -32,27 +27,54 @@
|
||||
<div class="acc-text">{{ t('profile.uidLabel', { uid: userIdText }) }}</div>
|
||||
<div class="tag-text">{{ userTag }}</div>
|
||||
</div>
|
||||
<button class="edit-btn" type="button" @click="onEditProfile">{{ t('profile.edit') }}</button>
|
||||
<button class="edit-btn" type="button" @click="onEditProfile">
|
||||
{{ t('profile.edit') }}
|
||||
</button>
|
||||
</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 class="card wallet-card">
|
||||
<div class="wallet-head">
|
||||
<span class="wallet-title">{{ t('profile.walletOverview') }}</span>
|
||||
<button class="wallet-link" type="button" @click="goWallet">{{ t('profile.walletDetail') }} ></button>
|
||||
<button class="wallet-link" type="button" @click="goWallet">
|
||||
{{ t('profile.walletDetail') }} >
|
||||
</button>
|
||||
</div>
|
||||
<div class="wallet-balance">${{ totalBalance }}</div>
|
||||
<div class="wallet-sub">
|
||||
{{ t('profile.walletSub', { available: availableBalance, frozen: frozenBalance }) }}
|
||||
</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 class="card menu-card">
|
||||
<div class="menu-title">{{ t('profile.accountSettings') }}</div>
|
||||
<button v-for="item in settingItems" :key="item.label" class="menu-item" type="button" @click="goSetting(item)">
|
||||
<button
|
||||
v-for="item in settingItems"
|
||||
:key="item.label"
|
||||
class="menu-item"
|
||||
type="button"
|
||||
@click="goSetting(item)"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.action === 'locale'" class="menu-locale">{{ currentLocaleLabel }}</span>
|
||||
<span v-else-if="item.action === 'wallet'" class="menu-locale">{{ walletAddressShort }}</span>
|
||||
<span v-else-if="item.action === 'wallet'" class="menu-locale">{{
|
||||
walletAddressShort
|
||||
}}</span>
|
||||
<span v-else class="menu-arrow">></span>
|
||||
</button>
|
||||
</section>
|
||||
@ -83,7 +105,12 @@
|
||||
<div class="wallet-dialog-title">{{ t('profile.currentWalletAddress') }}</div>
|
||||
<div class="wallet-dialog-address">{{ walletAddressText }}</div>
|
||||
<div class="wallet-dialog-actions">
|
||||
<button class="wallet-copy-btn" type="button" :disabled="!walletAddress" @click="copyWalletAddress">
|
||||
<button
|
||||
class="wallet-copy-btn"
|
||||
type="button"
|
||||
:disabled="!walletAddress"
|
||||
@click="copyWalletAddress"
|
||||
>
|
||||
{{ t('profile.copyAddress') }}
|
||||
</button>
|
||||
</div>
|
||||
@ -104,7 +131,12 @@
|
||||
persistent-hint
|
||||
/>
|
||||
<div class="name-dialog-actions">
|
||||
<button class="name-dialog-cancel-btn" type="button" :disabled="isSaving" @click="closeEditNameDialog">
|
||||
<button
|
||||
class="name-dialog-cancel-btn"
|
||||
type="button"
|
||||
:disabled="isSaving"
|
||||
@click="closeEditNameDialog"
|
||||
>
|
||||
{{ t('profile.cancel') }}
|
||||
</button>
|
||||
<button class="name-dialog-save-btn" type="button" :disabled="isSaving" @click="saveName">
|
||||
@ -182,7 +214,9 @@ const walletAddress = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
const walletAddressText = computed(() => walletAddress.value || t('profile.walletAddressUnavailable'))
|
||||
const walletAddressText = computed(
|
||||
() => walletAddress.value || t('profile.walletAddressUnavailable'),
|
||||
)
|
||||
const walletAddressShort = computed(() => {
|
||||
const value = walletAddress.value
|
||||
if (!value) return t('profile.unbound')
|
||||
@ -271,6 +305,28 @@ function goWallet() {
|
||||
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 {
|
||||
const v = name.trim()
|
||||
if (!v) return t('profile.nameRequired')
|
||||
@ -535,6 +591,49 @@ onMounted(() => {
|
||||
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 {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
@ -584,25 +683,28 @@ onMounted(() => {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
.wallet-action-primary {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: #5b5bd6;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
border: 0;
|
||||
background: #5b5bd6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
.wallet-action-secondary {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #fcfcfc;
|
||||
color: #111827;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
|
||||
@ -187,6 +187,12 @@
|
||||
:bids-yes="orderBookBidsYes"
|
||||
:asks-no="orderBookAsksNo"
|
||||
: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-no="clobLastPriceNo"
|
||||
:spread-yes="clobSpreadYes"
|
||||
@ -615,6 +621,32 @@ const orderBookBestBidNoCents = computed(() => {
|
||||
if (!bids.length) return 0
|
||||
return Math.max(...bids.map((b) => b.price))
|
||||
})
|
||||
|
||||
/** outcomePrices 单项 → 订单簿锚价(美分):概率 0–1 或已是美分 1–100 */
|
||||
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 互补(100−Yes¢) */
|
||||
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 clobLastPriceNo = computed(() => clobLastPriceByToken.value[1])
|
||||
const clobSpreadYes = computed(() => clobSpreadByToken.value[0])
|
||||
@ -758,14 +790,36 @@ function disconnectClob() {
|
||||
clobLoading.value = false
|
||||
}
|
||||
|
||||
/** 传给 TradeComponent 的 market,供 Split 调用 /PmMarket/split;yesPrice/noPrice 取订单簿卖单最低价,无数据时为 0 */
|
||||
/**
|
||||
* 传给 TradeComponent:yesPrice/noPrice 优先取对应 token 订单簿 **卖单最低价**(美元 0–1),
|
||||
* 取不到再用当前市场的 outcomePrices。
|
||||
*/
|
||||
const tradeMarketPayload = computed(() => {
|
||||
const m = currentMarket.value
|
||||
const yesPrice = orderBookLowestAskYesCents.value / 100
|
||||
const noPrice = orderBookLowestAskNoCents.value / 100
|
||||
const yC = orderBookLowestAskYesCents.value
|
||||
const nC = orderBookLowestAskNoCents.value
|
||||
let yesPrice = yC > 0 ? yC / 100 : 0
|
||||
let noPrice = nC > 0 ? nC / 100 : 0
|
||||
const bestBidYesCents = orderBookBestBidYesCents.value
|
||||
const bestBidNoCents = orderBookBestBidNoCents.value
|
||||
|
||||
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 {
|
||||
marketId: getMarketId(m),
|
||||
yesPrice,
|
||||
@ -773,6 +827,7 @@ const tradeMarketPayload = computed(() => {
|
||||
title: m.question,
|
||||
clobTokenIds: m.clobTokenIds,
|
||||
outcomes: m.outcomes,
|
||||
outcomePrices: m.outcomePrices,
|
||||
bestBidYesCents,
|
||||
bestBidNoCents,
|
||||
}
|
||||
@ -1152,7 +1207,7 @@ function ensureChartSeries() {
|
||||
chartSeries = chartInstance.addSeries(LineSeries, {
|
||||
color: lineColor,
|
||||
lineWidth: 2,
|
||||
lineType: LineType.Curved,
|
||||
lineType: LineType.Simple,
|
||||
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
|
||||
crosshairMarkerVisible: true,
|
||||
lastValueVisible: true,
|
||||
|
||||
@ -75,6 +75,12 @@
|
||||
v-for="pos in paginatedPositions"
|
||||
:key="pos.id"
|
||||
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-left">
|
||||
@ -91,12 +97,7 @@
|
||||
<span v-else class="position-icon-char">{{ pos.iconChar || '•' }}</span>
|
||||
</div>
|
||||
<div class="design-pos-title-col">
|
||||
<div class="position-mobile-title marquee-container">
|
||||
<div class="marquee-track">
|
||||
<span>{{ pos.market }}</span>
|
||||
<span aria-hidden="true">{{ pos.market }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<MarqueeTitle :text="pos.market" title-class="position-mobile-title" />
|
||||
<div class="design-pos-tags">
|
||||
<span
|
||||
v-if="pos.outcomeTag"
|
||||
@ -150,18 +151,33 @@
|
||||
<div v-else-if="filteredOpenOrders.length === 0" class="empty-cell">
|
||||
{{ t('wallet.noOpenOrdersFound') }}
|
||||
</div>
|
||||
<div v-for="ord in paginatedOpenOrders" :key="ord.id" class="order-mobile-card design-order-card">
|
||||
<div
|
||||
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="order-mobile-icon" :class="ord.iconClass">
|
||||
<span class="position-icon-char">{{ ord.iconChar || '•' }}</span>
|
||||
<img
|
||||
v-if="ord.imageUrl"
|
||||
:src="ord.imageUrl"
|
||||
alt=""
|
||||
class="position-icon-img"
|
||||
/>
|
||||
<span v-else class="position-icon-char">{{ ord.iconChar || '•' }}</span>
|
||||
</div>
|
||||
<div class="order-mobile-main design-order-title-col">
|
||||
<div class="order-mobile-title marquee-container order-title-marquee">
|
||||
<div class="marquee-track">
|
||||
<span>{{ getOrderDisplayTitle(ord) }}</span>
|
||||
<span aria-hidden="true">{{ getOrderDisplayTitle(ord) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<MarqueeTitle
|
||||
:text="getOrderDisplayTitle(ord)"
|
||||
title-class="order-mobile-title"
|
||||
fast
|
||||
/>
|
||||
<div class="design-order-tags">
|
||||
<span class="order-side-pill" :class="getOrderSideClass(ord)">{{
|
||||
getOrderActionLabel(ord)
|
||||
@ -216,7 +232,15 @@
|
||||
v-for="h in paginatedHistory"
|
||||
:key="h.id"
|
||||
class="history-mobile-card"
|
||||
:class="isFundingHistory(h) ? 'design-funding-card' : 'design-trade-card'"
|
||||
:class="[
|
||||
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)">
|
||||
<div class="design-history-top">
|
||||
@ -226,12 +250,7 @@
|
||||
<span v-else class="position-icon-char">{{ h.iconChar || '•' }}</span>
|
||||
</div>
|
||||
<div class="history-mobile-main">
|
||||
<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>
|
||||
<MarqueeTitle :text="h.market" title-class="history-mobile-title" fast />
|
||||
<div class="history-mobile-activity">
|
||||
{{ h.timeAgo || h.activityDetail || h.activity }}
|
||||
</div>
|
||||
@ -261,12 +280,7 @@
|
||||
<span v-else class="position-icon-char">{{ getFundingIconText(h) }}</span>
|
||||
</div>
|
||||
<div class="history-mobile-main">
|
||||
<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>
|
||||
<MarqueeTitle :text="getFundingTitle(h)" title-class="history-mobile-title" fast />
|
||||
<div class="history-mobile-activity">{{ h.timeAgo || '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -329,7 +343,7 @@
|
||||
<template v-else>
|
||||
<div class="empty-cell">{{ t('common.noData') }}</div>
|
||||
</template>
|
||||
<div v-if="false && currentListTotal > 0" class="pagination-bar">
|
||||
<div v-if="currentListTotal > 0" class="pagination-bar">
|
||||
<span class="pagination-info">
|
||||
{{ currentPageStart }}–{{ currentPageEnd }} of {{ currentListTotal }}
|
||||
</span>
|
||||
@ -456,6 +470,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const { t } = useI18n()
|
||||
@ -463,13 +478,19 @@ import { createChart, AreaSeries, LineType, LastPriceAnimationMode } from 'light
|
||||
import { toLwcData } from '../composables/useLightweightChart'
|
||||
import DepositDialog from '../components/DepositDialog.vue'
|
||||
import WithdrawDialog from '../components/WithdrawDialog.vue'
|
||||
import MarqueeTitle from '../components/MarqueeTitle.vue'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { useLocaleStore } from '../stores/locale'
|
||||
import { useAuthError } from '../composables/useAuthError'
|
||||
import { cancelOrder as apiCancelOrder } from '../api/order'
|
||||
import { getOrderList, mapOrderToOpenOrderItem, OrderStatus } from '../api/order'
|
||||
import { getHistoryRecordListClient, getHistoryRecordList } from '../api/historyRecord'
|
||||
import {
|
||||
getHistoryRecordListClient,
|
||||
getHistoryRecordList,
|
||||
HISTORY_RECORD_TYPE,
|
||||
} from '../api/historyRecord'
|
||||
import { getPositionList, mapPositionToDisplayItem, claimPosition } from '../api/position'
|
||||
import { buildTradeDetailPushOptions } from '../api/event'
|
||||
import {
|
||||
getSettlementRequestsListClient,
|
||||
amountToUsdcDisplay,
|
||||
@ -486,6 +507,7 @@ import { USE_MOCK_WALLET } from '../config/mock'
|
||||
import { CrossChainUSDTAuth } from '../../sdk/approve'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
|
||||
const router = useRouter()
|
||||
const { mobile } = useDisplay()
|
||||
const userStore = useUserStore()
|
||||
const { formatAuthError } = useAuthError()
|
||||
@ -600,6 +622,10 @@ interface Position {
|
||||
tokenID?: string
|
||||
/** 所属市场是否已关闭,marketClosed=true 表示可结算/可领取 */
|
||||
marketClosed?: boolean
|
||||
/** 跳转市场详情:事件 ID(路径) */
|
||||
tradeEventId?: string
|
||||
/** 跳转市场详情:事件 slug */
|
||||
tradeEventSlug?: string
|
||||
}
|
||||
|
||||
/** 从 avgNow "72¢ → 0.5¢" 解析出 [avg, now] */
|
||||
@ -627,6 +653,11 @@ interface OpenOrder {
|
||||
tokenID?: string
|
||||
/** 已成交数量达到原始总数量,不可撤单 */
|
||||
fullyFilled?: boolean
|
||||
/** 来自订单 pmMarket.image / icon */
|
||||
imageUrl?: string
|
||||
tradeEventId?: string
|
||||
tradeEventSlug?: string
|
||||
detailMarketId?: string
|
||||
}
|
||||
interface HistoryItem {
|
||||
id: string
|
||||
@ -647,6 +678,71 @@ interface HistoryItem {
|
||||
iconClass?: string
|
||||
/** 图标 URL(来自 record.icon) */
|
||||
imageUrl?: string
|
||||
/** API type:TRADE、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 {
|
||||
@ -707,12 +803,36 @@ function getFundingIconClass(h: HistoryItem): string {
|
||||
return isWithdrawalHistory(h) ? 'funding-icon-withdraw' : 'funding-icon-deposit'
|
||||
}
|
||||
|
||||
function getHistoryTagLabel(h: HistoryItem): 'BUY' | 'SELL' {
|
||||
return h.profitLossNegative ? 'BUY' : 'SELL'
|
||||
function getHistoryTagLabel(h: HistoryItem): string {
|
||||
if (!h.recordType) {
|
||||
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 {
|
||||
if (!h.recordType) {
|
||||
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] : [])
|
||||
@ -798,7 +918,9 @@ async function loadOpenOrders() {
|
||||
const list = res.data?.list ?? []
|
||||
const openOnly = list.filter((o) => (o.status ?? 1) === OrderStatus.Live)
|
||||
openOrderList.value = openOnly.map(mapOrderToOpenOrderItem)
|
||||
openOrderTotal.value = openOnly.length
|
||||
const apiTotal = res.data?.total
|
||||
openOrderTotal.value =
|
||||
typeof apiTotal === 'number' && Number.isFinite(apiTotal) ? apiTotal : openOnly.length
|
||||
} else {
|
||||
openOrderList.value = []
|
||||
openOrderTotal.value = 0
|
||||
@ -1253,7 +1375,7 @@ function initPlChart() {
|
||||
bottomColor: color + '08',
|
||||
lineColor: color,
|
||||
lineWidth: 2,
|
||||
lineType: LineType.Curved,
|
||||
lineType: LineType.Simple,
|
||||
lastPriceAnimation: LastPriceAnimationMode.OnDataUpdate,
|
||||
priceFormat: { type: 'price', precision: 2 },
|
||||
})
|
||||
@ -1522,6 +1644,15 @@ async function submitAuthorize() {
|
||||
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 {
|
||||
margin-bottom: 12px;
|
||||
min-height: 30px;
|
||||
@ -1689,7 +1820,7 @@ async function submitAuthorize() {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.design-pos-card .position-mobile-title {
|
||||
.design-pos-card :deep(.position-mobile-title) {
|
||||
margin-bottom: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
@ -1701,33 +1832,6 @@ async function submitAuthorize() {
|
||||
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 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
@ -1738,22 +1842,6 @@ async function submitAuthorize() {
|
||||
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 {
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
@ -1878,8 +1966,8 @@ async function submitAuthorize() {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.history-mobile-card.design-trade-card .history-mobile-title,
|
||||
.history-mobile-card.design-funding-card .history-mobile-title {
|
||||
.history-mobile-card.design-trade-card :deep(.history-mobile-title),
|
||||
.history-mobile-card.design-funding-card :deep(.history-mobile-title) {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0;
|
||||
@ -1921,6 +2009,12 @@ async function submitAuthorize() {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.history-tag-type {
|
||||
color: #6d28d9;
|
||||
border: 1.5px solid #7c3aed;
|
||||
background: rgba(124, 58, 237, 0.06);
|
||||
}
|
||||
|
||||
.funding-icon-deposit {
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
@ -2174,7 +2268,7 @@ async function submitAuthorize() {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.position-mobile-title {
|
||||
.design-pos-title-col :deep(.position-mobile-title) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
@ -2293,7 +2387,7 @@ async function submitAuthorize() {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-mobile-title {
|
||||
.order-mobile-main :deep(.order-mobile-title) {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
@ -2409,7 +2503,7 @@ async function submitAuthorize() {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.history-mobile-title {
|
||||
.history-mobile-main :deep(.history-mobile-title) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user