feat: redesign match center schedule page
This commit is contained in:
@@ -3,25 +3,13 @@
|
||||
<view class="match-header" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="match-header__title">
|
||||
<text class="match-header__title-text">赛事中心</text>
|
||||
<AiAssistantEntry size="compact" />
|
||||
</view>
|
||||
|
||||
<view class="topic-entry" @tap="openWorldCupCenter">
|
||||
<view class="topic-entry__badge">
|
||||
<text class="topic-entry__badge-year">2026</text>
|
||||
<text class="topic-entry__badge-label">FIFA</text>
|
||||
</view>
|
||||
<view class="topic-entry__content">
|
||||
<text class="topic-entry__title">2026年国际足联世界杯</text>
|
||||
<text class="topic-entry__meta">2026年6月11日至7月19日</text>
|
||||
</view>
|
||||
<view class="topic-entry__action">
|
||||
<u-icon name="arrow-right" size="26" color="#ffffff"></u-icon>
|
||||
<view class="match-header__assistant">
|
||||
<AiAssistantEntry size="compact" variant="orb" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<MatchCenterTabsPanel />
|
||||
<MatchCenterTabsPanel @open-worldcup="openWorldCupCenter" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -83,7 +71,7 @@ onMounted(() => {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f5f5;
|
||||
background: #f7f8fb;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -92,85 +80,27 @@ onMounted(() => {
|
||||
flex-shrink: 0;
|
||||
|
||||
&__title {
|
||||
padding: 16rpx 24rpx 12rpx;
|
||||
position: relative;
|
||||
min-height: 88rpx;
|
||||
padding: 14rpx 32rpx 10rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__title-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-entry {
|
||||
margin: 0 24rpx 12rpx;
|
||||
padding: 18rpx 18rpx 18rpx 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(135deg, #2457ff 0%, #4452f2 100%);
|
||||
color: #fff;
|
||||
|
||||
&__badge {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 42rpx;
|
||||
font-weight: 800;
|
||||
color: #151a2b;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
&__badge-year {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
&__badge-label {
|
||||
margin-top: 4rpx;
|
||||
font-size: 16rpx;
|
||||
line-height: 1;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
&__content {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 22rpx;
|
||||
line-height: 1.3;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
&__action {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 26rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
flex-shrink: 0;
|
||||
&__assistant {
|
||||
position: absolute;
|
||||
right: 28rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<view class="center-match-card" :class="`center-match-card--${cardType}`">
|
||||
<view class="center-match-card__head">
|
||||
<view class="center-match-card__league">
|
||||
<image v-if="match.league_icon" class="center-match-card__league-icon" :src="match.league_icon"
|
||||
mode="aspectFit" referrerpolicy="no-referrer" />
|
||||
<view v-else class="center-match-card__league-dot" />
|
||||
<text>{{ match.league_name || '赛事' }}</text>
|
||||
<text v-if="match.round_name" class="center-match-card__round">· {{ match.round_name }}</text>
|
||||
</view>
|
||||
<view v-if="isLive" class="center-match-card__live-tag">
|
||||
<view class="center-match-card__live-bars">
|
||||
<view />
|
||||
<view />
|
||||
<view />
|
||||
</view>
|
||||
<text>直播中</text>
|
||||
</view>
|
||||
<text v-else class="center-match-card__date">{{ dateText }}</text>
|
||||
</view>
|
||||
|
||||
<view class="center-match-card__body">
|
||||
<view class="center-match-card__team">
|
||||
<image v-if="match.home_icon" class="center-match-card__team-logo" :src="match.home_icon"
|
||||
mode="aspectFit" referrerpolicy="no-referrer" />
|
||||
<view v-else class="center-match-card__team-logo center-match-card__team-logo--fallback">
|
||||
<text>{{ teamInitial(match.home_team) }}</text>
|
||||
</view>
|
||||
<text class="center-match-card__team-name">{{ match.home_team || '主队' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="center-match-card__center">
|
||||
<template v-if="isUpcoming">
|
||||
<text class="center-match-card__day">{{ dayText }}</text>
|
||||
<text class="center-match-card__time">{{ timeText }}</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="center-match-card__score">{{ scoreText }}</text>
|
||||
<text class="center-match-card__minute" :class="{ 'center-match-card__minute--live': isLive }">
|
||||
{{ statusMeta }}
|
||||
</text>
|
||||
<text v-if="isLive" class="center-match-card__status-text">直播中</text>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view class="center-match-card__team center-match-card__team--away">
|
||||
<image v-if="match.away_icon" class="center-match-card__team-logo" :src="match.away_icon"
|
||||
mode="aspectFit" referrerpolicy="no-referrer" />
|
||||
<view v-else class="center-match-card__team-logo center-match-card__team-logo--fallback">
|
||||
<text>{{ teamInitial(match.away_team) }}</text>
|
||||
</view>
|
||||
<text class="center-match-card__team-name">{{ match.away_team || '客队' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="center-match-card__actions">
|
||||
<view class="center-match-card__button center-match-card__button--outline" @tap.stop="emit('detail')">
|
||||
<text>比赛详情</text>
|
||||
</view>
|
||||
<view class="center-match-card__button center-match-card__button--primary" @tap.stop="handlePrimaryAction">
|
||||
<text>{{ primaryActionText }}</text>
|
||||
<view v-if="isLive" class="center-match-card__play-icon" />
|
||||
<text v-else class="center-match-card__ai-mark">AI</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { getLocalMatchDate } from '@/utils/match-time'
|
||||
|
||||
const props = defineProps<{
|
||||
match: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'detail'): void
|
||||
(e: 'ai'): void
|
||||
}>()
|
||||
|
||||
const status = computed(() => Number(props.match?.status ?? 0))
|
||||
const isLive = computed(() => status.value === 1)
|
||||
const isUpcoming = computed(() => status.value === 0)
|
||||
const cardType = computed(() => {
|
||||
if (isLive.value) return 'live'
|
||||
if (isUpcoming.value) return 'upcoming'
|
||||
if (status.value === 2) return 'ended'
|
||||
return 'other'
|
||||
})
|
||||
|
||||
const matchDate = computed(() => getLocalMatchDate(props.match?.match_time, props.match))
|
||||
|
||||
const dateText = computed(() => {
|
||||
const date = matchDate.value
|
||||
if (!date) return ''
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||||
})
|
||||
|
||||
const timeText = computed(() => {
|
||||
const date = matchDate.value
|
||||
if (!date) return '--:--'
|
||||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
})
|
||||
|
||||
const dayText = computed(() => {
|
||||
const date = matchDate.value
|
||||
if (!date) return '待定'
|
||||
const today = new Date()
|
||||
const target = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()
|
||||
const current = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime()
|
||||
const offset = Math.round((target - current) / 86400000)
|
||||
if (offset === 0) return '今日'
|
||||
if (offset === 1) return '明日'
|
||||
if (offset === -1) return '昨日'
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`
|
||||
})
|
||||
|
||||
const scoreText = computed(() => `${Number(props.match?.home_score || 0)} : ${Number(props.match?.away_score || 0)}`)
|
||||
|
||||
const statusMeta = computed(() => {
|
||||
if (isLive.value) return String(props.match?.current_minute || '').trim() || timeText.value
|
||||
if (status.value === 2) return '已结束'
|
||||
if (status.value === 3) return '已推迟'
|
||||
return timeText.value
|
||||
})
|
||||
|
||||
const primaryActionText = computed(() => isLive.value ? '看直播' : 'AI 分析')
|
||||
|
||||
const teamInitial = (name: any) => String(name || '?').trim().slice(0, 1)
|
||||
|
||||
const handlePrimaryAction = () => {
|
||||
if (isLive.value) {
|
||||
emit('detail')
|
||||
return
|
||||
}
|
||||
emit('ai')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.center-match-card {
|
||||
padding: 22rpx 24rpx 20rpx;
|
||||
border: 1rpx solid #e7eaf0;
|
||||
border-radius: 24rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 10rpx 28rpx rgba(34, 48, 86, 0.07);
|
||||
|
||||
&__head,
|
||||
&__league,
|
||||
&__live-tag,
|
||||
&__actions,
|
||||
&__button,
|
||||
&__live-bars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__head {
|
||||
min-width: 0;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
&__league {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: #202534;
|
||||
font-size: 25rpx;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__league-icon {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
margin-right: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__league-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
margin-right: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #156bf6;
|
||||
box-shadow: 0 0 0 6rpx #eaf2ff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__round {
|
||||
min-width: 0;
|
||||
color: #5f6675;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__date {
|
||||
color: #8b92a0;
|
||||
font-size: 22rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__live-tag {
|
||||
height: 38rpx;
|
||||
padding: 0 14rpx;
|
||||
gap: 8rpx;
|
||||
border-radius: 10rpx;
|
||||
background: #fff0f0;
|
||||
color: #f04444;
|
||||
font-size: 21rpx;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__live-bars {
|
||||
height: 20rpx;
|
||||
gap: 4rpx;
|
||||
align-items: flex-end;
|
||||
|
||||
view {
|
||||
width: 4rpx;
|
||||
border-radius: 4rpx;
|
||||
background: #f04444;
|
||||
|
||||
&:nth-child(1) {
|
||||
height: 10rpx;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
height: 20rpx;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
height: 15rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 170rpx minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
min-height: 190rpx;
|
||||
padding: 18rpx 8rpx 12rpx;
|
||||
}
|
||||
|
||||
&__team,
|
||||
&__center {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__team-logo {
|
||||
width: 86rpx;
|
||||
height: 86rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--fallback {
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(145deg, #eaf2ff 0%, #d9e7ff 100%);
|
||||
color: #176cf6;
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
&__team-name {
|
||||
width: 100%;
|
||||
margin-top: 12rpx;
|
||||
color: #171b26;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__score {
|
||||
color: #111522;
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
letter-spacing: 3rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__minute,
|
||||
&__day {
|
||||
margin-top: 14rpx;
|
||||
color: #4e5667;
|
||||
font-size: 23rpx;
|
||||
|
||||
&--live {
|
||||
color: #10a83d;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&__status-text {
|
||||
margin-top: 5rpx;
|
||||
color: #10a83d;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
&__time {
|
||||
margin-top: 6rpx;
|
||||
color: #116cf7;
|
||||
font-size: 38rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
gap: 26rpx;
|
||||
}
|
||||
|
||||
&__button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 68rpx;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
border-radius: 14rpx;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
box-sizing: border-box;
|
||||
|
||||
&--outline {
|
||||
border: 2rpx solid #1470f8;
|
||||
color: #1470f8;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
color: #fff;
|
||||
background: linear-gradient(100deg, #1976ff 0%, #0862ef 100%);
|
||||
box-shadow: 0 8rpx 18rpx rgba(20, 105, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__play-icon {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 9rpx solid transparent;
|
||||
border-bottom: 9rpx solid transparent;
|
||||
border-left: 14rpx solid #fff;
|
||||
}
|
||||
|
||||
&__ai-mark {
|
||||
height: 34rpx;
|
||||
min-width: 34rpx;
|
||||
padding: 0 6rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
font-size: 18rpx;
|
||||
line-height: 34rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&--ended {
|
||||
background: #fafbfc;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,13 +5,34 @@
|
||||
:class="{ 'content-tabs__item--active': activeTab === tab.key }" @tap="switchContentTab(tab.key)">
|
||||
<text>{{ tab.label }}</text>
|
||||
</view>
|
||||
<view class="content-tabs__item" @tap="emit('open-worldcup')">
|
||||
<text>热门联赛</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab === 'schedule'" class="date-filter">
|
||||
<view class="date-filter__previous" @tap="shiftDateWindow(-1)">
|
||||
<text>‹</text>
|
||||
</view>
|
||||
<view class="date-filter__dates">
|
||||
<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>
|
||||
<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 sportTabs" :key="idx" class="sport-tab"
|
||||
:class="{ 'sport-tab--active': currentSport === tab.value }" @tap="switchSport(tab.value)">
|
||||
<text class="sport-tab__icon">{{ tab.icon }}</text>
|
||||
<text class="sport-tab__label">{{ tab.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -21,13 +42,12 @@
|
||||
<view class="league-tabs__inner">
|
||||
<view class="league-tab" :class="{ 'league-tab--active': currentLeague === '' }"
|
||||
@tap="switchLeague('')">
|
||||
<text>全部<text v-if="leagueTotalCount" class="league-tab__count">·{{ leagueTotalCount }}</text></text>
|
||||
<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 v-if="league.count" class="league-tab__count">·{{ league.count
|
||||
}}</text></text>
|
||||
<text>{{ league.league_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -40,15 +60,15 @@
|
||||
<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__arrow"
|
||||
:class="{ 'match-group__arrow--collapsed': collapsedGroups[group.type] }">
|
||||
<text>▼</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)">
|
||||
<match-card :match="item"
|
||||
:show-live-ai="group.type === 'live' || group.type === 'upcoming'" />
|
||||
<MatchCenterScheduleCard :match="item" @detail="goDetail(item)" @ai="goAiAnalysis(item)" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -56,7 +76,7 @@
|
||||
<u-loading mode="circle" />
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<view v-if="!scheduleLoading && !liveList.length && !upcomingList.length && !endedList.length"
|
||||
<view v-if="!scheduleLoading && !matchGroups.length"
|
||||
class="content-empty">
|
||||
<u-empty text="暂无赛事" mode="data" />
|
||||
</view>
|
||||
@@ -227,13 +247,16 @@
|
||||
<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 MatchCard from '@/components/match-card/match-card.vue'
|
||||
import MatchCenterScheduleCard from './MatchCenterScheduleCard.vue'
|
||||
import { getLocalMatchDate } from '@/utils/match-time'
|
||||
import {
|
||||
getMatchLeagues,
|
||||
getMatchList,
|
||||
@@ -248,6 +271,10 @@ type MatchCenterTab = 'schedule' | 'live' | 'odds'
|
||||
type OddsShowType = '' | 'live' | 'today' | 'early'
|
||||
type OddsPlatformOption = { key: string; label: string }
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'open-worldcup'): void
|
||||
}>()
|
||||
|
||||
const showStandaloneOddsTab = false
|
||||
|
||||
const allContentTabs: { key: MatchCenterTab; label: string }[] = [
|
||||
@@ -275,6 +302,8 @@ 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[]>([])
|
||||
@@ -308,6 +337,83 @@ const collapsedGroups = ref<Record<string, boolean>>({
|
||||
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 [-1, 0, 1].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[]) => {
|
||||
const dateFiltered = selectedDateKey.value
|
||||
? items.filter((item: any) => getMatchDateKey(item) === selectedDateKey.value)
|
||||
: items
|
||||
if (currentLeague.value) return dateFiltered
|
||||
const hotItems = dateFiltered.filter((item: any) => Number(item?.is_hot) === 1)
|
||||
return hotItems.length ? hotItems : dateFiltered
|
||||
}
|
||||
|
||||
const leagueTabs = computed(() => {
|
||||
if (currentSport.value === 0) {
|
||||
const merged: any[] = []
|
||||
@@ -320,20 +426,19 @@ const leagueTabs = computed(() => {
|
||||
return allLeagues.value[currentSport.value] || []
|
||||
})
|
||||
|
||||
const leagueTotalCount = computed(() => {
|
||||
return leagueTabs.value.reduce((sum: number, league: any) => sum + (league.count || 0), 0)
|
||||
})
|
||||
|
||||
const matchGroups = computed(() => {
|
||||
const groups: { title: string; type: string; items: any[]; order: number }[] = []
|
||||
if (liveList.value.length > 0) {
|
||||
groups.push({ title: '进行中', type: 'live', items: liveList.value, order: 0 })
|
||||
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 (upcomingList.value.length > 0) {
|
||||
groups.push({ title: '未开始', type: 'upcoming', items: upcomingList.value, order: 1 })
|
||||
if (visibleUpcomingList.length > 0) {
|
||||
groups.push({ title: '即将开始', type: 'upcoming', items: visibleUpcomingList, order: 1 })
|
||||
}
|
||||
if (endedList.value.length > 0) {
|
||||
groups.push({ title: '已结束', type: 'ended', items: endedList.value, order: 2 })
|
||||
if (visibleEndedList.length > 0) {
|
||||
groups.push({ title: '已结束', type: 'ended', items: visibleEndedList, order: 2 })
|
||||
}
|
||||
return groups
|
||||
})
|
||||
@@ -362,7 +467,11 @@ const initLayout = () => {
|
||||
|
||||
const fetchSportTypes = async () => {
|
||||
try {
|
||||
sportTabs.value = await getSportTypes() || []
|
||||
const types = await getSportTypes() || []
|
||||
sportTabs.value = [
|
||||
{ value: 0, label: '全部' },
|
||||
...types.filter((item: any) => Number(item?.value || 0) > 0),
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('获取运动类型失败', error)
|
||||
}
|
||||
@@ -401,6 +510,7 @@ const fetchMatchList = async (refresh = false) => {
|
||||
}
|
||||
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)
|
||||
@@ -534,6 +644,12 @@ const goDetail = (item: any) => {
|
||||
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)
|
||||
@@ -690,74 +806,160 @@ onMounted(() => {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
background: #f7f8fb;
|
||||
font-family: "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
}
|
||||
|
||||
.content-tabs {
|
||||
padding: 0 24rpx 14rpx;
|
||||
margin: 0 34rpx 16rpx;
|
||||
padding: 8rpx;
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
gap: 8rpx;
|
||||
border: 1rpx solid #dfe3eb;
|
||||
border-radius: 24rpx;
|
||||
background: #fff;
|
||||
|
||||
&__item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 58rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #f1f5ff;
|
||||
color: #185dff;
|
||||
font-size: 26rpx;
|
||||
height: 70rpx;
|
||||
border-radius: 18rpx;
|
||||
color: #404654;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 58rpx;
|
||||
line-height: 70rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__item--active {
|
||||
background: #185dff;
|
||||
background: linear-gradient(110deg, #1875ff 0%, #0863f3 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 8rpx 20rpx rgba(24, 93, 255, 0.2);
|
||||
box-shadow: 0 8rpx 20rpx rgba(20, 105, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.date-filter {
|
||||
height: 94rpx;
|
||||
padding: 0 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
background: #fff;
|
||||
|
||||
&__previous,
|
||||
&__calendar {
|
||||
width: 58rpx;
|
||||
height: 58rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__previous {
|
||||
color: #20283a;
|
||||
font-size: 58rpx;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__calendar {
|
||||
border: 1rpx solid #e0e4eb;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&__dates {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
&__date {
|
||||
position: relative;
|
||||
height: 70rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
border-radius: 16rpx;
|
||||
color: #5b606b;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -9rpx;
|
||||
width: 34rpx;
|
||||
height: 5rpx;
|
||||
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: 29rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__date-week {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.sport-tabs {
|
||||
white-space: nowrap;
|
||||
height: 80rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
height: 94rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sport-tabs__inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 60rpx;
|
||||
padding: 0 12rpx;
|
||||
gap: 4rpx;
|
||||
height: 94rpx;
|
||||
padding: 0 32rpx;
|
||||
gap: 28rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sport-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f4f5f7;
|
||||
justify-content: center;
|
||||
min-width: 116rpx;
|
||||
height: 62rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #f5f6f9;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
&__icon {
|
||||
font-size: 32rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
font-size: 27rpx;
|
||||
color: #313745;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #e5edff;
|
||||
background: linear-gradient(110deg, #1875ff 0%, #0863f3 100%);
|
||||
box-shadow: 0 8rpx 18rpx rgba(20, 105, 246, 0.17);
|
||||
|
||||
.sport-tab__label {
|
||||
color: #185dff;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -765,13 +967,14 @@ onMounted(() => {
|
||||
.league-tabs {
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #eceef3;
|
||||
}
|
||||
|
||||
.league-tabs__inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 16rpx;
|
||||
gap: 4rpx;
|
||||
padding: 0 32rpx 22rpx;
|
||||
gap: 28rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -779,45 +982,48 @@ onMounted(() => {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 18rpx;
|
||||
font-size: 24rpx;
|
||||
color: #808080;
|
||||
background: #fff;
|
||||
min-width: 116rpx;
|
||||
height: 62rpx;
|
||||
padding: 0 18rpx;
|
||||
border: 1rpx solid transparent;
|
||||
border-radius: 16rpx;
|
||||
font-size: 26rpx;
|
||||
color: #3f4552;
|
||||
background: #f5f6f9;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
&--active {
|
||||
background: #185dff;
|
||||
color: #fff;
|
||||
border-color: #1470f8;
|
||||
background: #fff;
|
||||
color: #1470f8;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.match-content {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
background: #fff;
|
||||
background: #f7f8fb;
|
||||
}
|
||||
|
||||
.match-group {
|
||||
margin-bottom: 2rpx;
|
||||
padding-top: 14rpx;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 18rpx 18rpx 8rpx;
|
||||
gap: 6rpx;
|
||||
background: #fff;
|
||||
padding: 14rpx 34rpx 16rpx;
|
||||
gap: 10rpx;
|
||||
background: #f7f8fb;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
|
||||
&--live {
|
||||
@@ -834,36 +1040,45 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 31rpx;
|
||||
font-weight: 800;
|
||||
color: #222735;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
font-size: 23rpx;
|
||||
color: #7f8592;
|
||||
}
|
||||
|
||||
&__more {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
color: #747b88;
|
||||
font-size: 23rpx;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
margin-left: auto;
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
color: #737b89;
|
||||
font-size: 42rpx;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
transition: transform 0.25s ease;
|
||||
|
||||
&--collapsed {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
&--ended,
|
||||
&--ended .match-group__header {
|
||||
background: #f3f4f6;
|
||||
&__more--collapsed &__arrow {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.match-card-wrap {
|
||||
position: relative;
|
||||
margin: 0 18rpx 8rpx;
|
||||
margin: 0 34rpx 22rpx;
|
||||
}
|
||||
|
||||
.match-content__bottom-space {
|
||||
height: calc(120rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.content-loading,
|
||||
|
||||
Reference in New Issue
Block a user