新增:多Market卡片UI
This commit is contained in:
parent
f0c1be71cb
commit
a572fdfa99
@ -56,7 +56,11 @@ const currentRoute = computed(() => {
|
|||||||
</template>
|
</template>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
<v-main>
|
<v-main>
|
||||||
<router-view />
|
<router-view v-slot="{ Component }">
|
||||||
|
<keep-alive :include="['Home']">
|
||||||
|
<component :is="Component" />
|
||||||
|
</keep-alive>
|
||||||
|
</router-view>
|
||||||
</v-main>
|
</v-main>
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -49,11 +49,18 @@ export interface PmEventListItem {
|
|||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 对应 definitions polymarket.PmMarket 常用字段;outcomePrices 首项为 Yes 价格 */
|
/**
|
||||||
|
* 对应 definitions polymarket.PmMarket 常用字段
|
||||||
|
* - outcomes: 选项展示文案,如 ["Yes", "No"] 或 ["Up", "Down"],与 outcomePrices 一一对应
|
||||||
|
* - outcomePrices: 各选项价格(首项为第一选项概率,如 Yes/Up)
|
||||||
|
*/
|
||||||
export interface PmEventMarketItem {
|
export interface PmEventMarketItem {
|
||||||
ID?: number
|
ID?: number
|
||||||
question?: string
|
question?: string
|
||||||
slug?: string
|
slug?: string
|
||||||
|
/** 选项展示文案,与 outcomePrices 顺序一致 */
|
||||||
|
outcomes?: string[]
|
||||||
|
/** 各选项价格,outcomes[0] 对应 outcomePrices[0] */
|
||||||
outcomePrices?: string[] | number[]
|
outcomePrices?: string[] | number[]
|
||||||
endDate?: string
|
endDate?: string
|
||||||
volume?: number
|
volume?: number
|
||||||
@ -137,7 +144,23 @@ export async function findPmEvent(
|
|||||||
return get<PmEventDetailResponse>('/PmEvent/findPmEvent', { ID: id }, config)
|
return get<PmEventDetailResponse>('/PmEvent/findPmEvent', { ID: id }, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 首页卡片项(与 mapEventItemToCard 返回结构一致,用于缓存) */
|
/** 多选项卡片中单个选项(用于左右滑动切换) */
|
||||||
|
export interface EventCardOutcome {
|
||||||
|
title: string
|
||||||
|
/** 第一选项概率(来自 outcomePrices[0]) */
|
||||||
|
chanceValue: number
|
||||||
|
/** 第一选项按钮文案(来自 outcomes[0],如 Yes / Up) */
|
||||||
|
yesLabel?: string
|
||||||
|
/** 第二选项按钮文案(来自 outcomes[1],如 No / Down) */
|
||||||
|
noLabel?: string
|
||||||
|
/** 可选,用于交易时区分 market */
|
||||||
|
marketId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页卡片项(与 mapEventItemToCard 返回结构一致,用于缓存)
|
||||||
|
* displayType:single = 单一 Yes/No,multi = 多个选项左右滑动
|
||||||
|
*/
|
||||||
export interface EventCardItem {
|
export interface EventCardItem {
|
||||||
id: string
|
id: string
|
||||||
marketTitle: string
|
marketTitle: string
|
||||||
@ -146,6 +169,15 @@ export interface EventCardItem {
|
|||||||
imageUrl: string
|
imageUrl: string
|
||||||
category: string
|
category: string
|
||||||
expiresAt: string
|
expiresAt: string
|
||||||
|
/** 展示类型:单一二元 或 多选项滑动 */
|
||||||
|
displayType: 'single' | 'multi'
|
||||||
|
/** 多选项时每个选项的标题与概率,按顺序滑动展示 */
|
||||||
|
outcomes?: EventCardOutcome[]
|
||||||
|
/** 单一类型时可选按钮文案,如 "Up"/"Down" */
|
||||||
|
yesLabel?: string
|
||||||
|
noLabel?: string
|
||||||
|
/** 是否显示 NEW 角标 */
|
||||||
|
isNew?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
|
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
|
||||||
@ -173,23 +205,31 @@ export function clearEventListCache(): void {
|
|||||||
eventListCache = null
|
eventListCache = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function marketChance(market: PmEventMarketItem): number {
|
||||||
|
const raw = market?.outcomePrices?.[0]
|
||||||
|
if (raw == null) return 17
|
||||||
|
const yesPrice = parseFloat(String(raw))
|
||||||
|
if (!Number.isFinite(yesPrice)) return 17
|
||||||
|
return Math.min(100, Math.max(0, Math.round(yesPrice * 100)))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将 list 单项映射为首页 MarketCard 所需字段
|
* 将 list 单项映射为首页 MarketCard 所需字段
|
||||||
* 字段对应:ID→id, title→marketTitle, image→imageUrl, volume→marketInfo,
|
* - 单一 market 或无 market:displayType single,当前逻辑
|
||||||
* outcomePrices[0]→chanceValue, series[0].title→category, endDate→expiresAt
|
* - 多个 markets:displayType multi,outcomes 为每项标题+概率,卡片内左右滑动切换
|
||||||
*/
|
*/
|
||||||
export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
||||||
const id = String(item.ID ?? '')
|
const id = String(item.ID ?? '')
|
||||||
const marketTitle = item.title ?? ''
|
const marketTitle = item.title ?? ''
|
||||||
const imageUrl = item.image ?? item.icon ?? ''
|
const imageUrl = item.image ?? item.icon ?? ''
|
||||||
|
|
||||||
|
const markets = item.markets ?? []
|
||||||
|
const multi = markets.length > 1
|
||||||
|
|
||||||
let chanceValue = 17
|
let chanceValue = 17
|
||||||
const market = item.markets?.[0]
|
const firstMarket = markets[0]
|
||||||
if (market?.outcomePrices?.[0] != null) {
|
if (firstMarket?.outcomePrices?.[0] != null) {
|
||||||
const yesPrice = parseFloat(String(market.outcomePrices[0]))
|
chanceValue = marketChance(firstMarket)
|
||||||
if (Number.isFinite(yesPrice)) {
|
|
||||||
chanceValue = Math.min(100, Math.max(0, Math.round(yesPrice * 100)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let marketInfo = '$0 Vol.'
|
let marketInfo = '$0 Vol.'
|
||||||
@ -216,6 +256,16 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
|||||||
|
|
||||||
const category = item.series?.[0]?.title ?? item.tags?.[0]?.label ?? ''
|
const category = item.series?.[0]?.title ?? item.tags?.[0]?.label ?? ''
|
||||||
|
|
||||||
|
const outcomes: EventCardOutcome[] | undefined = multi
|
||||||
|
? markets.map((m) => ({
|
||||||
|
title: m.question ?? '',
|
||||||
|
chanceValue: marketChance(m),
|
||||||
|
yesLabel: m.outcomes?.[0] ?? 'Yes',
|
||||||
|
noLabel: m.outcomes?.[1] ?? 'No',
|
||||||
|
marketId: m.ID != null ? String(m.ID) : undefined,
|
||||||
|
}))
|
||||||
|
: undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
marketTitle,
|
marketTitle,
|
||||||
@ -224,5 +274,10 @@ export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
category,
|
category,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
|
displayType: multi ? 'multi' : 'single',
|
||||||
|
outcomes,
|
||||||
|
yesLabel: firstMarket?.outcomes?.[0] ?? 'Yes',
|
||||||
|
noLabel: firstMarket?.outcomes?.[1] ?? 'No',
|
||||||
|
isNew: item.new === true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
115
src/api/mockEventList.ts
Normal file
115
src/api/mockEventList.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import type { PmEventListItem } from './event'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟事件列表,用于本地测试:
|
||||||
|
* - 动态按钮文案(Up/Down、Yes/No)
|
||||||
|
* - 多 markets 轮播
|
||||||
|
*/
|
||||||
|
export const MOCK_EVENT_LIST: PmEventListItem[] = [
|
||||||
|
// 1. 单一 market,按钮文案为 Up/Down(测试动态 outcomes)
|
||||||
|
{
|
||||||
|
ID: 9001,
|
||||||
|
title: 'S&P 500 (SPX) Opens Up or Down on February 10?',
|
||||||
|
slug: 'spx-up-down-feb10',
|
||||||
|
ticker: 'SPX',
|
||||||
|
image: '',
|
||||||
|
icon: '',
|
||||||
|
volume: 11000,
|
||||||
|
endDate: '2026-02-10T21:00:00.000Z',
|
||||||
|
new: true,
|
||||||
|
markets: [
|
||||||
|
{
|
||||||
|
ID: 90011,
|
||||||
|
question: 'S&P 500 (SPX) Opens Up or Down on February 10?',
|
||||||
|
outcomes: ['Up', 'Down'],
|
||||||
|
outcomePrices: [0.48, 0.52],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [{ ID: 1, title: 'Economy', ticker: 'ECON' }],
|
||||||
|
tags: [{ label: 'Markets', slug: 'markets' }],
|
||||||
|
},
|
||||||
|
// 2. 多个 markets,测试左右滑动轮播(NFL Champion 多支队伍)
|
||||||
|
{
|
||||||
|
ID: 9002,
|
||||||
|
title: 'NFL Champion 2027',
|
||||||
|
slug: 'nfl-champion-2027',
|
||||||
|
ticker: 'NFL27',
|
||||||
|
image: '',
|
||||||
|
icon: '',
|
||||||
|
volume: 196000,
|
||||||
|
endDate: '2027-02-15T00:00:00.000Z',
|
||||||
|
new: true,
|
||||||
|
markets: [
|
||||||
|
{
|
||||||
|
ID: 90021,
|
||||||
|
question: 'Seattle Seahawks',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.12, 0.88],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90022,
|
||||||
|
question: 'Buffalo Bills',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.08, 0.92],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90023,
|
||||||
|
question: 'Kansas City Chiefs',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.18, 0.82],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [{ ID: 2, title: 'Sports', ticker: 'SPORT' }],
|
||||||
|
tags: [{ label: 'NFL', slug: 'nfl' }],
|
||||||
|
},
|
||||||
|
// 3. 单一 market,Yes/No,带 + NEW
|
||||||
|
{
|
||||||
|
ID: 9003,
|
||||||
|
title: 'Will Trump pardon Ghislaine Maxwell by end of 2026?',
|
||||||
|
slug: 'trump-pardon-maxwell-2026',
|
||||||
|
ticker: 'POL',
|
||||||
|
image: '',
|
||||||
|
icon: '',
|
||||||
|
volume: 361000,
|
||||||
|
endDate: '2026-12-31T23:59:59.000Z',
|
||||||
|
new: false,
|
||||||
|
markets: [
|
||||||
|
{
|
||||||
|
ID: 90031,
|
||||||
|
question: 'Will Trump pardon Ghislaine Maxwell by end of 2026?',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.09, 0.91],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [{ ID: 3, title: 'Politics', ticker: 'POL' }],
|
||||||
|
tags: [{ label: 'Politics', slug: 'politics' }],
|
||||||
|
},
|
||||||
|
// 4. 两个 markets,不同 outcomes 文案(混合 Yes/No)
|
||||||
|
{
|
||||||
|
ID: 9004,
|
||||||
|
title: 'Which team wins the 2026 World Cup?',
|
||||||
|
slug: 'world-cup-2026-winner',
|
||||||
|
ticker: 'FIFA',
|
||||||
|
image: '',
|
||||||
|
icon: '',
|
||||||
|
volume: 52000,
|
||||||
|
endDate: '2026-07-19T00:00:00.000Z',
|
||||||
|
new: true,
|
||||||
|
markets: [
|
||||||
|
{
|
||||||
|
ID: 90041,
|
||||||
|
question: 'Brazil',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.22, 0.78],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 90042,
|
||||||
|
question: 'Germany',
|
||||||
|
outcomes: ['Yes', 'No'],
|
||||||
|
outcomePrices: [0.15, 0.85],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [{ ID: 4, title: 'Sports', ticker: 'SPORT' }],
|
||||||
|
tags: [{ label: 'World Cup', slug: 'world-cup' }],
|
||||||
|
},
|
||||||
|
]
|
||||||
@ -1,9 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-card class="market-card" elevation="0" :rounded="'lg'" @click="navigateToDetail">
|
<v-card class="market-card" elevation="0" :rounded="'lg'" @click="navigateToDetail">
|
||||||
<div class="market-card-content">
|
<div class="market-card-content">
|
||||||
<!-- Top Section -->
|
<!-- Top Section:头像 + 标题 + 半圆概率 / 多选项时仅标题 -->
|
||||||
<div class="top-section">
|
<div class="top-section">
|
||||||
<!-- Market Image and Title -->
|
|
||||||
<div class="image-title-container">
|
<div class="image-title-container">
|
||||||
<v-avatar class="market-image" :size="40" color="#f0f0f0" rounded="sm">
|
<v-avatar class="market-image" :size="40" color="#f0f0f0" rounded="sm">
|
||||||
<v-img v-if="props.imageUrl" :src="props.imageUrl" cover />
|
<v-img v-if="props.imageUrl" :src="props.imageUrl" cover />
|
||||||
@ -12,12 +11,10 @@
|
|||||||
{{ marketTitle }}
|
{{ marketTitle }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 单一类型:右侧半圆进度条 -->
|
||||||
<!-- Chance Container:半圆进度条 -->
|
<div v-if="!isMulti" class="chance-container">
|
||||||
<div class="chance-container">
|
|
||||||
<div class="semi-progress-wrap">
|
<div class="semi-progress-wrap">
|
||||||
<svg class="semi-progress-svg" viewBox="0 0 60 36" preserveAspectRatio="xMidYMin meet">
|
<svg class="semi-progress-svg" viewBox="0 0 60 36" preserveAspectRatio="xMidYMin meet">
|
||||||
<!-- 半圆弧开口向下:M 左上 → A 弧至 右上(弧朝下) -->
|
|
||||||
<path
|
<path
|
||||||
class="semi-progress-track"
|
class="semi-progress-track"
|
||||||
d="M 4 30 A 26 26 0 0 1 56 30"
|
d="M 4 30 A 26 26 0 0 1 56 30"
|
||||||
@ -45,31 +42,117 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Options Section:点击 Yes/No 弹出交易框,阻止冒泡不触发卡片跳转 -->
|
<!-- 单一类型:中间 Yes/No 或 Up/Down -->
|
||||||
|
<template v-if="!isMulti">
|
||||||
<div class="options-section">
|
<div class="options-section">
|
||||||
<v-btn
|
<v-btn
|
||||||
class="option-yes"
|
class="option-yes"
|
||||||
:color="'#e6f9e6'"
|
:color="'#e6f9e6'"
|
||||||
:rounded="'sm'"
|
:rounded="'sm'"
|
||||||
:text="true"
|
:text="true"
|
||||||
@click.stop="openTrade('yes')"
|
elevation="0"
|
||||||
|
@click.stop="openTradeSingle('yes')"
|
||||||
>
|
>
|
||||||
<span class="option-text-yes">Yes</span>
|
<span class="option-text-yes">{{ yesLabel }}</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
class="option-no"
|
class="option-no"
|
||||||
:color="'#ffe6e6'"
|
:color="'#ffe6e6'"
|
||||||
:rounded="'sm'"
|
:rounded="'sm'"
|
||||||
:text="true"
|
:text="true"
|
||||||
@click.stop="openTrade('no')"
|
elevation="0"
|
||||||
|
@click.stop="openTradeSingle('no')"
|
||||||
>
|
>
|
||||||
<span class="option-text-no">No</span>
|
<span class="option-text-no">{{ noLabel }}</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 多选项类型:左右滑动轮播,每页一项 = 左侧 title+% 右侧 Yes/No -->
|
||||||
|
<div v-else class="multi-section">
|
||||||
|
<v-carousel
|
||||||
|
v-model="currentSlide"
|
||||||
|
class="outcome-carousel"
|
||||||
|
:continuous="true"
|
||||||
|
:show-arrows="false"
|
||||||
|
hide-delimiters
|
||||||
|
height="35"
|
||||||
|
>
|
||||||
|
<v-carousel-item
|
||||||
|
v-for="(outcome, idx) in props.outcomes"
|
||||||
|
:key="idx"
|
||||||
|
class="outcome-slide"
|
||||||
|
>
|
||||||
|
<div class="outcome-slide-inner">
|
||||||
|
<div class="outcome-row">
|
||||||
|
<div class="outcome-item">
|
||||||
|
<span class="outcome-title">{{ outcome.title }}</span>
|
||||||
|
<span class="outcome-chance-value">{{ outcome.chanceValue }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="outcome-buttons">
|
||||||
|
<v-btn
|
||||||
|
class="option-yes option-yes-no-compact"
|
||||||
|
:color="'#e6f9e6'"
|
||||||
|
:rounded="'sm'"
|
||||||
|
:text="true"
|
||||||
|
elevation="0"
|
||||||
|
@click.stop="openTradeMulti('yes', outcome)"
|
||||||
|
>
|
||||||
|
<span class="option-text-yes">{{ outcome.yesLabel ?? 'Yes' }}</span>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
class="option-no option-yes-no-compact"
|
||||||
|
:color="'#ffe6e6'"
|
||||||
|
:rounded="'sm'"
|
||||||
|
:text="true"
|
||||||
|
elevation="0"
|
||||||
|
@click.stop="openTradeMulti('no', outcome)"
|
||||||
|
>
|
||||||
|
<span class="option-text-no">{{ outcome.noLabel ?? 'No' }}</span>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-carousel-item>
|
||||||
|
</v-carousel>
|
||||||
|
<div class="carousel-controls" @mousedown.stop @touchstart.stop>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="carousel-arrow carousel-arrow--left"
|
||||||
|
:disabled="outcomeCount <= 1"
|
||||||
|
aria-label="上一项"
|
||||||
|
@click.stop="prevSlide"
|
||||||
|
>
|
||||||
|
<v-icon size="18">mdi-chevron-left</v-icon>
|
||||||
|
</button>
|
||||||
|
<div class="carousel-dots">
|
||||||
|
<button
|
||||||
|
v-for="(_, idx) in (props.outcomes ?? [])"
|
||||||
|
:key="idx"
|
||||||
|
type="button"
|
||||||
|
:class="['carousel-dot', { active: currentSlide === idx }]"
|
||||||
|
:aria-label="`选项 ${idx + 1}`"
|
||||||
|
@click.stop="currentSlide = idx"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="carousel-arrow carousel-arrow--right"
|
||||||
|
:disabled="outcomeCount <= 1"
|
||||||
|
aria-label="下一项"
|
||||||
|
@click.stop="nextSlide"
|
||||||
|
>
|
||||||
|
<v-icon size="18">mdi-chevron-right</v-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Bottom Section -->
|
<!-- Bottom Section -->
|
||||||
<div class="bottom-section">
|
<div class="bottom-section">
|
||||||
<span class="market-info">{{ marketInfo }}</span>
|
<span class="market-info">
|
||||||
|
<span v-if="props.isNew" class="new-badge">+ NEW</span>
|
||||||
|
{{ marketInfo }}
|
||||||
|
</span>
|
||||||
<div class="icons-container">
|
<div class="icons-container">
|
||||||
<v-icon class="gift-icon" size="16">mdi-gift</v-icon>
|
<v-icon class="gift-icon" size="16">mdi-gift</v-icon>
|
||||||
<v-icon class="bookmark-icon" size="16">mdi-bookmark</v-icon>
|
<v-icon class="bookmark-icon" size="16">mdi-bookmark</v-icon>
|
||||||
@ -80,47 +163,69 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { EventCardOutcome } from '../api/event'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
openTrade: [side: 'yes' | 'no', market?: { id: string; title: string }]
|
openTrade: [
|
||||||
|
side: 'yes' | 'no',
|
||||||
|
market?: { id: string; title: string; marketId?: string; outcomeTitle?: string }
|
||||||
|
]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = withDefaults(
|
||||||
marketTitle: {
|
defineProps<{
|
||||||
type: String,
|
marketTitle: string
|
||||||
default: 'Mamdan opens city-owned grocery store b...',
|
chanceValue: number
|
||||||
},
|
marketInfo: string
|
||||||
chanceValue: {
|
id: string
|
||||||
type: Number,
|
imageUrl?: string
|
||||||
default: 17,
|
category?: string
|
||||||
},
|
expiresAt?: string
|
||||||
marketInfo: {
|
/** 展示类型:single 单一 Yes/No,multi 多选项左右滑动 */
|
||||||
type: String,
|
displayType?: 'single' | 'multi'
|
||||||
default: '$155k Vol.',
|
/** 多选项时的选项列表(用于滑动) */
|
||||||
},
|
outcomes?: EventCardOutcome[]
|
||||||
id: {
|
/** 单一类型时左按钮文案,如 Up */
|
||||||
type: String,
|
yesLabel?: string
|
||||||
default: '1',
|
/** 单一类型时右按钮文案,如 Down */
|
||||||
},
|
noLabel?: string
|
||||||
/** 市场图片 URL,由卡片传入供详情页展示 */
|
/** 是否显示 + NEW */
|
||||||
imageUrl: {
|
isNew?: boolean
|
||||||
type: String,
|
}>(),
|
||||||
default: '',
|
{
|
||||||
},
|
marketTitle: 'Mamdan opens city-owned grocery store b...',
|
||||||
/** 分类标签,如 "Economy · World" */
|
chanceValue: 17,
|
||||||
category: {
|
marketInfo: '$155k Vol.',
|
||||||
type: String,
|
id: '1',
|
||||||
default: '',
|
imageUrl: '',
|
||||||
},
|
category: '',
|
||||||
/** 结算/到期日期,如 "Mar 31, 2026" */
|
expiresAt: '',
|
||||||
expiresAt: {
|
displayType: 'single',
|
||||||
type: String,
|
outcomes: () => [],
|
||||||
default: '',
|
yesLabel: 'Yes',
|
||||||
},
|
noLabel: 'No',
|
||||||
})
|
isNew: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const isMulti = computed(
|
||||||
|
() => props.displayType === 'multi' && (props.outcomes?.length ?? 0) > 1
|
||||||
|
)
|
||||||
|
const currentSlide = ref(0)
|
||||||
|
const outcomeCount = computed(() => (props.outcomes ?? []).length)
|
||||||
|
|
||||||
|
function prevSlide() {
|
||||||
|
if (outcomeCount.value <= 1) return
|
||||||
|
currentSlide.value = (currentSlide.value - 1 + outcomeCount.value) % outcomeCount.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextSlide() {
|
||||||
|
if (outcomeCount.value <= 1) return
|
||||||
|
currentSlide.value = (currentSlide.value + 1) % outcomeCount.value
|
||||||
|
}
|
||||||
|
|
||||||
// 半圆进度条:stroke-dashoffset,半圆弧长 ≈ π*26(左底 0° 顺时针至右底 180°)
|
// 半圆进度条:stroke-dashoffset,半圆弧长 ≈ π*26(左底 0° 顺时针至右底 180°)
|
||||||
const SEMI_CIRCLE_LENGTH = Math.PI * 26
|
const SEMI_CIRCLE_LENGTH = Math.PI * 26
|
||||||
@ -155,7 +260,6 @@ const semiProgressColor = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 跳转到交易详情页面,将标题、图片等通过 query 传入
|
|
||||||
const navigateToDetail = () => {
|
const navigateToDetail = () => {
|
||||||
router.push({
|
router.push({
|
||||||
path: `/trade-detail/${props.id}`,
|
path: `/trade-detail/${props.id}`,
|
||||||
@ -170,17 +274,26 @@ const navigateToDetail = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击 Yes/No 时弹出交易框(由父组件监听 openTrade 并打开弹层)
|
function openTradeSingle(side: 'yes' | 'no') {
|
||||||
function openTrade(side: 'yes' | 'no') {
|
|
||||||
emit('openTrade', side, { id: props.id, title: props.marketTitle })
|
emit('openTrade', side, { id: props.id, title: props.marketTitle })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openTradeMulti(side: 'yes' | 'no', outcome: EventCardOutcome) {
|
||||||
|
emit('openTrade', side, {
|
||||||
|
id: props.id,
|
||||||
|
title: outcome.title,
|
||||||
|
marketId: outcome.marketId,
|
||||||
|
outcomeTitle: outcome.title,
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* 单 market 与多 market 统一高度 */
|
||||||
.market-card {
|
.market-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 310px;
|
width: 310px;
|
||||||
height: 160px;
|
height: 176px;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #e7e7e7;
|
border: 1px solid #e7e7e7;
|
||||||
@ -190,8 +303,6 @@ function openTrade(side: 'yes' | 'no') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.market-card:hover {
|
.market-card:hover {
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
|
||||||
border-color: #d0d0d0;
|
border-color: #d0d0d0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +310,16 @@ function openTrade(side: 'yes' | 'no') {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 扁平化:卡片内所有按钮无阴影 */
|
||||||
|
.market-card :deep(.v-btn),
|
||||||
|
.market-card .carousel-arrow {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
.market-card :deep(.v-btn::before) {
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Top Section */
|
/* Top Section */
|
||||||
@ -221,18 +341,25 @@ function openTrade(side: 'yes' | 'no') {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 覆盖 Vuetify v-card-title 的 padding、white-space,保证两行完整显示且不露第三行 */
|
||||||
.market-title {
|
.market-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
|
white-space: normal;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
/* 仅文字区域两行高,无 padding 占用 */
|
||||||
|
max-height: 2.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chance-container {
|
.chance-container {
|
||||||
@ -287,10 +414,11 @@ function openTrade(side: 'yes' | 'no') {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Options Section */
|
/* Options Section:单 market 时靠底对齐 */
|
||||||
.options-section {
|
.options-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-yes {
|
.option-yes {
|
||||||
@ -319,12 +447,13 @@ function openTrade(side: 'yes' | 'no') {
|
|||||||
color: #ff0000;
|
color: #ff0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bottom Section */
|
/* Bottom Section:禁止被挤压,避免与 multi-section 重叠 */
|
||||||
.bottom-section {
|
.bottom-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.market-info {
|
.market-info {
|
||||||
@ -346,4 +475,170 @@ function openTrade(side: 'yes' | 'no') {
|
|||||||
.bookmark-icon {
|
.bookmark-icon {
|
||||||
color: #808080;
|
color: #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 多选项:左右滑动区域,整体靠底对齐;预留底部 padding 避免与 bottom-section 重叠 */
|
||||||
|
.multi-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-carousel {
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-carousel :deep(.v-carousel__container) {
|
||||||
|
touch-action: pan-y pinch-zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-slide {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-slide-inner {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 6px 12px 6px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-item {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-title {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #000000;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-chance-value {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #000000;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-buttons .option-yes-no-compact {
|
||||||
|
min-width: 44px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outcome-buttons .option-text-yes,
|
||||||
|
.outcome-buttons .option-text-no {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-arrow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s, color 0.2s;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.carousel-arrow:hover:not(:disabled) {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.carousel-arrow:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-dot:hover {
|
||||||
|
background-color: #bdbdbd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-dot.active {
|
||||||
|
background-color: rgb(var(--v-theme-primary));
|
||||||
|
width: 8px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 6px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #000;
|
||||||
|
background-color: #fef08a;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -34,6 +34,11 @@ const router = createRouter({
|
|||||||
component: Wallet
|
component: Wallet
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
scrollBehavior(to, from, savedPosition) {
|
||||||
|
if (savedPosition) return savedPosition
|
||||||
|
if (to.hash) return { el: to.hash }
|
||||||
|
return { top: 0 }
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-page">
|
<div class="home-page">
|
||||||
<v-container class="home-container">
|
<v-container fluid class="home-container">
|
||||||
<v-row justify="center" align="center" class="home-tabs">
|
<v-row justify="center" align="center" class="home-tabs">
|
||||||
<v-tabs v-model="activeTab" class="home-tab-bar">
|
<v-tabs v-model="activeTab" class="home-tab-bar">
|
||||||
<v-tab value="overview">Market Overview</v-tab>
|
<v-tab value="overview">Market Overview</v-tab>
|
||||||
@ -12,7 +12,7 @@
|
|||||||
<div ref="scrollRef" class="home-list-scroll">
|
<div ref="scrollRef" class="home-list-scroll">
|
||||||
<v-pull-to-refresh class="pull-to-refresh" @load="onRefresh">
|
<v-pull-to-refresh class="pull-to-refresh" @load="onRefresh">
|
||||||
<div class="pull-to-refresh-inner">
|
<div class="pull-to-refresh-inner">
|
||||||
<div class="home-list">
|
<div ref="listRef" class="home-list" :style="gridListStyle">
|
||||||
<MarketCard
|
<MarketCard
|
||||||
v-for="card in eventList"
|
v-for="card in eventList"
|
||||||
:key="card.id"
|
:key="card.id"
|
||||||
@ -23,6 +23,11 @@
|
|||||||
:image-url="card.imageUrl"
|
:image-url="card.imageUrl"
|
||||||
:category="card.category"
|
:category="card.category"
|
||||||
:expires-at="card.expiresAt"
|
:expires-at="card.expiresAt"
|
||||||
|
:display-type="card.displayType"
|
||||||
|
:outcomes="card.outcomes"
|
||||||
|
:yes-label="card.yesLabel"
|
||||||
|
:no-label="card.noLabel"
|
||||||
|
:is-new="card.isNew"
|
||||||
@open-trade="onCardOpenTrade"
|
@open-trade="onCardOpenTrade"
|
||||||
/>
|
/>
|
||||||
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
||||||
@ -155,6 +160,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
defineOptions({ name: 'Home' })
|
||||||
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
|
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import MarketCard from '../components/MarketCard.vue'
|
import MarketCard from '../components/MarketCard.vue'
|
||||||
@ -202,9 +208,29 @@ function onCardOpenTrade(side: 'yes' | 'no', market?: { id: string; title: strin
|
|||||||
}
|
}
|
||||||
const sentinelRef = ref<HTMLElement | null>(null)
|
const sentinelRef = ref<HTMLElement | null>(null)
|
||||||
let observer: IntersectionObserver | null = null
|
let observer: IntersectionObserver | null = null
|
||||||
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
const SCROLL_LOAD_THRESHOLD = 280
|
const SCROLL_LOAD_THRESHOLD = 280
|
||||||
|
|
||||||
|
/** 卡片最小宽度(与 MarketCard 一致),用于动态计算列数 */
|
||||||
|
const CARD_MIN_WIDTH = 310
|
||||||
|
const GRID_GAP = 20
|
||||||
|
|
||||||
|
const listRef = ref<HTMLElement | null>(null)
|
||||||
|
const gridColumns = ref(1)
|
||||||
|
|
||||||
|
const gridListStyle = computed(() => ({
|
||||||
|
gridTemplateColumns: `repeat(${gridColumns.value}, 1fr)`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
function updateGridColumns() {
|
||||||
|
const el = listRef.value
|
||||||
|
if (!el) return
|
||||||
|
const width = el.getBoundingClientRect().width
|
||||||
|
const n = Math.floor((width + GRID_GAP) / (CARD_MIN_WIDTH + GRID_GAP))
|
||||||
|
gridColumns.value = Math.max(1, n)
|
||||||
|
}
|
||||||
|
|
||||||
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
||||||
async function loadEvents(page: number, append: boolean) {
|
async function loadEvents(page: number, append: boolean) {
|
||||||
try {
|
try {
|
||||||
@ -256,12 +282,10 @@ function loadMore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkScrollLoad() {
|
function checkScrollLoad() {
|
||||||
const el = scrollRef.value
|
if (loadingMore.value || eventList.value.length === 0 || noMoreEvents.value) return
|
||||||
if (!el || loadingMore.value || eventList.value.length === 0 || noMoreEvents.value) return
|
const { scrollY, innerHeight } = window
|
||||||
const { scrollTop, clientHeight, scrollHeight } = el
|
const scrollHeight = document.documentElement.scrollHeight
|
||||||
if (scrollHeight - scrollTop - clientHeight < SCROLL_LOAD_THRESHOLD) {
|
if (scrollHeight - scrollY - innerHeight < SCROLL_LOAD_THRESHOLD) loadMore()
|
||||||
loadMore()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -275,20 +299,24 @@ onMounted(() => {
|
|||||||
loadEvents(0, false)
|
loadEvents(0, false)
|
||||||
}
|
}
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const scrollEl = scrollRef.value
|
|
||||||
const sentinel = sentinelRef.value
|
const sentinel = sentinelRef.value
|
||||||
if (!sentinel || !scrollEl) return
|
if (sentinel) {
|
||||||
|
|
||||||
observer = new IntersectionObserver(
|
observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
if (!entries[0]?.isIntersecting) return
|
if (!entries[0]?.isIntersecting) return
|
||||||
loadMore()
|
loadMore()
|
||||||
},
|
},
|
||||||
{ root: scrollEl, rootMargin: `${SCROLL_LOAD_THRESHOLD}px`, threshold: 0 }
|
{ root: null, rootMargin: `${SCROLL_LOAD_THRESHOLD}px`, threshold: 0 }
|
||||||
)
|
)
|
||||||
observer.observe(sentinel)
|
observer.observe(sentinel)
|
||||||
|
}
|
||||||
scrollEl.addEventListener('scroll', checkScrollLoad, { passive: true })
|
window.addEventListener('scroll', checkScrollLoad, { passive: true })
|
||||||
|
const listEl = listRef.value
|
||||||
|
if (listEl) {
|
||||||
|
updateGridColumns()
|
||||||
|
resizeObserver = new ResizeObserver(updateGridColumns)
|
||||||
|
resizeObserver.observe(listEl)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -296,13 +324,21 @@ onUnmounted(() => {
|
|||||||
const sentinel = sentinelRef.value
|
const sentinel = sentinelRef.value
|
||||||
if (observer && sentinel) observer.unobserve(sentinel)
|
if (observer && sentinel) observer.unobserve(sentinel)
|
||||||
observer = null
|
observer = null
|
||||||
scrollRef.value?.removeEventListener('scroll', checkScrollLoad)
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
resizeObserver = null
|
||||||
|
}
|
||||||
|
window.removeEventListener('scroll', checkScrollLoad)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* fluid 后无断点 max-width,用自定义 max-width 让列表在 2137px 等宽屏下能算到 6 列 */
|
||||||
.home-container {
|
.home-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
max-width: 2560px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-header {
|
.home-header {
|
||||||
@ -326,22 +362,17 @@ onUnmounted(() => {
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 不设固定高度与 overflow,列表随页面(窗口)滚动,便于 Vue Router scrollBehavior 自动恢复位置 */
|
||||||
.home-list-scroll {
|
.home-list-scroll {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
/* 占满剩余视口高度,内容多时在内部滚动 */
|
|
||||||
min-height: calc(100vh - 64px - 48px - 32px);
|
|
||||||
max-height: calc(100vh - 64px - 48px - 32px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 卡片最小宽度与 MarketCard 一致 310px;auto-fill 按可用宽度自动 1~4 列,避免第三列被裁 */
|
/* 列数由 JS 根据容器宽度与 CARD_MIN_WIDTH 连续计算,避免断点导致 6→4 跳变 */
|
||||||
.home-list {
|
.home-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-list-empty {
|
.home-list-empty {
|
||||||
@ -404,12 +435,6 @@ onUnmounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 大屏最多 4 列,避免过宽时出现 5 列以上 */
|
|
||||||
@media (min-width: 1320px) {
|
|
||||||
.home-list {
|
|
||||||
grid-template-columns: repeat(4, minmax(310px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-subtitle {
|
.home-subtitle {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user