Compare commits
3 Commits
6e3981a637
...
8c455ba00a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c455ba00a | ||
|
|
6b12938c89 | ||
|
|
95d20a9bb1 |
@ -249,11 +249,11 @@ export class ClobSdk {
|
||||
// 2. 基于 'e' 字段的事件
|
||||
switch (msg.e) {
|
||||
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));
|
||||
break;
|
||||
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));
|
||||
break;
|
||||
case 'trade':
|
||||
|
||||
@ -112,27 +112,34 @@ export class UserSdk {
|
||||
// 拼接 Token
|
||||
const separator = baseUrl.includes('?') ? '&' : '?';
|
||||
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() {
|
||||
if (this.ws) {
|
||||
console.warn("UserSdk: WebSocket already connected or connecting");
|
||||
console.warn('[UserSdk] 已连接或正在连接,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`UserSdk: Connecting to ${this.url}...`);
|
||||
console.log('[UserSdk] 正在连接...', this.url.replace(/token=[^&]+/, 'token=***'));
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.onopen = (event: Event) => {
|
||||
console.log("UserSdk: Connected");
|
||||
console.log('[UserSdk] 已连接');
|
||||
this.reconnectAttempts = 0;
|
||||
this.listeners.connect.forEach(cb => cb(event));
|
||||
};
|
||||
|
||||
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.listeners.disconnect.forEach(cb => cb(event));
|
||||
|
||||
@ -142,7 +149,7 @@ export class UserSdk {
|
||||
};
|
||||
|
||||
this.ws.onerror = (event: Event) => {
|
||||
console.error("UserSdk: Error", event);
|
||||
console.error('[UserSdk] WebSocket 错误', event);
|
||||
this.listeners.error.forEach(cb => cb(event));
|
||||
};
|
||||
|
||||
@ -152,18 +159,19 @@ export class UserSdk {
|
||||
const msg = JSON.parse(raw) as UserMsg;
|
||||
this.handleMessage(msg);
|
||||
} catch (e) {
|
||||
console.error("UserSdk: Failed to parse message", event.data);
|
||||
console.error('[UserSdk] 消息解析失败', event.data, e);
|
||||
}
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
console.error("UserSdk: Connection failed", e);
|
||||
console.error('[UserSdk] 连接失败', e);
|
||||
if (this.autoReconnect) this.handleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
public disconnect() {
|
||||
console.log('[UserSdk] 主动断开连接');
|
||||
this.isExplicitClose = true;
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
@ -175,31 +183,35 @@ export class UserSdk {
|
||||
private handleMessage(msg: UserMsg) {
|
||||
switch (msg.type) {
|
||||
case 'welcome':
|
||||
console.log('[UserSdk] 收到 welcome', msg.msg ?? msg);
|
||||
this.listeners.welcome.forEach(cb => cb(msg));
|
||||
break;
|
||||
case 'order_update':
|
||||
console.log('[UserSdk] 收到 order_update', msg.data);
|
||||
this.listeners.order.forEach(cb => cb(msg.data));
|
||||
break;
|
||||
case 'position_update':
|
||||
console.log('[UserSdk] 收到 position_update', msg.data);
|
||||
this.listeners.position.forEach(cb => cb(msg.data));
|
||||
break;
|
||||
case 'balance_update':
|
||||
console.log('[UserSdk] 收到 balance_update', msg.data);
|
||||
this.listeners.balance.forEach(cb => cb(msg.data));
|
||||
break;
|
||||
default:
|
||||
console.warn("UserSdk: Unknown message type", msg);
|
||||
console.warn('[UserSdk] 未知消息类型', msg);
|
||||
}
|
||||
}
|
||||
|
||||
// 重连逻辑
|
||||
private handleReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.error("UserSdk: Max reconnect attempts reached");
|
||||
console.error('[UserSdk] 已达最大重连次数', this.maxReconnectAttempts);
|
||||
return;
|
||||
}
|
||||
|
||||
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(() => {
|
||||
this.connect();
|
||||
}, this.reconnectInterval);
|
||||
|
||||
31
src/App.vue
31
src/App.vue
@ -37,6 +37,7 @@ watch(
|
||||
<template>
|
||||
<v-app>
|
||||
<v-app-bar color="primary" dark>
|
||||
<div class="app-bar-inner">
|
||||
<v-btn
|
||||
v-if="currentRoute !== '/'"
|
||||
icon
|
||||
@ -101,20 +102,38 @@ watch(
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</div>
|
||||
</v-app-bar>
|
||||
<v-main>
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="['Home']">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
<div class="main-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="['Home']">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</v-main>
|
||||
<Toast />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
|
||||
@ -230,7 +230,6 @@ export async function getPmTagMain(): Promise<CategoryTreeResponse> {
|
||||
const res = await get<{ code: number; data: GetPmTagCatalogData; msg: string }>(
|
||||
'/pmTagCatalog/getPmTagCatalogPublic'
|
||||
)
|
||||
console.log('[getPmTagCatalogPublic] 原始响应:', JSON.stringify(res, null, 2))
|
||||
let raw: PmTagCatalogItem[] = []
|
||||
const d = res.data
|
||||
if (d && typeof d === 'object' && !Array.isArray(d)) {
|
||||
|
||||
@ -99,18 +99,35 @@ export interface OrderBookRow {
|
||||
|
||||
const props = withDefaults(
|
||||
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[]
|
||||
bids?: OrderBookRow[]
|
||||
lastPrice?: number
|
||||
spread?: number
|
||||
loading?: boolean
|
||||
connected?: boolean
|
||||
/** 市场 Yes 选项文案,来自 market.outcomes[0] */
|
||||
yesLabel?: string
|
||||
/** 市场 No 选项文案,来自 market.outcomes[1] */
|
||||
noLabel?: string
|
||||
}>(),
|
||||
{
|
||||
asksYes: () => [],
|
||||
bidsYes: () => [],
|
||||
asksNo: () => [],
|
||||
bidsNo: () => [],
|
||||
lastPriceYes: undefined,
|
||||
lastPriceNo: undefined,
|
||||
spreadYes: undefined,
|
||||
spreadNo: undefined,
|
||||
asks: () => [],
|
||||
bids: () => [],
|
||||
lastPrice: undefined,
|
||||
@ -122,33 +139,60 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
// State
|
||||
// State:up = Yes 交易,down = No 交易
|
||||
const activeTrade = ref('up')
|
||||
|
||||
// 使用 props 或回退到 mock 数据(来自 mockData 统一封装)
|
||||
const internalAsks = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_ASKS])
|
||||
const internalBids = ref<OrderBookRow[]>([...MOCK_ORDER_BOOK_BIDS])
|
||||
const internalLastPrice = ref(MOCK_ORDER_BOOK_LAST_PRICE)
|
||||
const internalSpread = ref(MOCK_ORDER_BOOK_SPREAD)
|
||||
|
||||
// 当有外部数据时使用 props,否则在 USE_MOCK_ORDER_BOOK 时用 mock
|
||||
const asks = computed(() =>
|
||||
props.asks?.length ? props.asks : USE_MOCK_ORDER_BOOK ? internalAsks.value : [],
|
||||
)
|
||||
const bids = computed(() =>
|
||||
props.bids?.length ? props.bids : USE_MOCK_ORDER_BOOK ? internalBids.value : [],
|
||||
)
|
||||
const displayLastPrice = computed(() =>
|
||||
props.lastPrice ?? (USE_MOCK_ORDER_BOOK ? internalLastPrice.value : 0),
|
||||
)
|
||||
const displaySpread = computed(() =>
|
||||
props.spread ?? (USE_MOCK_ORDER_BOOK ? internalSpread.value : 0),
|
||||
)
|
||||
// 根据 activeTrade 选择当前 tab 对应的数据
|
||||
const asks = computed(() => {
|
||||
const isYes = activeTrade.value === 'up'
|
||||
const fromYes = isYes ? props.asksYes : props.asksNo
|
||||
const fromLegacy = props.asks
|
||||
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 : []
|
||||
})
|
||||
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 更新
|
||||
let mockInterval: ReturnType<typeof setInterval> | undefined
|
||||
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) => {
|
||||
if (hasRealData && mockInterval) {
|
||||
clearInterval(mockInterval)
|
||||
|
||||
@ -1684,7 +1684,6 @@ const showDepositForBuy = computed(() => !canAffordBuy.value)
|
||||
|
||||
/** Buy 模式且余额不足时显示 Deposit,否则显示 Buy Yes/No */
|
||||
const deposit = () => {
|
||||
console.log('Depositing amount:', amount.value)
|
||||
// 实际应用中,这里应该调用存款API
|
||||
}
|
||||
|
||||
@ -1731,7 +1730,6 @@ async function submitOrder() {
|
||||
return
|
||||
}
|
||||
const uid = userStore?.user?.ID ?? 0
|
||||
console.log('[submitOrder] 用户信息: user=', userStore.user, 'uid=', uid)
|
||||
const userIdNum =
|
||||
typeof uid === 'number'
|
||||
? uid
|
||||
@ -1739,7 +1737,6 @@ async function submitOrder() {
|
||||
? parseInt(String(uid), 10)
|
||||
: 0
|
||||
if (!Number.isFinite(userIdNum) || userIdNum <= 0) {
|
||||
console.warn('[submitOrder] 用户信息异常: user=', userStore.user, 'uid=', uid)
|
||||
orderError.value = t('trade.userError')
|
||||
return
|
||||
}
|
||||
|
||||
@ -194,9 +194,6 @@ async function submitWithdraw() {
|
||||
submitting.value = true
|
||||
try {
|
||||
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')
|
||||
close()
|
||||
} finally {
|
||||
|
||||
@ -75,12 +75,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
balance.value = formatUsdcBalance(String(avail))
|
||||
}
|
||||
})
|
||||
sdk.onConnect(() => {
|
||||
console.log('[UserStore] UserSocket 已连接')
|
||||
})
|
||||
sdk.onDisconnect(() => {
|
||||
console.log('[UserStore] UserSocket 已断开')
|
||||
})
|
||||
sdk.onConnect(() => {})
|
||||
sdk.onDisconnect(() => {})
|
||||
sdk.onError((e) => {
|
||||
console.error('[UserStore] UserSocket 错误:', e)
|
||||
})
|
||||
@ -161,7 +157,6 @@ export const useUserStore = defineStore('user', () => {
|
||||
if (!headers) return
|
||||
try {
|
||||
const res = await getUserInfo(headers)
|
||||
console.log('[fetchUserInfo] 接口响应:', JSON.stringify(res, null, 2))
|
||||
const data = res.data as Record<string, unknown> | undefined
|
||||
// 接口返回 data.userInfo 或 data.user,取实际用户对象;若仍含 userInfo 则再取一层
|
||||
let u = (data?.userInfo ?? data?.user ?? data) as Record<string, unknown>
|
||||
|
||||
@ -14,14 +14,6 @@
|
||||
</template>
|
||||
|
||||
<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-col cols="12" class="chart-col">
|
||||
@ -34,6 +26,9 @@
|
||||
>
|
||||
<div class="chart-header">
|
||||
<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">
|
||||
<v-btn variant="text" size="small" class="past-btn">Past ▾</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[] {
|
||||
const range = selectedTimeRange.value
|
||||
return markets.value.map((market) => {
|
||||
const list = markets.value
|
||||
return list.map((market, i) => {
|
||||
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 {
|
||||
name: name + (name.length >= 32 ? '…' : ''),
|
||||
name,
|
||||
data: generateDataForMarket(chance || 20, range),
|
||||
}
|
||||
})
|
||||
@ -437,7 +436,7 @@ function buildOption(seriesArr: ChartSeriesItem[], containerWidth?: number) {
|
||||
itemGap: 12,
|
||||
},
|
||||
grid: {
|
||||
left: 16,
|
||||
left: 8,
|
||||
right: 48,
|
||||
top: 40,
|
||||
bottom: isMobile ? 44 : 28,
|
||||
@ -463,6 +462,15 @@ function initChart() {
|
||||
chartInstance = echarts.init(chartContainerRef.value)
|
||||
const w = chartContainerRef.value.clientWidth
|
||||
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() {
|
||||
@ -561,7 +569,6 @@ function onTradeSubmit(payload: {
|
||||
marketId?: string
|
||||
}) {
|
||||
// 可在此调用下单 API,payload 含 marketId(当前市场)
|
||||
console.log('Trade submit', payload)
|
||||
}
|
||||
|
||||
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 {
|
||||
margin-bottom: 24px;
|
||||
@ -767,10 +787,17 @@ watch(
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0 0 12px 0;
|
||||
margin: 0 0 4px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.chart-meta {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 0 0 8px 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chart-controls-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -872,30 +899,6 @@ watch(
|
||||
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 {
|
||||
border: 1px solid #e7e7e7;
|
||||
border-radius: 12px;
|
||||
|
||||
@ -612,7 +612,6 @@ async function loadEvents(page: number, append: boolean, keyword?: string) {
|
||||
})
|
||||
} catch (e) {
|
||||
if (!append) eventList.value = []
|
||||
console.warn('getPmEventList failed', e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,59 +1,32 @@
|
||||
<template>
|
||||
<v-container class="login-container">
|
||||
<v-row justify="center" align="center" class="login-row">
|
||||
<v-col cols="12" md="6" lg="4">
|
||||
<v-card class="login-card">
|
||||
<v-col cols="12" sm="10" md="6" lg="4">
|
||||
<v-card class="login-card" elevation="0" rounded="lg">
|
||||
<v-card-title class="login-title">Sign In</v-card-title>
|
||||
<v-card-text>
|
||||
<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-card-text class="login-body">
|
||||
<p class="login-desc">Connect your wallet to sign in</p>
|
||||
<v-btn
|
||||
class="wallet-btn"
|
||||
color="success"
|
||||
size="large"
|
||||
block
|
||||
rounded="lg"
|
||||
@click="connectWithWallet"
|
||||
:loading="isConnecting"
|
||||
:disabled="isConnecting"
|
||||
>
|
||||
<v-icon start>mdi-wallet</v-icon>
|
||||
{{ isConnecting ? 'Connecting...' : 'Connect with Wallet' }}
|
||||
</v-btn>
|
||||
|
||||
<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>
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<v-btn class="login-btn" color="primary" type="submit" block> Sign In </v-btn>
|
||||
|
||||
<div class="divider">
|
||||
<span>or</span>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
class="wallet-btn"
|
||||
color="success"
|
||||
block
|
||||
@click="connectWithWallet"
|
||||
:loading="isConnecting"
|
||||
:disabled="isConnecting"
|
||||
>
|
||||
<span class="wallet-icon">🟡</span>
|
||||
{{ isConnecting ? 'Connecting...' : 'Connect with Wallet' }}
|
||||
</v-btn>
|
||||
|
||||
<div v-if="errorMessage" class="error-message">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<div class="login-links">
|
||||
<router-link to="/register">Don't have an account? Sign Up</router-link>
|
||||
</div>
|
||||
</v-form>
|
||||
<div class="login-links">
|
||||
<router-link to="/register">Don't have an account? Sign Up</router-link>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@ -72,102 +45,60 @@ import { post } from '@/api/request'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
const isConnecting = ref(false)
|
||||
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 () => {
|
||||
isConnecting.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
// Check if Ethereum wallet is installed
|
||||
if (!window.ethereum) {
|
||||
throw new Error(
|
||||
'Ethereum wallet is not installed. Please install MetaMask, TokenPocket or another Ethereum wallet to continue.',
|
||||
)
|
||||
}
|
||||
|
||||
// Request account access
|
||||
const accounts = await window.ethereum.request({
|
||||
method: 'eth_requestAccounts',
|
||||
})
|
||||
|
||||
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)) {
|
||||
throw new Error('Invalid wallet address format. Please check your wallet connection.')
|
||||
}
|
||||
|
||||
// Get chain ID
|
||||
const chainId = await window.ethereum.request({
|
||||
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 domain = window.location.host
|
||||
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 signer = await provider.getSigner()
|
||||
const nonce = new Date().getTime().toString()
|
||||
|
||||
const siwe = new SiweMessage({
|
||||
scheme,
|
||||
domain,
|
||||
address: signer.address,
|
||||
statement,
|
||||
statement: 'Sign in to PolyMarket',
|
||||
uri: origin,
|
||||
version: '1',
|
||||
chainId: chainId,
|
||||
nonce,
|
||||
})
|
||||
const message = siwe.prepareMessage()
|
||||
// 去掉message 头部的http://
|
||||
// 将 message 中匹配 signer.address 值的内容替换为小写
|
||||
// const lowerAddress = signer.address.toLowerCase()
|
||||
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({
|
||||
method: 'personal_sign',
|
||||
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 } }>(
|
||||
'/base/walletLogin',
|
||||
{
|
||||
@ -177,20 +108,13 @@ const connectWithWallet = async () => {
|
||||
walletAddress,
|
||||
},
|
||||
)
|
||||
// 调试:打印登录信息数据结构
|
||||
console.log('[walletLogin] 登录响应:', JSON.stringify(loginData, null, 2))
|
||||
|
||||
if (loginData.code === 0 && loginData.data) {
|
||||
const data = loginData.data as Record<string, unknown>
|
||||
const token = data.token as string | undefined
|
||||
// 兼容 data.user 或 data 本身即用户对象(部分接口将 user 平铺在 data 下)
|
||||
const user =
|
||||
(data.user 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 })
|
||||
await userStore.fetchUserInfo()
|
||||
await userStore.fetchUsdcBalance()
|
||||
@ -218,72 +142,57 @@ const connectWithWallet = async () => {
|
||||
}
|
||||
|
||||
.login-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
padding: 40px 32px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #0066cc;
|
||||
margin-bottom: 24px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-top: 24px;
|
||||
height: 48px;
|
||||
.login-body {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.divider::before,
|
||||
.divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.divider span {
|
||||
padding: 0 16px;
|
||||
color: #808080;
|
||||
font-size: 14px;
|
||||
.login-desc {
|
||||
font-size: 0.95rem;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
margin: 0 0 28px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wallet-btn {
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wallet-icon {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-transform: none;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 16px;
|
||||
color: #ff0000;
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
color: #dc2626;
|
||||
background: #fef2f2;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.login-links {
|
||||
margin-top: 16px;
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-links a {
|
||||
color: #0066cc;
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login-links a:hover {
|
||||
|
||||
@ -45,12 +45,16 @@
|
||||
</v-card>
|
||||
|
||||
<!-- 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
|
||||
:asks="orderBookAsks"
|
||||
:bids="orderBookBids"
|
||||
:last-price="clobLastPrice"
|
||||
:spread="clobSpread"
|
||||
:asks-yes="orderBookAsksYes"
|
||||
:bids-yes="orderBookBidsYes"
|
||||
:asks-no="orderBookAsksNo"
|
||||
:bids-no="orderBookBidsNo"
|
||||
:last-price-yes="clobLastPriceYes"
|
||||
:last-price-no="clobLastPriceNo"
|
||||
:spread-yes="clobSpreadYes"
|
||||
:spread-no="clobSpreadNo"
|
||||
:loading="clobLoading"
|
||||
:connected="clobConnected"
|
||||
:yes-label="yesLabel"
|
||||
@ -296,6 +300,23 @@ async function loadEventDetail() {
|
||||
})
|
||||
if (res.code === 0 || res.code === 200) {
|
||||
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 {
|
||||
detailError.value = res.msg || t('error.loadFailed')
|
||||
eventDetail.value = null
|
||||
@ -340,33 +361,65 @@ const currentMarket = computed(() => {
|
||||
})
|
||||
|
||||
// --- CLOB WebSocket 订单簿与成交 ---
|
||||
// 按 token 索引区分:0 = Yes,1 = No
|
||||
type OrderBookRows = { price: number; shares: number }[]
|
||||
const clobSdkRef = ref<ClobSdk | null>(null)
|
||||
const orderBookAsks = ref<{ price: number; shares: number }[]>([])
|
||||
const orderBookBids = ref<{ price: number; shares: number }[]>([])
|
||||
const clobLastPrice = ref<number | undefined>(undefined)
|
||||
const clobSpread = ref<number | undefined>(undefined)
|
||||
const orderBookByToken = ref<Record<number, { asks: OrderBookRows; bids: OrderBookRows }>>({
|
||||
0: { asks: [], bids: [] },
|
||||
1: { asks: [], bids: [] },
|
||||
})
|
||||
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 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 []
|
||||
return Object.entries(record)
|
||||
.filter(([, shares]) => shares > 0)
|
||||
.map(([p, shares]) => ({
|
||||
price: Math.round(parseFloat(p) /100),
|
||||
price: Math.round(parseFloat(p) / 100),
|
||||
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) {
|
||||
orderBookAsks.value = priceSizeToRows(msg.s).sort((a, b) => a.price - b.price)
|
||||
orderBookBids.value = priceSizeToRows(msg.b).sort((a, b) => b.price - a.price)
|
||||
updateSpreadFromBook()
|
||||
const idx = getTokenIndex(msg)
|
||||
const asks = priceSizeToRows(msg.s).sort((a, b) => a.price - b.price)
|
||||
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) {
|
||||
const idx = getTokenIndex(msg)
|
||||
const mergeDelta = (
|
||||
current: { price: number; shares: number }[],
|
||||
current: OrderBookRows,
|
||||
delta: Record<string, number> | undefined,
|
||||
asc: boolean,
|
||||
) => {
|
||||
@ -382,16 +435,23 @@ function applyPriceSizeDelta(msg: PriceSizePolyMsg) {
|
||||
.map(([price, shares]) => ({ price, shares }))
|
||||
.sort((a, b) => (asc ? a.price - b.price : b.price - a.price))
|
||||
}
|
||||
orderBookAsks.value = mergeDelta(orderBookAsks.value, msg.s, true)
|
||||
orderBookBids.value = mergeDelta(orderBookBids.value, msg.b, false)
|
||||
updateSpreadFromBook()
|
||||
const prev = orderBookByToken.value[idx] ?? { asks: [], bids: [] }
|
||||
const asks = mergeDelta(prev.asks, msg.s, true)
|
||||
const bids = mergeDelta(prev.bids, msg.b, false)
|
||||
orderBookByToken.value = {
|
||||
...orderBookByToken.value,
|
||||
[idx]: { asks, bids },
|
||||
}
|
||||
updateSpreadForToken(idx)
|
||||
}
|
||||
|
||||
function updateSpreadFromBook() {
|
||||
const bestAsk = orderBookAsks.value[0]?.price
|
||||
const bestBid = orderBookBids.value[0]?.price
|
||||
function updateSpreadForToken(idx: number) {
|
||||
const book = orderBookByToken.value[idx]
|
||||
if (!book) return
|
||||
const bestAsk = book.asks[0]?.price
|
||||
const bestBid = book.bids[0]?.price
|
||||
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
|
||||
clobLoading.value = true
|
||||
clobConnected.value = false
|
||||
orderBookAsks.value = []
|
||||
orderBookBids.value = []
|
||||
clobTokenIdsRef.value = tokenIds
|
||||
orderBookByToken.value = { 0: { asks: [], bids: [] }, 1: { asks: [], bids: [] } }
|
||||
clobLastPriceByToken.value = { 0: undefined, 1: undefined }
|
||||
clobSpreadByToken.value = { 0: undefined, 1: undefined }
|
||||
|
||||
const options = {
|
||||
url: getClobWsUrl(),
|
||||
@ -425,7 +487,10 @@ function connectClob(tokenIds: string[]) {
|
||||
sdk.onTrade((msg: TradePolyMsg) => {
|
||||
const priceNum = parseFloat(msg.p)
|
||||
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 列表
|
||||
const side = msg.side?.toLowerCase() === 'buy' ? 'Yes' : 'No'
|
||||
@ -791,7 +856,7 @@ function buildOption(chartData: [number, number][], containerWidth?: number) {
|
||||
axisPointer: { animation: false },
|
||||
},
|
||||
grid: {
|
||||
left: 48,
|
||||
left: 8,
|
||||
right: 48,
|
||||
top: 16,
|
||||
bottom: isMobile ? 44 : 28,
|
||||
@ -902,10 +967,12 @@ watch(
|
||||
clobTokenIds,
|
||||
(tokenIds) => {
|
||||
if (tokenIds.length > 0) {
|
||||
// 用接口返回的 Yes 价格作为初始 lastPrice
|
||||
const payload = tradeMarketPayload.value
|
||||
if (payload?.yesPrice != null) {
|
||||
clobLastPrice.value = Math.round(payload.yesPrice * 100)
|
||||
clobLastPriceByToken.value = {
|
||||
...clobLastPriceByToken.value,
|
||||
0: Math.round(payload.yesPrice * 100),
|
||||
}
|
||||
}
|
||||
connectClob(tokenIds)
|
||||
} else {
|
||||
@ -1097,7 +1164,7 @@ onUnmounted(() => {
|
||||
|
||||
/* Polymarket 样式分时图卡片(扁平化) */
|
||||
.chart-card.polymarket-chart {
|
||||
margin-top: 32px;
|
||||
margin-top: 0;
|
||||
padding: 20px 24px 16px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e7e7e7;
|
||||
@ -1204,6 +1271,7 @@ onUnmounted(() => {
|
||||
|
||||
/* Order Book Card Styles(扁平化) */
|
||||
.order-book-card {
|
||||
margin-top: 16px;
|
||||
padding: 0;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e7e7e7;
|
||||
@ -1323,13 +1391,13 @@ onUnmounted(() => {
|
||||
/* Responsive order book adjustments */
|
||||
@media (max-width: 600px) {
|
||||
.order-book-card {
|
||||
margin-top: 24px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Comments / Top Holders / Activity(扁平化) */
|
||||
.activity-card {
|
||||
margin-top: 32px;
|
||||
margin-top: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user