优化:交易组件的价格显示单位与外面一致

This commit is contained in:
ivan 2026-02-26 20:40:07 +08:00
parent 8c455ba00a
commit da9de8c772
4 changed files with 92 additions and 29 deletions

View File

@ -203,13 +203,13 @@ export interface EventCardOutcome {
title: string
/** 第一选项概率(来自 outcomePrices[0] */
chanceValue: number
/** 第一选项按钮文案(来自 outcomes[0],如 Yes / Up */
/** Yes 价格 01来自 outcomePrices[0],供交易组件使用 */
yesPrice?: number
/** No 价格 01来自 outcomePrices[1],供交易组件使用 */
noPrice?: number
yesLabel?: string
/** 第二选项按钮文案(来自 outcomes[1],如 No / Down */
noLabel?: string
/** 可选,用于交易时区分 market */
marketId?: string
/** 用于下单 tokenId与 outcomes 顺序一致 */
clobTokenIds?: string[]
}
@ -240,6 +240,10 @@ export interface EventCardItem {
marketId?: string
/** 用于下单 tokenId单 market 时取自 firstMarket.clobTokenIds */
clobTokenIds?: string[]
/** Yes 价格 01来自 outcomePrices[0],供交易组件使用 */
yesPrice?: number
/** No 价格 01来自 outcomePrices[1],供交易组件使用 */
noPrice?: number
}
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
@ -322,17 +326,38 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
const category = item.series?.[0]?.title ?? item.tags?.[0]?.label ?? ''
function parseOutcomePrices(m: PmEventMarketItem): { yesPrice: number; noPrice: number } {
const y = m?.outcomePrices?.[0]
const n = m?.outcomePrices?.[1]
const yesPrice =
y != null && Number.isFinite(parseFloat(String(y)))
? Math.min(1, Math.max(0, parseFloat(String(y))))
: 0.5
const noPrice =
n != null && Number.isFinite(parseFloat(String(n)))
? Math.min(1, Math.max(0, parseFloat(String(n))))
: 1 - yesPrice
return { yesPrice, noPrice }
}
const outcomes: EventCardOutcome[] | undefined = multi
? markets.map((m) => ({
? markets.map((m) => {
const { yesPrice, noPrice } = parseOutcomePrices(m)
return {
title: m.question ?? '',
chanceValue: marketChance(m),
yesPrice,
noPrice,
yesLabel: m.outcomes?.[0] ?? 'Yes',
noLabel: m.outcomes?.[1] ?? 'No',
marketId: getMarketId(m),
clobTokenIds: m.clobTokenIds,
}))
}
})
: undefined
const firstPrices = firstMarket ? parseOutcomePrices(firstMarket) : { yesPrice: 0.5, noPrice: 0.5 }
return {
id,
slug: item.slug ?? undefined,
@ -349,5 +374,7 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
isNew: item.new === true,
marketId: getMarketId(firstMarket),
clobTokenIds: firstMarket?.clobTokenIds,
yesPrice: firstPrices.yesPrice,
noPrice: firstPrices.noPrice,
}
}

View File

@ -181,6 +181,8 @@ const emit = defineEmits<{
clobTokenIds?: string[]
yesLabel?: string
noLabel?: string
yesPrice?: number
noPrice?: number
},
]
}>()
@ -210,6 +212,10 @@ const props = withDefaults(
marketId?: string
/** 用于下单 tokenId单 market 时 */
clobTokenIds?: string[]
/** Yes 价格 01供交易组件使用 */
yesPrice?: number
/** No 价格 01供交易组件使用 */
noPrice?: number
}>(),
{
marketTitle: 'Mamdan opens city-owned grocery store b...',
@ -225,6 +231,8 @@ const props = withDefaults(
noLabel: 'No',
isNew: false,
marketId: undefined,
yesPrice: undefined,
noPrice: undefined,
},
)
@ -306,6 +314,8 @@ function openTradeSingle(side: 'yes' | 'no') {
clobTokenIds: props.clobTokenIds,
yesLabel: props.yesLabel,
noLabel: props.noLabel,
yesPrice: props.yesPrice,
noPrice: props.noPrice,
})
}
@ -318,6 +328,8 @@ function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
clobTokenIds: outcome.clobTokenIds,
yesLabel: outcome.yesLabel,
noLabel: outcome.noLabel,
yesPrice: outcome.yesPrice,
noPrice: outcome.noPrice,
})
}
</script>

View File

