feat: add match handicap comparison panel

This commit is contained in:
hajimi
2026-08-03 02:38:11 +08:00
parent f53873c49b
commit 348ffa8db4
2 changed files with 264 additions and 517 deletions
+142 -205
View File
@@ -1,7 +1,7 @@
<template>
<view class="match-detail">
<match-detail-header :status-bar-height="statusBarHeight" title="赛事详情"
:subtitle="match.league_name || '赛事信息加载中'" @back="goBack" @share="showSharePopup = true" />
<match-detail-header :status-bar-height="statusBarHeight" :title="matchDetailTitle" @back="goBack"
@share="showSharePopup = true" />
<!-- 加载中 -->
<view class="detail-loading" v-if="loading">
@@ -11,69 +11,8 @@
<!-- 内容区域 -->
<scroll-view v-else scroll-y class="detail-content" :style="{ height: contentHeight + 'px' }">
<!-- 比赛信息卡片进行中/已结束 -->
<view class="match-info-wrap" v-if="match.status !== 0">
<match-card :match="match" />
</view>
<!-- 比赛信息卡片未开始 -->
<view class="match-info-wrap" v-else>
<view class="upcoming-card">
<view class="upcoming-card__header">
<view class="upcoming-card__tags">
<text class="upcoming-card__league">{{ match.league_name }}</text>
<view v-if="match.round_name" class="upcoming-card__tag upcoming-card__tag--blue">
<text>{{ match.round_name }}</text>
</view>
<view v-if="match.is_hot" class="upcoming-card__hot">
<text></text>
</view>
</view>
</view>
<view class="upcoming-card__teams">
<view class="upcoming-card__team">
<image v-if="match.home_icon" class="upcoming-card__icon" :src="match.home_icon"
mode="aspectFit" />
<text class="upcoming-card__name">{{ match.home_team }}</text>
</view>
<view class="upcoming-card__center">
<text class="upcoming-card__score">0</text>
<text class="upcoming-card__vs">VS</text>
<text class="upcoming-card__score">0</text>
</view>
<view class="upcoming-card__team">
<image v-if="match.away_icon" class="upcoming-card__icon" :src="match.away_icon"
mode="aspectFit" />
<text class="upcoming-card__name">{{ match.away_team }}</text>
</view>
</view>
<view class="upcoming-card__divider"></view>
<view class="upcoming-card__footer">
<view class="upcoming-card__status">
<text>未开始</text>
</view>
<text class="upcoming-card__time">{{ formatMatchTime(match.match_time) }}</text>
</view>
</view>
</view>
<view v-if="liveList.length" class="live-entrance">
<view class="live-entrance__player-card">
<view class="live-entrance__player-header">
<view class="live-entrance__player-title-wrap">
<text class="live-entrance__player-title">{{ activeLiveLineTitle }}</text>
</view>
<view class="live-entrance__player-actions">
<u-button v-if="activeLivePlayUrl" size="mini" shape="circle" plain
@click.stop="openLiveFullscreen">
全屏播放
</u-button>
<u-button size="mini" shape="circle" plain @click.stop="toggleLiveLineSelector">
{{ showLiveLineSelector ? '收起线路' : '切换线路' }}
</u-button>
</view>
</view>
<view v-if="activeLivePlayUrl" class="live-entrance__player-wrap">
<video id="match-live-player" :key="activeLivePlayerKey" class="live-entrance__player"
:src="activeLivePlayUrl" :poster="match.home_icon || match.away_icon || ''" :autoplay="true"
@@ -81,6 +20,18 @@
:show-fullscreen-btn="true" :page-gesture="true" object-fit="contain"
@play="handleLivePlayerPlay" @loadedmetadata="handleLivePlayerLoaded"
@waiting="handleLivePlayerWaiting" @error="handleLivePlayerError" />
<view v-if="match.status === 1" class="live-entrance__live-status">
<text>直播中</text>
<text class="live-entrance__live-minute">{{ match.current_minute || '进行中' }}</text>
</view>
<view v-if="activeLivePlayUrl" class="live-entrance__fullscreen" @tap.stop="openLiveFullscreen">
<text class="live-entrance__fullscreen-icon"></text>
<text>全屏</text>
</view>
<view v-if="livePlayerLoading" class="live-entrance__player-loading">
<u-loading mode="circle" color="#ffffff" />
<text class="live-entrance__player-loading-text">正在连接直播</text>
</view>
</view>
<view v-else class="live-entrance__player-empty">
<text class="live-entrance__player-empty-title">当前线路暂无可用直播放流</text>
@@ -91,27 +42,32 @@
</u-button>
</view>
<text v-if="activeLiveError" class="live-entrance__player-error">{{ activeLiveError }}</text>
<view v-if="showLiveLineSelector" class="live-entrance__selector">
<view v-for="(item, index) in resolvedLiveList" :key="item.lineKey"
class="live-entrance__selector-item"
:class="{ 'live-entrance__selector-item--active': item.lineKey === activeLiveLineKey }"
@tap="selectLiveLine(item)">
<view class="live-entrance__selector-main">
<view class="live-entrance__selector-index">
<text>{{ index + 1 }}</text>
</view>
<view class="live-entrance__selector-content">
<text class="live-entrance__selector-title">{{ item.displayTitle }}</text>
<text class="live-entrance__selector-desc">{{ item.metaText }}</text>
</view>
</view>
<text class="live-entrance__selector-tag">
{{ item.lineKey === activeLiveLineKey ? '当前播放' : item.hasDirectPlay ? '点击切换' : '源站兜底' }}
</text>
<view class="live-entrance__player-header">
<view class="live-entrance__player-title-wrap">
<text class="live-entrance__eyebrow">{{ resolvedLiveList.length > 1 ? `直播间 · ${resolvedLiveList.length}` : '直播间' }}</text>
<text class="live-entrance__player-title">{{ activeLiveLineTitle }}</text>
<text v-if="activeLiveMetaText" class="live-entrance__player-meta">{{ activeLiveMetaText }}</text>
</view>
<view v-if="activeLiveSourceUrl" class="live-entrance__source-action" @tap.stop="openActiveLiveSource">
<text>源站</text>
<text></text>
</view>
</view>
<text v-if="activeLiveError" class="live-entrance__player-error">{{ activeLiveError }}</text>
<scroll-view v-if="resolvedLiveList.length > 1" scroll-x class="live-entrance__room-tabs"
show-scrollbar="false">
<view class="live-entrance__room-tabs-inner">
<view v-for="(item, index) in resolvedLiveList" :key="item.lineKey"
class="live-entrance__room-tab"
:class="{ 'live-entrance__room-tab--active': item.lineKey === activeLiveLineKey }"
@tap="selectLiveLine(item)">
<text class="live-entrance__room-index">{{ index + 1 }}</text>
<text class="live-entrance__room-name">{{ item.anchor_name || item.displayTitle }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
@@ -136,6 +92,33 @@
</view>
</view>
<view class="detail-scoreboard">
<view class="detail-scoreboard__team">
<image v-if="match.home_icon" class="detail-scoreboard__logo" :src="match.home_icon" mode="aspectFit" />
<view v-else class="detail-scoreboard__logo detail-scoreboard__logo--placeholder">
<text>{{ (match.home_team || '?').slice(0, 1) }}</text>
</view>
<text class="detail-scoreboard__name">{{ match.home_team || '主队' }}</text>
</view>
<view class="detail-scoreboard__center">
<view v-if="match.status !== 0" class="detail-scoreboard__score">
<text>{{ match.home_score ?? 0 }}</text>
<text class="detail-scoreboard__colon">:</text>
<text>{{ match.away_score ?? 0 }}</text>
</view>
<text v-else class="detail-scoreboard__start-time">{{ formatMatchTime(match.match_time) || '未开始' }}</text>
<text class="detail-scoreboard__meta">{{ match.league_name || '赛事' }}{{ match.round_name ? ` · ${match.round_name}` : '' }}</text>
<text v-if="match.status === 1 && match.current_minute" class="detail-scoreboard__live-minute">{{ match.current_minute }}</text>
</view>
<view class="detail-scoreboard__team">
<image v-if="match.away_icon" class="detail-scoreboard__logo" :src="match.away_icon" mode="aspectFit" />
<view v-else class="detail-scoreboard__logo detail-scoreboard__logo--placeholder">
<text>{{ (match.away_team || '?').slice(0, 1) }}</text>
</view>
<text class="detail-scoreboard__name">{{ match.away_team || '客队' }}</text>
</view>
</view>
<scroll-view scroll-x class="detail-tabs" show-scrollbar="false">
<view class="detail-tabs__inner">
<view v-for="tab in tabList" :key="tab.key" class="detail-tabs__item"
@@ -242,7 +225,7 @@
</view>
</scroll-view>
<!-- ====== 赔率 ====== -->
<!-- ====== 盘口 ====== -->
<scroll-view v-if="activeTab === 'odds'" scroll-y class="detail-tab-scroll">
<MatchOddsDetail :home-team="match.home_team" :away-team="match.away_team"
:match-time="match.match_time" :competition-id="match.competition_id"
@@ -524,9 +507,14 @@ const liveTextList = ref<any[]>([])
const activeLiveLineKey = ref('')
const activeLiveError = ref('')
const livePlayerLoading = ref(false)
const showLiveLineSelector = ref(false)
let liveTextTimer: ReturnType<typeof setInterval> | null = null
const matchDetailTitle = computed(() => {
const homeTeam = String(match.value?.home_team || '').trim()
const awayTeam = String(match.value?.away_team || '').trim()
return homeTeam && awayTeam ? `${homeTeam} vs ${awayTeam}` : '赛事详情'
})
const thirdPartyLiveUrl = computed(() => String(match.value?.live_url || '').trim())
const liveList = computed<MatchLiveLineItem[]>(() => {
return Array.isArray(match.value?.live_list) ? match.value.live_list : []
@@ -550,7 +538,7 @@ const liveSourceText = computed(() => {
const tabList = [
{ key: 'live', label: '文字直播' },
{ key: 'data', label: '比赛数据' },
{ key: 'odds', label: '赔率' },
{ key: 'odds', label: '盘口' },
{ key: 'lineup', label: '阵容' },
{ key: 'recent', label: '近期战绩' },
{ key: 'ai', label: 'AI分析' },
@@ -711,7 +699,6 @@ watch(
activeLiveLineKey.value = ''
activeLiveError.value = ''
livePlayerLoading.value = false
showLiveLineSelector.value = false
return
}
if (list.some((item) => item.lineKey === activeLiveLineKey.value)) return
@@ -865,20 +852,11 @@ const selectLiveLine = (item: MatchDetailLiveLine) => {
activeLiveLineKey.value = item.lineKey
activeLiveError.value = ''
livePlayerLoading.value = !!item.playUrl
showLiveLineSelector.value = false
if (item.playUrl) {
triggerLiveAutoPlay()
}
}
const toggleLiveLineSelector = () => {
if (!resolvedLiveList.value.length) {
uni.showToast({ title: '暂无直播线路', icon: 'none' })
return
}
showLiveLineSelector.value = !showLiveLineSelector.value
}
const openActiveLiveSource = () => {
navigateToLiveWebview(activeLiveSourceUrl.value)
}
@@ -1059,140 +1037,99 @@ onUnload(() => {
padding-bottom: 40rpx;
}
.match-info-wrap {
margin: 24rpx;
}
.upcoming-card {
.detail-scoreboard {
display: flex;
align-items: center;
justify-content: space-between;
background: #fff;
border-radius: 24rpx;
padding: 32rpx;
&__header {
margin-bottom: 24rpx;
}
&__tags {
display: flex;
align-items: center;
gap: 8rpx;
flex-wrap: wrap;
}
&__league {
font-size: 24rpx;
color: #808080;
}
&__tag {
padding: 2rpx 10rpx;
border-radius: 6rpx;
font-size: 22rpx;
&--blue {
background: #e5edff;
text {
color: #185dff;
font-size: 22rpx;
}
}
}
&__hot {
width: 44rpx;
height: 42rpx;
border-radius: 10rpx;
background: #d84293;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 24rpx;
font-weight: 600;
color: #fff;
}
}
&__teams {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
}
min-height: 270rpx;
padding: 28rpx 38rpx 30rpx;
border-bottom: 1rpx solid #eef0f5;
&__team {
flex: 1;
width: 190rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
gap: 14rpx;
min-width: 0;
}
&__icon {
width: 64rpx;
height: 64rpx;
&__logo {
width: 92rpx;
height: 92rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&--placeholder {
border-radius: 50%;
background: #eef3ff;
color: #1468f5;
font-size: 38rpx;
font-weight: 700;
}
}
&__name {
font-size: 28rpx;
font-weight: 600;
color: #000;
width: 100%;
font-size: 30rpx;
font-weight: 700;
color: #121826;
text-align: center;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__center {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
padding: 0 16rpx;
}
&__score {
font-size: 40rpx;
font-weight: 700;
color: #000;
}
&__vs {
font-size: 24rpx;
color: #808080;
}
&__divider {
height: 2rpx;
background: #f2f2f2;
margin: 16rpx 0;
}
&__footer {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
gap: 12rpx;
}
&__status {
display: inline-flex;
&__score {
display: flex;
align-items: baseline;
gap: 24rpx;
font-size: 68rpx;
line-height: 1;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: #090d18;
}
&__colon {
font-size: 54rpx;
}
&__start-time {
min-height: 68rpx;
display: flex;
align-items: center;
justify-content: center;
height: 50rpx;
padding: 0 24rpx;
border-radius: 25rpx;
background: #e5edff;
text {
font-size: 24rpx;
color: #185dff;
font-weight: 500;
}
font-size: 36rpx;
font-weight: 800;
color: #1468f5;
line-height: 1.2;
}
&__time {
&__meta {
font-size: 26rpx;
line-height: 1.35;
color: #7b8495;
text-align: center;
}
&__live-minute {
margin-top: -6rpx;
font-size: 24rpx;
color: #808080;
font-weight: 700;
color: #12a34a;
}
}