1560 lines
47 KiB
Vue
1560 lines
47 KiB
Vue
<template>
|
||
<view class="match-center-tabs">
|
||
<view class="content-tabs">
|
||
<view v-for="tab in contentTabs" :key="tab.key" class="content-tabs__item"
|
||
:class="{ 'content-tabs__item--active': activeTab === tab.key && !hotLeagueMode }"
|
||
@tap="switchContentTab(tab.key)">
|
||
<text>{{ tab.label }}</text>
|
||
</view>
|
||
<view class="content-tabs__item" :class="{ 'content-tabs__item--active': hotLeagueMode }"
|
||
@tap="openHotLeagues">
|
||
<text>热门联赛</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="activeTab === 'schedule'" class="date-filter">
|
||
<view class="date-filter__previous" @tap="shiftDateWindow(-1)">
|
||
<text>‹</text>
|
||
</view>
|
||
<scroll-view scroll-x class="date-filter__dates" show-scrollbar="false">
|
||
<view class="date-filter__dates-inner">
|
||
<view v-for="item in dateTabs" :key="item.key" class="date-filter__date"
|
||
:class="{ 'date-filter__date--active': selectedDateKey === item.key }"
|
||
@tap="selectMatchDate(item.key)">
|
||
<text class="date-filter__date-main">{{ item.label }}</text>
|
||
<text class="date-filter__date-week">{{ item.week }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<picker mode="date" :value="selectedDateKey" @change="handleDatePickerChange">
|
||
<view class="date-filter__calendar">
|
||
<u-icon name="calendar" size="36" color="#20283a"></u-icon>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<scroll-view v-if="activeTab === 'schedule'" scroll-x class="sport-tabs">
|
||
<view class="sport-tabs__inner">
|
||
<view v-for="(tab, idx) in visibleSportTabs" :key="idx" class="sport-tab"
|
||
:class="{ 'sport-tab--active': currentSport === tab.value }" @tap="switchSport(tab.value)">
|
||
<text class="sport-tab__label">{{ tab.label }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<scroll-view v-if="activeTab === 'schedule' && leagueTabs.length > 0" scroll-x class="league-tabs">
|
||
<view class="league-tabs__inner">
|
||
<view class="league-tab" :class="{ 'league-tab--active': currentLeague === '' }"
|
||
@tap="switchLeague('')">
|
||
<text>全部</text>
|
||
</view>
|
||
<view v-for="(league, idx) in leagueTabs" :key="idx" class="league-tab"
|
||
:class="{ 'league-tab--active': currentLeague === league.league_name }"
|
||
@tap="switchLeague(league.league_name)">
|
||
<text>{{ league.league_name }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<scroll-view scroll-y class="match-content" @scrolltolower="handleScrollToLower">
|
||
<block v-if="activeTab === 'schedule'">
|
||
<view v-for="group in matchGroups" :key="group.title" class="match-group"
|
||
:class="{ 'match-group--ended': group.type === 'ended' }">
|
||
<view class="match-group__header" @tap="toggleGroup(group.type)">
|
||
<view class="match-group__dot" :class="'match-group__dot--' + group.type"></view>
|
||
<text class="match-group__title">{{ group.title }}</text>
|
||
<text class="match-group__count">{{ group.items.length }}场</text>
|
||
<view class="match-group__more"
|
||
:class="{ 'match-group__more--collapsed': collapsedGroups[group.type] }">
|
||
<text>{{ collapsedGroups[group.type] ? '展开' : '查看更多' }}</text>
|
||
<text class="match-group__arrow">›</text>
|
||
</view>
|
||
</view>
|
||
<view v-for="item in group.items" :key="item.id || item.match_id"
|
||
v-show="!collapsedGroups[group.type]" class="match-card-wrap" @tap="goDetail(item)">
|
||
<MatchCenterScheduleCard :match="item" @detail="goDetail(item)" @ai="goAiAnalysis(item)" />
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="scheduleLoading" class="content-loading">
|
||
<u-loading mode="circle" />
|
||
<text>加载中...</text>
|
||
</view>
|
||
<view v-if="!scheduleLoading && !matchGroups.length" class="content-empty">
|
||
<u-empty :text="hotLeagueMode && !leagueTabs.length ? '暂无热门联赛' : '暂无赛事'" mode="data" />
|
||
</view>
|
||
<view v-if="endedNoMore && endedList.length" class="content-nomore">
|
||
<text>没有更多赛事了</text>
|
||
</view>
|
||
</block>
|
||
|
||
<block v-else-if="activeTab === 'live'">
|
||
<view v-if="liveLoading" class="content-loading">
|
||
<u-loading mode="circle" />
|
||
<text>加载中...</text>
|
||
</view>
|
||
<view v-else-if="liveCards.length" class="match-live-grid">
|
||
<view v-for="live in liveCards" :key="`live-${live.live_id}`" class="match-live-card"
|
||
@tap="openLiveCard(live)">
|
||
<view class="match-live-card__player-wrap"
|
||
:class="{ 'match-live-card__player-wrap--poster': useLiveCardPosterMode }">
|
||
<video v-if="!useLiveCardPosterMode" :id="buildLiveVideoId(live)"
|
||
class="match-live-card__player" :src="resolveLivePlayUrl(live)"
|
||
:poster="resolveLivePoster(live)" :autoplay="true" :muted="true"
|
||
:controls="!hideLiveCardControlsOnAndroid"
|
||
:show-play-btn="!hideLiveCardControlsOnAndroid"
|
||
:show-fullscreen-btn="!hideLiveCardControlsOnAndroid" object-fit="cover" />
|
||
<view v-else class="match-live-card__poster">
|
||
<image v-if="resolveLivePoster(live)" class="match-live-card__poster-image"
|
||
:src="resolveLivePoster(live)" mode="aspectFill" />
|
||
<view v-else class="match-live-card__poster-empty">
|
||
<text>{{ livePosterTitle(live) }}</text>
|
||
</view>
|
||
<view class="match-live-card__poster-shade"></view>
|
||
<view class="match-live-card__play">
|
||
<view class="match-live-card__play-icon"></view>
|
||
</view>
|
||
<text class="match-live-card__status">{{ liveStatusText(live) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="match-live-card__teams">
|
||
<text class="match-live-card__line">{{ liveLineTitle(live) }}</text>
|
||
</view>
|
||
|
||
<view class="match-live-card__meta">
|
||
<view class="match-live-card__anchor">
|
||
<image v-if="liveAnchorAvatar(live)" class="match-live-card__avatar"
|
||
:src="liveAnchorAvatar(live)" mode="aspectFill" />
|
||
<view v-else class="match-live-card__avatar match-live-card__avatar--empty">
|
||
<text>主</text>
|
||
</view>
|
||
<text class="match-live-card__anchor-name">{{ liveAnchorName(live) }}</text>
|
||
</view>
|
||
<view class="match-live-card__stats">
|
||
<text class="match-live-card__stat">观看 {{ formatLiveCount(live.room_view_count)
|
||
}}</text>
|
||
<text class="match-live-card__stat">关注 {{ formatLiveCount(live.room_focus_count)
|
||
}}</text>
|
||
</view>
|
||
<text class="match-live-card__notice">{{ liveRoomNotice(live) }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="content-empty">
|
||
<u-empty text="暂无直播" mode="data" />
|
||
</view>
|
||
</block>
|
||
|
||
<block v-else>
|
||
<view class="match-odds-filter">
|
||
<scroll-view scroll-x class="match-odds-filter__tabs match-odds-filter__platforms"
|
||
show-scrollbar="false">
|
||
<view class="match-odds-filter__tabs-inner">
|
||
<view v-for="item in oddsPlatformOptions" :key="item.key" class="match-odds-filter__tab"
|
||
:class="{ 'match-odds-filter__tab--active': oddsSourceSite === item.key }"
|
||
@tap="switchOddsSourceSite(item.key)">
|
||
<text>{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 暂不展示盘口类型筛选,保留状态和接口参数供后续恢复。 -->
|
||
<!-- <scroll-view scroll-x class="match-odds-filter__tabs" show-scrollbar="false">
|
||
<view class="match-odds-filter__tabs-inner">
|
||
<view v-for="item in oddsShowTypeOptions" :key="item.key" class="match-odds-filter__tab"
|
||
:class="{ 'match-odds-filter__tab--active': oddsShowType === item.key }"
|
||
@tap="switchOddsShowType(item.key)">
|
||
<text>{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view> -->
|
||
|
||
<view class="match-odds-filter__search">
|
||
<u-icon name="search" size="30" color="#94a3b8"></u-icon>
|
||
<input v-model="oddsSearchText" class="match-odds-filter__input" confirm-type="search"
|
||
placeholder="搜索联赛/球队" placeholder-class="match-odds-filter__placeholder"
|
||
@confirm="confirmOddsSearch" />
|
||
<view v-if="oddsSearchText || oddsKeyword" class="match-odds-filter__clear"
|
||
@tap="clearOddsKeyword">
|
||
<u-icon name="close" size="24" color="#94a3b8"></u-icon>
|
||
</view>
|
||
<view class="match-odds-filter__button" @tap="confirmOddsSearch">
|
||
<text>筛选</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="match-odds-filter__meta">
|
||
<text>{{ oddsFilterSummary }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="oddsLoading" class="content-loading">
|
||
<u-loading mode="circle" />
|
||
<text>加载中...</text>
|
||
</view>
|
||
<block v-else-if="oddsList.length">
|
||
<view v-for="match in oddsList" :key="oddsMatchKey(match)" class="match-odds-card"
|
||
@tap="goOddsMatchDetail(match)">
|
||
<view class="match-odds-card__head">
|
||
<text class="match-odds-card__league">{{ match.league_name || '赛事赔率' }}</text>
|
||
<text class="match-odds-card__time">{{ oddsTimeText(match) }}</text>
|
||
</view>
|
||
|
||
<view class="match-odds-card__teams">
|
||
<view class="match-odds-card__team">
|
||
<image v-if="match.home_icon" class="match-odds-card__logo" :src="match.home_icon"
|
||
mode="aspectFit" />
|
||
<text class="match-odds-card__team-name">{{ match.home_team }}</text>
|
||
</view>
|
||
<view class="match-odds-card__vs">
|
||
<text>{{ oddsCenterText(match) }}</text>
|
||
</view>
|
||
<view class="match-odds-card__team match-odds-card__team--right">
|
||
<image v-if="match.away_icon" class="match-odds-card__logo" :src="match.away_icon"
|
||
mode="aspectFit" />
|
||
<text class="match-odds-card__team-name">{{ match.away_team }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-for="platform in match.platforms"
|
||
:key="`${oddsMatchKey(match)}-${platform.platform_key}-${platform.source_match_id}`"
|
||
class="match-odds-platform">
|
||
<view class="match-odds-platform__head">
|
||
<view class="match-odds-platform__name">
|
||
<text>{{ platform.platform_name }}</text>
|
||
<text class="match-odds-platform__tag">{{ platform.show_type_text }}</text>
|
||
</view>
|
||
<text class="match-odds-platform__update">{{ formatOddsUpdateTime(platform.update_time)
|
||
}}</text>
|
||
</view>
|
||
|
||
<view v-for="market in platform.markets"
|
||
:key="`${platform.source_match_id}-${market.label}`" class="match-odds-market">
|
||
<view class="match-odds-market__title">
|
||
<text>{{ market.label }}</text>
|
||
<text v-if="market.line" class="match-odds-market__line">{{ market.line }}</text>
|
||
</view>
|
||
<view class="match-odds-market__values">
|
||
<view v-for="item in market.values" :key="`${market.label}-${item.label}`"
|
||
class="match-odds-value">
|
||
<text class="match-odds-value__label">{{ item.label }}</text>
|
||
<text class="match-odds-value__number">{{ item.value }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</block>
|
||
<view v-else class="content-empty">
|
||
<u-empty text="暂无赔率" mode="data" />
|
||
</view>
|
||
</block>
|
||
|
||
<view class="match-content__bottom-space" />
|
||
</scroll-view>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import MatchCenterScheduleCard from './MatchCenterScheduleCard.vue'
|
||
import { getLocalMatchDate } from '@/utils/match-time'
|
||
import {
|
||
getMatchLeagues,
|
||
getMatchList,
|
||
getMatchLiveCards,
|
||
getMatchOdds,
|
||
getSportTypes,
|
||
type MatchLiveCardItem,
|
||
type MatchOddsMatchItem,
|
||
} from '@/api/match'
|
||
|
||
type MatchCenterTab = 'schedule' | 'live' | 'odds'
|
||
type OddsShowType = '' | 'live' | 'today' | 'early'
|
||
type OddsPlatformOption = { key: string; label: string }
|
||
|
||
const showStandaloneOddsTab = false
|
||
|
||
const allContentTabs: { key: MatchCenterTab; label: string }[] = [
|
||
{ key: 'schedule', label: '赛程' },
|
||
{ key: 'live', label: '直播' },
|
||
{ key: 'odds', label: '赔率' },
|
||
]
|
||
const contentTabs = allContentTabs.filter((item) => showStandaloneOddsTab || item.key !== 'odds')
|
||
|
||
const oddsShowTypeOptions: { key: OddsShowType; label: string }[] = [
|
||
{ key: '', label: '全部盘口' },
|
||
{ key: 'live', label: '滚球盘' },
|
||
{ key: 'today', label: '今日盘' },
|
||
{ key: 'early', label: '早盘' },
|
||
]
|
||
|
||
const defaultOddsPlatformOptions: OddsPlatformOption[] = [
|
||
{ key: '', label: '全部平台' },
|
||
{ key: 'hg', label: '皇冠' },
|
||
{ key: 'titan007', label: '球探' },
|
||
]
|
||
|
||
const activeTab = ref<MatchCenterTab>('schedule')
|
||
const hotLeagueMode = ref(false)
|
||
const sportTabs = ref<any[]>([])
|
||
const currentSport = ref(0)
|
||
const currentLeague = ref('')
|
||
const allLeagues = ref<Record<number, any[]>>({})
|
||
const selectedDateKey = ref('')
|
||
const dateWindowAnchorKey = ref('')
|
||
|
||
const liveList = ref<any[]>([])
|
||
const upcomingList = ref<any[]>([])
|
||
const endedList = ref<any[]>([])
|
||
const endedPage = ref(1)
|
||
const endedPageSize = 20
|
||
const endedNoMore = ref(false)
|
||
const scheduleLoading = ref(false)
|
||
|
||
const liveCards = ref<MatchLiveCardItem[]>([])
|
||
const liveLoading = ref(false)
|
||
const liveLoaded = ref(false)
|
||
const hideLiveCardControlsOnAndroid = ref(false)
|
||
const useLiveCardPosterMode = ref(false)
|
||
|
||
const oddsList = ref<MatchOddsMatchItem[]>([])
|
||
const oddsLoading = ref(false)
|
||
const oddsLoaded = ref(false)
|
||
const oddsPlatformOptions = ref<OddsPlatformOption[]>([...defaultOddsPlatformOptions])
|
||
const oddsSourceSite = ref('')
|
||
const oddsShowType = ref<OddsShowType>('')
|
||
const oddsKeyword = ref('')
|
||
const oddsSearchText = ref('')
|
||
const oddsRequestSeq = ref(0)
|
||
|
||
const collapsedGroups = ref<Record<string, boolean>>({
|
||
live: false,
|
||
upcoming: false,
|
||
postponed: false,
|
||
ended: false,
|
||
other: false,
|
||
})
|
||
|
||
const formatDateKey = (date: Date) => {
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
return `${year}-${month}-${day}`
|
||
}
|
||
|
||
const parseDateKey = (key: string) => {
|
||
const [year, month, day] = String(key || '').split('-').map(Number)
|
||
if (!year || !month || !day) return new Date()
|
||
return new Date(year, month - 1, day)
|
||
}
|
||
|
||
const addDateDays = (key: string, days: number) => {
|
||
const date = parseDateKey(key)
|
||
date.setDate(date.getDate() + days)
|
||
return formatDateKey(date)
|
||
}
|
||
|
||
const getMatchDateKey = (match: any) => {
|
||
const date = getLocalMatchDate(match?.match_time, match)
|
||
return date ? formatDateKey(date) : ''
|
||
}
|
||
|
||
const dateTabs = computed(() => {
|
||
const anchorKey = dateWindowAnchorKey.value || selectedDateKey.value || formatDateKey(new Date())
|
||
return [-3, -2, -1, 0, 1, 2, 3].map((offset) => {
|
||
const key = addDateDays(anchorKey, offset)
|
||
const date = parseDateKey(key)
|
||
const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][date.getDay()]
|
||
return {
|
||
key,
|
||
label: `${date.getMonth() + 1}/${date.getDate()}`,
|
||
week,
|
||
}
|
||
})
|
||
})
|
||
|
||
const selectMatchDate = (key: string) => {
|
||
selectedDateKey.value = key
|
||
}
|
||
|
||
const shiftDateWindow = (days: number) => {
|
||
const anchorKey = dateWindowAnchorKey.value || selectedDateKey.value || formatDateKey(new Date())
|
||
const nextKey = addDateDays(anchorKey, days)
|
||
dateWindowAnchorKey.value = nextKey
|
||
selectedDateKey.value = nextKey
|
||
}
|
||
|
||
const handleDatePickerChange = (event: any) => {
|
||
const key = String(event?.detail?.value || '')
|
||
if (!key) return
|
||
dateWindowAnchorKey.value = key
|
||
selectedDateKey.value = key
|
||
}
|
||
|
||
const ensureSelectedMatchDate = () => {
|
||
const allMatches = [...liveList.value, ...upcomingList.value, ...endedList.value]
|
||
const availableKeys = new Set(allMatches.map(getMatchDateKey).filter(Boolean))
|
||
if (selectedDateKey.value && availableKeys.has(selectedDateKey.value)) return
|
||
|
||
const todayKey = formatDateKey(new Date())
|
||
const preferredMatch = liveList.value[0] || upcomingList.value[0] || endedList.value[0]
|
||
const preferredKey = availableKeys.has(todayKey) ? todayKey : getMatchDateKey(preferredMatch)
|
||
selectedDateKey.value = preferredKey || todayKey
|
||
dateWindowAnchorKey.value = selectedDateKey.value
|
||
}
|
||
|
||
const filterScheduleItems = (items: any[]) => {
|
||
return selectedDateKey.value
|
||
? items.filter((item: any) => getMatchDateKey(item) === selectedDateKey.value)
|
||
: items
|
||
}
|
||
|
||
const leagueTabs = computed(() => {
|
||
let leagues: any[] = []
|
||
if (currentSport.value === 0) {
|
||
Object.keys(allLeagues.value || {}).forEach((key) => {
|
||
const items = (allLeagues.value as any)[key]
|
||
if (Array.isArray(items)) leagues.push(...items)
|
||
})
|
||
} else {
|
||
leagues = allLeagues.value[currentSport.value] || []
|
||
}
|
||
if (hotLeagueMode.value) {
|
||
leagues = leagues.filter((item: any) => Number(item?.is_hot) === 1)
|
||
}
|
||
return leagues
|
||
})
|
||
|
||
const visibleSportTabs = computed(() => {
|
||
if (!hotLeagueMode.value) return sportTabs.value
|
||
|
||
const hotSportTypes = new Set<number>()
|
||
Object.keys(allLeagues.value).forEach((sportType) => {
|
||
const leagues = allLeagues.value[Number(sportType)] || []
|
||
if (leagues.some((league: any) => Number(league?.is_hot) === 1)) {
|
||
hotSportTypes.add(Number(sportType))
|
||
}
|
||
})
|
||
|
||
return sportTabs.value.filter((tab: any) => Number(tab?.value) === 0 || hotSportTypes.has(Number(tab?.value)))
|
||
})
|
||
|
||
const matchGroups = computed(() => {
|
||
const groups: { title: string; type: string; items: any[]; order: number }[] = []
|
||
const visibleLiveList = filterScheduleItems(liveList.value)
|
||
const visibleUpcomingList = filterScheduleItems(upcomingList.value)
|
||
const visibleEndedList = filterScheduleItems(endedList.value)
|
||
if (visibleLiveList.length > 0) {
|
||
groups.push({ title: '进行中', type: 'live', items: visibleLiveList, order: 0 })
|
||
}
|
||
if (visibleUpcomingList.length > 0) {
|
||
groups.push({ title: '即将开始', type: 'upcoming', items: visibleUpcomingList, order: 1 })
|
||
}
|
||
if (visibleEndedList.length > 0) {
|
||
groups.push({ title: '已结束', type: 'ended', items: visibleEndedList, order: 2 })
|
||
}
|
||
return groups
|
||
})
|
||
|
||
const oddsFilterSummary = computed(() => {
|
||
const platformLabel = oddsPlatformOptions.value.find(
|
||
(item) => item.key === oddsSourceSite.value,
|
||
)?.label || '全部平台'
|
||
const showTypeLabel = oddsShowTypeOptions.find((item) => item.key === oddsShowType.value)?.label || '全部盘口'
|
||
const keyword = oddsKeyword.value.trim()
|
||
const parts = [platformLabel, showTypeLabel]
|
||
if (keyword) parts.push(`"${keyword}"`)
|
||
parts.push(`${oddsList.value.length}场`)
|
||
return parts.join(' · ')
|
||
})
|
||
|
||
const initLayout = () => {
|
||
const info = uni.getSystemInfoSync()
|
||
const isAndroidPlatform = String(info.platform || '').toLowerCase() === 'android'
|
||
hideLiveCardControlsOnAndroid.value = isAndroidPlatform
|
||
useLiveCardPosterMode.value = false
|
||
// #ifdef APP-PLUS
|
||
useLiveCardPosterMode.value = isAndroidPlatform
|
||
// #endif
|
||
}
|
||
|
||
const fetchSportTypes = async () => {
|
||
try {
|
||
const types = await getSportTypes() || []
|
||
sportTabs.value = [
|
||
{ value: 0, label: '全部' },
|
||
...types.filter((item: any) => Number(item?.value || 0) > 0),
|
||
]
|
||
} catch (error) {
|
||
console.error('获取运动类型失败', error)
|
||
}
|
||
}
|
||
|
||
const fetchLeagues = async () => {
|
||
try {
|
||
allLeagues.value = await getMatchLeagues() || {}
|
||
} catch (error) {
|
||
console.error('获取联赛列表失败', error)
|
||
}
|
||
}
|
||
|
||
const fetchMatchList = async (refresh = false) => {
|
||
if (scheduleLoading.value) return
|
||
if (refresh) {
|
||
endedPage.value = 1
|
||
endedNoMore.value = false
|
||
liveList.value = []
|
||
upcomingList.value = []
|
||
endedList.value = []
|
||
}
|
||
scheduleLoading.value = true
|
||
try {
|
||
const params: Record<string, any> = {
|
||
ended_page: endedPage.value,
|
||
ended_page_size: endedPageSize,
|
||
}
|
||
if (currentSport.value > 0) params.sport_type = currentSport.value
|
||
if (currentLeague.value) params.league_name = currentLeague.value
|
||
if (hotLeagueMode.value && !currentLeague.value) params.hot_only = 1
|
||
if (!refresh) params.ended_only = 1
|
||
const data = await getMatchList(params)
|
||
if (refresh) {
|
||
liveList.value = data?.live || []
|
||
upcomingList.value = data?.upcoming || []
|
||
}
|
||
const newEnded = data?.ended?.lists || []
|
||
endedList.value = refresh ? newEnded : endedList.value.concat(newEnded)
|
||
ensureSelectedMatchDate()
|
||
if (newEnded.length < endedPageSize) endedNoMore.value = true
|
||
} catch (error) {
|
||
console.error('获取赛事失败', error)
|
||
} finally {
|
||
scheduleLoading.value = false
|
||
}
|
||
}
|
||
|
||
const fetchLiveCards = async (force = false) => {
|
||
if (!force && (liveLoaded.value || liveLoading.value)) return
|
||
liveLoading.value = true
|
||
try {
|
||
const data = await getMatchLiveCards()
|
||
liveCards.value = Array.isArray(data?.list) ? data.list : []
|
||
liveLoaded.value = true
|
||
} catch (error) {
|
||
console.error('获取直播失败', error)
|
||
liveCards.value = []
|
||
} finally {
|
||
liveLoading.value = false
|
||
}
|
||
}
|
||
|
||
const buildOddsRequestParams = () => {
|
||
const params: {
|
||
sport_type?: number
|
||
league_name?: string
|
||
show_type?: 'live' | 'today' | 'early'
|
||
source_site?: string
|
||
keyword?: string
|
||
include_special: number
|
||
limit: number
|
||
} = {
|
||
include_special: 0,
|
||
limit: 300,
|
||
}
|
||
if (currentSport.value > 0) params.sport_type = currentSport.value
|
||
if (currentLeague.value) params.league_name = currentLeague.value
|
||
if (oddsShowType.value) params.show_type = oddsShowType.value
|
||
if (oddsSourceSite.value) params.source_site = oddsSourceSite.value
|
||
const keyword = oddsKeyword.value.trim()
|
||
if (keyword) params.keyword = keyword
|
||
return params
|
||
}
|
||
|
||
const fetchOdds = async (force = false) => {
|
||
if (!force && (oddsLoaded.value || oddsLoading.value)) return
|
||
const requestSeq = oddsRequestSeq.value + 1
|
||
oddsRequestSeq.value = requestSeq
|
||
oddsLoading.value = true
|
||
try {
|
||
const data = await getMatchOdds(buildOddsRequestParams())
|
||
if (requestSeq === oddsRequestSeq.value) {
|
||
oddsList.value = Array.isArray(data?.list) ? data.list : []
|
||
const platformOptions = Array.isArray(data?.platform_options)
|
||
? data.platform_options
|
||
: []
|
||
const platformMap = new Map(
|
||
defaultOddsPlatformOptions.map((item) => [item.key, item]),
|
||
)
|
||
platformOptions.forEach((item: any) => {
|
||
if (item?.key) {
|
||
const key = String(item.key)
|
||
platformMap.set(key, {
|
||
key,
|
||
label: key === 'hg' ? '皇冠' : String(item.label || item.key).trim(),
|
||
})
|
||
}
|
||
})
|
||
oddsPlatformOptions.value = Array.from(platformMap.values())
|
||
oddsLoaded.value = true
|
||
}
|
||
} catch (error) {
|
||
console.error('获取赔率失败', error)
|
||
if (requestSeq === oddsRequestSeq.value) oddsList.value = []
|
||
} finally {
|
||
if (requestSeq === oddsRequestSeq.value) oddsLoading.value = false
|
||
}
|
||
}
|
||
|
||
const refreshCurrentTab = () => {
|
||
if (activeTab.value === 'schedule') {
|
||
void fetchMatchList(true)
|
||
} else if (activeTab.value === 'live') {
|
||
liveLoaded.value = false
|
||
void fetchLiveCards(true)
|
||
} else {
|
||
oddsLoaded.value = false
|
||
void fetchOdds(true)
|
||
}
|
||
}
|
||
|
||
const switchContentTab = (tab: MatchCenterTab) => {
|
||
if (activeTab.value === tab && !hotLeagueMode.value) return
|
||
hotLeagueMode.value = false
|
||
activeTab.value = tab
|
||
if (tab === 'schedule') void fetchMatchList(true)
|
||
if (tab === 'live') void fetchLiveCards()
|
||
if (tab === 'odds') void fetchOdds()
|
||
}
|
||
|
||
const openHotLeagues = () => {
|
||
if (hotLeagueMode.value) return
|
||
hotLeagueMode.value = true
|
||
activeTab.value = 'schedule'
|
||
currentSport.value = 0
|
||
currentLeague.value = ''
|
||
void fetchMatchList(true)
|
||
}
|
||
|
||
const switchSport = (value: number) => {
|
||
if (currentSport.value === value) return
|
||
currentSport.value = value
|
||
currentLeague.value = ''
|
||
liveLoaded.value = false
|
||
oddsLoaded.value = false
|
||
refreshCurrentTab()
|
||
}
|
||
|
||
const switchLeague = (name: string) => {
|
||
if (currentLeague.value === name) return
|
||
currentLeague.value = name
|
||
liveLoaded.value = false
|
||
oddsLoaded.value = false
|
||
refreshCurrentTab()
|
||
}
|
||
|
||
const toggleGroup = (type: string) => {
|
||
collapsedGroups.value[type] = !collapsedGroups.value[type]
|
||
}
|
||
|
||
const handleScrollToLower = () => {
|
||
if (activeTab.value !== 'schedule' || endedNoMore.value || scheduleLoading.value) return
|
||
endedPage.value++
|
||
void fetchMatchList()
|
||
}
|
||
|
||
const goDetail = (item: any) => {
|
||
const id = item?.id || item?.match_id
|
||
if (!id) return
|
||
uni.navigateTo({ url: `/pages/match_detail/match_detail?id=${id}` })
|
||
}
|
||
|
||
const goAiAnalysis = (item: any) => {
|
||
const id = item?.id || item?.match_id
|
||
if (!id) return
|
||
uni.navigateTo({ url: `/pages/ai_analysis/ai_analysis?type=match&id=${id}` })
|
||
}
|
||
|
||
const goOddsMatchDetail = (match: MatchOddsMatchItem) => {
|
||
if (!match?.id) return
|
||
goDetail(match)
|
||
}
|
||
|
||
const switchOddsShowType = (showType: OddsShowType) => {
|
||
if (oddsShowType.value === showType) return
|
||
oddsShowType.value = showType
|
||
oddsLoaded.value = false
|
||
void fetchOdds(true)
|
||
}
|
||
|
||
const switchOddsSourceSite = (sourceSite: string) => {
|
||
if (oddsSourceSite.value === sourceSite) return
|
||
oddsSourceSite.value = sourceSite
|
||
oddsLoaded.value = false
|
||
void fetchOdds(true)
|
||
}
|
||
|
||
const confirmOddsSearch = () => {
|
||
const keyword = oddsSearchText.value.trim()
|
||
if (oddsKeyword.value === keyword && oddsLoaded.value) return
|
||
oddsKeyword.value = keyword
|
||
oddsLoaded.value = false
|
||
void fetchOdds(true)
|
||
}
|
||
|
||
const clearOddsKeyword = () => {
|
||
if (!oddsSearchText.value && !oddsKeyword.value) return
|
||
oddsSearchText.value = ''
|
||
oddsKeyword.value = ''
|
||
oddsLoaded.value = false
|
||
void fetchOdds(true)
|
||
}
|
||
|
||
const normalizeLiveText = (value: unknown) => String(value || '').trim()
|
||
|
||
const liveLineTitle = (live: MatchLiveCardItem) => {
|
||
const teams = [normalizeLiveText(live.home_team), normalizeLiveText(live.away_team)].filter(Boolean)
|
||
return teams.length ? teams.join(' VS ') : normalizeLiveText(live.title) || '直播线路'
|
||
}
|
||
|
||
const liveAnchorAvatar = (live: MatchLiveCardItem) => normalizeLiveText(live.anchor_avatar)
|
||
const liveAnchorName = (live: MatchLiveCardItem) => normalizeLiveText(live.anchor_name) || '主播'
|
||
const liveRoomNotice = (live: MatchLiveCardItem) => normalizeLiveText(live.room_notice) || '暂无公告'
|
||
|
||
const resolveLivePoster = (live: MatchLiveCardItem) => {
|
||
return liveAnchorAvatar(live) || normalizeLiveText(live.home_icon) || normalizeLiveText(live.away_icon)
|
||
}
|
||
|
||
const livePosterTitle = (live: MatchLiveCardItem) => liveLineTitle(live)
|
||
|
||
const formatLiveCount = (value: unknown) => {
|
||
const count = Number(value || 0)
|
||
if (!Number.isFinite(count) || count <= 0) return '0'
|
||
if (count >= 10000) {
|
||
return `${(count / 10000).toFixed(count >= 100000 ? 0 : 1).replace(/\.0$/, '')}万`
|
||
}
|
||
return String(Math.floor(count))
|
||
}
|
||
|
||
const buildLiveVideoId = (live: MatchLiveCardItem) => `match-live-${live.live_id || live.id || 0}`
|
||
|
||
const resolveLivePlayUrl = (live: MatchLiveCardItem) => {
|
||
const urls: string[] = [live.play_url, live.default_play_url].map(normalizeLiveText).filter(Boolean)
|
||
if (Array.isArray(live.stream_options)) {
|
||
live.stream_options.forEach((option) => {
|
||
urls.push(
|
||
normalizeLiveText(option?.url),
|
||
normalizeLiveText(option?.play_url),
|
||
normalizeLiveText(option?.play_url_m3u8),
|
||
normalizeLiveText(option?.play_url_hd_m3u8),
|
||
normalizeLiveText(option?.play_url_flv),
|
||
normalizeLiveText(option?.play_url_hd_flv),
|
||
)
|
||
})
|
||
}
|
||
return urls.find((url) => /\.(m3u8|mp4|flv)(\?|$)/i.test(url)) || urls[0] || ''
|
||
}
|
||
|
||
const resolveLiveSourceUrl = (live: MatchLiveCardItem) => {
|
||
return normalizeLiveText(live.source_url) || resolveLivePlayUrl(live)
|
||
}
|
||
|
||
const liveStatusText = (live: MatchLiveCardItem) => {
|
||
if ((live.status ?? 0) === 1) return normalizeLiveText(live.current_minute) || '进行中'
|
||
return '未开赛'
|
||
}
|
||
|
||
const openLiveCard = (live: MatchLiveCardItem) => {
|
||
const playUrl = resolveLivePlayUrl(live)
|
||
const sourceUrl = resolveLiveSourceUrl(live)
|
||
if (!playUrl && !sourceUrl) {
|
||
uni.showToast({ title: '暂无直播链接', icon: 'none' })
|
||
return
|
||
}
|
||
if (useLiveCardPosterMode.value) {
|
||
uni.navigateTo({ url: `/pages/webview/webview?url=${encodeURIComponent(sourceUrl || playUrl)}` })
|
||
return
|
||
}
|
||
try {
|
||
const videoContext = uni.createVideoContext(buildLiveVideoId(live))
|
||
videoContext?.requestFullScreen?.({ direction: 90 })
|
||
videoContext?.play?.()
|
||
} catch {
|
||
uni.showToast({ title: '当前设备暂不支持全屏', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
const formatMatchTime = (timestamp?: number) => {
|
||
const value = Number(timestamp || 0)
|
||
if (!value) return '--'
|
||
const date = new Date(value * 1000)
|
||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||
}
|
||
|
||
const oddsMatchKey = (match: MatchOddsMatchItem) => {
|
||
const sourceId = match.source_match_ids?.[0] || ''
|
||
return `${match.home_team}-${match.away_team}-${match.match_time_text || match.match_time || sourceId}`
|
||
}
|
||
|
||
const oddsCenterText = (match: MatchOddsMatchItem) => {
|
||
if (match.status === 1) return match.current_minute || '进行中'
|
||
if (match.status === 2) return `${match.home_score || 0}-${match.away_score || 0}`
|
||
return 'VS'
|
||
}
|
||
|
||
const oddsTimeText = (match: MatchOddsMatchItem) => {
|
||
return match.match_time ? formatMatchTime(match.match_time) : match.match_time_text || '--'
|
||
}
|
||
|
||
const formatOddsUpdateTime = (timestamp?: number) => {
|
||
const value = Number(timestamp || 0)
|
||
if (!value) return ''
|
||
const date = new Date(value * 1000)
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hour = String(date.getHours()).padStart(2, '0')
|
||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||
return `${month}-${day} ${hour}:${minute}`
|
||
}
|
||
|
||
onMounted(() => {
|
||
initLayout()
|
||
void fetchSportTypes()
|
||
void fetchLeagues()
|
||
void fetchMatchList(true)
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.match-center-tabs {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #f7f8fb;
|
||
font-family: "PingFang SC", "Noto Sans SC", sans-serif;
|
||
}
|
||
|
||
.content-tabs {
|
||
margin: 0 24rpx 10rpx;
|
||
padding: 6rpx;
|
||
display: flex;
|
||
gap: 6rpx;
|
||
border: 1rpx solid #dfe3eb;
|
||
border-radius: 18rpx;
|
||
background: #fff;
|
||
|
||
&__item {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 56rpx;
|
||
border-radius: 14rpx;
|
||
color: #404654;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
line-height: 56rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
&__item--active {
|
||
background: linear-gradient(110deg, #1875ff 0%, #0863f3 100%);
|
||
color: #fff;
|
||
box-shadow: 0 6rpx 14rpx rgba(20, 105, 246, 0.16);
|
||
}
|
||
}
|
||
|
||
.date-filter {
|
||
height: 76rpx;
|
||
padding: 0 24rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
background: #fff;
|
||
|
||
&__previous,
|
||
&__calendar {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
&__previous {
|
||
color: #20283a;
|
||
font-size: 48rpx;
|
||
font-weight: 300;
|
||
line-height: 1;
|
||
}
|
||
|
||
&__calendar {
|
||
border: 1rpx solid #e0e4eb;
|
||
border-radius: 12rpx;
|
||
background: #fff;
|
||
}
|
||
|
||
&__dates {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 76rpx;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__dates-inner {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
height: 76rpx;
|
||
min-width: 100%;
|
||
padding: 0 2rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
&__date {
|
||
position: relative;
|
||
width: 112rpx;
|
||
flex: 0 0 112rpx;
|
||
height: 56rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5rpx;
|
||
border-radius: 12rpx;
|
||
color: #5b606b;
|
||
|
||
&::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 50%;
|
||
bottom: -7rpx;
|
||
width: 28rpx;
|
||
height: 4rpx;
|
||
border-radius: 999rpx;
|
||
background: transparent;
|
||
transform: translateX(-50%);
|
||
}
|
||
|
||
&--active {
|
||
background: linear-gradient(110deg, #1875ff 0%, #0863f3 100%);
|
||
color: #fff;
|
||
|
||
&::after {
|
||
background: #156df6;
|
||
}
|
||
}
|
||
}
|
||
|
||
&__date-main {
|
||
font-size: 25rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
&__date-week {
|
||
font-size: 19rpx;
|
||
}
|
||
}
|
||
|
||
.sport-tabs {
|
||
white-space: nowrap;
|
||
height: 76rpx;
|
||
background: #fff;
|
||
}
|
||
|
||
.sport-tabs__inner {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 76rpx;
|
||
padding: 0 24rpx;
|
||
gap: 16rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.sport-tab {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 96rpx;
|
||
height: 50rpx;
|
||
padding: 0 14rpx;
|
||
border-radius: 12rpx;
|
||
background: #f5f6f9;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
|
||
&__label {
|
||
font-size: 24rpx;
|
||
color: #313745;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
&--active {
|
||
background: linear-gradient(110deg, #1875ff 0%, #0863f3 100%);
|
||
box-shadow: 0 8rpx 18rpx rgba(20, 105, 246, 0.17);
|
||
|
||
.sport-tab__label {
|
||
color: #fff;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
}
|
||
|
||
.league-tabs {
|
||
white-space: nowrap;
|
||
background: #fff;
|
||
border-bottom: 1rpx solid #eceef3;
|
||
}
|
||
|
||
.league-tabs__inner {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 0 24rpx 14rpx;
|
||
gap: 16rpx;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.league-tab {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 96rpx;
|
||
height: 50rpx;
|
||
padding: 0 14rpx;
|
||
border: 1rpx solid transparent;
|
||
border-radius: 12rpx;
|
||
font-size: 23rpx;
|
||
color: #3f4552;
|
||
background: #f5f6f9;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
box-sizing: border-box;
|
||
|
||
&--active {
|
||
border-color: #1470f8;
|
||
background: #fff;
|
||
color: #1470f8;
|
||
font-weight: 700;
|
||
}
|
||
|
||
}
|
||
|
||
.match-content {
|
||
flex: 1;
|
||
height: 0;
|
||
min-height: 0;
|
||
background: #f7f8fb;
|
||
}
|
||
|
||
.match-group {
|
||
padding-top: 8rpx;
|
||
|
||
&__header {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10rpx 24rpx;
|
||
gap: 8rpx;
|
||
background: #f7f8fb;
|
||
}
|
||
|
||
&__dot {
|
||
width: 14rpx;
|
||
height: 14rpx;
|
||
border-radius: 50%;
|
||
|
||
&--live {
|
||
background: #10b981;
|
||
}
|
||
|
||
&--upcoming {
|
||
background: #185dff;
|
||
}
|
||
|
||
&--ended {
|
||
background: #999;
|
||
}
|
||
}
|
||
|
||
&__title {
|
||
font-size: 28rpx;
|
||
font-weight: 800;
|
||
color: #222735;
|
||
}
|
||
|
||
&__count {
|
||
font-size: 21rpx;
|
||
color: #7f8592;
|
||
}
|
||
|
||
&__more {
|
||
margin-left: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6rpx;
|
||
color: #747b88;
|
||
font-size: 21rpx;
|
||
}
|
||
|
||
&__arrow {
|
||
color: #737b89;
|
||
font-size: 34rpx;
|
||
font-weight: 300;
|
||
line-height: 1;
|
||
transition: transform 0.25s ease;
|
||
}
|
||
|
||
&__more--collapsed &__arrow {
|
||
transform: rotate(-90deg);
|
||
}
|
||
}
|
||
|
||
.match-card-wrap {
|
||
position: relative;
|
||
margin: 0 24rpx 14rpx;
|
||
}
|
||
|
||
.match-content__bottom-space {
|
||
height: calc(100rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.content-loading,
|
||
.content-empty,
|
||
.content-nomore {
|
||
padding: 50rpx 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10rpx;
|
||
color: #8b93a7;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.content-loading {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.content-nomore {
|
||
padding: 30rpx 0;
|
||
}
|
||
|
||
.match-live-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
padding: 12rpx 12rpx 0;
|
||
}
|
||
|
||
.match-live-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
width: calc(50% - 12rpx);
|
||
min-width: 0;
|
||
box-sizing: border-box;
|
||
margin: 0 6rpx 12rpx;
|
||
padding: 16rpx;
|
||
border-radius: 22rpx;
|
||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||
box-shadow: 0 8rpx 24rpx rgba(37, 99, 235, 0.08);
|
||
}
|
||
|
||
.match-live-card__player-wrap {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 210rpx;
|
||
border-radius: 18rpx;
|
||
overflow: hidden;
|
||
background: #0f172a;
|
||
}
|
||
|
||
.match-live-card__player-wrap--poster {
|
||
background: linear-gradient(135deg, #0f172a 0%, #1e40af 100%);
|
||
}
|
||
|
||
.match-live-card__player,
|
||
.match-live-card__poster,
|
||
.match-live-card__poster-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
.match-live-card__poster {
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.match-live-card__poster-empty {
|
||
width: 100%;
|
||
height: 100%;
|
||
padding: 18rpx;
|
||
box-sizing: border-box;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
color: rgba(255, 255, 255, 0.86);
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.match-live-card__poster-shade {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
background: rgba(15, 23, 42, 0.28);
|
||
}
|
||
|
||
.match-live-card__play {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
width: 58rpx;
|
||
height: 58rpx;
|
||
border-radius: 50%;
|
||
transform: translate(-50%, -50%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.24);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.78);
|
||
}
|
||
|
||
.match-live-card__play-icon {
|
||
width: 0;
|
||
height: 0;
|
||
margin-left: 4rpx;
|
||
border-top: 12rpx solid transparent;
|
||
border-bottom: 12rpx solid transparent;
|
||
border-left: 18rpx solid #fff;
|
||
}
|
||
|
||
.match-live-card__status {
|
||
position: absolute;
|
||
right: 12rpx;
|
||
top: 12rpx;
|
||
padding: 0 12rpx;
|
||
border-radius: 999rpx;
|
||
background: rgba(15, 23, 42, 0.58);
|
||
color: #fff;
|
||
font-size: 20rpx;
|
||
line-height: 34rpx;
|
||
}
|
||
|
||
.match-live-card__teams,
|
||
.match-live-card__anchor,
|
||
.match-live-card__stats {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.match-live-card__line {
|
||
min-width: 0;
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.match-live-card__meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
min-width: 0;
|
||
color: #64748b;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.match-live-card__anchor {
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.match-live-card__avatar {
|
||
width: 34rpx;
|
||
height: 34rpx;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
background: #e2e8f0;
|
||
}
|
||
|
||
.match-live-card__avatar--empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18rpx;
|
||
}
|
||
|
||
.match-live-card__anchor-name,
|
||
.match-live-card__notice {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.match-live-card__anchor-name {
|
||
flex: 1;
|
||
color: #334155;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.match-live-card__stats {
|
||
flex-wrap: wrap;
|
||
gap: 8rpx 12rpx;
|
||
}
|
||
|
||
.match-odds-filter {
|
||
padding: 14rpx 18rpx 8rpx;
|
||
}
|
||
|
||
.match-odds-filter__tabs {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.match-odds-filter__platforms {
|
||
margin-bottom: 12rpx;
|
||
}
|
||
|
||
.match-odds-filter__tabs-inner {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.match-odds-filter__tab {
|
||
height: 56rpx;
|
||
padding: 0 22rpx;
|
||
border-radius: 14rpx;
|
||
background: #f1f5f9;
|
||
color: #475569;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
line-height: 56rpx;
|
||
}
|
||
|
||
.match-odds-filter__tab--active {
|
||
background: #1d4ed8;
|
||
color: #fff;
|
||
}
|
||
|
||
.match-odds-filter__search {
|
||
height: 72rpx;
|
||
margin-top: 14rpx;
|
||
padding: 0 10rpx 0 18rpx;
|
||
border: 1rpx solid #e2e8f0;
|
||
border-radius: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.match-odds-filter__input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 70rpx;
|
||
color: #111827;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.match-odds-filter__placeholder {
|
||
color: #94a3b8;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.match-odds-filter__clear {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
border-radius: 14rpx;
|
||
background: #f1f5f9;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.match-odds-filter__button {
|
||
width: 92rpx;
|
||
height: 52rpx;
|
||
border-radius: 14rpx;
|
||
background: #0f172a;
|
||
color: #fff;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
line-height: 52rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.match-odds-filter__meta {
|
||
margin-top: 10rpx;
|
||
color: #64748b;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.match-odds-card {
|
||
margin: 12rpx 18rpx;
|
||
padding: 18rpx 16rpx;
|
||
border-radius: 18rpx;
|
||
background: #fff;
|
||
box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.06);
|
||
}
|
||
|
||
.match-odds-card__head,
|
||
.match-odds-platform__head,
|
||
.match-odds-platform__name,
|
||
.match-odds-card__teams,
|
||
.match-odds-card__team {
|
||
display: flex;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.match-odds-card__head,
|
||
.match-odds-platform__head {
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.match-odds-card__league {
|
||
flex: 1;
|
||
min-width: 0;
|
||
color: #64748b;
|
||
font-size: 22rpx;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.match-odds-card__time,
|
||
.match-odds-platform__update {
|
||
color: #94a3b8;
|
||
font-size: 20rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.match-odds-card__teams {
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
padding: 16rpx 0 12rpx;
|
||
}
|
||
|
||
.match-odds-card__team {
|
||
flex: 1;
|
||
gap: 8rpx;
|
||
|
||
&--right {
|
||
justify-content: flex-end;
|
||
}
|
||
}
|
||
|
||
.match-odds-card__logo {
|
||
width: 42rpx;
|
||
height: 42rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.match-odds-card__team-name {
|
||
min-width: 0;
|
||
color: #111827;
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.match-odds-card__vs {
|
||
width: 104rpx;
|
||
flex-shrink: 0;
|
||
text-align: center;
|
||
color: #2563eb;
|
||
font-size: 28rpx;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.match-odds-platform {
|
||
padding-top: 14rpx;
|
||
border-top: 1rpx solid #eef2f7;
|
||
|
||
&+& {
|
||
margin-top: 14rpx;
|
||
}
|
||
}
|
||
|
||
.match-odds-platform__name {
|
||
gap: 8rpx;
|
||
color: #0f172a;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.match-odds-platform__tag {
|
||
padding: 0 10rpx;
|
||
border-radius: 999rpx;
|
||
background: #eaf1ff;
|
||
color: #2563eb;
|
||
font-size: 20rpx;
|
||
line-height: 32rpx;
|
||
}
|
||
|
||
.match-odds-market {
|
||
display: grid;
|
||
grid-template-columns: 104rpx minmax(0, 1fr);
|
||
gap: 10rpx;
|
||
margin-top: 10rpx;
|
||
}
|
||
|
||
.match-odds-market__title {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 4rpx;
|
||
color: #475569;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.match-odds-market__line {
|
||
color: #0f172a;
|
||
font-size: 24rpx;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.match-odds-market__values {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.match-odds-value {
|
||
min-width: 0;
|
||
padding: 10rpx 8rpx;
|
||
border-radius: 12rpx;
|
||
background: #f8fafc;
|
||
text-align: center;
|
||
}
|
||
|
||
.match-odds-value__label,
|
||
.match-odds-value__number {
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.match-odds-value__label {
|
||
color: #64748b;
|
||
font-size: 20rpx;
|
||
}
|
||
|
||
.match-odds-value__number {
|
||
margin-top: 4rpx;
|
||
color: #dc2626;
|
||
font-size: 26rpx;
|
||
font-weight: 800;
|
||
}
|
||
</style>
|