feat: add uniapp match live stream switching
This commit is contained in:
@@ -62,8 +62,31 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="liveList.length" class="live-entrance">
|
||||
<view class="live-entrance__header">
|
||||
<text class="live-entrance__heading">直播线路</text>
|
||||
<text class="live-entrance__subheading">选择线路后通过内置 WebView 打开</text>
|
||||
</view>
|
||||
<view v-for="(item, index) in liveList" :key="item.id || item.title || item.name || index"
|
||||
class="live-entrance__card" @tap="openLiveLine(item)">
|
||||
<view class="live-entrance__left">
|
||||
<view class="live-entrance__badge">
|
||||
<text>{{ index + 1 }}</text>
|
||||
</view>
|
||||
<view class="live-entrance__content">
|
||||
<text class="live-entrance__title">{{ getLiveLineTitle(item) }}</text>
|
||||
<text class="live-entrance__desc">{{ getLiveLineMeta(item) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="live-entrance__action">
|
||||
<text>切换线路</text>
|
||||
<u-icon name="arrow-right" size="26" color="#c7d2fe"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Tab 导航栏 -->
|
||||
<view v-if="thirdPartyLiveUrl" class="live-entrance">
|
||||
<view v-else-if="thirdPartyLiveUrl" class="live-entrance">
|
||||
<view class="live-entrance__card" @tap="openThirdPartyLive">
|
||||
<view class="live-entrance__left">
|
||||
<view class="live-entrance__badge">
|
||||
@@ -412,6 +435,8 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<MatchLivePopup v-model:show="showLivePopup" :line="selectedLiveLine" @close="handleLivePopupClose" />
|
||||
|
||||
<!-- 分享弹窗 -->
|
||||
<SharePopup v-if="showSharePopup" v-model:show="showSharePopup" page-type="match"
|
||||
:path="`/pages/match_detail/match_detail?id=${match.id || ''}`"
|
||||
@@ -423,10 +448,12 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, nextTick, onMounted } from 'vue'
|
||||
import MatchDetailHeader from '@/components/match-detail-header/match-detail-header.vue'
|
||||
import MatchLivePopup from '@/components/match-live-popup/match-live-popup.vue'
|
||||
import SharePopup from '@/components/share-popup/share-popup.vue'
|
||||
import GlobalPopup from '@/components/global-popup/global-popup.vue'
|
||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||
import { getMatchDetail, getMatchLiveText, getMatchLineup } from '@/api/match'
|
||||
import type { MatchLiveLineItem } from '@/api/match'
|
||||
import { checkAiUnlock, getMatchPredictSection } from '@/api/ai'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { getLocalMatchDate } from '@/utils/match-time'
|
||||
@@ -447,9 +474,14 @@ const aiInlineLoading = ref(false)
|
||||
const aiInline = ref<any>({})
|
||||
const showSharePopup = ref(false)
|
||||
const liveTextList = ref<any[]>([])
|
||||
const showLivePopup = ref(false)
|
||||
const selectedLiveLine = ref<MatchLiveLineItem | null>(null)
|
||||
let liveTextTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const thirdPartyLiveUrl = computed(() => String(match.value?.live_url || '').trim())
|
||||
const liveList = computed<MatchLiveLineItem[]>(() => {
|
||||
return Array.isArray(match.value?.live_list) ? match.value.live_list : []
|
||||
})
|
||||
|
||||
const liveSourceText = computed(() => {
|
||||
const raw = match.value?.living_tv ?? match.value?.live_source ?? match.value?.livingTv ?? ''
|
||||
@@ -480,6 +512,41 @@ const homeSubs = computed(() => lineupList.value.filter((p: any) => p.team_side
|
||||
const awayStarters = computed(() => lineupList.value.filter((p: any) => p.team_side === 2 && p.is_starter === 1))
|
||||
const awaySubs = computed(() => lineupList.value.filter((p: any) => p.team_side === 2 && p.is_starter === 0))
|
||||
|
||||
const normalizeLiveText = (value: unknown) => String(value || '').trim()
|
||||
|
||||
const getLiveLineTitle = (item: MatchLiveLineItem) => {
|
||||
return (
|
||||
normalizeLiveText(item?.title) ||
|
||||
normalizeLiveText(item?.name) ||
|
||||
normalizeLiveText(item?.label) ||
|
||||
normalizeLiveText(item?.live_tag) ||
|
||||
normalizeLiveText(item?.source_name) ||
|
||||
'直播线路'
|
||||
)
|
||||
}
|
||||
|
||||
const formatLiveLineUpdate = (value: unknown) => {
|
||||
const text = normalizeLiveText(value)
|
||||
if (!text) return ''
|
||||
const hhmmss = text.match(/(\d{2}:\d{2}:\d{2})$/)
|
||||
if (hhmmss) return hhmmss[1]
|
||||
const hhmm = text.match(/(\d{2}:\d{2})$/)
|
||||
if (hhmm) return hhmm[1]
|
||||
if (/^\d{10,13}$/.test(text)) {
|
||||
const raw = text.length === 13 ? Number(text) : Number(text) * 1000
|
||||
return formatLiveTime(raw / 1000)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const getLiveLineMeta = (item: MatchLiveLineItem) => {
|
||||
const parts = [
|
||||
normalizeLiveText(item?.status_text) || normalizeLiveText(item?.status_desc) || normalizeLiveText(item?.status),
|
||||
formatLiveLineUpdate(item?.updated_at || item?.update_time),
|
||||
].filter(Boolean)
|
||||
return parts.length ? parts.join(' · ') : '点击选择直播线路'
|
||||
}
|
||||
|
||||
const techStatsList = computed(() => {
|
||||
const m = match.value as any
|
||||
// 优先使用 tech_stats JSON(兼容所有运动类型)
|
||||
@@ -540,6 +607,8 @@ onMounted(() => {
|
||||
|
||||
const fetchDetail = async (id: number) => {
|
||||
loading.value = true
|
||||
showLivePopup.value = false
|
||||
selectedLiveLine.value = null
|
||||
try {
|
||||
const data = await getMatchDetail({ id })
|
||||
match.value = data || {}
|
||||
@@ -604,6 +673,15 @@ const openThirdPartyLive = () => {
|
||||
uni.navigateTo({ url: `/pages/webview/webview?url=${encodeURIComponent(url)}` })
|
||||
}
|
||||
|
||||
const openLiveLine = (item: MatchLiveLineItem) => {
|
||||
selectedLiveLine.value = item
|
||||
showLivePopup.value = true
|
||||
}
|
||||
|
||||
const handleLivePopupClose = () => {
|
||||
showLivePopup.value = false
|
||||
}
|
||||
|
||||
const formatLiveTime = (ts: any) => {
|
||||
if (!ts) return ''
|
||||
const s = String(ts)
|
||||
@@ -1560,6 +1638,29 @@ onUnload(() => {
|
||||
|
||||
.live-entrance {
|
||||
margin: 0 24rpx 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
&__heading {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
&__subheading {
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__card {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user