Compare commits

...

3 Commits

Author SHA1 Message Date
ivan
8c455ba00a 优化:订单簿信息的完善 2026-02-26 19:50:44 +08:00
ivan
6b12938c89 优化:UI和日志 2026-02-26 19:29:20 +08:00
ivan
95d20a9bb1 优化:UI调整 2026-02-26 17:32:12 +08:00
12 changed files with 304 additions and 262 deletions

View File

@ -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', { m: msg.m, t: msg.t, bids: msg.b, asks: msg.s }); console.log('[ClobSdk] 回调: price_size_all', { i: msg.i, 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', { m: msg.m, t: msg.t, bids: msg.b, asks: msg.s }); console.log('[ClobSdk] 回调: price_size_delta', { i: msg.i, 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':

View File

@ -112,27 +112,34 @@ 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: WebSocket already connected or connecting"); console.warn('[UserSdk] 已连接或正在连接,跳过');
return; return;
} }
try { try {
console.log(`UserSdk: Connecting to ${this.url}...`); console.log('[UserSdk] 正在连接...', this.url.replace(/token=[^&]+/, 'token=***'));
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: Connected"); console.log('[UserSdk] 已连接');
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: Disconnected (Code: ${event.code})`); console.log('[UserSdk] 已断开', { code: event.code, reason: event.reason, wasClean: event.wasClean });
this.ws = null; this.ws = null;
this.listeners.disconnect.forEach(cb => cb(event)); this.listeners.disconnect.forEach(cb => cb(event));
@ -142,7 +149,7 @@ export class UserSdk {
}; };
this.ws.onerror = (event: Event) => { this.ws.onerror = (event: Event) => {
console.error("UserSdk: Error", event); console.error('[UserSdk] WebSocket 错误', event);
this.listeners.error.forEach(cb => cb(event)); this.listeners.error.forEach(cb => cb(event));
}; };
@ -152,18 +159,19 @@ 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: Failed to parse message", event.data); console.error('[UserSdk] 消息解析失败', event.data, e);
} }
}; };
} catch (e) { } catch (e) {
console.error("UserSdk: Connection failed", e); console.error('[UserSdk] 连接失败', 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();
@ -175,31 +183,35 @@ 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: Unknown message type", msg); console.warn('[UserSdk] 未知消息类型', msg);
} }
} }
// 重连逻辑 // 重连逻辑
private handleReconnect() { private handleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) { if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error("UserSdk: Max reconnect attempts reached"); console.error('[UserSdk] 已达最大重连次数', this.maxReconnectAttempts);
return; return;
} }
this.reconnectAttempts++; this.reconnectAttempts++;
console.log(`UserSdk: Reconnecting in ${this.reconnectInterval}ms (Attempt ${this.reconnectAttempts})...`); console.log('[UserSdk] 重连中...', { attempt: this.reconnectAttempts, max: this.maxReconnectAttempts, delayMs: this.reconnectInterval });
setTimeout(() => { setTimeout(() => {
this.connect(); this.connect();
}, this.reconnectInterval); }, this.reconnectInterval);

View File

@ -37,6 +37,7 @@ 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
@ -101,20 +102,38 @@ 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>
/* Global styles can be added here */ .app-bar-inner {
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;

View File

@ -230,7 +230,6 @@ 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)) {

View File

@ -99,18 +99,35 @@ 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,
@ -122,33 +139,60 @@ const props = withDefaults(
}, },
) )
// State // Stateup = Yes down = No
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)
// 使 props USE_MOCK_ORDER_BOOK mock // activeTrade tab
const asks = computed(() => const asks = computed(() => {
props.asks?.length ? props.asks : USE_MOCK_ORDER_BOOK ? internalAsks.value : [], const isYes = activeTrade.value === 'up'
) const fromYes = isYes ? props.asksYes : props.asksNo
const bids = computed(() => const fromLegacy = props.asks
props.bids?.length ? props.bids : USE_MOCK_ORDER_BOOK ? internalBids.value : [], const hasYesNo = (fromYes?.length ?? 0) > 0
) const hasLegacy = (fromLegacy?.length ?? 0) > 0
const displayLastPrice = computed(() => if (hasYesNo) return fromYes ?? []
props.lastPrice ?? (USE_MOCK_ORDER_BOOK ? internalLastPrice.value : 0), if (hasLegacy) return fromLegacy ?? []
) return USE_MOCK_ORDER_BOOK ? internalAsks.value : []
const displaySpread = computed(() => })
props.spread ?? (USE_MOCK_ORDER_BOOK ? internalSpread.value : 0), const bids = computed(() => {
) 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)

View File

@ -1684,7 +1684,6 @@ 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
} }
@ -1731,7 +1730,6 @@ 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
@ -1739,7 +1737,6 @@ 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
} }

View File

@ -194,9 +194,6 @@ 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 {

View File

@ -75,12 +75,8 @@ export const useUserStore = defineStore('user', () => {
balance.value = formatUsdcBalance(String(avail)) balance.value = formatUsdcBalance(String(avail))
} }
}) })
sdk.onConnect(() => { sdk.onConnect(() => {})
console.log('[UserStore] UserSocket 已连接') sdk.onDisconnect(() => {})
})
sdk.onDisconnect(() => {
console.log('[UserStore] UserSocket 已断开')
})
sdk.onError((e) => { sdk.onError((e) => {
console.error('[UserStore] UserSocket 错误:', e) console.error('[UserStore] UserSocket 错误:', e)
}) })
@ -161,7 +157,6 @@ 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>

View File

@ -14,14 +14,6 @@
</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">
@ -34,6 +26,9 @@
> >
<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>
@ -353,11 +348,15 @@ function generateDataForMarket(baseChance: number, range: string): [number, numb
function generateAllData(): ChartSeriesItem[] { function generateAllData(): ChartSeriesItem[] {
const range = selectedTimeRange.value const range = selectedTimeRange.value
return markets.value.map((market) => { const list = markets.value
return list.map((market, i) => {
const chance = marketChance(market) const chance = marketChance(market)
const name = (market.question || 'Market').slice(0, 32) const base = (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.length >= 32 ? '…' : ''), name,
data: generateDataForMarket(chance || 20, range), data: generateDataForMarket(chance || 20, range),
} }
}) })
@ -437,7 +436,7 @@ function buildOption(seriesArr: ChartSeriesItem[], containerWidth?: number) {
itemGap: 12, itemGap: 12,
}, },
grid: { grid: {
left: 16, left: 8,
right: 48, right: 48,
top: 40, top: 40,
bottom: isMobile ? 44 : 28, bottom: isMobile ? 44 : 28,
@ -463,6 +462,15 @@ 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() {
@ -561,7 +569,6 @@ function onTradeSubmit(payload: {
marketId?: string marketId?: string
}) { }) {
// APIpayload marketId // APIpayload marketId
console.log('Trade submit', payload)
} }
const toastStore = useToastStore() const toastStore = useToastStore()
@ -749,6 +756,19 @@ 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;
@ -767,10 +787,17 @@ watch(
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
color: #111827; color: #111827;
margin: 0 0 12px 0; margin: 0 0 4px 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;
@ -872,30 +899,6 @@ 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;

View File

@ -612,7 +612,6 @@ 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)
} }
} }

View File

@ -1,48 +1,22 @@
<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" md="6" lg="4"> <v-col cols="12" sm="10" md="6" lg="4">
<v-card class="login-card"> <v-card class="login-card" elevation="0" rounded="lg">
<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> <v-card-text class="login-body">
<v-form @submit.prevent="handleLogin"> <p class="login-desc">Connect your wallet to sign in</p>
<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"
> >
<span class="wallet-icon">🟡</span> <v-icon start>mdi-wallet</v-icon>
{{ isConnecting ? 'Connecting...' : 'Connect with Wallet' }} {{ isConnecting ? 'Connecting...' : 'Connect with Wallet' }}
</v-btn> </v-btn>
@ -53,7 +27,6 @@
<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>
@ -72,102 +45,60 @@ 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, statement: 'Sign in to PolyMarket',
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',
{ {
@ -177,20 +108,13 @@ 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()
@ -218,72 +142,57 @@ const connectWithWallet = async () => {
} }
.login-card { .login-card {
border-radius: 12px; border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); padding: 40px 32px;
padding: 24px; border: 1px solid rgba(0, 0, 0, 0.08);
} }
.login-title { .login-title {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: bold; font-weight: 600;
color: #0066cc; color: #111827;
margin-bottom: 24px; margin-bottom: 8px;
text-align: center; text-align: center;
} }
.login-btn { .login-body {
margin-top: 24px; padding-top: 8px;
height: 48px;
} }
.divider { .login-desc {
display: flex; font-size: 0.95rem;
align-items: center; color: #6b7280;
justify-content: center; text-align: center;
margin: 24px 0; margin: 0 0 28px 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;
display: flex; font-weight: 600;
align-items: center; text-transform: none;
justify-content: center; letter-spacing: 0.02em;
gap: 8px;
}
.wallet-icon {
font-size: 16px;
} }
.error-message { .error-message {
margin-top: 16px; margin-top: 20px;
color: #ff0000; padding: 12px;
color: #dc2626;
background: #fef2f2;
border-radius: 8px;
text-align: center; text-align: center;
font-size: 14px; font-size: 0.875rem;
} }
.login-links { .login-links {
margin-top: 16px; margin-top: 24px;
text-align: center; text-align: center;
} }
.login-links a { .login-links a {
color: #0066cc; color: #2563eb;
text-decoration: none; text-decoration: none;
font-size: 0.9rem;
} }
.login-links a:hover { .login-links a:hover {

View File

@ -45,12 +45,16 @@
</v-card> </v-card>
<!-- Order Book Section --> <!-- Order Book Section -->
<v-card class="order-book-card" elevation="0" rounded="lg" style="margin-top: 32px"> <v-card class="order-book-card" elevation="0" rounded="lg">
<OrderBook <OrderBook
:asks="orderBookAsks" :asks-yes="orderBookAsksYes"
:bids="orderBookBids" :bids-yes="orderBookBidsYes"
:last-price="clobLastPrice" :asks-no="orderBookAsksNo"
:spread="clobSpread" :bids-no="orderBookBidsNo"
: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"
@ -296,6 +300,23 @@ 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
@ -340,33 +361,65 @@ const currentMarket = computed(() => {
}) })
// --- CLOB WebSocket 簿 --- // --- CLOB WebSocket 簿 ---
// token 0 = Yes1 = No
type OrderBookRows = { price: number; shares: number }[]
const clobSdkRef = ref<ClobSdk | null>(null) const clobSdkRef = ref<ClobSdk | null>(null)
const orderBookAsks = ref<{ price: number; shares: number }[]>([]) const orderBookByToken = ref<Record<number, { asks: OrderBookRows; bids: OrderBookRows }>>({
const orderBookBids = ref<{ price: number; shares: number }[]>([]) 0: { asks: [], bids: [] },
const clobLastPrice = ref<number | undefined>(undefined) 1: { asks: [], bids: [] },
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[]>([])
function priceSizeToRows(record: Record<string, number> | undefined): { price: number; shares: number }[] { const orderBookAsksYes = computed(() => orderBookByToken.value[0]?.asks ?? [])
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)
.map(([p, shares]) => ({ .map(([p, shares]) => ({
price: Math.round(parseFloat(p) /100), price: Math.round(parseFloat(p) / 100),
shares, shares,
})) }))
} }
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) {
orderBookAsks.value = priceSizeToRows(msg.s).sort((a, b) => a.price - b.price) const idx = getTokenIndex(msg)
orderBookBids.value = priceSizeToRows(msg.b).sort((a, b) => b.price - a.price) const asks = priceSizeToRows(msg.s).sort((a, b) => a.price - b.price)
updateSpreadFromBook() const bids = priceSizeToRows(msg.b).sort((a, b) => b.price - a.price)
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: { price: number; shares: number }[], current: OrderBookRows,
delta: Record<string, number> | undefined, delta: Record<string, number> | undefined,
asc: boolean, asc: boolean,
) => { ) => {
@ -382,16 +435,23 @@ 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))
} }
orderBookAsks.value = mergeDelta(orderBookAsks.value, msg.s, true) const prev = orderBookByToken.value[idx] ?? { asks: [], bids: [] }
orderBookBids.value = mergeDelta(orderBookBids.value, msg.b, false) const asks = mergeDelta(prev.asks, msg.s, true)
updateSpreadFromBook() const bids = mergeDelta(prev.bids, msg.b, false)
orderBookByToken.value = {
...orderBookByToken.value,
[idx]: { asks, bids },
}
updateSpreadForToken(idx)
} }
function updateSpreadFromBook() { function updateSpreadForToken(idx: number) {
const bestAsk = orderBookAsks.value[0]?.price const book = orderBookByToken.value[idx]
const bestBid = orderBookBids.value[0]?.price if (!book) return
const bestAsk = book.asks[0]?.price
const bestBid = book.bids[0]?.price
if (bestAsk != null && bestBid != null) { if (bestAsk != null && bestBid != null) {
clobSpread.value = bestAsk - bestBid clobSpreadByToken.value = { ...clobSpreadByToken.value, [idx]: bestAsk - bestBid }
} }
} }
@ -400,8 +460,10 @@ function connectClob(tokenIds: string[]) {
clobSdkRef.value = null clobSdkRef.value = null
clobLoading.value = true clobLoading.value = true
clobConnected.value = false clobConnected.value = false
orderBookAsks.value = [] clobTokenIdsRef.value = tokenIds
orderBookBids.value = [] orderBookByToken.value = { 0: { asks: [], bids: [] }, 1: { asks: [], bids: [] } }
clobLastPriceByToken.value = { 0: undefined, 1: undefined }
clobSpreadByToken.value = { 0: undefined, 1: undefined }
const options = { const options = {
url: getClobWsUrl(), url: getClobWsUrl(),
@ -425,7 +487,10 @@ 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)) {
clobLastPrice.value = Math.round(priceNum * 100) const priceCents = 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'
@ -791,7 +856,7 @@ function buildOption(chartData: [number, number][], containerWidth?: number) {
axisPointer: { animation: false }, axisPointer: { animation: false },
}, },
grid: { grid: {
left: 48, left: 8,
right: 48, right: 48,
top: 16, top: 16,
bottom: isMobile ? 44 : 28, bottom: isMobile ? 44 : 28,
@ -902,10 +967,12 @@ 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) {
clobLastPrice.value = Math.round(payload.yesPrice * 100) clobLastPriceByToken.value = {
...clobLastPriceByToken.value,
0: Math.round(payload.yesPrice * 100),
}
} }
connectClob(tokenIds) connectClob(tokenIds)
} else { } else {
@ -1097,7 +1164,7 @@ onUnmounted(() => {
/* Polymarket 样式分时图卡片(扁平化) */ /* Polymarket 样式分时图卡片(扁平化) */
.chart-card.polymarket-chart { .chart-card.polymarket-chart {
margin-top: 32px; margin-top: 0;
padding: 20px 24px 16px; padding: 20px 24px 16px;
background-color: #ffffff; background-color: #ffffff;
border: 1px solid #e7e7e7; border: 1px solid #e7e7e7;
@ -1204,6 +1271,7 @@ 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;
@ -1323,13 +1391,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: 24px; margin-top: 16px;
} }
} }
/* Comments / Top Holders / Activity扁平化 */ /* Comments / Top Holders / Activity扁平化 */
.activity-card { .activity-card {
margin-top: 32px; margin-top: 16px;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
overflow: hidden; overflow: hidden;
box-shadow: none; box-shadow: none;