新增:列表数据接口
This commit is contained in:
parent
08ae68b767
commit
e603e04d0f
167
src/api/event.ts
Normal file
167
src/api/event.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import { get } from './request'
|
||||||
|
|
||||||
|
/** 分页结果 */
|
||||||
|
export interface PageResult<T> {
|
||||||
|
list: T[]
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接口 list 单项结构(与 /PmEvent/getPmEventPublic 返回一致) */
|
||||||
|
export interface PmEventListItem {
|
||||||
|
ID: number
|
||||||
|
CreatedAt?: string
|
||||||
|
UpdatedAt?: string
|
||||||
|
ticker?: string
|
||||||
|
slug?: string
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
resolutionSource?: string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
image?: string
|
||||||
|
icon?: string
|
||||||
|
active?: boolean
|
||||||
|
closed?: boolean
|
||||||
|
volume?: number
|
||||||
|
markets?: Array<{
|
||||||
|
ID?: number
|
||||||
|
question?: string
|
||||||
|
outcomePrices?: string[]
|
||||||
|
endDate?: string
|
||||||
|
volume?: number
|
||||||
|
[key: string]: unknown
|
||||||
|
}>
|
||||||
|
series?: Array<{
|
||||||
|
ID?: number
|
||||||
|
ticker?: string
|
||||||
|
title?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}>
|
||||||
|
tags?: Array<{ label?: string; slug?: string; [key: string]: unknown }>
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 接口统一响应 */
|
||||||
|
export interface PmEventListResponse {
|
||||||
|
code: number
|
||||||
|
data: PageResult<PmEventListItem>
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetPmEventListParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
/** 创建时间范围,如 ['2025-01-01', '2025-12-31'] */
|
||||||
|
createdAtRange?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取 Event 列表(公开接口,不需要鉴权)
|
||||||
|
* GET /PmEvent/getPmEventPublic
|
||||||
|
*/
|
||||||
|
export async function getPmEventPublic(
|
||||||
|
params: GetPmEventListParams = {}
|
||||||
|
): Promise<PmEventListResponse> {
|
||||||
|
const { page = 0, pageSize = 10, keyword, createdAtRange } = params
|
||||||
|
const query: Record<string, string | number | string[] | undefined> = {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
}
|
||||||
|
if (keyword != null && keyword !== '') query.keyword = keyword
|
||||||
|
if (createdAtRange != null && createdAtRange.length) query.createdAtRange = createdAtRange
|
||||||
|
|
||||||
|
return get<PmEventListResponse>('/PmEvent/getPmEventPublic', query)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 首页卡片项(与 mapEventItemToCard 返回结构一致,用于缓存) */
|
||||||
|
export interface EventCardItem {
|
||||||
|
id: string
|
||||||
|
marketTitle: string
|
||||||
|
chanceValue: number
|
||||||
|
marketInfo: string
|
||||||
|
imageUrl: string
|
||||||
|
category: string
|
||||||
|
expiresAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 内存缓存:列表数据,切换页面时复用,下拉刷新时清空 */
|
||||||
|
let eventListCache: {
|
||||||
|
list: EventCardItem[]
|
||||||
|
page: number
|
||||||
|
total: number
|
||||||
|
pageSize: number
|
||||||
|
} | null = null
|
||||||
|
|
||||||
|
export function getEventListCache(): typeof eventListCache {
|
||||||
|
return eventListCache
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setEventListCache(data: {
|
||||||
|
list: EventCardItem[]
|
||||||
|
page: number
|
||||||
|
total: number
|
||||||
|
pageSize: number
|
||||||
|
}) {
|
||||||
|
eventListCache = data
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearEventListCache(): void {
|
||||||
|
eventListCache = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 list 单项映射为首页 MarketCard 所需字段
|
||||||
|
* 字段对应:ID→id, title→marketTitle, image→imageUrl, volume→marketInfo,
|
||||||
|
* outcomePrices[0]→chanceValue, series[0].title→category, endDate→expiresAt
|
||||||
|
*/
|
||||||
|
export function mapEventItemToCard(item: PmEventListItem): EventCardItem {
|
||||||
|
const id = String(item.ID ?? '')
|
||||||
|
const marketTitle = item.title ?? ''
|
||||||
|
const imageUrl = item.image ?? item.icon ?? ''
|
||||||
|
|
||||||
|
let chanceValue = 17
|
||||||
|
const market = item.markets?.[0]
|
||||||
|
if (market?.outcomePrices?.[0] != null) {
|
||||||
|
const yesPrice = parseFloat(String(market.outcomePrices[0]))
|
||||||
|
if (Number.isFinite(yesPrice)) {
|
||||||
|
chanceValue = Math.min(100, Math.max(0, Math.round(yesPrice * 100)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let marketInfo = '$0 Vol.'
|
||||||
|
if (item.volume != null && Number.isFinite(item.volume)) {
|
||||||
|
const v = item.volume
|
||||||
|
if (v >= 1000) {
|
||||||
|
marketInfo = `$${(v / 1000).toFixed(1)}k Vol.`
|
||||||
|
} else {
|
||||||
|
marketInfo = `$${Math.round(v)} Vol.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expiresAt = ''
|
||||||
|
if (item.endDate) {
|
||||||
|
try {
|
||||||
|
const d = new Date(item.endDate)
|
||||||
|
if (!Number.isNaN(d.getTime())) {
|
||||||
|
expiresAt = d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
expiresAt = item.endDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = item.series?.[0]?.title ?? item.tags?.[0]?.label ?? ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
marketTitle,
|
||||||
|
chanceValue,
|
||||||
|
marketInfo,
|
||||||
|
imageUrl,
|
||||||
|
category,
|
||||||
|
expiresAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/api/request.ts
Normal file
41
src/api/request.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 请求基础 URL,默认 https://api.xtrader.vip,可通过环境变量 VITE_API_BASE_URL 覆盖
|
||||||
|
*/
|
||||||
|
const BASE_URL = typeof import.meta !== 'undefined' && (import.meta as unknown as { env?: Record<string, string> }).env?.VITE_API_BASE_URL
|
||||||
|
? (import.meta as unknown as { env: Record<string, string> }).env.VITE_API_BASE_URL
|
||||||
|
: 'https://api.xtrader.vip'
|
||||||
|
|
||||||
|
export interface RequestConfig {
|
||||||
|
/** 请求头,如 { 'x-token': token } */
|
||||||
|
headers?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带 x-token 等自定义头的 GET 请求
|
||||||
|
*/
|
||||||
|
export async function get<T = unknown>(
|
||||||
|
path: string,
|
||||||
|
params?: Record<string, string | number | string[] | undefined>,
|
||||||
|
config?: RequestConfig
|
||||||
|
): Promise<T> {
|
||||||
|
const url = new URL(path, BASE_URL || window.location.origin)
|
||||||
|
if (params) {
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value === undefined) return
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => url.searchParams.append(key, String(v)))
|
||||||
|
} else {
|
||||||
|
url.searchParams.set(key, String(value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...config?.headers,
|
||||||
|
}
|
||||||
|
const res = await fetch(url.toString(), { method: 'GET', headers })
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
||||||
|
}
|
||||||
|
return res.json() as Promise<T>
|
||||||
|
}
|
||||||
@ -13,23 +13,37 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chance Container -->
|
<!-- Chance Container:半圆进度条 -->
|
||||||
<div class="chance-container">
|
<div class="chance-container">
|
||||||
<v-progress-circular
|
<div class="semi-progress-wrap">
|
||||||
class="progress-bar"
|
<svg class="semi-progress-svg" viewBox="0 0 60 36" preserveAspectRatio="xMidYMin meet">
|
||||||
:size="60"
|
<!-- 半圆弧开口向下:M 左上 → A 弧至 右上(弧朝下) -->
|
||||||
:width="4"
|
<path
|
||||||
:value="chanceValue"
|
class="semi-progress-track"
|
||||||
:color="progressColor"
|
d="M 4 30 A 26 26 0 0 1 56 30"
|
||||||
:background="'#e0e0e0'"
|
fill="none"
|
||||||
>
|
stroke="#e8e8e8"
|
||||||
<template v-slot:default>
|
stroke-width="5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="semi-progress-fill"
|
||||||
|
d="M 4 30 A 26 26 0 0 1 56 30"
|
||||||
|
fill="none"
|
||||||
|
:stroke="semiProgressColor"
|
||||||
|
stroke-width="5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-dasharray="81.68"
|
||||||
|
:stroke-dashoffset="semiProgressOffset"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class="semi-progress-inner">
|
||||||
<span class="chance-value">{{ chanceValue }}%</span>
|
<span class="chance-value">{{ chanceValue }}%</span>
|
||||||
</template>
|
|
||||||
</v-progress-circular>
|
|
||||||
<span class="chance-label">chance</span>
|
<span class="chance-label">chance</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Options Section:点击 Yes/No 弹出交易框,阻止冒泡不触发卡片跳转 -->
|
<!-- Options Section:点击 Yes/No 弹出交易框,阻止冒泡不触发卡片跳转 -->
|
||||||
<div class="options-section">
|
<div class="options-section">
|
||||||
@ -108,11 +122,37 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算进度条颜色,从红色(0%)到绿色(100%)
|
// 半圆进度条:stroke-dashoffset,半圆弧长 ≈ π*26(左底 0° 顺时针至右底 180°)
|
||||||
const progressColor = computed(() => {
|
const SEMI_CIRCLE_LENGTH = Math.PI * 26
|
||||||
// 红色在HSL中是0度,绿色是120度
|
const semiProgressOffset = computed(() => {
|
||||||
const hue = (props.chanceValue / 100) * 120
|
const p = Math.min(100, Math.max(0, props.chanceValue)) / 100
|
||||||
return `hsl(${hue}, 100%, 50%)`
|
return SEMI_CIRCLE_LENGTH * (1 - p)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 进度条纯色:0% 红 → 50% 图片橙黄 #FFBB5C → 100% 绿(两段 RGB 插值)
|
||||||
|
const COLOR_RED = { r: 239, g: 68, b: 68 }
|
||||||
|
const COLOR_ORANGE_YELLOW = { r: 255, g: 187, b: 92 }
|
||||||
|
const COLOR_GREEN = { r: 34, g: 197, b: 94 }
|
||||||
|
function rgbToHex(r: number, g: number, b: number): string {
|
||||||
|
const toByte = (v: number) => Math.min(255, Math.max(0, Math.round(v)))
|
||||||
|
return `#${toByte(r).toString(16).padStart(2, '0')}${toByte(g).toString(16).padStart(2, '0')}${toByte(b).toString(16).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
const semiProgressColor = computed(() => {
|
||||||
|
const t = Math.min(1, Math.max(0, props.chanceValue / 100))
|
||||||
|
if (t <= 0.5) {
|
||||||
|
const u = t * 2
|
||||||
|
return rgbToHex(
|
||||||
|
COLOR_RED.r + (COLOR_ORANGE_YELLOW.r - COLOR_RED.r) * u,
|
||||||
|
COLOR_RED.g + (COLOR_ORANGE_YELLOW.g - COLOR_RED.g) * u,
|
||||||
|
COLOR_RED.b + (COLOR_ORANGE_YELLOW.b - COLOR_RED.b) * u
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const u = (t - 0.5) * 2
|
||||||
|
return rgbToHex(
|
||||||
|
COLOR_ORANGE_YELLOW.r + (COLOR_GREEN.r - COLOR_ORANGE_YELLOW.r) * u,
|
||||||
|
COLOR_ORANGE_YELLOW.g + (COLOR_GREEN.g - COLOR_ORANGE_YELLOW.g) * u,
|
||||||
|
COLOR_ORANGE_YELLOW.b + (COLOR_GREEN.b - COLOR_ORANGE_YELLOW.b) * u
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 跳转到交易详情页面,将标题、图片等通过 query 传入
|
// 跳转到交易详情页面,将标题、图片等通过 query 传入
|
||||||
@ -196,31 +236,55 @@ function openTrade(side: 'yes' | 'no') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chance-container {
|
.chance-container {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semi-progress-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 60px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semi-progress-svg {
|
||||||
|
display: block;
|
||||||
|
width: 60px;
|
||||||
|
height: 36px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.semi-progress-track,
|
||||||
|
.semi-progress-fill {
|
||||||
|
transition: stroke-dashoffset 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 半圆弧 viewBox 0 0 60 36:弧的视觉中心约 y=17,百分比文字的 top 对齐该中心 */
|
||||||
|
.semi-progress-inner {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 17px;
|
||||||
|
transform: translateX(-50%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
justify-content: flex-start;
|
||||||
width: 60px;
|
pointer-events: none;
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chance-value {
|
.chance-value {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #000000;
|
color: #000000;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chance-label {
|
.chance-label {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: #808080;
|
color: #9ca3af;
|
||||||
margin-top: 4px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Options Section */
|
/* Options Section */
|
||||||
|
|||||||
@ -14,19 +14,28 @@
|
|||||||
<div class="pull-to-refresh-inner">
|
<div class="pull-to-refresh-inner">
|
||||||
<div class="home-list">
|
<div class="home-list">
|
||||||
<MarketCard
|
<MarketCard
|
||||||
v-for="id in listLength"
|
v-for="card in eventList"
|
||||||
:key="id"
|
:key="card.id"
|
||||||
:id="String(id)"
|
:id="card.id"
|
||||||
|
:market-title="card.marketTitle"
|
||||||
|
:chance-value="card.chanceValue"
|
||||||
|
:market-info="card.marketInfo"
|
||||||
|
:image-url="card.imageUrl"
|
||||||
|
:category="card.category"
|
||||||
|
:expires-at="card.expiresAt"
|
||||||
@open-trade="onCardOpenTrade"
|
@open-trade="onCardOpenTrade"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="eventList.length === 0 && !loadingMore" class="home-list-empty">
|
||||||
|
暂无数据
|
||||||
</div>
|
</div>
|
||||||
<div class="load-more-footer">
|
</div>
|
||||||
|
<div v-if="eventList.length > 0" class="load-more-footer">
|
||||||
<div ref="sentinelRef" class="load-more-sentinel" aria-hidden="true" />
|
<div ref="sentinelRef" class="load-more-sentinel" aria-hidden="true" />
|
||||||
<div v-if="loadingMore" class="load-more-indicator">
|
<div v-if="loadingMore" class="load-more-indicator">
|
||||||
<v-progress-circular indeterminate size="24" width="2" />
|
<v-progress-circular indeterminate size="24" width="2" />
|
||||||
<span>加载中...</span>
|
<span>加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="listLength >= maxItems" class="no-more-tip">没有更多了</div>
|
<div v-else-if="noMoreEvents" class="no-more-tip">没有更多了</div>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-else
|
v-else
|
||||||
class="load-more-btn"
|
class="load-more-btn"
|
||||||
@ -150,18 +159,36 @@ 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'
|
||||||
import TradeComponent from '../components/TradeComponent.vue'
|
import TradeComponent from '../components/TradeComponent.vue'
|
||||||
|
import {
|
||||||
|
getPmEventPublic,
|
||||||
|
mapEventItemToCard,
|
||||||
|
getEventListCache,
|
||||||
|
setEventListCache,
|
||||||
|
clearEventListCache,
|
||||||
|
type EventCardItem,
|
||||||
|
} from '../api/event'
|
||||||
|
|
||||||
const { mobile } = useDisplay()
|
const { mobile } = useDisplay()
|
||||||
const isMobile = computed(() => mobile.value)
|
const isMobile = computed(() => mobile.value)
|
||||||
|
|
||||||
const activeTab = ref('overview')
|
const activeTab = ref('overview')
|
||||||
|
|
||||||
const INITIAL_COUNT = 10
|
|
||||||
const PAGE_SIZE = 10
|
const PAGE_SIZE = 10
|
||||||
const maxItems = 50
|
|
||||||
|
|
||||||
const listLength = ref(INITIAL_COUNT)
|
/** 接口返回的列表(已映射为卡片所需结构) */
|
||||||
|
const eventList = ref<EventCardItem[]>([])
|
||||||
|
/** 当前页码(0-based) */
|
||||||
|
const eventPage = ref(0)
|
||||||
|
/** 接口返回的 total */
|
||||||
|
const eventTotal = ref(0)
|
||||||
|
const eventPageSize = ref(PAGE_SIZE)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
|
|
||||||
|
const noMoreEvents = computed(() => {
|
||||||
|
if (eventList.value.length === 0) return false
|
||||||
|
return eventList.value.length >= eventTotal.value || (eventPage.value + 1) * eventPageSize.value >= eventTotal.value
|
||||||
|
})
|
||||||
|
|
||||||
const footerLang = ref('English')
|
const footerLang = ref('English')
|
||||||
const tradeDialogOpen = ref(false)
|
const tradeDialogOpen = ref(false)
|
||||||
const tradeDialogSide = ref<'yes' | 'no'>('yes')
|
const tradeDialogSide = ref<'yes' | 'no'>('yes')
|
||||||
@ -178,25 +205,59 @@ let observer: IntersectionObserver | null = null
|
|||||||
|
|
||||||
const SCROLL_LOAD_THRESHOLD = 280
|
const SCROLL_LOAD_THRESHOLD = 280
|
||||||
|
|
||||||
|
/** 请求事件列表并追加或覆盖到 eventList(公开接口,无需鉴权);成功后会更新内存缓存 */
|
||||||
|
async function loadEvents(page: number, append: boolean) {
|
||||||
|
try {
|
||||||
|
const res = await getPmEventPublic(
|
||||||
|
{ page, pageSize: PAGE_SIZE }
|
||||||
|
)
|
||||||
|
if (res.code !== 0 && res.code !== 200) {
|
||||||
|
throw new Error(res.msg || '请求失败')
|
||||||
|
}
|
||||||
|
const data = res.data
|
||||||
|
if (!data?.list || !Array.isArray(data.list)) {
|
||||||
|
if (!append) eventList.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const mapped = data.list.map((item) => mapEventItemToCard(item))
|
||||||
|
eventTotal.value = data.total ?? 0
|
||||||
|
eventPageSize.value = data.pageSize && data.pageSize > 0 ? data.pageSize : PAGE_SIZE
|
||||||
|
eventPage.value = data.page ?? page
|
||||||
|
if (append) {
|
||||||
|
eventList.value = [...eventList.value, ...mapped]
|
||||||
|
} else {
|
||||||
|
eventList.value = mapped
|
||||||
|
}
|
||||||
|
setEventListCache({
|
||||||
|
list: eventList.value,
|
||||||
|
page: eventPage.value,
|
||||||
|
total: eventTotal.value,
|
||||||
|
pageSize: eventPageSize.value,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
if (!append) eventList.value = []
|
||||||
|
console.warn('getPmEventList failed', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onRefresh({ done }: { done: () => void }) {
|
function onRefresh({ done }: { done: () => void }) {
|
||||||
setTimeout(() => {
|
clearEventListCache()
|
||||||
listLength.value = INITIAL_COUNT
|
eventPage.value = 0
|
||||||
done()
|
loadEvents(0, false).finally(() => done())
|
||||||
}, 600)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMore() {
|
function loadMore() {
|
||||||
if (loadingMore.value || listLength.value >= maxItems) return
|
if (loadingMore.value || noMoreEvents.value || eventList.value.length === 0) return
|
||||||
loadingMore.value = true
|
loadingMore.value = true
|
||||||
setTimeout(() => {
|
const nextPage = eventPage.value + 1
|
||||||
listLength.value = Math.min(listLength.value + PAGE_SIZE, maxItems)
|
loadEvents(nextPage, true).finally(() => {
|
||||||
loadingMore.value = false
|
loadingMore.value = false
|
||||||
}, 400)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkScrollLoad() {
|
function checkScrollLoad() {
|
||||||
const el = scrollRef.value
|
const el = scrollRef.value
|
||||||
if (!el || loadingMore.value || listLength.value >= maxItems) return
|
if (!el || loadingMore.value || eventList.value.length === 0 || noMoreEvents.value) return
|
||||||
const { scrollTop, clientHeight, scrollHeight } = el
|
const { scrollTop, clientHeight, scrollHeight } = el
|
||||||
if (scrollHeight - scrollTop - clientHeight < SCROLL_LOAD_THRESHOLD) {
|
if (scrollHeight - scrollTop - clientHeight < SCROLL_LOAD_THRESHOLD) {
|
||||||
loadMore()
|
loadMore()
|
||||||
@ -204,6 +265,15 @@ function checkScrollLoad() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
const cached = getEventListCache()
|
||||||
|
if (cached && cached.list.length > 0) {
|
||||||
|
eventList.value = cached.list
|
||||||
|
eventPage.value = cached.page
|
||||||
|
eventTotal.value = cached.total
|
||||||
|
eventPageSize.value = cached.pageSize
|
||||||
|
} else {
|
||||||
|
loadEvents(0, false)
|
||||||
|
}
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const scrollEl = scrollRef.value
|
const scrollEl = scrollRef.value
|
||||||
const sentinel = sentinelRef.value
|
const sentinel = sentinelRef.value
|
||||||
@ -271,10 +341,17 @@ onUnmounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
padding-bottom: 8px;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-list-empty {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 48px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.home-list > * {
|
.home-list > * {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user