Compare commits
No commits in common. "8c455ba00a10369d8f84275b2a404325ad70bbed" and "6e3981a63788be1aeb36b831c1b958f81d5f0e68" have entirely different histories.
8c455ba00a
...
6e3981a637
@ -249,11 +249,11 @@ export class ClobSdk {
|
|||||||
// 2. 基于 'e' 字段的事件
|
// 2. 基于 'e' 字段的事件
|
||||||
switch (msg.e) {
|
switch (msg.e) {
|
||||||
case 'price_size_all':
|
case 'price_size_all':
|
||||||
console.log('[ClobSdk] 回调: price_size_all', { i: msg.i, m: msg.m, t: msg.t, bids: msg.b, asks: msg.s });
|
console.log('[ClobSdk] 回调: price_size_all', { m: msg.m, t: msg.t, bids: msg.b, asks: msg.s });
|
||||||
this.listeners.priceSizeAll.forEach(cb => cb(msg as PriceSizePolyMsg));
|
this.listeners.priceSizeAll.forEach(cb => cb(msg as PriceSizePolyMsg));
|
||||||
break;
|
break;
|
||||||
case 'price_size_delta':
|
case 'price_size_delta':
|
||||||
console.log('[ClobSdk] 回调: price_size_delta', { i: msg.i, m: msg.m, t: msg.t, bids: msg.b, asks: msg.s });
|
console.log('[ClobSdk] 回调: price_size_delta', { m: msg.m, t: msg.t, bids: msg.b, asks: msg.s });
|
||||||
this.listeners.priceSizeDelta.forEach(cb => cb(msg as PriceSizePolyMsg));
|
this.listeners.priceSizeDelta.forEach(cb => cb(msg as PriceSizePolyMsg));
|
||||||
break;
|
break;
|
||||||
case 'trade':
|
case 'trade':
|
||||||
|
|||||||
@ -112,34 +112,27 @@ export class UserSdk {
|
|||||||
// 拼接 Token
|
// 拼接 Token
|
||||||
const separator = baseUrl.includes('?') ? '&' : '?';
|
const separator = baseUrl.includes('?') ? '&' : '?';
|
||||||
this.url = `${baseUrl}${separator}token=${this.token}`;
|
this.url = `${baseUrl}${separator}token=${this.token}`;
|
||||||
|
|
||||||
console.log('[UserSdk] 初始化:', {
|
|
||||||
url: this.url.replace(/token=[^&]+/, 'token=***'),
|
|
||||||
autoReconnect: this.autoReconnect,
|
|
||||||
reconnectInterval: this.reconnectInterval,
|
|
||||||
maxReconnectAttempts: this.maxReconnectAttempts,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接
|
// 连接
|
||||||
public connect() {
|
public connect() {
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
console.warn('[UserSdk] 已连接或正在连接,跳过');
|
console.warn("UserSdk: WebSocket already connected or connecting");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[UserSdk] 正在连接...', this.url.replace(/token=[^&]+/, 'token=***'));
|
console.log(`UserSdk: Connecting to ${this.url}...`);
|
||||||
this.ws = new WebSocket(this.url);
|
this.ws = new WebSocket(this.url);
|
||||||
|
|
||||||
this.ws.onopen = (event: Event) => {
|
this.ws.onopen = (event: Event) => {
|
||||||
console.log('[UserSdk] 已连接');
|
console.log("UserSdk: Connected");
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
this.listeners.connect.forEach(cb => cb(event));
|
this.listeners.connect.forEach(cb => cb(event));
|
||||||
};
|
};
|
||||||
|
|
||||||
this.ws.onclose = (event: CloseEvent) => {
|
this.ws.onclose = (event: CloseEvent) => {
|
||||||
console.log('[UserSdk] 已断开', { code: event.code, reason: event.reason, wasClean: event.wasClean });
|
console.log(`UserSdk: Disconnected (Code: ${event.code})`);
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
this.listeners.disconnect.forEach(cb => cb(event));
|
this.listeners.disconnect.forEach(cb => cb(event));
|
||||||
|
|
||||||
@ -149,7 +142,7 @@ export class UserSdk {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.ws.onerror = (event: Event) => {
|
this.ws.onerror = (event: Event) => {
|
||||||
console.error('[UserSdk] WebSocket 错误', event);
|
console.error("UserSdk: Error", event);
|
||||||
this.listeners.error.forEach(cb => cb(event));
|
this.listeners.error.forEach(cb => cb(event));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -159,19 +152,18 @@ export class UserSdk {
|
|||||||
const msg = JSON.parse(raw) as UserMsg;
|
const msg = JSON.parse(raw) as UserMsg;
|
||||||
this.handleMessage(msg);
|
this.handleMessage(msg);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[UserSdk] 消息解析失败', event.data, e);
|
console.error("UserSdk: Failed to parse message", event.data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[UserSdk] 连接失败', e);
|
console.error("UserSdk: Connection failed", e);
|
||||||
if (this.autoReconnect) this.handleReconnect();
|
if (this.autoReconnect) this.handleReconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 断开连接
|
// 断开连接
|
||||||
public disconnect() {
|
public disconnect() {
|
||||||
console.log('[UserSdk] 主动断开连接');
|
|
||||||
this.isExplicitClose = true;
|
this.isExplicitClose = true;
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
this.ws.close();
|
this.ws.close();
|
||||||
@ -183,35 +175,31 @@ export class UserSdk {
|
|||||||
private handleMessage(msg: UserMsg) {
|
private handleMessage(msg: UserMsg) {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'welcome':
|
case 'welcome':
|
||||||
console.log('[UserSdk] 收到 welcome', msg.msg ?? msg);
|
|
||||||
this.listeners.welcome.forEach(cb => cb(msg));
|
this.listeners.welcome.forEach(cb => cb(msg));
|
||||||
break;
|
break;
|
||||||
case 'order_update':
|
case 'order_update':
|
||||||
console.log('[UserSdk] 收到 order_update', msg.data);
|
|
||||||
this.listeners.order.forEach(cb => cb(msg.data));
|
this.listeners.order.forEach(cb => cb(msg.data));
|
||||||
break;
|
break;
|
||||||
case 'position_update':
|
case 'position_update':
|
||||||
console.log('[UserSdk] 收到 position_update', msg.data);
|
|
||||||
this.listeners.position.forEach(cb => cb(msg.data));
|
this.listeners.position.forEach(cb => cb(msg.data));
|
||||||
break;
|
break;
|
||||||
case 'balance_update':
|
case 'balance_update':
|
||||||
console.log('[UserSdk] 收到 balance_update', msg.data);
|
|
||||||
this.listeners.balance.forEach(cb => cb(msg.data));
|
this.listeners.balance.forEach(cb => cb(msg.data));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn('[UserSdk] 未知消息类型', msg);
|
console.warn("UserSdk: Unknown message type", msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重连逻辑
|
// 重连逻辑
|
||||||
private handleReconnect() {
|
private handleReconnect() {
|
||||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||||
console.error('[UserSdk] 已达最大重连次数', this.maxReconnectAttempts);
|
console.error("UserSdk: Max reconnect attempts reached");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reconnectAttempts++;
|
this.reconnectAttempts++;
|
||||||
console.log('[UserSdk] 重连中...', { attempt: this.reconnectAttempts, max: this.maxReconnectAttempts, delayMs: this.reconnectInterval });
|
console.log(`UserSdk: Reconnecting in ${this.reconnectInterval}ms (Attempt ${this.reconnectAttempts})...`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.connect();
|
this.connect();
|
||||||
}, this.reconnectInterval);
|
}, this.reconnectInterval);
|
||||||
|
|||||||
21
src/App.vue
21
src/App.vue
@ -37,7 +37,6 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<v-app-bar color="primary" dark>
|
<v-app-bar color="primary" dark>
|
||||||
<div class="app-bar-inner">
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="currentRoute !== '/'"
|
v-if="currentRoute !== '/'"
|
||||||
icon
|
icon
|
||||||
@ -102,38 +101,20 @@ watch(
|
|||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-main>
|
<v-main>
|
||||||
<div class="main-content">
|
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive :include="['Home']">
|
<keep-alive :include="['Home']">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
</div>
|
|
||||||
</v-main>
|
</v-main>
|
||||||
<Toast />
|
<Toast />
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.app-bar-inner {
|
/* Global styles can be added here */
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||
@ -230,6 +230,7 @@ export async function getPmTagMain(): Promise<CategoryTreeResponse> {
|
|||||||
const res = await get<{ code: number; data: GetPmTagCatalogData; msg: string }>(
|
const res = await get<{ code: number; data: GetPmTagCatalogData; msg: string }>(
|
||||||
'/pmTagCatalog/getPmTagCatalogPublic'
|
'/pmTagCatalog/getPmTagCatalogPublic'
|
||||||
)
|
)
|
||||||
|
console.log('[getPmTagCatalogPublic] 原始响应:', JSON.stringify(res, null, 2))
|
||||||
let raw: PmTagCatalogItem[] = []
|
let raw: PmTagCatalogItem[] = []
|
||||||
const d = res.data
|
const d = res.data
|
||||||
if (d && typeof d === 'object' && !Array.isArray(d)) {
|
if (d && typeof d === 'object' && !Array.isArray(d)) {
|
||||||
|
|||||||
@ -99,35 +99,18 @@ export interface OrderBookRow {
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
/** Yes token 订单簿(index 0) */
|
|
||||||
asksYes?: OrderBookRow[]
|
|
||||||
bidsYes?: OrderBookRow[]
|
|
||||||
/** No token 订单簿(index 1) */
|
|
||||||
asksNo?: OrderBookRow[]
|
|
||||||
bidsNo?: OrderBookRow[]
|
|
||||||
lastPriceYes?: number
|
|
||||||
lastPriceNo?: number
|
|
||||||
spreadYes?: number
|
|
||||||
spreadNo?: number
|
|
||||||
/** @deprecated 兼容旧用法,优先使用 asksYes/asksNo */
|
|
||||||
asks?: OrderBookRow[]
|
asks?: OrderBookRow[]
|
||||||
bids?: OrderBookRow[]
|
bids?: OrderBookRow[]
|
||||||
lastPrice?: number
|
lastPrice?: number
|
||||||
spread?: number
|
spread?: number
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
connected?: boolean
|
connected?: boolean
|
||||||
|
/** 市场 Yes 选项文案,来自 market.outcomes[0] */
|
||||||
yesLabel?: string
|
yesLabel?: string
|
||||||
|
/** 市场 No 选项文案,来自 market.outcomes[1] */
|
||||||
noLabel?: string
|
noLabel?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
asksYes: () => [],
|
|
||||||
bidsYes: () => [],
|
|
||||||
asksNo: () => [],
|
|
||||||
bidsNo: () => [],
|
|
||||||
lastPriceYes: undefined,
|
|
||||||
lastPriceNo: undefined,
|
|
||||||
spreadYes: undefined,
|
|
||||||
spreadNo: undefined,
|
|
||||||
asks: () => [],
|
asks: () => [],
|
||||||
bids: () => [],
|
bids: () => [],
|
||||||
lastPrice: undefined,
|
lastPrice: undefined,
|
||||||
@ -139,60 +122,33 @@ const props = withDefaults(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// State:up = Yes 交易,down = No 交易
|
// State
|
||||||
const activeTrade = ref('up')
|
const activeTrade = ref('up')
|
||||||
|
|
||||||
|
// 使用 props 或回退到 mock 数据(来自 mockData 统一封装)
|
||||||
const internalAsks = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_ASKS])
|
const internalAsks = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_ASKS])
|
||||||
const internalBids = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_BIDS])
|
const internalBids = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_BIDS])
|
||||||
const internalLastPrice = ref(MOCK_ORDER_BOOK_LAST_PRICE)
|
const internalLastPrice = ref(MOCK_ORDER_BOOK_LAST_PRICE)
|
||||||
const internalSpread = ref(MOCK_ORDER_BOOK_SPREAD)
|
const internalSpread = ref(MOCK_ORDER_BOOK_SPREAD)
|
||||||
|
|
||||||
// 根据 activeTrade 选择当前 tab 对应的数据
|
// 当有外部数据时使用 props,否则在 USE_MOCK_ORDER_BOOK 时用 mock
|
||||||
const asks = computed(() => {
|
const asks = computed(() =>
|
||||||
const isYes = activeTrade.value === 'up'
|
props.asks?.length ? props.asks : USE_MOCK_ORDER_BOOK ? internalAsks.value : [],
|
||||||
const fromYes = isYes ? props.asksYes : props.asksNo
|
)
|
||||||
const fromLegacy = props.asks
|
const bids = computed(() =>
|
||||||
const hasYesNo = (fromYes?.length ?? 0) > 0
|
props.bids?.length ? props.bids : USE_MOCK_ORDER_BOOK ? internalBids.value : [],
|
||||||
const hasLegacy = (fromLegacy?.length ?? 0) > 0
|
)
|
||||||
if (hasYesNo) return fromYes ?? []
|
const displayLastPrice = computed(() =>
|
||||||
if (hasLegacy) return fromLegacy ?? []
|
props.lastPrice ?? (USE_MOCK_ORDER_BOOK ? internalLastPrice.value : 0),
|
||||||
return USE_MOCK_ORDER_BOOK ? internalAsks.value : []
|
)
|
||||||
})
|
const displaySpread = computed(() =>
|
||||||
const bids = computed(() => {
|
props.spread ?? (USE_MOCK_ORDER_BOOK ? internalSpread.value : 0),
|
||||||
const isYes = activeTrade.value === 'up'
|
)
|
||||||
const fromYes = isYes ? props.bidsYes : props.bidsNo
|
|
||||||
const fromLegacy = props.bids
|
|
||||||
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 : []
|
|
||||||
})
|
|
||||||
const displayLastPrice = computed(() => {
|
|
||||||
const isYes = activeTrade.value === 'up'
|
|
||||||
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
|
|
||||||
})
|
|
||||||
const displaySpread = computed(() => {
|
|
||||||
const isYes = activeTrade.value === 'up'
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
// 仅在没有外部数据且开启 mock 时运行 mock 更新
|
// 仅在没有外部数据且开启 mock 时运行 mock 更新
|
||||||
let mockInterval: ReturnType<typeof setInterval> | undefined
|
let mockInterval: ReturnType<typeof setInterval> | undefined
|
||||||
watch(
|
watch(
|
||||||
() =>
|
() => props.connected || (props.asks?.length ?? 0) > 0,
|
||||||
props.connected ||
|
|
||||||
(props.asksYes?.length ?? 0) > 0 ||
|
|
||||||
(props.bidsYes?.length ?? 0) > 0 ||
|
|
||||||
(props.asksNo?.length ?? 0) > 0 ||
|
|
||||||
(props.bidsNo?.length ?? 0) > 0 ||
|
|
||||||
(props.asks?.length ?? 0) > 0,
|
|
||||||
(hasRealData) => {
|
(hasRealData) => {
|
||||||
if (hasRealData && mockInterval) {
|
if (hasRealData && mockInterval) {
|
||||||
clearInterval(mockInterval)
|
clearInterval(mockInterval)
|
||||||
|
|||||||
@ -1684,6 +1684,7 @@ const showDepositForBuy = computed(() => !canAffordBuy.value)
|
|||||||
|
|
||||||
/** Buy 模式且余额不足时显示 Deposit,否则显示 Buy Yes/No */
|
/** Buy 模式且余额不足时显示 Deposit,否则显示 Buy Yes/No */
|
||||||
const deposit = () => {
|
const deposit = () => {
|
||||||
|
console.log('Depositing amount:', amount.value)
|
||||||
// 实际应用中,这里应该调用存款API
|
// 实际应用中,这里应该调用存款API
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1730,6 +1731,7 @@ async function submitOrder() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const uid = userStore?.user?.ID ?? 0
|
const uid = userStore?.user?.ID ?? 0
|
||||||
|
console.log('[submitOrder] 用户信息: user=', userStore.user, 'uid=', uid)
|
||||||
const userIdNum =
|
const userIdNum =
|
||||||
typeof uid === 'number'
|
typeof uid === 'number'
|
||||||
? uid
|
? uid
|
||||||
@ -1737,6 +1739,7 @@ async function submitOrder() {
|
|||||||
? parseInt(String(uid), 10)
|
? parseInt(String(uid), 10)
|
||||||
: 0
|
: 0
|
||||||
if (!Number.isFinite(userIdNum) || userIdNum <= 0) {
|
if (!Number.isFinite(userIdNum) || userIdNum <= 0) {
|
||||||
|
console.warn('[submitOrder] 用户信息异常: user=', userStore.user, 'uid=', uid)
|
||||||
orderError.value = t('trade.userError')
|
orderError.value = t('trade.userError')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -194,6 +194,9 @@ async function submitWithdraw() {
|
|||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await new Promise((r) => setTimeout(r, 800))
|
await new Promise((r) => setTimeout(r, 800))
|
||||||
|
const dest =
|
||||||
|
destinationType.value === 'wallet' ? connectedAddress.value : customAddress.value.trim()
|
||||||
|
console.log('Withdraw', { amount: amount.value, network: selectedNetwork.value, to: dest })
|
||||||
emit('success')
|
emit('success')
|
||||||
close()
|
close()
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -75,8 +75,12 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
balance.value = formatUsdcBalance(String(avail))
|
balance.value = formatUsdcBalance(String(avail))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
sdk.onConnect(() => {})
|
sdk.onConnect(() => {
|
||||||
sdk.onDisconnect(() => {})
|
console.log('[UserStore] UserSocket 已连接')
|
||||||
|
})
|
||||||
|
sdk.onDisconnect(() => {
|
||||||
|
console.log('[UserStore] UserSocket 已断开')
|
||||||
|
})
|
||||||
sdk.onError((e) => {
|
sdk.onError((e) => {
|
||||||
console.error('[UserStore] UserSocket 错误:', e)
|
console.error('[UserStore] UserSocket 错误:', e)
|
||||||
})
|
})
|
||||||
@ -157,6 +161,7 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
if (!headers) return
|
if (!headers) return
|
||||||
try {
|
try {
|
||||||
const res = await getUserInfo(headers)
|
const res = await getUserInfo(headers)
|
||||||
|
console.log('[fetchUserInfo] 接口响应:', JSON.stringify(res, null, 2))
|
||||||
const data = res.data as Record<string, unknown> | undefined
|
const data = res.data as Record<string, unknown> | undefined
|
||||||
// 接口返回 data.userInfo 或 data.user,取实际用户对象;若仍含 userInfo 则再取一层
|
// 接口返回 data.userInfo 或 data.user,取实际用户对象;若仍含 userInfo 则再取一层
|
||||||
let u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown>
|
let u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown>
|
||||||
|
|||||||
@ -14,6 +14,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="eventDetail">
|
<template v-else-if="eventDetail">
|
||||||
|
<div class="event-header">
|
||||||
|
<h1 class="event-title">{{ eventDetail.title }}</h1>
|
||||||
|
<p v-if="eventDetail.series?.length || eventDetail.tags?.length" class="event-meta">
|
||||||
|
{{ categoryText }}
|
||||||
|
</p>
|
||||||
|
<p v-if="eventVolume" class="event-volume">{{ eventVolume }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<v-row align="stretch" class="event-markets-row">
|
<v-row align="stretch" class="event-markets-row">
|
||||||
<!-- 左侧:分时图 + 市场列表 -->
|
<!-- 左侧:分时图 + 市场列表 -->
|
||||||
<v-col cols="12" class="chart-col">
|
<v-col cols="12" class="chart-col">
|
||||||
@ -26,9 +34,6 @@
|
|||||||
>
|
>
|
||||||
<div class="chart-header">
|
<div class="chart-header">
|
||||||
<h1 class="chart-title">{{ eventDetail?.title || 'All markets' }}</h1>
|
<h1 class="chart-title">{{ eventDetail?.title || 'All markets' }}</h1>
|
||||||
<p v-if="eventDetail.series?.length || eventDetail.tags?.length" class="chart-meta">
|
|
||||||
{{ categoryText }}
|
|
||||||
</p>
|
|
||||||
<div class="chart-controls-row">
|
<div class="chart-controls-row">
|
||||||
<v-btn variant="text" size="small" class="past-btn">Past ▾</v-btn>
|
<v-btn variant="text" size="small" class="past-btn">Past ▾</v-btn>
|
||||||
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
|
<v-btn class="date-pill" size="small" rounded="pill">{{ resolutionDate }}</v-btn>
|
||||||
@ -348,15 +353,11 @@ function generateDataForMarket(baseChance: number, range: string): [number, numb
|
|||||||
|
|
||||||
function generateAllData(): ChartSeriesItem[] {
|
function generateAllData(): ChartSeriesItem[] {
|
||||||
const range = selectedTimeRange.value
|
const range = selectedTimeRange.value
|
||||||
const list = markets.value
|
return markets.value.map((market) => {
|
||||||
return list.map((market, i) => {
|
|
||||||
const chance = marketChance(market)
|
const chance = marketChance(market)
|
||||||
const base = (market.question || 'Market').slice(0, 32)
|
const name = (market.question || 'Market').slice(0, 32)
|
||||||
const baseName = base + (base.length >= 32 ? '…' : '')
|
|
||||||
// 确保图例 name 唯一,否则 ECharts 会合并导致左右切换箭头不显示
|
|
||||||
const name = list.length > 1 ? `${baseName} (${i + 1}/${list.length})` : baseName
|
|
||||||
return {
|
return {
|
||||||
name,
|
name: name + (name.length >= 32 ? '…' : ''),
|
||||||
data: generateDataForMarket(chance || 20, range),
|
data: generateDataForMarket(chance || 20, range),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -436,7 +437,7 @@ function buildOption(seriesArr: ChartSeriesItem[], containerWidth?: number) {
|
|||||||
itemGap: 12,
|
itemGap: 12,
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: 8,
|
left: 16,
|
||||||
right: 48,
|
right: 48,
|
||||||
top: 40,
|
top: 40,
|
||||||
bottom: isMobile ? 44 : 28,
|
bottom: isMobile ? 44 : 28,
|
||||||
@ -462,15 +463,6 @@ function initChart() {
|
|||||||
chartInstance = echarts.init(chartContainerRef.value)
|
chartInstance = echarts.init(chartContainerRef.value)
|
||||||
const w = chartContainerRef.value.clientWidth
|
const w = chartContainerRef.value.clientWidth
|
||||||
chartInstance.setOption(buildOption(chartData.value, w))
|
chartInstance.setOption(buildOption(chartData.value, w))
|
||||||
|
|
||||||
// 数据打印,便于分析
|
|
||||||
console.log('[EventMarkets] 数据分析:', {
|
|
||||||
marketsCount: markets.value.length,
|
|
||||||
chartSeriesCount: chartData.value.length,
|
|
||||||
containerWidth: w,
|
|
||||||
legendData: chartData.value.map((s) => ({ name: s.name, nameLen: s.name.length, points: s.data.length })),
|
|
||||||
eventDetail: eventDetail.value ? { title: eventDetail.value.title, id: eventDetail.value.ID } : null,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChartData() {
|
function updateChartData() {
|
||||||
@ -569,6 +561,7 @@ function onTradeSubmit(payload: {
|
|||||||
marketId?: string
|
marketId?: string
|
||||||
}) {
|
}) {
|
||||||
// 可在此调用下单 API,payload 含 marketId(当前市场)
|
// 可在此调用下单 API,payload 含 marketId(当前市场)
|
||||||
|
console.log('Trade submit', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
@ -756,19 +749,6 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 与列表页、单 market 页保持一致的左右间距 */
|
|
||||||
@media (max-width: 599px) {
|
|
||||||
.event-markets-container {
|
|
||||||
padding: 16px;
|
|
||||||
padding-left: 16px;
|
|
||||||
padding-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-card.polymarket-chart {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 分时图卡片(扁平化) */
|
/* 分时图卡片(扁平化) */
|
||||||
.chart-card.polymarket-chart {
|
.chart-card.polymarket-chart {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
@ -787,17 +767,10 @@ watch(
|
|||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #111827;
|
color: #111827;
|
||||||
margin: 0 0 4px 0;
|
margin: 0 0 12px 0;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-meta {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #6b7280;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-controls-row {
|
.chart-controls-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -899,6 +872,30 @@ watch(
|
|||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-meta {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-volume {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.markets-list-card {
|
.markets-list-card {
|
||||||
border: 1px solid #e7e7e7;
|
border: 1px solid #e7e7e7;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|||||||
@ -612,6 +612,7 @@ async function loadEvents(page: number, append: boolean, keyword?: string) {
|
|||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!append) eventList.value = []
|
if (!append) eventList.value = []
|
||||||
|
console.warn('getPmEventList failed', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container class="login-container">
|
<v-container class="login-container">
|
||||||
<v-row justify="center" align="center" class="login-row">
|
<v-row justify="center" align="center" class="login-row">
|
||||||
<v-col cols="12" sm="10" md="6" lg="4">
|
<v-col cols="12" md="6" lg="4">
|
||||||
<v-card class="login-card" elevation="0" rounded="lg">
|
<v-card class="login-card">
|
||||||
<v-card-title class="login-title">Sign In</v-card-title>
|
<v-card-title class="login-title">Sign In</v-card-title>
|
||||||
<v-card-text class="login-body">
|
<v-card-text>
|
||||||
<p class="login-desc">Connect your wallet to sign in</p>
|
<v-form @submit.prevent="handleLogin">
|
||||||
|
<v-text-field
|
||||||
|
v-model="email"
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
:rules="[
|
||||||
|
(value) => !!value || 'Email is required',
|
||||||
|
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'Email is invalid',
|
||||||
|
]"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="password"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
:rules="[
|
||||||
|
(value) => !!value || 'Password is required',
|
||||||
|
(value) => value.length >= 6 || 'Password must be at least 6 characters',
|
||||||
|
]"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-btn class="login-btn" color="primary" type="submit" block> Sign In </v-btn>
|
||||||
|
|
||||||
|
<div class="divider">
|
||||||
|
<span>or</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
class="wallet-btn"
|
class="wallet-btn"
|
||||||
color="success"
|
color="success"
|
||||||
size="large"
|
|
||||||
block
|
block
|
||||||
rounded="lg"
|
|
||||||
@click="connectWithWallet"
|
@click="connectWithWallet"
|
||||||
:loading="isConnecting"
|
:loading="isConnecting"
|
||||||
:disabled="isConnecting"
|
:disabled="isConnecting"
|
||||||
>
|
>
|
||||||
<v-icon start>mdi-wallet</v-icon>
|
<span class="wallet-icon">🟡</span>
|
||||||
{{ isConnecting ? 'Connecting...' : 'Connect with Wallet' }}
|
{{ isConnecting ? 'Connecting...' : 'Connect with Wallet' }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
@ -27,6 +53,7 @@
|
|||||||
<div class="login-links">
|
<div class="login-links">
|
||||||
<router-link to="/register">Don't have an account? Sign Up</router-link>
|
<router-link to="/register">Don't have an account? Sign Up</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@ -45,60 +72,102 @@ import { post } from '@/api/request'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
const isConnecting = ref(false)
|
const isConnecting = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
// Here you would typically make an API call to authenticate the user
|
||||||
|
console.log('Logging in with:', email.value, password.value)
|
||||||
|
|
||||||
|
// For demo purposes, we'll just navigate to the home page
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
const connectWithWallet = async () => {
|
const connectWithWallet = async () => {
|
||||||
isConnecting.value = true
|
isConnecting.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check if Ethereum wallet is installed
|
||||||
if (!window.ethereum) {
|
if (!window.ethereum) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Ethereum wallet is not installed. Please install MetaMask, TokenPocket or another Ethereum wallet to continue.',
|
'Ethereum wallet is not installed. Please install MetaMask, TokenPocket or another Ethereum wallet to continue.',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request account access
|
||||||
const accounts = await window.ethereum.request({
|
const accounts = await window.ethereum.request({
|
||||||
method: 'eth_requestAccounts',
|
method: 'eth_requestAccounts',
|
||||||
})
|
})
|
||||||
|
|
||||||
const walletAddress = accounts[0]
|
const walletAddress = accounts[0]
|
||||||
|
console.log('Connected to wallet with account:', walletAddress)
|
||||||
|
|
||||||
|
// Validate wallet address format
|
||||||
if (!walletAddress || !/^0x[0-9a-fA-F]{40}$/.test(walletAddress)) {
|
if (!walletAddress || !/^0x[0-9a-fA-F]{40}$/.test(walletAddress)) {
|
||||||
throw new Error('Invalid wallet address format. Please check your wallet connection.')
|
throw new Error('Invalid wallet address format. Please check your wallet connection.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get chain ID
|
||||||
const chainId = await window.ethereum.request({
|
const chainId = await window.ethereum.request({
|
||||||
method: 'eth_chainId',
|
method: 'eth_chainId',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Create Siwe-formatted message manually
|
||||||
|
// const scheme = window.location.protocol.slice(0, -1);
|
||||||
|
// const domain = 'polymarket.com';//window.location.host
|
||||||
|
// const uri = 'https://api.xtrader.vip/base/walletLogin';//window.location.origin
|
||||||
|
|
||||||
const scheme = window.location.protocol.slice(0, -1)
|
const scheme = window.location.protocol.slice(0, -1)
|
||||||
const domain = window.location.host
|
const domain = window.location.host
|
||||||
const origin = window.location.origin
|
const origin = window.location.origin
|
||||||
const nonce = new Date().getTime().toString()
|
|
||||||
|
|
||||||
|
const statement = 'Sign in to PolyMarket'
|
||||||
|
const version = '1'
|
||||||
|
|
||||||
|
const issuedAt = new Date().toISOString()
|
||||||
const provider = new BrowserProvider(window.ethereum)
|
const provider = new BrowserProvider(window.ethereum)
|
||||||
const signer = await provider.getSigner()
|
const signer = await provider.getSigner()
|
||||||
|
const nonce = new Date().getTime().toString()
|
||||||
|
|
||||||
const siwe = new SiweMessage({
|
const siwe = new SiweMessage({
|
||||||
scheme,
|
scheme,
|
||||||
domain,
|
domain,
|
||||||
address: signer.address,
|
address: signer.address,
|
||||||
statement: 'Sign in to PolyMarket',
|
statement,
|
||||||
uri: origin,
|
uri: origin,
|
||||||
version: '1',
|
version: '1',
|
||||||
chainId: chainId,
|
chainId: chainId,
|
||||||
nonce,
|
nonce,
|
||||||
})
|
})
|
||||||
const message = siwe.prepareMessage()
|
const message = siwe.prepareMessage()
|
||||||
|
// 去掉message 头部的http://
|
||||||
|
// 将 message 中匹配 signer.address 值的内容替换为小写
|
||||||
|
// const lowerAddress = signer.address.toLowerCase()
|
||||||
const message1 = message.replace(/^https?:\/\//, '')
|
const message1 = message.replace(/^https?:\/\//, '')
|
||||||
|
// console.log('Cleaned Siwe message:', cleanedMessage)
|
||||||
|
// const cleanedMessage = message1.replace(/^https?:\/\//, '')
|
||||||
|
// console.log('Cleaned Siwe message:', cleanedMessage)
|
||||||
|
|
||||||
|
// Generate nonce for security
|
||||||
|
// const nonce = siwe.nonce //Math.floor(Math.random() * 1000000).toString()
|
||||||
|
|
||||||
|
// Construct Siwe message according to EIP-4361 standard
|
||||||
|
// const message = `${domain} wants you to sign in with your Ethereum account:\n${walletAddress}\n\n${statement}\n\nURI: ${origin}\nVersion: ${version}\nChain ID: ${parseInt(chainId, 16)}\nNonce: ${nonce}\nIssued At: ${issuedAt}`
|
||||||
|
|
||||||
|
console.log('Generated Siwe-formatted message:', message1)
|
||||||
|
|
||||||
|
// Request signature
|
||||||
const signature = await window.ethereum.request({
|
const signature = await window.ethereum.request({
|
||||||
method: 'personal_sign',
|
method: 'personal_sign',
|
||||||
params: [message1, signer.address],
|
params: [message1, signer.address],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('Signature:', signature)
|
||||||
|
|
||||||
|
// Call login API(使用 BASE_URL,可通过 VITE_API_BASE_URL 配置)
|
||||||
const loginData = await post<{ code: number; data?: { token: string; user?: UserInfo } }>(
|
const loginData = await post<{ code: number; data?: { token: string; user?: UserInfo } }>(
|
||||||
'/base/walletLogin',
|
'/base/walletLogin',
|
||||||
{
|
{
|
||||||
@ -108,13 +177,20 @@ const connectWithWallet = async () => {
|
|||||||
walletAddress,
|
walletAddress,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
// 调试:打印登录信息数据结构
|
||||||
|
console.log('[walletLogin] 登录响应:', JSON.stringify(loginData, null, 2))
|
||||||
|
|
||||||
if (loginData.code === 0 && loginData.data) {
|
if (loginData.code === 0 && loginData.data) {
|
||||||
const data = loginData.data as Record<string, unknown>
|
const data = loginData.data as Record<string, unknown>
|
||||||
const token = data.token as string | undefined
|
const token = data.token as string | undefined
|
||||||
|
// 兼容 data.user 或 data 本身即用户对象(部分接口将 user 平铺在 data 下)
|
||||||
const user =
|
const user =
|
||||||
(data.user as UserInfo | undefined) ??
|
(data.user as UserInfo | undefined) ??
|
||||||
(data.ID != null || data.id != null || data.userName != null ? (data as unknown as UserInfo) : undefined)
|
(data.ID != null || data.id != null || data.userName != null ? (data as unknown as UserInfo) : undefined)
|
||||||
|
if (!user) {
|
||||||
|
console.warn('[walletLogin] data 中无 user,将依赖 fetchUserInfo 拉取。data 结构:', Object.keys(data))
|
||||||
|
}
|
||||||
|
console.log('[walletLogin] 存入 store 的 user:', JSON.stringify(user, null, 2))
|
||||||
userStore.setUser({ token, user })
|
userStore.setUser({ token, user })
|
||||||
await userStore.fetchUserInfo()
|
await userStore.fetchUserInfo()
|
||||||
await userStore.fetchUsdcBalance()
|
await userStore.fetchUsdcBalance()
|
||||||
@ -142,57 +218,72 @@ const connectWithWallet = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-card {
|
.login-card {
|
||||||
border-radius: 16px;
|
border-radius: 12px;
|
||||||
padding: 40px 32px;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-title {
|
.login-title {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: bold;
|
||||||
color: #111827;
|
color: #0066cc;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-body {
|
.login-btn {
|
||||||
padding-top: 8px;
|
margin-top: 24px;
|
||||||
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-desc {
|
.divider {
|
||||||
font-size: 0.95rem;
|
display: flex;
|
||||||
color: #6b7280;
|
align-items: center;
|
||||||
text-align: center;
|
justify-content: center;
|
||||||
margin: 0 0 28px 0;
|
margin: 24px 0;
|
||||||
line-height: 1.5;
|
}
|
||||||
|
|
||||||
|
.divider::before,
|
||||||
|
.divider::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider span {
|
||||||
|
padding: 0 16px;
|
||||||
|
color: #808080;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-btn {
|
.wallet-btn {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
font-weight: 600;
|
display: flex;
|
||||||
text-transform: none;
|
align-items: center;
|
||||||
letter-spacing: 0.02em;
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-icon {
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
margin-top: 20px;
|
margin-top: 16px;
|
||||||
padding: 12px;
|
color: #ff0000;
|
||||||
color: #dc2626;
|
|
||||||
background: #fef2f2;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 0.875rem;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-links {
|
.login-links {
|
||||||
margin-top: 24px;
|
margin-top: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-links a {
|
.login-links a {
|
||||||
color: #2563eb;
|
color: #0066cc;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-links a:hover {
|
.login-links a:hover {
|
||||||
|
|||||||
@ -45,16 +45,12 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- Order Book Section -->
|
<!-- Order Book Section -->
|
||||||
<v-card class="order-book-card" elevation="0" rounded="lg">
|
<v-card class="order-book-card" elevation="0" rounded="lg" style="margin-top: 32px">
|
||||||
<OrderBook
|
<OrderBook
|
||||||
:asks-yes="orderBookAsksYes"
|
:asks="orderBookAsks"
|
||||||
:bids-yes="orderBookBidsYes"
|
:bids="orderBookBids"
|
||||||
:asks-no="orderBookAsksNo"
|
:last-price="clobLastPrice"
|
||||||
:bids-no="orderBookBidsNo"
|
:spread="clobSpread"
|
||||||
:last-price-yes="clobLastPriceYes"
|
|
||||||
:last-price-no="clobLastPriceNo"
|
|
||||||
:spread-yes="clobSpreadYes"
|
|
||||||
:spread-no="clobSpreadNo"
|
|
||||||
:loading="clobLoading"
|
:loading="clobLoading"
|
||||||
:connected="clobConnected"
|
:connected="clobConnected"
|
||||||
:yes-label="yesLabel"
|
:yes-label="yesLabel"
|
||||||
@ -300,23 +296,6 @@ async function loadEventDetail() {
|
|||||||
})
|
})
|
||||||
if (res.code === 0 || res.code === 200) {
|
if (res.code === 0 || res.code === 200) {
|
||||||
eventDetail.value = res.data ?? null
|
eventDetail.value = res.data ?? null
|
||||||
// 单个市场详情数据打印,便于分析
|
|
||||||
const ev = eventDetail.value
|
|
||||||
console.log('[TradeDetail] 单个市场详情:', {
|
|
||||||
id: ev?.ID ?? ev?.id,
|
|
||||||
title: ev?.title,
|
|
||||||
slug: ev?.slug,
|
|
||||||
volume: ev?.volume,
|
|
||||||
endDate: ev?.endDate,
|
|
||||||
marketsCount: ev?.markets?.length ?? 0,
|
|
||||||
firstMarket: ev?.markets?.[0]
|
|
||||||
? {
|
|
||||||
question: ev.markets[0].question,
|
|
||||||
outcomePrices: ev.markets[0].outcomePrices,
|
|
||||||
marketId: ev.markets[0].marketId,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
detailError.value = res.msg || t('error.loadFailed')
|
detailError.value = res.msg || t('error.loadFailed')
|
||||||
eventDetail.value = null
|
eventDetail.value = null
|
||||||
@ -361,30 +340,15 @@ const currentMarket = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// --- CLOB WebSocket 订单簿与成交 ---
|
// --- CLOB WebSocket 订单簿与成交 ---
|
||||||
// 按 token 索引区分:0 = Yes,1 = No
|
|
||||||
type OrderBookRows = { price: number; shares: number }[]
|
|
||||||
const clobSdkRef = ref<ClobSdk | null>(null)
|
const clobSdkRef = ref<ClobSdk | null>(null)
|
||||||
const orderBookByToken = ref<Record<number, { asks: OrderBookRows; bids: OrderBookRows }>>({
|
const orderBookAsks = ref<{ price: number; shares: number }[]>([])
|
||||||
0: { asks: [], bids: [] },
|
const orderBookBids = ref<{ price: number; shares: number }[]>([])
|
||||||
1: { asks: [], bids: [] },
|
const clobLastPrice = ref<number | undefined>(undefined)
|
||||||
})
|
const clobSpread = ref<number | undefined>(undefined)
|
||||||
const clobLastPriceByToken = ref<Record<number, number | undefined>>({ 0: undefined, 1: undefined })
|
|
||||||
const clobSpreadByToken = ref<Record<number, number | undefined>>({ 0: undefined, 1: undefined })
|
|
||||||
const clobConnected = ref(false)
|
const clobConnected = ref(false)
|
||||||
const clobLoading = ref(false)
|
const clobLoading = ref(false)
|
||||||
/** 当前订阅的 tokenIds,用于根据 msg.m 匹配 Yes(0)/No(1) */
|
|
||||||
const clobTokenIdsRef = ref<string[]>([])
|
|
||||||
|
|
||||||
const orderBookAsksYes = computed(() => orderBookByToken.value[0]?.asks ?? [])
|
function priceSizeToRows(record: Record<string, number> | undefined): { price: number; shares: number }[] {
|
||||||
const orderBookBidsYes = computed(() => orderBookByToken.value[0]?.bids ?? [])
|
|
||||||
const orderBookAsksNo = computed(() => orderBookByToken.value[1]?.asks ?? [])
|
|
||||||
const orderBookBidsNo = computed(() => orderBookByToken.value[1]?.bids ?? [])
|
|
||||||
const clobLastPriceYes = computed(() => clobLastPriceByToken.value[0])
|
|
||||||
const clobLastPriceNo = computed(() => clobLastPriceByToken.value[1])
|
|
||||||
const clobSpreadYes = computed(() => clobSpreadByToken.value[0])
|
|
||||||
const clobSpreadNo = computed(() => clobSpreadByToken.value[1])
|
|
||||||
|
|
||||||
function priceSizeToRows(record: Record<string, number> | undefined): OrderBookRows {
|
|
||||||
if (!record) return []
|
if (!record) return []
|
||||||
return Object.entries(record)
|
return Object.entries(record)
|
||||||
.filter(([, shares]) => shares > 0)
|
.filter(([, shares]) => shares > 0)
|
||||||
@ -394,32 +358,15 @@ function priceSizeToRows(record: Record<string, number> | undefined): OrderBookR
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTokenIndex(msg: PriceSizePolyMsg): number {
|
|
||||||
const ids = clobTokenIdsRef.value
|
|
||||||
const m = msg.m
|
|
||||||
if (m != null && ids.length >= 2) {
|
|
||||||
const idx = ids.findIndex((id) => String(id) === String(m))
|
|
||||||
if (idx === 0 || idx === 1) return idx
|
|
||||||
}
|
|
||||||
if (typeof msg.i === 'number' && (msg.i === 0 || msg.i === 1)) return msg.i
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyPriceSizeAll(msg: PriceSizePolyMsg) {
|
function applyPriceSizeAll(msg: PriceSizePolyMsg) {
|
||||||
const idx = getTokenIndex(msg)
|
orderBookAsks.value = priceSizeToRows(msg.s).sort((a, b) => a.price - b.price)
|
||||||
const asks = priceSizeToRows(msg.s).sort((a, b) => a.price - b.price)
|
orderBookBids.value = priceSizeToRows(msg.b).sort((a, b) => b.price - a.price)
|
||||||
const bids = priceSizeToRows(msg.b).sort((a, b) => b.price - a.price)
|
updateSpreadFromBook()
|
||||||
orderBookByToken.value = {
|
|
||||||
...orderBookByToken.value,
|
|
||||||
[idx]: { asks, bids },
|
|
||||||
}
|
|
||||||
updateSpreadForToken(idx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyPriceSizeDelta(msg: PriceSizePolyMsg) {
|
function applyPriceSizeDelta(msg: PriceSizePolyMsg) {
|
||||||
const idx = getTokenIndex(msg)
|
|
||||||
const mergeDelta = (
|
const mergeDelta = (
|
||||||
current: OrderBookRows,
|
current: { price: number; shares: number }[],
|
||||||
delta: Record<string, number> | undefined,
|
delta: Record<string, number> | undefined,
|
||||||
asc: boolean,
|
asc: boolean,
|
||||||
) => {
|
) => {
|
||||||
@ -435,23 +382,16 @@ function applyPriceSizeDelta(msg: PriceSizePolyMsg) {
|
|||||||
.map(([price, shares]) => ({ price, shares }))
|
.map(([price, shares]) => ({ price, shares }))
|
||||||
.sort((a, b) => (asc ? a.price - b.price : b.price - a.price))
|
.sort((a, b) => (asc ? a.price - b.price : b.price - a.price))
|
||||||
}
|
}
|
||||||
const prev = orderBookByToken.value[idx] ?? { asks: [], bids: [] }
|
orderBookAsks.value = mergeDelta(orderBookAsks.value, msg.s, true)
|
||||||
const asks = mergeDelta(prev.asks, msg.s, true)
|
orderBookBids.value = mergeDelta(orderBookBids.value, msg.b, false)
|
||||||
const bids = mergeDelta(prev.bids, msg.b, false)
|
updateSpreadFromBook()
|
||||||
orderBookByToken.value = {
|
|
||||||
...orderBookByToken.value,
|
|
||||||
[idx]: { asks, bids },
|
|
||||||
}
|
|
||||||
updateSpreadForToken(idx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSpreadForToken(idx: number) {
|
function updateSpreadFromBook() {
|
||||||
const book = orderBookByToken.value[idx]
|
const bestAsk = orderBookAsks.value[0]?.price
|
||||||
if (!book) return
|
const bestBid = orderBookBids.value[0]?.price
|
||||||
const bestAsk = book.asks[0]?.price
|
|
||||||
const bestBid = book.bids[0]?.price
|
|
||||||
if (bestAsk != null && bestBid != null) {
|
if (bestAsk != null && bestBid != null) {
|
||||||
clobSpreadByToken.value = { ...clobSpreadByToken.value, [idx]: bestAsk - bestBid }
|
clobSpread.value = bestAsk - bestBid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -460,10 +400,8 @@ function connectClob(tokenIds: string[]) {
|
|||||||
clobSdkRef.value = null
|
clobSdkRef.value = null
|
||||||
clobLoading.value = true
|
clobLoading.value = true
|
||||||
clobConnected.value = false
|
clobConnected.value = false
|
||||||
clobTokenIdsRef.value = tokenIds
|
orderBookAsks.value = []
|
||||||
orderBookByToken.value = { 0: { asks: [], bids: [] }, 1: { asks: [], bids: [] } }
|
orderBookBids.value = []
|
||||||
clobLastPriceByToken.value = { 0: undefined, 1: undefined }
|
|
||||||
clobSpreadByToken.value = { 0: undefined, 1: undefined }
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
url: getClobWsUrl(),
|
url: getClobWsUrl(),
|
||||||
@ -487,10 +425,7 @@ function connectClob(tokenIds: string[]) {
|
|||||||
sdk.onTrade((msg: TradePolyMsg) => {
|
sdk.onTrade((msg: TradePolyMsg) => {
|
||||||
const priceNum = parseFloat(msg.p)
|
const priceNum = parseFloat(msg.p)
|
||||||
if (Number.isFinite(priceNum)) {
|
if (Number.isFinite(priceNum)) {
|
||||||
const priceCents = Math.round(priceNum * 100)
|
clobLastPrice.value = Math.round(priceNum * 100)
|
||||||
const side = msg.side?.toLowerCase()
|
|
||||||
const tokenIdx = side === 'buy' ? 0 : 1
|
|
||||||
clobLastPriceByToken.value = { ...clobLastPriceByToken.value, [tokenIdx]: priceCents }
|
|
||||||
}
|
}
|
||||||
// 追加到 Activity 列表
|
// 追加到 Activity 列表
|
||||||
const side = msg.side?.toLowerCase() === 'buy' ? 'Yes' : 'No'
|
const side = msg.side?.toLowerCase() === 'buy' ? 'Yes' : 'No'
|
||||||
@ -856,7 +791,7 @@ function buildOption(chartData: [number, number][], containerWidth?: number) {
|
|||||||
axisPointer: { animation: false },
|
axisPointer: { animation: false },
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: 8,
|
left: 48,
|
||||||
right: 48,
|
right: 48,
|
||||||
top: 16,
|
top: 16,
|
||||||
bottom: isMobile ? 44 : 28,
|
bottom: isMobile ? 44 : 28,
|
||||||
@ -967,12 +902,10 @@ watch(
|
|||||||
clobTokenIds,
|
clobTokenIds,
|
||||||
(tokenIds) => {
|
(tokenIds) => {
|
||||||
if (tokenIds.length > 0) {
|
if (tokenIds.length > 0) {
|
||||||
|
// 用接口返回的 Yes 价格作为初始 lastPrice
|
||||||
const payload = tradeMarketPayload.value
|
const payload = tradeMarketPayload.value
|
||||||
if (payload?.yesPrice != null) {
|
if (payload?.yesPrice != null) {
|
||||||
clobLastPriceByToken.value = {
|
clobLastPrice.value = Math.round(payload.yesPrice * 100)
|
||||||
...clobLastPriceByToken.value,
|
|
||||||
0: Math.round(payload.yesPrice * 100),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
connectClob(tokenIds)
|
connectClob(tokenIds)
|
||||||
} else {
|
} else {
|
||||||
@ -1164,7 +1097,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* Polymarket 样式分时图卡片(扁平化) */
|
/* Polymarket 样式分时图卡片(扁平化) */
|
||||||
.chart-card.polymarket-chart {
|
.chart-card.polymarket-chart {
|
||||||
margin-top: 0;
|
margin-top: 32px;
|
||||||
padding: 20px 24px 16px;
|
padding: 20px 24px 16px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border: 1px solid #e7e7e7;
|
border: 1px solid #e7e7e7;
|
||||||
@ -1271,7 +1204,6 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
/* Order Book Card Styles(扁平化) */
|
/* Order Book Card Styles(扁平化) */
|
||||||
.order-book-card {
|
.order-book-card {
|
||||||
margin-top: 16px;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border: 1px solid #e7e7e7;
|
border: 1px solid #e7e7e7;
|
||||||
@ -1391,13 +1323,13 @@ onUnmounted(() => {
|
|||||||
/* Responsive order book adjustments */
|
/* Responsive order book adjustments */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.order-book-card {
|
.order-book-card {
|
||||||
margin-top: 16px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Comments / Top Holders / Activity(扁平化) */
|
/* Comments / Top Holders / Activity(扁平化) */
|
||||||
.activity-card {
|
.activity-card {
|
||||||
margin-top: 16px;
|
margin-top: 32px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user