@ -293,14 +293,15 @@
<v-icon>mdi-minus</v-icon>
</v-btn>
<v-text-field
:model-value="limitPrice"
:model-value="limitPriceCentsDisplay"
type="number"
min="0"
max="1"
max="100"
step="0.01"
class="price-input-field"
hide-details
density="compact"
suffix="¢"
@update:model-value="onLimitPriceInput"
@blur="onLimitPriceBlur"
@keydown="onLimitPriceKeydown"
@ -670,14 +671,15 @@
><v-icon>mdi-minus</v-icon></v-btn
>
<v-text-field
:model-value="limitPrice"
:model-value="limitPriceCentsDisplay"
type="number"
min="0"
max="1"
max="100"
step="0.01"
class="price-input-field"
hide-details
density="compact"
suffix="¢"
@update:model-value="onLimitPriceInput"
@blur="onLimitPriceBlur"
@keydown="onLimitPriceKeydown"
@ -1059,15 +1061,17 @@
><v-icon>mdi-minus</v-icon></v-btn
>
<v-text-field
:model-value="limitPrice"
:model-value="limitPriceCentsDisplay"
type="number"
min="0"
max="1"
max="100"
step="0.01"
class="price-input-field"
hide-details
density="compact"
suffix="¢"
@update:model-value="onLimitPriceInput"
@blur="onLimitPriceBlur"
@keydown="onLimitPriceKeydown"
@paste="onLimitPricePaste"
></v-text-field>
@ -1446,7 +1450,7 @@ const activeTab = ref('buy')
const limitType = ref('Limit')
const expirationEnabled = ref(false)
const selectedOption = ref<'yes' | 'no'>(props.initialOption ?? 'no')
const limitPrice = ref(0.82) //
const limitPrice = ref(0.82) // 01
const shares = ref(20) //
const expirationTime = ref('5m') //
const EXPIRATION_VALUES = ['5m', '15m', '30m', '1h', '2h', '4h', '8h', '12h', '1d', '2d', '3d'] as const
@ -1485,6 +1489,8 @@ const emit = defineEmits<{
}>()
// Computed properties
/** Limit Price 显示值(美分),与 Yes/No 按钮单位一致 */
const limitPriceCentsDisplay = computed(() => Math.round(limitPrice.value * 10000) / 100)
const currentPrice = computed(() => {
return `${(limitPrice.value * 100).toFixed(0)}¢`
})
@ -1571,11 +1577,11 @@ const handleOptionChange = (option: 'yes' | 'no') => {
emit('optionChange', option)
}
/** 仅接受 135 个允许档位:输入值吸附到最近档位,非法值忽略 */
/** 输入为美分0100与 Yes/No 按钮单位一致 */
function onLimitPriceInput(v: unknown) {
const num = v == null ? NaN : Number(v)
if (!Number.isFinite(num) || num < 0 || num > 1) return
limitPrice.value = snapToAllowedPrice(num)
if (!Number.isFinite(num) || num < 0 || num > 100) return
limitPrice.value = snapToAllowedPrice(num / 100)
}
/** 失焦时吸附到允许档位 */
@ -1583,7 +1589,7 @@ function onLimitPriceBlur() {
limitPrice.value = snapToAllowedPrice(limitPrice.value)
}
/** 只允许数字和小数点输入 */
/** 只允许数字和小数点输入(美分 0100 */
function onLimitPriceKeydown(e: KeyboardEvent) {
const key = e.key
const allowed = ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End']
@ -1596,11 +1602,11 @@ function onLimitPriceKeydown(e: KeyboardEvent) {
e.preventDefault()
}
/** 粘贴时只接受有效数字 */
/** 粘贴时只接受有效美分数0100 */
function onLimitPricePaste(e: ClipboardEvent) {
const text = e.clipboardData?.getData('text') ?? ''
const num = parseFloat(text)
if (!Number.isFinite(num) || num < 0 || num > 1) {
if (!Number.isFinite(num) || num < 0 || num > 100) {
e.preventDefault()
}
}

View File

@ -165,6 +165,8 @@
:is-new="card.isNew"
:market-id="card.marketId"
:clob-token-ids="card.clobTokenIds"
:yes-price="card.yesPrice"
:no-price="card.noPrice"
@open-trade="onCardOpenTrade"
/>
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
@ -514,12 +516,23 @@ const tradeDialogMarket = ref<{
clobTokenIds?: string[]
yesLabel?: string
noLabel?: string
yesPrice?: number
noPrice?: number
} | null>(null)
const scrollRef = ref<HTMLElement | null>(null)
function onCardOpenTrade(
side: 'yes' | 'no',
market?: { id: string; title: string; marketId?: string; yesLabel?: string; noLabel?: string },
market?: {
id: string
title: string
marketId?: string
yesLabel?: string
noLabel?: string
yesPrice?: number
noPrice?: number
clobTokenIds?: string[]
},
) {
tradeDialogSide.value = side
tradeDialogMarket.value = market ?? null
@ -537,9 +550,14 @@ const homeTradeMarketPayload = computed(() => {
const m = tradeDialogMarket.value
if (!m) return undefined
const marketId = m.marketId ?? m.id
const chance = 50
const yesPrice = Math.min(1, Math.max(0, chance / 100))
const noPrice = 1 - yesPrice
const yesPrice =
m.yesPrice != null && Number.isFinite(m.yesPrice)
? Math.min(1, Math.max(0, m.yesPrice))
: 0.5
const noPrice =
m.noPrice != null && Number.isFinite(m.noPrice)
? Math.min(1, Math.max(0, m.noPrice))
: 1 - yesPrice
const outcomes =
m.yesLabel != null || m.noLabel != null
? [m.yesLabel ?? 'Yes', m.noLabel ?? 'No']