Compare commits

..

No commits in common. "8c455ba00a10369d8f84275b2a404325ad70bbed" and "6e3981a63788be1aeb36b831c1b958f81d5f0e68" have entirely different histories.

12 changed files with 262 additions and 304 deletions

View File

@ -249,11 +249,11 @@ export class ClobSdk {
// 2. 基于 'e' 字段的事件
switch (msg.e) {
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));
break;
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));
break;
case 'trade':

View File

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

View File

@ -37,7 +37,6 @@ watch(
<template>
<v-app>
<v-app-bar color="primary" dark>
<div class="app-bar-inner">
<v-btn
v-if="currentRoute !== '/'"
icon
@ -102,38 +101,20 @@ watch(
</v-list>
</v-menu>
</template>
</div>
</v-app-bar>
<v-main>
<div class="main-content">
<router-view v-slot="{ Component }">
<keep-alive :include="['Home']">
<component :is="Component" />
</keep-alive>
</router-view>
</div>
<router-view v-slot="{ Component }">
<keep-alive :include="['Home']">
<component :is="Component" />
</keep-alive>
</router-view>
</v-main>
<Toast />
</v-app>
</template>
<style scoped>
.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%;
}
/* Global styles can be added here */
.active {
font-weight: bold;
text-decoration: underline;

View File

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

View File

@ -99,35 +99,18 @@ 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,
@ -139,60 +122,33 @@ const props = withDefaults(
},
)
// Stateup = Yes down = No
// State
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)
// 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
})
// 使 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),
)
// mock mock
let mockInterval: ReturnType<typeof setInterval> | undefined
watch(
() =>
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,
() => props.connected || (props.asks?.length ?? 0) > 0,
(hasRealData) => {
if (hasRealData && mockInterval) {
clearInterval(mockInterval)

View File

@ -1684,6 +1684,7 @@ const showDepositForBuy = computed(() => !canAffordBuy.value)
/** Buy 模式且余额不足时显示 Deposit否则显示 Buy Yes/No */
const deposit = () => {
console.log('Depositing amount:', amount.value)
// API
}
@ -1730,6 +1731,7 @@ async function submitOrder() {
return
}
const uid = userStore?.user?.ID ?? 0
console.log('[submitOrder] 用户信息: user=', userStore.user, 'uid=', uid)
const userIdNum =
typeof uid === 'number'
? uid
@ -1737,6 +1739,7 @@ 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
}

View File

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

View File

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

View File

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

View File

@ -612,6 +612,7 @@ async function loadEvents(page: number, append: boolean, keyword?: string) {
})
} catch (e) {
if (!append) eventList.value = []
console.warn('getPmEventList failed', e)
}
}

View File

