Compare commits

..

No commits in common. "c976333b7244d47d53970920e0e23f92ba9d87b0" and "7bc5831edd7d760bd40d354275f2db014843bb17" have entirely different histories.

17 changed files with 152 additions and 2185 deletions

File diff suppressed because it is too large Load Diff

View File

@ -64,49 +64,9 @@ export interface PmEventMarketItem {
clobTokenIds?: string[] clobTokenIds?: string[]
endDate?: string endDate?: string
volume?: number volume?: number
/** 为 true 时表示市场已关闭/已结算,不再交易 */
closed?: boolean
[key: string]: unknown [key: string]: unknown
} }
/** 市场是否已关闭(不再接受下单) */
export function isPmMarketClosed(m: PmEventMarketItem | null | undefined): boolean {
return m?.closed === true
}
/**
* outcomePrices outcomes 01 1100
*/
export function getPmMarketSettledWinner(m: PmEventMarketItem): {
label: string
winnerIndex: number
} | null {
if (!isPmMarketClosed(m)) return null
const prices = m.outcomePrices
const outcomes = m.outcomes
if (!prices?.length) {
return { label: outcomes?.[0] ?? '—', winnerIndex: 0 }
}
let bestIdx = 0
let bestProb = -1
for (let i = 0; i < prices.length; i++) {
const p = parseFloat(String(prices[i]))
if (!Number.isFinite(p)) continue
const prob = p > 1 ? Math.min(1, p / 100) : p
if (prob > bestProb) {
bestProb = prob
bestIdx = i
}
}
if (bestProb < 0) {
return { label: outcomes?.[0] ?? '—', winnerIndex: 0 }
}
const label =
outcomes?.[bestIdx] ??
(bestIdx === 0 ? 'Yes' : bestIdx === 1 ? 'No' : `#${bestIdx + 1}`)
return { label, winnerIndex: bestIdx }
}
/** 从市场项取 marketId兼容 ID / id */ /** 从市场项取 marketId兼容 ID / id */
export function getMarketId(m: PmEventMarketItem | null | undefined): string | undefined { export function getMarketId(m: PmEventMarketItem | null | undefined): string | undefined {
if (!m) return undefined if (!m) return undefined
@ -277,24 +237,36 @@ export function readTradeRouteEventSlug(obj: unknown): string | undefined {
} }
export interface TradeDetailRouteInput { export interface TradeDetailRouteInput {
/** 事件数字 ID;无 slug 时用作路径段 */ /** 事件数字 ID(与首页 MarketCard :id 一致),优先用于路径参数 */
eventId?: string | number | null eventId?: string | number | null
/** 事件 slug,优先作为路径参数(详情页仅用其请求 findPmEvent */ /** 事件 slug;无 eventId 时用作路径;路径为数字 ID 时可作为 query.slug 传给 findPmEvent */
eventSlug?: string | null eventSlug?: string | null
/** 市场行 ID对应详情页 query.marketId */
marketId?: string | null
/** 详情页标题回显 query.title */
title?: string | null
} }
/** /**
* `/trade-detail/:id` query slug ID * MarketCardEventMarkets trade-detail router.push
* `/trade-detail/:id`id ID slug
*/ */
export function buildTradeDetailPushOptions( export function buildTradeDetailPushOptions(
input: TradeDetailRouteInput, input: TradeDetailRouteInput,
): { name: 'trade-detail'; params: { id: string } } | null { ): { name: 'trade-detail'; params: { id: string }; query: Record<string, string> } | null {
const slug = input.eventSlug?.trim() || ''
const eid = const eid =
input.eventId != null && String(input.eventId).trim() !== '' ? String(input.eventId).trim() : '' input.eventId != null && String(input.eventId).trim() !== '' ? String(input.eventId).trim() : ''
const pathId = slug || eid const slugOnly = input.eventSlug?.trim() || ''
if (!pathId) return null if (!eid && !slugOnly) return null
return { name: 'trade-detail', params: { id: pathId } } const pathId = eid || slugOnly
const numId = parseInt(eid, 10)
const isNumericEventPath =
eid !== '' && Number.isFinite(numId) && String(numId) === eid && numId >= 1
const query: Record<string, string> = {}
if (input.title?.trim()) query.title = input.title.trim()
if (input.marketId?.trim()) query.marketId = input.marketId.trim()
if (isNumericEventPath && slugOnly) query.slug = slugOnly
return { name: 'trade-detail', params: { id: pathId }, query }
} }
/** 多选项卡片中单个选项(用于左右滑动切换) */ /** 多选项卡片中单个选项(用于左右滑动切换) */

View File

@ -68,12 +68,10 @@ export function usdcToAmount(displayAmount: number): number {
return Math.round(displayAmount * USDC_SCALE) return Math.round(displayAmount * USDC_SCALE)
} }
/** 提现状态:审核中、提现成功(含 processed、审核不通过、提现失败 */ /** 提现状态:审核中、提现成功、审核不通过、提现失败 */
export const WITHDRAW_STATUS = { export const WITHDRAW_STATUS = {
PENDING: 'pending', PENDING: 'pending',
SUCCESS: 'success', SUCCESS: 'success',
/** 与 success 同义,后端已处理完成 */
PROCESSED: 'processed',
REJECTED: 'rejected', REJECTED: 'rejected',
FAILED: 'failed', FAILED: 'failed',
} as const } as const

View File

