新增:market关闭时候的页面样式,优化传参
This commit is contained in:
parent
fc480dc4a8
commit
c976333b72
File diff suppressed because it is too large
Load Diff
@ -64,9 +64,49 @@ export interface PmEventMarketItem {
|
||||
clobTokenIds?: string[]
|
||||
endDate?: string
|
||||
volume?: number
|
||||
/** 为 true 时表示市场已关闭/已结算,不再交易 */
|
||||
closed?: boolean
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** 市场是否已关闭(不再接受下单) */
|
||||
export function isPmMarketClosed(m: PmEventMarketItem | null | undefined): boolean {
|
||||
return m?.closed === true
|
||||
}
|
||||
|
||||
/**
|
||||
* 已结算市场的结果角标:取 outcomePrices 中概率最高项对应 outcomes 文案(兼容 0–1 或 1–100)。
|
||||
*/
|
||||
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 */
|
||||
export function getMarketId(m: PmEventMarketItem | null | undefined): string | undefined {
|
||||
if (!m) return undefined
|
||||
@ -237,36 +277,24 @@ export function readTradeRouteEventSlug(obj: unknown): string | undefined {
|
||||
}
|
||||
|
||||
export interface TradeDetailRouteInput {
|
||||
/** 事件数字 ID(与首页 MarketCard :id 一致),优先用于路径参数 */
|
||||
/** 事件数字 ID;无 slug 时用作路径段 */
|
||||
eventId?: string | number | null
|
||||
/** 事件 slug;无 eventId 时用作路径;路径为数字 ID 时可作为 query.slug 传给 findPmEvent */
|
||||
/** 事件 slug,优先作为路径参数(详情页仅用其请求 findPmEvent) */
|
||||
eventSlug?: string | null
|
||||
/** 市场行 ID,对应详情页 query.marketId */
|
||||
marketId?: string | null
|
||||
/** 详情页标题回显 query.title */
|
||||
title?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造与 MarketCard、EventMarkets → trade-detail 一致的 router.push 参数。
|
||||
* 路径:`/trade-detail/:id`,id 为事件数字 ID 或事件 slug。
|
||||
* 跳转详情:`/trade-detail/:id` 仅传路径参数,无 query;优先 slug,否则数字 ID。
|
||||
*/
|
||||
export function buildTradeDetailPushOptions(
|
||||
input: TradeDetailRouteInput,
|
||||
): { name: 'trade-detail'; params: { id: string }; query: Record<string, string> } | null {
|
||||
): { name: 'trade-detail'; params: { id: string } } | null {
|
||||
const slug = input.eventSlug?.trim() || ''
|
||||
const eid =
|
||||
input.eventId != null && String(input.eventId).trim() !== '' ? String(input.eventId).trim() : ''
|
||||
const slugOnly = input.eventSlug?.trim() || ''
|
||||
if (!eid && !slugOnly) return null
|
||||
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 }
|
||||
const pathId = slug || eid
|
||||
if (!pathId) return null
|
||||
return { name: 'trade-detail', params: { id: pathId } }
|
||||
}
|
||||
|
||||
/** 多选项卡片中单个选项(用于左右滑动切换) */
|
||||
|
||||
@ -232,26 +232,13 @@ const semiProgressColor = computed(() => {
|
||||
})
|
||||
|
||||
const navigateToDetail = () => {
|
||||
const segment = (props.slug?.trim() || props.id || '').trim()
|
||||
if (!segment) return
|
||||
if (props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1) {
|
||||
router.push({
|
||||
path: `/event/${props.id}/markets`,
|
||||
query: { ...(props.slug && { slug: props.slug }) },
|
||||
})
|
||||
router.push({ name: 'event-markets', params: { id: segment } })
|
||||
return
|
||||
}
|
||||
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 }),
|
||||
},
|
||||
})
|
||||
router.push({ name: 'trade-detail', params: { id: segment } })
|
||||
}
|
||||
|
||||
function openTradeSingle(side: 'yes' | 'no') {
|
||||
|
||||
@ -77,6 +77,10 @@
|
||||
"pleaseSelectMarket": "Please select a market (with clobTokenIds)",
|
||||
"userError": "User info error",
|
||||
"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": {
|
||||
"5m": "5m",
|
||||
"15m": "15m",
|
||||
@ -106,7 +110,8 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"priceZero": "0¢",
|
||||
"moreActions": "More actions"
|
||||
"moreActions": "More actions",
|
||||
"settledSubMarketsTitle": "Sub-markets (settled)"
|
||||
},
|
||||
"searchPage": {
|
||||
"title": "Search",
|
||||
|
||||
@ -77,6 +77,10 @@
|
||||
"pleaseSelectMarket": "市場を選択してください(clobTokenIds が必要)",
|
||||
"userError": "ユーザー情報エラー",
|
||||
"orderFailed": "注文に失敗しました",
|
||||
"marketClosedTitle": "マーケットは閉鎖されています",
|
||||
"marketClosedDesc": "このマーケットは決済済みです。取引や新規注文はできません。",
|
||||
"marketClosedOutcome": "結果:{outcome}",
|
||||
"marketClosedBanner": "このマーケットは閉鎖され決済されています。",
|
||||
"expiration": {
|
||||
"5m": "5分",
|
||||
"15m": "15分",
|
||||
@ -106,7 +110,8 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"priceZero": "0¢",
|
||||
"moreActions": "その他の操作"
|
||||
"moreActions": "その他の操作",
|
||||
"settledSubMarketsTitle": "サブマーケット(決済済み)"
|
||||
},
|
||||
"searchPage": {
|
||||
"title": "検索",
|
||||
|
||||
@ -77,6 +77,10 @@
|
||||
"pleaseSelectMarket": "시장을 선택하세요 (clobTokenIds 필요)",
|
||||
"userError": "사용자 정보 오류",
|
||||
"orderFailed": "주문 실패",
|
||||
"marketClosedTitle": "마켓이 종료되었습니다",
|
||||
"marketClosedDesc": "이 마켓은 정산되었으며 거래와 신규 주문을 받지 않습니다.",
|
||||
"marketClosedOutcome": "결과: {outcome}",
|
||||
"marketClosedBanner": "이 마켓은 종료되어 정산되었습니다.",
|
||||
"expiration": {
|
||||
"5m": "5분",
|
||||
"15m": "15분",
|
||||
@ -106,7 +110,8 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"priceZero": "0¢",
|
||||
"moreActions": "더보기"
|
||||
"moreActions": "더보기",
|
||||
"settledSubMarketsTitle": "하위 마켓(정산 완료)"
|
||||
},
|
||||
"searchPage": {
|
||||
"title": "검색",
|
||||
|
||||
@ -77,6 +77,10 @@
|
||||
"pleaseSelectMarket": "请先选择市场(需包含 clobTokenIds)",
|
||||
"userError": "用户信息异常",
|
||||
"orderFailed": "下单失败",
|
||||
"marketClosedTitle": "市场已关闭",
|
||||
"marketClosedDesc": "本市场已结算,不再接受交易与下单。",
|
||||
"marketClosedOutcome": "结果:{outcome}",
|
||||
"marketClosedBanner": "该市场已关闭并完成结算。",
|
||||
"expiration": {
|
||||
"5m": "5分钟",
|
||||
"15m": "15分钟",
|
||||
@ -106,7 +110,8 @@
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"priceZero": "0¢",
|
||||
"moreActions": "更多操作"
|
||||
"moreActions": "更多操作",
|
||||
"settledSubMarketsTitle": "子市场(已结算)"
|
||||
},
|
||||
"searchPage": {
|
||||
"title": "搜索",
|
||||
|
||||
@ -77,6 +77,10 @@
|
||||
"pleaseSelectMarket": "請先選擇市場(需包含 clobTokenIds)",
|
||||
"userError": "用戶資訊異常",
|
||||
"orderFailed": "下單失敗",
|
||||
"marketClosedTitle": "市場已關閉",
|
||||
"marketClosedDesc": "本市場已結算,不再接受交易與下單。",
|
||||
"marketClosedOutcome": "結果:{outcome}",
|
||||
"marketClosedBanner": "該市場已關閉並完成結算。",
|
||||
"expiration": {
|
||||
"5m": "5分鐘",
|
||||
"15m": "15分鐘",
|
||||
@ -106,7 +110,8 @@
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"priceZero": "0¢",
|
||||
"moreActions": "更多操作"
|
||||
"moreActions": "更多操作",
|
||||
"settledSubMarketsTitle": "子市場(已結算)"
|
||||
},
|
||||
"searchPage": {
|
||||
"title": "搜尋",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { HOME_MAIN_SCROLL_STORAGE_KEY } from './scrollKeys'
|
||||
import Home from '../views/Home.vue'
|
||||
import Trade from '../views/Trade.vue'
|
||||
import Login from '../views/Login.vue'
|
||||
@ -67,6 +68,24 @@ const router = createRouter({
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
const el = document.querySelector('[data-main-scroll]')
|
||||
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) {
|
||||
el.scrollTo({ top: savedPosition.top, left: savedPosition.left ?? 0 })
|
||||
return
|
||||
|
||||
2
src/router/scrollKeys.ts
Normal file
2
src/router/scrollKeys.ts
Normal file
@ -0,0 +1,2 @@
|
||||
/** 离开首页时写入 [data-main-scroll].scrollTop,从详情/多市场返回首页时恢复 */
|
||||
export const HOME_MAIN_SCROLL_STORAGE_KEY = 'pcv-home-main-scroll-top'
|
||||
@ -94,12 +94,17 @@
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<!-- 市场列表 -->
|
||||
<v-card class="markets-list-card" elevation="0" rounded="lg">
|
||||
<!-- 可交易市场列表 -->
|
||||
<v-card
|
||||
v-if="activeMarkets.length > 0"
|
||||
class="markets-list-card"
|
||||
elevation="0"
|
||||
rounded="lg"
|
||||
>
|
||||
<div class="markets-list">
|
||||
<div
|
||||
v-for="(market, index) in markets"
|
||||
:key="market.ID ?? index"
|
||||
v-for="(market, index) in activeMarkets"
|
||||
:key="market.ID ?? market.id ?? index"
|
||||
class="market-row"
|
||||
:class="{ selected: selectedMarketIndex === index }"
|
||||
@click="goToTradeDetail(market)"
|
||||
@ -134,11 +139,46 @@
|
||||
</div>
|
||||
</div>
|
||||
</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-if="!isMobile" cols="12" class="trade-col">
|
||||
<div v-if="markets.length > 0" class="trade-sidebar">
|
||||
<div v-if="activeMarkets.length > 0" class="trade-sidebar">
|
||||
<TradeComponent
|
||||
:market="tradeMarketPayload"
|
||||
:initial-option="tradeInitialOption"
|
||||
@ -149,7 +189,7 @@
|
||||
</v-row>
|
||||
|
||||
<!-- 移动端:单 market 时显示固定底部 Yes/No 栏 + 三点菜单(Merge/Split) -->
|
||||
<template v-if="isMobile && markets.length === 1">
|
||||
<template v-if="isMobile && activeMarkets.length === 1">
|
||||
<div class="mobile-trade-bar-spacer" aria-hidden="true"></div>
|
||||
<div class="mobile-trade-bar">
|
||||
<v-btn
|
||||
@ -200,7 +240,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<!-- 移动端交易弹窗:单 market 与多 market 均需(多 market 时通过列表 Buy Yes/No 打开) -->
|
||||
<template v-if="isMobile && markets.length > 0">
|
||||
<template v-if="isMobile && activeMarkets.length > 0">
|
||||
<v-bottom-sheet v-model="tradeSheetOpen" content-class="event-markets-trade-sheet">
|
||||
<TradeComponent
|
||||
v-if="tradeSheetRenderContent"
|
||||
@ -237,6 +277,8 @@ import {
|
||||
findPmEvent,
|
||||
getMarketId,
|
||||
getClobTokenId,
|
||||
getPmMarketSettledWinner,
|
||||
isPmMarketClosed,
|
||||
type FindPmEventParams,
|
||||
type PmEventListItem,
|
||||
type PmEventMarketItem,
|
||||
@ -282,9 +324,13 @@ const markets = computed(() => {
|
||||
const list = eventDetail.value?.markets ?? []
|
||||
return list.length > 0 ? list : []
|
||||
})
|
||||
const selectedMarket = computed(() => markets.value[selectedMarketIndex.value] ?? null)
|
||||
/** 移动端底部栏显示的市场(选中项或首个),仅在 markets.length > 0 时使用 */
|
||||
const barMarket = computed(() => selectedMarket.value ?? markets.value[0])
|
||||
/** 仍可交易的市场(closed !== true) */
|
||||
const activeMarkets = computed(() => markets.value.filter((m) => !isPmMarketClosed(m)))
|
||||
/** 已结算/已关闭的子市场 */
|
||||
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 m = selectedMarket.value
|
||||
@ -316,6 +362,26 @@ function formatVolume(volume: number | undefined): string {
|
||||
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 {
|
||||
if (!endDate) return ''
|
||||
try {
|
||||
@ -581,20 +647,13 @@ function noPrice(market: PmEventMarketItem): string {
|
||||
return `${Math.round(p * 100)}¢`
|
||||
}
|
||||
|
||||
function goToTradeDetail(market: PmEventMarketItem, side?: 'yes' | 'no') {
|
||||
const eventId = route.params.id
|
||||
const marketId = market.ID != null ? String(market.ID) : undefined
|
||||
router.push({
|
||||
path: `/trade-detail/${eventId}`,
|
||||
query: {
|
||||
title: market.question ?? eventDetail.value?.title,
|
||||
marketId,
|
||||
marketInfo: formatVolume(market.volume),
|
||||
chance: String(marketChance(market)),
|
||||
...(side && { side }),
|
||||
...(eventDetail.value?.slug && { slug: eventDetail.value.slug }),
|
||||
},
|
||||
})
|
||||
function goToTradeDetail(_market: PmEventMarketItem, _side?: 'yes' | 'no') {
|
||||
void _market
|
||||
void _side
|
||||
const slug = eventDetail.value?.slug?.trim()
|
||||
const eventKey = slug || String(route.params.id ?? '').trim()
|
||||
if (!eventKey) return
|
||||
router.push({ name: 'trade-detail', params: { id: eventKey } })
|
||||
}
|
||||
|
||||
async function loadEventDetail() {
|
||||
@ -607,10 +666,9 @@ async function loadEventDetail() {
|
||||
}
|
||||
const numId = parseInt(idStr, 10)
|
||||
const isNumericId = Number.isFinite(numId) && String(numId) === idStr && numId >= 1
|
||||
const slugFromQuery = (route.query.slug as string)?.trim()
|
||||
const params: FindPmEventParams = {
|
||||
id: isNumericId ? numId : undefined,
|
||||
slug: isNumericId ? slugFromQuery || undefined : idStr,
|
||||
slug: isNumericId ? undefined : idStr,
|
||||
}
|
||||
|
||||
detailError.value = null
|
||||
@ -646,6 +704,16 @@ async function loadEventDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
activeMarkets,
|
||||
(list) => {
|
||||
if (selectedMarketIndex.value >= list.length) {
|
||||
selectedMarketIndex.value = Math.max(0, list.length - 1)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
loadEventDetail()
|
||||
window.addEventListener('resize', handleResize)
|
||||
@ -1082,6 +1150,96 @@ watch(
|
||||
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 {
|
||||
height: 72px;
|
||||
|
||||
@ -227,6 +227,8 @@ import {
|
||||
computed,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import { HOME_MAIN_SCROLL_STORAGE_KEY } from '../router/scrollKeys'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import MarketCard from '../components/MarketCard.vue'
|
||||
import TradeComponent from '../components/TradeComponent.vue'
|
||||
@ -644,6 +646,13 @@ function checkScrollLoad() {
|
||||
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(() => {
|
||||
if (props.initialSearchExpanded) expandSearch()
|
||||
loadCategory()
|
||||
|
||||
@ -302,27 +302,15 @@ function openResult(item: SearchResultItem) {
|
||||
const card = mapEventItemToCard(item.raw)
|
||||
if (!card.id) return
|
||||
|
||||
const segment = (card.slug?.trim() || card.id || '').trim()
|
||||
if (!segment) return
|
||||
|
||||
if (card.displayType === 'multi' && (card.outcomes?.length ?? 0) > 1) {
|
||||
router.push({
|
||||
path: `/event/${card.id}/markets`,
|
||||
query: { ...(card.slug && { slug: card.slug }) },
|
||||
})
|
||||
router.push({ name: 'event-markets', params: { id: segment } })
|
||||
return
|
||||
}
|
||||
|
||||
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 }),
|
||||
},
|
||||
})
|
||||
router.push({ name: 'trade-detail', params: { id: segment } })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -61,6 +61,9 @@
|
||||
</template>
|
||||
<template v-else> {{ currentChance }}% {{ t('common.chance') }} </template>
|
||||
</div>
|
||||
<p v-if="currentMarketClosed" class="chart-closed-banner">
|
||||
{{ t('trade.marketClosedBanner') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
@ -92,6 +95,23 @@
|
||||
</div>
|
||||
</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-tabs
|
||||
@ -150,6 +170,7 @@
|
||||
color="primary"
|
||||
class="position-sell-btn"
|
||||
:disabled="
|
||||
currentMarketClosed ||
|
||||
!(pos.availableSharesNum != null && pos.availableSharesNum > 0)
|
||||
"
|
||||
@click="openSellFromPosition(pos)"
|
||||
@ -199,7 +220,7 @@
|
||||
</v-card>
|
||||
|
||||
<!-- Order Book Section -->
|
||||
<v-card class="order-book-card" elevation="0" rounded="lg">
|
||||
<v-card v-if="!currentMarketClosed" class="order-book-card" elevation="0" rounded="lg">
|
||||
<OrderBook
|
||||
:asks-yes="orderBookAsksYes"
|
||||
:bids-yes="orderBookBidsYes"
|
||||
@ -257,11 +278,18 @@
|
||||
|
||||
<!-- 右侧:交易组件(固定宽度),传入当前市场以便 Split 调用拆单接口;移动端隐藏,改用底部栏+弹窗 -->
|
||||
<v-col v-if="!isMobile" cols="12" class="trade-col">
|
||||
<div class="trade-sidebar">
|
||||
<div v-if="currentMarketClosed" class="trade-sidebar market-closed-pane">
|
||||
<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
|
||||
ref="tradeComponentRef"
|
||||
:market="tradeMarketPayload"
|
||||
:initial-option="tradeInitialOption"
|
||||
:positions="tradePositionsForComponent"
|
||||
@merge-success="onMergeSuccess"
|
||||
@split-success="onSplitSuccess"
|
||||
@ -270,7 +298,7 @@
|
||||
</v-col>
|
||||
|
||||
<!-- 移动端:固定底部 Yes/No 栏 + 三点菜单(Merge/Split) -->
|
||||
<template v-if="isMobile && tradeMarketPayload">
|
||||
<template v-if="isMobile && tradeMarketPayload && !currentMarketClosed">
|
||||
<div class="mobile-trade-bar-spacer" aria-hidden="true"></div>
|
||||
<div class="mobile-trade-bar">
|
||||
<v-btn
|
||||
@ -381,6 +409,8 @@ import {
|
||||
findPmEvent,
|
||||
getMarketId,
|
||||
getClobTokenId,
|
||||
getPmMarketSettledWinner,
|
||||
isPmMarketClosed,
|
||||
type FindPmEventParams,
|
||||
type PmEventListItem,
|
||||
} from '../api/event'
|
||||
@ -486,10 +516,9 @@ async function loadEventDetail() {
|
||||
}
|
||||
const numId = parseInt(idStr, 10)
|
||||
const isNumericId = Number.isFinite(numId) && String(numId) === idStr && numId >= 1
|
||||
const slugFromQuery = (route.query.slug as string)?.trim()
|
||||
const params: FindPmEventParams = {
|
||||
id: isNumericId ? numId : undefined,
|
||||
slug: isNumericId ? slugFromQuery || undefined : idStr,
|
||||
slug: isNumericId ? undefined : idStr,
|
||||
}
|
||||
|
||||
detailError.value = null
|
||||
@ -535,18 +564,15 @@ function onRefresh({ done }: { done: () => void }) {
|
||||
)
|
||||
}
|
||||
|
||||
// 标题、成交量、到期日:优先接口详情,其次卡片 query,最后占位
|
||||
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?'
|
||||
})
|
||||
// 标题、成交量、到期日:仅使用接口详情(路径只带 slug/id,无 query)
|
||||
const marketTitle = computed(() => eventDetail.value?.title ?? '')
|
||||
const marketVolume = computed(() => {
|
||||
if (eventDetail.value?.volume != null) return formatVolume(eventDetail.value.volume)
|
||||
return (route.query.marketInfo as string) || '$398,719'
|
||||
return formatVolume(undefined)
|
||||
})
|
||||
const marketExpiresAt = computed(() => {
|
||||
if (eventDetail.value?.endDate) return formatExpiresAt(eventDetail.value.endDate)
|
||||
return (route.query.expiresAt as string) || 'Mar 31, 2026'
|
||||
return ''
|
||||
})
|
||||
const resolutionDate = computed(() => {
|
||||
const s = marketExpiresAt.value
|
||||
@ -593,17 +619,19 @@ function setChartMode(mode: 'yesno' | 'crypto') {
|
||||
updateChartData()
|
||||
}
|
||||
|
||||
/** 当前市场(用于交易组件与 Split 拆单):query.marketId 匹配或取第一个 */
|
||||
/** 当前市场(用于交易组件与 Split 拆单):取接口返回的第一个市场 */
|
||||
const currentMarket = computed(() => {
|
||||
const list = eventDetail.value?.markets ?? []
|
||||
if (list.length === 0) return null
|
||||
const qId = route.query.marketId
|
||||
if (qId != null && String(qId).trim() !== '') {
|
||||
const qStr = String(qId).trim()
|
||||
const found = list.find((m) => getMarketId(m) === qStr)
|
||||
if (found) return found
|
||||
}
|
||||
return list[0]
|
||||
return list[0] ?? null
|
||||
})
|
||||
|
||||
const currentMarketClosed = computed(() => isPmMarketClosed(currentMarket.value))
|
||||
|
||||
const settledOutcomeLabel = computed(() => {
|
||||
const m = currentMarket.value
|
||||
if (!m) return ''
|
||||
const w = getPmMarketSettledWinner(m)
|
||||
return w?.label ?? ''
|
||||
})
|
||||
|
||||
// --- CLOB WebSocket 订单簿与成交 ---
|
||||
@ -861,23 +889,6 @@ const tradeMarketPayload = computed(() => {
|
||||
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
|
||||
})
|
||||
|
||||
@ -1216,12 +1227,7 @@ const currentChance = computed(() => {
|
||||
const d = data.value
|
||||
const last = d.length > 0 ? d[d.length - 1] : undefined
|
||||
if (last != null) return last[1]
|
||||
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
|
||||
return 0
|
||||
})
|
||||
|
||||
const lineColor = '#2563eb'
|
||||
@ -1966,6 +1972,59 @@ onUnmounted(() => {
|
||||
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) {
|
||||
.trade-detail-row {
|
||||
flex-wrap: nowrap;
|
||||
|
||||
@ -767,17 +767,10 @@ function canOpenTradeDetail(opts: { tradeEventId?: string; tradeEventSlug?: stri
|
||||
return !!(opts.tradeEventId?.trim() || opts.tradeEventSlug?.trim())
|
||||
}
|
||||
|
||||
function openTradeDetailFromWallet(input: {
|
||||
tradeEventId?: string
|
||||
tradeEventSlug?: string
|
||||
marketId?: string
|
||||
title?: string
|
||||
}) {
|
||||
function openTradeDetailFromWallet(input: { tradeEventId?: string; tradeEventSlug?: string }) {
|
||||
const loc = buildTradeDetailPushOptions({
|
||||
eventId: input.tradeEventId,
|
||||
eventSlug: input.tradeEventSlug,
|
||||
marketId: input.marketId,
|
||||
title: input.title,
|
||||
})
|
||||
if (loc) router.push(loc)
|
||||
}
|
||||
@ -787,8 +780,6 @@ function onPositionRowClick(pos: Position) {
|
||||
openTradeDetailFromWallet({
|
||||
tradeEventId: pos.tradeEventId,
|
||||
tradeEventSlug: pos.tradeEventSlug,
|
||||
marketId: pos.marketID,
|
||||
title: pos.market,
|
||||
})
|
||||
}
|
||||
|
||||
@ -797,8 +788,6 @@ function onOpenOrderRowClick(ord: OpenOrder) {
|
||||
openTradeDetailFromWallet({
|
||||
tradeEventId: ord.tradeEventId,
|
||||
tradeEventSlug: ord.tradeEventSlug,
|
||||
marketId: ord.detailMarketId,
|
||||
title: ord.market,
|
||||
})
|
||||
}
|
||||
|
||||
@ -813,8 +802,6 @@ function onHistoryRowClick(h: HistoryItem) {
|
||||
openTradeDetailFromWallet({
|
||||
tradeEventId: h.tradeEventId,
|
||||
tradeEventSlug: h.tradeEventSlug,
|
||||
marketId: h.detailMarketId,
|
||||
title: h.market,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user