@ -1,32 +1,59 @@
<template>
<v-container class="login-container">
<v-row justify="center" align="center" class="login-row">
<v-col cols="12" sm="10" md="6" lg="4">
<v-card class="login-card" elevation="0" rounded="lg">
<v-col cols="12" md="6" lg="4">
<v-card class="login-card">
<v-card-title class="login-title">Sign In</v-card-title>
<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-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>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<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 class="login-links">
<router-link to="/register">Don't have an account? Sign Up</router-link>
</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>
</v-card-text>
</v-card>
</v-col>
@ -45,60 +72,102 @@ 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: 'Sign in to PolyMarket',
statement,
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',
{
@ -108,13 +177,20 @@ 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()
@ -142,57 +218,72 @@ const connectWithWallet = async () => {
}
.login-card {
border-radius: 16px;
padding: 40px 32px;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 24px;
}
.login-title {
font-size: 1.5rem;
font-weight: 600;
color: #111827;
margin-bottom: 8px;
font-weight: bold;
color: #0066cc;
margin-bottom: 24px;
text-align: center;
}
.login-body {
padding-top: 8px;
.login-btn {
margin-top: 24px;
height: 48px;
}
.login-desc {
font-size: 0.95rem;
color: #6b7280;
text-align: center;
margin: 0 0 28px 0;
line-height: 1.5;
.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;
}
.wallet-btn {
height: 48px;
font-weight: 600;
text-transform: none;
letter-spacing: 0.02em;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.wallet-icon {
font-size: 16px;
}
.error-message {
margin-top: 20px;
padding: 12px;
color: #dc2626;
background: #fef2f2;
border-radius: 8px;
margin-top: 16px;
color: #ff0000;
text-align: center;
font-size: 0.875rem;
font-size: 14px;
}
.login-links {
margin-top: 24px;
margin-top: 16px;
text-align: center;
}
.login-links a {
color: #2563eb;
color: #0066cc;
text-decoration: none;
font-size: 0.9rem;
}
.login-links a:hover {

View File

@ -45,16 +45,12 @@
</v-card>
<!-- 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
: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"
:asks="orderBookAsks"
:bids="orderBookBids"
:last-price="clobLastPrice"
:spread="clobSpread"
:loading="clobLoading"
:connected="clobConnected"
:yes-label="yesLabel"
@ -300,23 +296,6 @@ 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
@ -361,65 +340,33 @@ const currentMarket = computed(() => {
})
// --- CLOB WebSocket 簿 ---
// token 0 = Yes1 = No
type OrderBookRows = { price: number; shares: number }[]
const clobSdkRef = ref<ClobSdk | null>(null)
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 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 clobConnected = ref(false)
const clobLoading = ref(false)
/** 当前订阅的 tokenIds用于根据 msg.m 匹配 Yes(0)/No(1) */
const clobTokenIdsRef = ref<string[]>([])
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 {
function priceSizeToRows(record: Record<string, number> | undefined): { price: number; shares: number }[] {
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) {
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)
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()
}
function applyPriceSizeDelta(msg: PriceSizePolyMsg) {
const idx = getTokenIndex(msg)
const mergeDelta = (
current: OrderBookRows,
current: { price: number; shares: number }[],
delta: Record<string, number> | undefined,
asc: boolean,
) => {
@ -435,23 +382,16 @@ function applyPriceSizeDelta(msg: PriceSizePolyMsg) {
.map(([price, shares]) => ({ price, shares }))
.sort((a, b) => (asc ? a.price - b.price : b.price - a.price))
}
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)
orderBookAsks.value = mergeDelta(orderBookAsks.value, msg.s, true)
orderBookBids.value = mergeDelta(orderBookBids.value, msg.b, false)
updateSpreadFromBook()
}
function updateSpreadForToken(idx: number) {
const book = orderBookByToken.value[idx]
if (!book) return
const bestAsk = book.asks[0]?.price
const bestBid = book.bids[0]?.price
function updateSpreadFromBook() {
const bestAsk = orderBookAsks.value[0]?.price
const bestBid = orderBookBids.value[0]?.price
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
clobLoading.value = true
clobConnected.value = false
clobTokenIdsRef.value = tokenIds
orderBookByToken.value = { 0: { asks: [], bids: [] }, 1: { asks: [], bids: [] } }
clobLastPriceByToken.value = { 0: undefined, 1: undefined }
clobSpreadByToken.value = { 0: undefined, 1: undefined }
orderBookAsks.value = []
orderBookBids.value = []
const options = {
url: getClobWsUrl(),
@ -487,10 +425,7 @@ function connectClob(tokenIds: string[]) {
sdk.onTrade((msg: TradePolyMsg) => {
const priceNum = parseFloat(msg.p)
if (Number.isFinite(priceNum)) {
const priceCents = Math.round(priceNum * 100)
const side = msg.side?.toLowerCase()
const tokenIdx = side === 'buy' ? 0 : 1
clobLastPriceByToken.value = { ...clobLastPriceByToken.value, [tokenIdx]: priceCents }
clobLastPrice.value = Math.round(priceNum * 100)
}
// Activity
const side = msg.side?.toLowerCase() === 'buy' ? 'Yes' : 'No'
@ -856,7 +791,7 @@ function buildOption(chartData: [number, number][], containerWidth?: number) {
axisPointer: { animation: false },
},
grid: {
left: 8,
left: 48,
right: 48,
top: 16,
bottom: isMobile ? 44 : 28,
@ -967,12 +902,10 @@ watch(
clobTokenIds,
(tokenIds) => {
if (tokenIds.length > 0) {
// Yes lastPrice
const payload = tradeMarketPayload.value
if (payload?.yesPrice != null) {
clobLastPriceByToken.value = {
...clobLastPriceByToken.value,
0: Math.round(payload.yesPrice * 100),
}
clobLastPrice.value = Math.round(payload.yesPrice * 100)
}
connectClob(tokenIds)
} else {
@ -1164,7 +1097,7 @@ onUnmounted(() => {
/* Polymarket 样式分时图卡片(扁平化) */
.chart-card.polymarket-chart {
margin-top: 0;
margin-top: 32px;
padding: 20px 24px 16px;
background-color: #ffffff;
border: 1px solid #e7e7e7;
@ -1271,7 +1204,6 @@ onUnmounted(() => {
/* Order Book Card Styles扁平化 */
.order-book-card {
margin-top: 16px;
padding: 0;
background-color: #ffffff;
border: 1px solid #e7e7e7;
@ -1391,13 +1323,13 @@ onUnmounted(() => {
/* Responsive order book adjustments */
@media (max-width: 600px) {
.order-book-card {
margin-top: 16px;
margin-top: 24px;
}
}
/* Comments / Top Holders / Activity扁平化 */
.activity-card {
margin-top: 16px;
margin-top: 32px;
border: 1px solid #e5e7eb;
overflow: hidden;
box-shadow: none;