@ -232,13 +232,26 @@ const semiProgressColor = computed(() => {
}) })
const navigateToDetail = () => { const navigateToDetail = () => {
const segment = (props.slug?.trim() || props.id || '').trim()
if (!segment) return
if (props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1) { if (props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1) {
router.push({ name: 'event-markets', params: { id: segment } }) router.push({
path: `/event/${props.id}/markets`,
query: { ...(props.slug && { slug: props.slug }) },
})
return return
} }
router.push({ name: 'trade-detail', params: { id: segment } }) router.push({
path: `/trade-detail/${props.id}`,
query: {
title: props.marketTitle,
imageUrl: props.imageUrl || undefined,
category: props.category || undefined,
marketInfo: props.marketInfo || undefined,
expiresAt: props.expiresAt || undefined,
chance: String(props.chanceValue),
...(props.marketId && { marketId: props.marketId }),
...(props.slug && { slug: props.slug }),
},
})
} }
function openTradeSingle(side: 'yes' | 'no') { function openTradeSingle(side: 'yes' | 'no') {

View File

@ -77,10 +77,6 @@
"pleaseSelectMarket": "Please select a market (with clobTokenIds)", "pleaseSelectMarket": "Please select a market (with clobTokenIds)",
"userError": "User info error", "userError": "User info error",
"orderFailed": "Order failed", "orderFailed": "Order failed",
"marketClosedTitle": "Market is closed",
"marketClosedDesc": "This market has settled. Trading and new orders are not available.",
"marketClosedOutcome": "Outcome: {outcome}",
"marketClosedBanner": "This market is closed and has settled.",
"expiration": { "expiration": {
"5m": "5m", "5m": "5m",
"15m": "15m", "15m": "15m",
@ -110,8 +106,7 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"priceZero": "0¢", "priceZero": "0¢",
"moreActions": "More actions", "moreActions": "More actions"
"settledSubMarketsTitle": "Sub-markets (settled)"
}, },
"searchPage": { "searchPage": {
"title": "Search", "title": "Search",

View File

@ -77,10 +77,6 @@
"pleaseSelectMarket": "市場を選択してくださいclobTokenIds が必要)", "pleaseSelectMarket": "市場を選択してくださいclobTokenIds が必要)",
"userError": "ユーザー情報エラー", "userError": "ユーザー情報エラー",
"orderFailed": "注文に失敗しました", "orderFailed": "注文に失敗しました",
"marketClosedTitle": "マーケットは閉鎖されています",
"marketClosedDesc": "このマーケットは決済済みです。取引や新規注文はできません。",
"marketClosedOutcome": "結果:{outcome}",
"marketClosedBanner": "このマーケットは閉鎖され決済されています。",
"expiration": { "expiration": {
"5m": "5分", "5m": "5分",
"15m": "15分", "15m": "15分",
@ -110,8 +106,7 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"priceZero": "0¢", "priceZero": "0¢",
"moreActions": "その他の操作", "moreActions": "その他の操作"
"settledSubMarketsTitle": "サブマーケット(決済済み)"
}, },
"searchPage": { "searchPage": {
"title": "検索", "title": "検索",

View File

@ -77,10 +77,6 @@
"pleaseSelectMarket": "시장을 선택하세요 (clobTokenIds 필요)", "pleaseSelectMarket": "시장을 선택하세요 (clobTokenIds 필요)",
"userError": "사용자 정보 오류", "userError": "사용자 정보 오류",
"orderFailed": "주문 실패", "orderFailed": "주문 실패",
"marketClosedTitle": "마켓이 종료되었습니다",
"marketClosedDesc": "이 마켓은 정산되었으며 거래와 신규 주문을 받지 않습니다.",
"marketClosedOutcome": "결과: {outcome}",
"marketClosedBanner": "이 마켓은 종료되어 정산되었습니다.",
"expiration": { "expiration": {
"5m": "5분", "5m": "5분",
"15m": "15분", "15m": "15분",
@ -110,8 +106,7 @@
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",
"priceZero": "0¢", "priceZero": "0¢",
"moreActions": "더보기", "moreActions": "더보기"
"settledSubMarketsTitle": "하위 마켓(정산 완료)"
}, },
"searchPage": { "searchPage": {
"title": "검색", "title": "검색",

View File

@ -77,10 +77,6 @@
"pleaseSelectMarket": "请先选择市场(需包含 clobTokenIds", "pleaseSelectMarket": "请先选择市场(需包含 clobTokenIds",
"userError": "用户信息异常", "userError": "用户信息异常",
"orderFailed": "下单失败", "orderFailed": "下单失败",
"marketClosedTitle": "市场已关闭",
"marketClosedDesc": "本市场已结算,不再接受交易与下单。",
"marketClosedOutcome": "结果:{outcome}",
"marketClosedBanner": "该市场已关闭并完成结算。",
"expiration": { "expiration": {
"5m": "5分钟", "5m": "5分钟",
"15m": "15分钟", "15m": "15分钟",
@ -110,8 +106,7 @@
"yes": "是", "yes": "是",
"no": "否", "no": "否",
"priceZero": "0¢", "priceZero": "0¢",
"moreActions": "更多操作", "moreActions": "更多操作"
"settledSubMarketsTitle": "子市场(已结算)"
}, },
"searchPage": { "searchPage": {
"title": "搜索", "title": "搜索",

View File

@ -77,10 +77,6 @@
"pleaseSelectMarket": "請先選擇市場(需包含 clobTokenIds", "pleaseSelectMarket": "請先選擇市場(需包含 clobTokenIds",
"userError": "用戶資訊異常", "userError": "用戶資訊異常",
"orderFailed": "下單失敗", "orderFailed": "下單失敗",
"marketClosedTitle": "市場已關閉",
"marketClosedDesc": "本市場已結算,不再接受交易與下單。",
"marketClosedOutcome": "結果:{outcome}",
"marketClosedBanner": "該市場已關閉並完成結算。",
"expiration": { "expiration": {
"5m": "5分鐘", "5m": "5分鐘",
"15m": "15分鐘", "15m": "15分鐘",
@ -110,8 +106,7 @@
"yes": "是", "yes": "是",
"no": "否", "no": "否",
"priceZero": "0¢", "priceZero": "0¢",
"moreActions": "更多操作", "moreActions": "更多操作"
"settledSubMarketsTitle": "子市場(已結算)"
}, },
"searchPage": { "searchPage": {
"title": "搜尋", "title": "搜尋",

View File

@ -1,5 +1,4 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { HOME_MAIN_SCROLL_STORAGE_KEY } from './scrollKeys'
import Home from '../views/Home.vue' import Home from '../views/Home.vue'
import Trade from '../views/Trade.vue' import Trade from '../views/Trade.vue'
import Login from '../views/Login.vue' import Login from '../views/Login.vue'
@ -68,24 +67,6 @@ const router = createRouter({
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
const el = document.querySelector('[data-main-scroll]') const el = document.querySelector('[data-main-scroll]')
if (el) { if (el) {
const shouldRestoreHomeScroll =
to.name === 'home' &&
from?.name != null &&
(from.name === 'trade-detail' || from.name === 'event-markets')
if (shouldRestoreHomeScroll) {
const raw = sessionStorage.getItem(HOME_MAIN_SCROLL_STORAGE_KEY)
const top = raw != null ? parseInt(raw, 10) : NaN
if (Number.isFinite(top) && top >= 0) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.scrollTo({ top, left: 0 })
})
})
return
}
}
if (savedPosition && from?.name) { if (savedPosition && from?.name) {
el.scrollTo({ top: savedPosition.top, left: savedPosition.left ?? 0 }) el.scrollTo({ top: savedPosition.top, left: savedPosition.left ?? 0 })
return return

View File

@ -1,2 +0,0 @@
/** 离开首页时写入 [data-main-scroll].scrollTop从详情/多市场返回首页时恢复 */
export const HOME_MAIN_SCROLL_STORAGE_KEY = 'pcv-home-main-scroll-top'

View File

@ -94,17 +94,12 @@
</div> </div>
</v-card> </v-card>
<!-- 可交易市场列表 --> <!-- 市场列表 -->
<v-card <v-card class="markets-list-card" elevation="0" rounded="lg">
v-if="activeMarkets.length > 0"
class="markets-list-card"
elevation="0"
rounded="lg"
>
<div class="markets-list"> <div class="markets-list">
<div <div
v-for="(market, index) in activeMarkets" v-for="(market, index) in markets"
:key="market.ID ?? market.id ?? index" :key="market.ID ?? index"
class="market-row" class="market-row"
:class="{ selected: selectedMarketIndex === index }" :class="{ selected: selectedMarketIndex === index }"
@click="goToTradeDetail(market)" @click="goToTradeDetail(market)"
@ -139,46 +134,11 @@
</div> </div>
</div> </div>
</v-card> </v-card>
<!-- 已结算子市场 design vb1xr 一致标题 + 行内结果角标 -->
<v-card
v-if="closedMarkets.length > 0"
class="markets-settled-card"
elevation="0"
rounded="lg"
>
<div class="markets-settled-head">
{{ t('eventMarkets.settledSubMarketsTitle') }}
</div>
<div class="markets-settled-list">
<div
v-for="(market, index) in closedMarkets"
:key="market.ID ?? market.id ?? `c-${index}`"
class="markets-settled-row"
@click="goToTradeDetail(market)"
>
<div class="markets-settled-row-text">
<span class="markets-settled-question">{{
market.question || t('eventMarkets.marketPlaceholder')
}}</span>
<span class="markets-settled-vol">{{ formatVolumeLine(market.volume) }}</span>
</div>
<span
v-if="settledWinnerPill(market)"
class="markets-settled-pill"
:class="settledPillClass(market)"
>
{{ settledWinnerPill(market)!.label }}
</span>
<v-icon class="markets-settled-chevron" size="18">mdi-chevron-right</v-icon>
</div>
</div>
</v-card>
</v-col> </v-col>
<!-- 右侧购买组件桌面端显示移动端用底部栏+弹窗 --> <!-- 右侧购买组件桌面端显示移动端用底部栏+弹窗 -->
<v-col v-if="!isMobile" cols="12" class="trade-col"> <v-col v-if="!isMobile" cols="12" class="trade-col">
<div v-if="activeMarkets.length > 0" class="trade-sidebar"> <div v-if="markets.length > 0" class="trade-sidebar">
<TradeComponent <TradeComponent
:market="tradeMarketPayload" :market="tradeMarketPayload"
:initial-option="tradeInitialOption" :initial-option="tradeInitialOption"
@ -189,7 +149,7 @@
</v-row> </v-row>
<!-- 移动端 market 时显示固定底部 Yes/No + 三点菜单Merge/Split --> <!-- 移动端 market 时显示固定底部 Yes/No + 三点菜单Merge/Split -->
<template v-if="isMobile && activeMarkets.length === 1"> <template v-if="isMobile && markets.length === 1">
<div class="mobile-trade-bar-spacer" aria-hidden="true"></div> <div class="mobile-trade-bar-spacer" aria-hidden="true"></div>
<div class="mobile-trade-bar"> <div class="mobile-trade-bar">
<v-btn <v-btn
@ -240,7 +200,7 @@
</div> </div>
</template> </template>
<!-- 移动端交易弹窗 market 与多 market 均需 market 时通过列表 Buy Yes/No 打开 --> <!-- 移动端交易弹窗 market 与多 market 均需 market 时通过列表 Buy Yes/No 打开 -->
<template v-if="isMobile && activeMarkets.length > 0"> <template v-if="isMobile && markets.length > 0">
<v-bottom-sheet v-model="tradeSheetOpen" content-class="event-markets-trade-sheet"> <v-bottom-sheet v-model="tradeSheetOpen" content-class="event-markets-trade-sheet">
<TradeComponent <TradeComponent
v-if="tradeSheetRenderContent" v-if="tradeSheetRenderContent"
@ -277,8 +237,6 @@ import {
findPmEvent, findPmEvent,
getMarketId, getMarketId,
getClobTokenId, getClobTokenId,
getPmMarketSettledWinner,
isPmMarketClosed,
type FindPmEventParams, type FindPmEventParams,
type PmEventListItem, type PmEventListItem,
type PmEventMarketItem, type PmEventMarketItem,
@ -324,13 +282,9 @@ const markets = computed(() => {
const list = eventDetail.value?.markets ?? [] const list = eventDetail.value?.markets ?? []
return list.length > 0 ? list : [] return list.length > 0 ? list : []
}) })
/** 仍可交易的市场closed !== true */ const selectedMarket = computed(() => markets.value[selectedMarketIndex.value] ?? null)
const activeMarkets = computed(() => markets.value.filter((m) => !isPmMarketClosed(m))) /** 移动端底部栏显示的市场(选中项或首个),仅在 markets.length > 0 时使用 */
/** 已结算/已关闭的子市场 */ const barMarket = computed(() => selectedMarket.value ?? markets.value[0])
const closedMarkets = computed(() => markets.value.filter((m) => isPmMarketClosed(m)))
const selectedMarket = computed(() => activeMarkets.value[selectedMarketIndex.value] ?? null)
/** 移动端底部栏显示的市场(选中项或首个),仅在有可交易市场时使用 */
const barMarket = computed(() => selectedMarket.value ?? activeMarkets.value[0])
/** 传给购买组件的市场数据(当前选中的市场) */ /** 传给购买组件的市场数据(当前选中的市场) */
const tradeMarketPayload = computed(() => { const tradeMarketPayload = computed(() => {
const m = selectedMarket.value const m = selectedMarket.value
@ -362,26 +316,6 @@ function formatVolume(volume: number | undefined): string {
return `$${Math.round(volume)} Vol.` return `$${Math.round(volume)} Vol.`
} }
/** 已结算列表 secondary 行Vol. $12.4M / k与设计稿一致 */
function formatVolumeLine(volume: number | undefined): string {
if (volume == null || !Number.isFinite(volume)) return 'Vol. —'
if (volume >= 1_000_000) return `Vol. $${(volume / 1_000_000).toFixed(1)}M`
if (volume >= 1000) return `Vol. $${(volume / 1000).toFixed(1)}k`
return `Vol. $${Math.round(volume)}`
}
function settledWinnerPill(market: PmEventMarketItem) {
return getPmMarketSettledWinner(market)
}
function settledPillClass(market: PmEventMarketItem): string {
const w = getPmMarketSettledWinner(market)
if (!w) return 'markets-settled-pill--neutral'
if (w.winnerIndex === 0) return 'markets-settled-pill--yes'
if (w.winnerIndex === 1) return 'markets-settled-pill--no'
return 'markets-settled-pill--neutral'
}
function formatExpiresAt(endDate: string | undefined): string { function formatExpiresAt(endDate: string | undefined): string {
if (!endDate) return '' if (!endDate) return ''
try { try {
@ -647,13 +581,20 @@ function noPrice(market: PmEventMarketItem): string {
return `${Math.round(p * 100)}¢` return `${Math.round(p * 100)}¢`
} }
function goToTradeDetail(_market: PmEventMarketItem, _side?: 'yes' | 'no') { function goToTradeDetail(market: PmEventMarketItem, side?: 'yes' | 'no') {
void _market const eventId = route.params.id
void _side const marketId = market.ID != null ? String(market.ID) : undefined
const slug = eventDetail.value?.slug?.trim() router.push({
const eventKey = slug || String(route.params.id ?? '').trim() path: `/trade-detail/${eventId}`,
if (!eventKey) return query: {
router.push({ name: 'trade-detail', params: { id: eventKey } }) title: market.question ?? eventDetail.value?.title,
marketId,
marketInfo: formatVolume(market.volume),
chance: String(marketChance(market)),
...(side && { side }),
...(eventDetail.value?.slug && { slug: eventDetail.value.slug }),
},
})
} }
async function loadEventDetail() { async function loadEventDetail() {
@ -666,9 +607,10 @@ async function loadEventDetail() {
} }
const numId = parseInt(idStr, 10) const numId = parseInt(idStr, 10)
const isNumericId = Number.isFinite(numId) && String(numId) === idStr && numId >= 1 const isNumericId = Number.isFinite(numId) && String(numId) === idStr && numId >= 1
const slugFromQuery = (route.query.slug as string)?.trim()
const params: FindPmEventParams = { const params: FindPmEventParams = {
id: isNumericId ? numId : undefined, id: isNumericId ? numId : undefined,
slug: isNumericId ? undefined : idStr, slug: isNumericId ? slugFromQuery || undefined : idStr,
} }
detailError.value = null detailError.value = null
@ -704,16 +646,6 @@ async function loadEventDetail() {
} }
} }
watch(
activeMarkets,
(list) => {
if (selectedMarketIndex.value >= list.length) {
selectedMarketIndex.value = Math.max(0, list.length - 1)
}
},
{ immediate: true },
)
onMounted(() => { onMounted(() => {
loadEventDetail() loadEventDetail()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
@ -1150,96 +1082,6 @@ watch(
color: #cc0000 !important; color: #cc0000 !important;
} }
/* 已结算子市场列表Pencil vb1xr */
.markets-settled-card {
margin-top: 16px;
border: 1px solid #e7e7e7;
border-radius: 12px;
overflow: hidden;
box-shadow: none;
background: #fff;
}
.markets-settled-head {
padding: 14px 16px 10px;
font-size: 14px;
font-weight: 600;
color: #111827;
border-bottom: 1px solid #e5e7eb;
}
.markets-settled-list {
display: flex;
flex-direction: column;
}
.markets-settled-row {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 16px;
border-bottom: 1px solid #f3f4f6;
cursor: pointer;
transition: background-color 0.15s ease;
}
.markets-settled-row:last-child {
border-bottom: none;
}
.markets-settled-row:hover {
background-color: #f9fafb;
}
.markets-settled-row-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.markets-settled-question {
font-size: 15px;
font-weight: 500;
color: #111827;
line-height: 1.35;
word-break: break-word;
}
.markets-settled-vol {
font-size: 13px;
color: #6b7280;
}
.markets-settled-pill {
flex-shrink: 0;
padding: 4px 12px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
}
.markets-settled-pill--yes {
background: #d1fae5;
color: #065f46;
}
.markets-settled-pill--no {
background: #fee2e2;
color: #991b1b;
}
.markets-settled-pill--neutral {
background: #f3f4f6;
color: #374151;
}
.markets-settled-chevron {
flex-shrink: 0;
color: #d1d5db;
}
/* 移动端底部交易栏 */ /* 移动端底部交易栏 */
.mobile-trade-bar-spacer { .mobile-trade-bar-spacer {
height: 72px; height: 72px;

View File

@ -227,8 +227,6 @@ import {
computed, computed,
watch, watch,
} from 'vue' } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { HOME_MAIN_SCROLL_STORAGE_KEY } from '../router/scrollKeys'
import { useDisplay } from 'vuetify' import { useDisplay } from 'vuetify'
import MarketCard from '../components/MarketCard.vue' import MarketCard from '../components/MarketCard.vue'
import TradeComponent from '../components/TradeComponent.vue' import TradeComponent from '../components/TradeComponent.vue'
@ -646,13 +644,6 @@ function checkScrollLoad() {
if (scrollHeight - scrollTop - clientHeight < SCROLL_LOAD_THRESHOLD) loadMore() if (scrollHeight - scrollTop - clientHeight < SCROLL_LOAD_THRESHOLD) loadMore()
} }
onBeforeRouteLeave(() => {
const el = getMainScrollEl()
if (el) {
sessionStorage.setItem(HOME_MAIN_SCROLL_STORAGE_KEY, String((el as HTMLElement).scrollTop))
}
})
onMounted(() => { onMounted(() => {
if (props.initialSearchExpanded) expandSearch() if (props.initialSearchExpanded) expandSearch()
loadCategory() loadCategory()

View File

@ -111,8 +111,9 @@ function formatTradingFee(rate: number): string {
/** 与接口 needDeposit / accumulated 同一套口径:大额按 USDC 6 位小数,否则按美元数值 */ /** 与接口 needDeposit / accumulated 同一套口径:大额按 USDC 6 位小数,否则按美元数值 */
function depositRawToUsd(raw: number): number { function depositRawToUsd(raw: number): number {
if (!Number.isFinite(raw) || raw <= 0) return 0 if (!Number.isFinite(raw) || raw <= 0) return 0
return raw / 1_000_000 return raw >= 1_000_000_000 ? raw / 1_000_000 : raw
} }
function parseUserAccumulatedUsd(user: Record<string, unknown>): number { function parseUserAccumulatedUsd(user: Record<string, unknown>): number {
const v = user.accumulated ?? user.Accumulated const v = user.accumulated ?? user.Accumulated
if (typeof v === 'number' && Number.isFinite(v)) return depositRawToUsd(v) if (typeof v === 'number' && Number.isFinite(v)) return depositRawToUsd(v)

View File

@ -302,15 +302,27 @@ function openResult(item: SearchResultItem) {
const card = mapEventItemToCard(item.raw) const card = mapEventItemToCard(item.raw)
if (!card.id) return if (!card.id) return
const segment = (card.slug?.trim() || card.id || '').trim()
if (!segment) return
if (card.displayType === 'multi' && (card.outcomes?.length ?? 0) > 1) { if (card.displayType === 'multi' && (card.outcomes?.length ?? 0) > 1) {
router.push({ name: 'event-markets', params: { id: segment } }) router.push({
path: `/event/${card.id}/markets`,
query: { ...(card.slug && { slug: card.slug }) },
})
return return
} }
router.push({ name: 'trade-detail', params: { id: segment } }) router.push({
path: `/trade-detail/${card.id}`,
query: {
title: card.marketTitle,
imageUrl: card.imageUrl || undefined,
category: card.category || undefined,
marketInfo: card.marketInfo || undefined,
expiresAt: card.expiresAt || undefined,
chance: String(card.chanceValue),
...(card.marketId && { marketId: card.marketId }),
...(card.slug && { slug: card.slug }),
},
})
} }
</script> </script>

View File

@ -61,9 +61,6 @@
</template> </template>
<template v-else> {{ currentChance }}% {{ t('common.chance') }} </template> <template v-else> {{ currentChance }}% {{ t('common.chance') }} </template>
</div> </div>
<p v-if="currentMarketClosed" class="chart-closed-banner">
{{ t('trade.marketClosedBanner') }}
</p>
</div> </div>
<!-- 图表区域 --> <!-- 图表区域 -->
@ -95,23 +92,6 @@
</div> </div>
</v-card> </v-card>
<!-- 移动端无右侧交易栏时展示已关闭说明 -->
<v-card
v-if="isMobile && currentMarketClosed"
class="market-closed-mobile-card"
elevation="0"
rounded="lg"
>
<div class="market-closed-pane market-closed-pane--mobile">
<v-icon size="40" color="grey-darken-1">mdi-lock-outline</v-icon>
<h2 class="market-closed-pane-title">{{ t('trade.marketClosedTitle') }}</h2>
<p class="market-closed-pane-desc">{{ t('trade.marketClosedDesc') }}</p>
<p v-if="settledOutcomeLabel" class="market-closed-pane-outcome">
{{ t('trade.marketClosedOutcome', { outcome: settledOutcomeLabel }) }}
</p>
</div>
</v-card>
<!-- 持仓 / 限价订单簿上方 --> <!-- 持仓 / 限价订单簿上方 -->
<v-card class="positions-orders-card" elevation="0" rounded="lg"> <v-card class="positions-orders-card" elevation="0" rounded="lg">
<v-tabs <v-tabs
@ -170,7 +150,6 @@
color="primary" color="primary"
class="position-sell-btn" class="position-sell-btn"
:disabled=" :disabled="
currentMarketClosed ||
!(pos.availableSharesNum != null && pos.availableSharesNum > 0) !(pos.availableSharesNum != null && pos.availableSharesNum > 0)
" "
@click="openSellFromPosition(pos)" @click="openSellFromPosition(pos)"
@ -220,7 +199,7 @@
</v-card> </v-card>
<!-- Order Book Section --> <!-- Order Book Section -->
<v-card v-if="!currentMarketClosed" class="order-book-card" elevation="0" rounded="lg"> <v-card class="order-book-card" elevation="0" rounded="lg">
<OrderBook <OrderBook
:asks-yes="orderBookAsksYes" :asks-yes="orderBookAsksYes"
:bids-yes="orderBookBidsYes" :bids-yes="orderBookBidsYes"
@ -278,18 +257,11 @@
<!-- 右侧交易组件固定宽度传入当前市场以便 Split 调用拆单接口移动端隐藏改用底部栏+弹窗 --> <!-- 右侧交易组件固定宽度传入当前市场以便 Split 调用拆单接口移动端隐藏改用底部栏+弹窗 -->
<v-col v-if="!isMobile" cols="12" class="trade-col"> <v-col v-if="!isMobile" cols="12" class="trade-col">
<div v-if="currentMarketClosed" class="trade-sidebar market-closed-pane"> <div class="trade-sidebar">
<v-icon size="44" color="grey-darken-1">mdi-lock-outline</v-icon>
<h2 class="market-closed-pane-title">{{ t('trade.marketClosedTitle') }}</h2>
<p class="market-closed-pane-desc">{{ t('trade.marketClosedDesc') }}</p>
<p v-if="settledOutcomeLabel" class="market-closed-pane-outcome">
{{ t('trade.marketClosedOutcome', { outcome: settledOutcomeLabel }) }}
</p>
</div>
<div v-else-if="tradeMarketPayload" class="trade-sidebar">
<TradeComponent <TradeComponent
ref="tradeComponentRef" ref="tradeComponentRef"
:market="tradeMarketPayload" :market="tradeMarketPayload"
:initial-option="tradeInitialOption"
:positions="tradePositionsForComponent" :positions="tradePositionsForComponent"
@merge-success="onMergeSuccess" @merge-success="onMergeSuccess"
@split-success="onSplitSuccess" @split-success="onSplitSuccess"
@ -298,7 +270,7 @@
</v-col> </v-col>
<!-- 移动端固定底部 Yes/No + 三点菜单Merge/Split --> <!-- 移动端固定底部 Yes/No + 三点菜单Merge/Split -->
<template v-if="isMobile && tradeMarketPayload && !currentMarketClosed"> <template v-if="isMobile && tradeMarketPayload">
<div class="mobile-trade-bar-spacer" aria-hidden="true"></div> <div class="mobile-trade-bar-spacer" aria-hidden="true"></div>
<div class="mobile-trade-bar"> <div class="mobile-trade-bar">
<v-btn <v-btn
@ -409,8 +381,6 @@ import {
findPmEvent, findPmEvent,
getMarketId, getMarketId,
getClobTokenId, getClobTokenId,
getPmMarketSettledWinner,
isPmMarketClosed,
type FindPmEventParams, type FindPmEventParams,
type PmEventListItem, type PmEventListItem,
} from '../api/event' } from '../api/event'
@ -516,9 +486,10 @@ async function loadEventDetail() {
} }
const numId = parseInt(idStr, 10) const numId = parseInt(idStr, 10)
const isNumericId = Number.isFinite(numId) && String(numId) === idStr && numId >= 1 const isNumericId = Number.isFinite(numId) && String(numId) === idStr && numId >= 1
const slugFromQuery = (route.query.slug as string)?.trim()
const params: FindPmEventParams = { const params: FindPmEventParams = {
id: isNumericId ? numId : undefined, id: isNumericId ? numId : undefined,
slug: isNumericId ? undefined : idStr, slug: isNumericId ? slugFromQuery || undefined : idStr,
} }
detailError.value = null detailError.value = null
@ -564,15 +535,18 @@ function onRefresh({ done }: { done: () => void }) {
) )
} }
// 使 slug/id query // query
const marketTitle = computed(() => eventDetail.value?.title ?? '') const marketTitle = computed(() => {
if (eventDetail.value?.title) return eventDetail.value.title
return (route.query.title as string) || 'U.S. anti-cartel ground operation in Mexico by March 31?'
})
const marketVolume = computed(() => { const marketVolume = computed(() => {
if (eventDetail.value?.volume != null) return formatVolume(eventDetail.value.volume) if (eventDetail.value?.volume != null) return formatVolume(eventDetail.value.volume)
return formatVolume(undefined) return (route.query.marketInfo as string) || '$398,719'
}) })
const marketExpiresAt = computed(() => { const marketExpiresAt = computed(() => {
if (eventDetail.value?.endDate) return formatExpiresAt(eventDetail.value.endDate) if (eventDetail.value?.endDate) return formatExpiresAt(eventDetail.value.endDate)
return '' return (route.query.expiresAt as string) || 'Mar 31, 2026'
}) })
const resolutionDate = computed(() => { const resolutionDate = computed(() => {
const s = marketExpiresAt.value const s = marketExpiresAt.value
@ -619,19 +593,17 @@ function setChartMode(mode: 'yesno' | 'crypto') {
updateChartData() updateChartData()
} }
/** 当前市场(用于交易组件与 Split 拆单):接口返回的第一个市场 */ /** 当前市场(用于交易组件与 Split 拆单):query.marketId 匹配或取第一个 */
const currentMarket = computed(() => { const currentMarket = computed(() => {
const list = eventDetail.value?.markets ?? [] const list = eventDetail.value?.markets ?? []
return list[0] ?? null if (list.length === 0) return null
}) const qId = route.query.marketId
if (qId != null && String(qId).trim() !== '') {
const currentMarketClosed = computed(() => isPmMarketClosed(currentMarket.value)) const qStr = String(qId).trim()
const found = list.find((m) => getMarketId(m) === qStr)
const settledOutcomeLabel = computed(() => { if (found) return found
const m = currentMarket.value }
if (!m) return '' return list[0]
const w = getPmMarketSettledWinner(m)
return w?.label ?? ''
}) })
// --- CLOB WebSocket 簿 --- // --- CLOB WebSocket 簿 ---
@ -889,6 +861,23 @@ const tradeMarketPayload = computed(() => {
bestBidNoCents, bestBidNoCents,
} }
} }
const qId = route.query.marketId
if (qId != null && String(qId).trim() !== '') {
return {
marketId: String(qId).trim(),
yesPrice,
noPrice,
title: (route.query.title as string) || undefined,
bestBidYesCents,
bestBidNoCents,
}
}
return undefined
})
const tradeInitialOption = computed(() => {
const side = route.query.side
if (side === 'yes' || side === 'no') return side
return undefined return undefined
}) })
@ -1227,7 +1216,12 @@ const currentChance = computed(() => {
const d = data.value const d = data.value
const last = d.length > 0 ? d[d.length - 1] : undefined const last = d.length > 0 ? d[d.length - 1] : undefined
if (last != null) return last[1] if (last != null) return last[1]
return 0 const q = route.query.chance
if (q != null) {
const n = Number(q)
if (Number.isFinite(n)) return Math.min(100, Math.max(0, Math.round(n)))
}
return 20
}) })
const lineColor = '#2563eb' const lineColor = '#2563eb'
@ -1972,59 +1966,6 @@ onUnmounted(() => {
flex-shrink: 0; flex-shrink: 0;
} }
.chart-closed-banner {
margin: 8px 0 0;
font-size: 13px;
color: #92400e;
background: #fffbeb;
border-radius: 8px;
padding: 8px 12px;
line-height: 1.4;
}
.market-closed-pane {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 28px 20px;
border: 1px solid #e7e7e7;
border-radius: 12px;
background: #fafafa;
}
.market-closed-pane--mobile {
padding: 20px 16px;
}
.market-closed-pane-title {
margin: 12px 0 8px;
font-size: 18px;
font-weight: 600;
color: #111827;
}
.market-closed-pane-desc {
margin: 0;
font-size: 14px;
color: #6b7280;
line-height: 1.5;
}
.market-closed-pane-outcome {
margin: 14px 0 0;
font-size: 15px;
font-weight: 600;
color: #111827;
}
.market-closed-mobile-card {
margin-bottom: 16px;
border: 1px solid #e7e7e7;
box-shadow: none !important;
overflow: hidden;
}
@media (min-width: 960px) { @media (min-width: 960px) {
.trade-detail-row { .trade-detail-row {
flex-wrap: nowrap; flex-wrap: nowrap;

View File

@ -767,10 +767,17 @@ function canOpenTradeDetail(opts: { tradeEventId?: string; tradeEventSlug?: stri
return !!(opts.tradeEventId?.trim() || opts.tradeEventSlug?.trim()) return !!(opts.tradeEventId?.trim() || opts.tradeEventSlug?.trim())
} }
function openTradeDetailFromWallet(input: { tradeEventId?: string; tradeEventSlug?: string }) { function openTradeDetailFromWallet(input: {
tradeEventId?: string
tradeEventSlug?: string
marketId?: string
title?: string
}) {
const loc = buildTradeDetailPushOptions({ const loc = buildTradeDetailPushOptions({
eventId: input.tradeEventId, eventId: input.tradeEventId,
eventSlug: input.tradeEventSlug, eventSlug: input.tradeEventSlug,
marketId: input.marketId,
title: input.title,
}) })
if (loc) router.push(loc) if (loc) router.push(loc)
} }
@ -780,6 +787,8 @@ function onPositionRowClick(pos: Position) {
openTradeDetailFromWallet({ openTradeDetailFromWallet({
tradeEventId: pos.tradeEventId, tradeEventId: pos.tradeEventId,
tradeEventSlug: pos.tradeEventSlug, tradeEventSlug: pos.tradeEventSlug,
marketId: pos.marketID,
title: pos.market,
}) })
} }
@ -788,6 +797,8 @@ function onOpenOrderRowClick(ord: OpenOrder) {
openTradeDetailFromWallet({ openTradeDetailFromWallet({
tradeEventId: ord.tradeEventId, tradeEventId: ord.tradeEventId,
tradeEventSlug: ord.tradeEventSlug, tradeEventSlug: ord.tradeEventSlug,
marketId: ord.detailMarketId,
title: ord.market,
}) })
} }
@ -802,6 +813,8 @@ function onHistoryRowClick(h: HistoryItem) {
openTradeDetailFromWallet({ openTradeDetailFromWallet({
tradeEventId: h.tradeEventId, tradeEventId: h.tradeEventId,
tradeEventSlug: h.tradeEventSlug, tradeEventSlug: h.tradeEventSlug,
marketId: h.detailMarketId,
title: h.market,
}) })
} }
@ -1161,12 +1174,7 @@ function getWithdrawStatusLabel(status: string | undefined): string {
const s = (status ?? '').toLowerCase() const s = (status ?? '').toLowerCase()
if (s === WITHDRAW_STATUS.PENDING || s === '0' || s === 'pending') if (s === WITHDRAW_STATUS.PENDING || s === '0' || s === 'pending')
return t('wallet.withdrawStatusPending') return t('wallet.withdrawStatusPending')
if ( if (s === WITHDRAW_STATUS.SUCCESS || s === '1' || s === 'success')
s === WITHDRAW_STATUS.SUCCESS ||
s === WITHDRAW_STATUS.PROCESSED ||
s === '1' ||
s === 'success'
)
return t('wallet.withdrawStatusSuccess') return t('wallet.withdrawStatusSuccess')
if (s === WITHDRAW_STATUS.REJECTED || s === '2' || s === 'rejected') if (s === WITHDRAW_STATUS.REJECTED || s === '2' || s === 'rejected')
return t('wallet.withdrawStatusRejected') return t('wallet.withdrawStatusRejected')
@ -1178,13 +1186,7 @@ function getWithdrawStatusLabel(status: string | undefined): string {
function getWithdrawStatusClass(status: string | undefined): string { function getWithdrawStatusClass(status: string | undefined): string {
const s = (status ?? '').toLowerCase() const s = (status ?? '').toLowerCase()
if (s === WITHDRAW_STATUS.PENDING || s === '0' || s === 'pending') return 'status-pending' if (s === WITHDRAW_STATUS.PENDING || s === '0' || s === 'pending') return 'status-pending'
if ( if (s === WITHDRAW_STATUS.SUCCESS || s === '1' || s === 'success') return 'status-success'
s === WITHDRAW_STATUS.SUCCESS ||
s === WITHDRAW_STATUS.PROCESSED ||
s === '1' ||
s === 'success'
)
return 'status-success'
if (s === WITHDRAW_STATUS.REJECTED || s === '2' || s === 'rejected') return 'status-rejected' if (s === WITHDRAW_STATUS.REJECTED || s === '2' || s === 'rejected') return 'status-rejected'
if (s === WITHDRAW_STATUS.FAILED || s === '3' || s === 'failed') return 'status-failed' if (s === WITHDRAW_STATUS.FAILED || s === '3' || s === 'failed') return 'status-failed'
return '' return ''
@ -2729,17 +2731,12 @@ async function submitAuthorize() {
flex-shrink: 0; flex-shrink: 0;
} }
.withdrawal-status-pill.status-pending, .withdrawal-status-pill.status-pending,
.withdrawal-status-pill.status-success,
.withdrawal-status-pill.status-failed { .withdrawal-status-pill.status-failed {
background: #f3f4f6; background: #f3f4f6;
color: #374151; color: #374151;
} }
.withdrawal-status-pill.status-success {
background: #dcfce7;
color: #166534;
border: 1px solid #bbf7d0;
}
.withdrawal-status-pill.status-rejected { .withdrawal-status-pill.status-rejected {
background: #fef2f2; background: #fef2f2;
color: #b91c1c; color: #b91c1c;