feat: add uniapp match live stream switching
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
MATCH_API = ROOT / "uniapp" / "src" / "api" / "match.ts"
|
||||||
|
MATCH_DETAIL = ROOT / "uniapp" / "src" / "pages" / "match_detail" / "match_detail.vue"
|
||||||
|
MATCH_LIVE_POPUP = ROOT / "uniapp" / "src" / "components" / "match-live-popup" / "match-live-popup.vue"
|
||||||
|
|
||||||
|
|
||||||
|
def read_text(path: Path) -> str:
|
||||||
|
return path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
assert MATCH_LIVE_POPUP.exists(), f"missing popup component: {MATCH_LIVE_POPUP}"
|
||||||
|
|
||||||
|
detail_source = read_text(MATCH_DETAIL)
|
||||||
|
popup_source = read_text(MATCH_LIVE_POPUP)
|
||||||
|
api_source = read_text(MATCH_API)
|
||||||
|
|
||||||
|
assert "live_list" in detail_source, "match detail should handle live_list"
|
||||||
|
assert "MatchLivePopup" in detail_source, "match detail should use MatchLivePopup"
|
||||||
|
assert "play_url_m3u8" in popup_source, "popup should support play_url_m3u8"
|
||||||
|
assert "stream_options" in popup_source, "popup should support stream_options"
|
||||||
|
assert "/pages/webview/webview?url=" in popup_source, "selected stream should route through existing webview page"
|
||||||
|
|
||||||
|
assert "/match/detail" in api_source, "match detail API path should stay on /match/detail"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,5 +1,39 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export interface MatchLiveStreamOption {
|
||||||
|
title?: string
|
||||||
|
name?: string
|
||||||
|
label?: string
|
||||||
|
source_name?: string
|
||||||
|
quality?: string
|
||||||
|
url?: string
|
||||||
|
play_url?: string
|
||||||
|
play_url_m3u8?: string
|
||||||
|
default_play_url?: string
|
||||||
|
live_url?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchLiveLineItem {
|
||||||
|
title?: string
|
||||||
|
name?: string
|
||||||
|
label?: string
|
||||||
|
live_tag?: string
|
||||||
|
source_name?: string
|
||||||
|
status?: string | number
|
||||||
|
status_text?: string
|
||||||
|
status_desc?: string
|
||||||
|
update_time?: string | number
|
||||||
|
updated_at?: string | number
|
||||||
|
source_url?: string
|
||||||
|
default_play_url?: string
|
||||||
|
play_url_m3u8?: string
|
||||||
|
play_url?: string
|
||||||
|
live_url?: string
|
||||||
|
stream_options?: MatchLiveStreamOption[]
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
export function getMatchList(data?: Record<string, any>) {
|
export function getMatchList(data?: Record<string, any>) {
|
||||||
return request.get({ url: '/match/lists', data: data }, { withToken: false })
|
return request.get({ url: '/match/lists', data: data }, { withToken: false })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
<template>
|
||||||
|
<u-popup v-model="showPopup" mode="bottom" safe-area-inset-bottom border-radius="24" closeable @close="handleClose">
|
||||||
|
<view class="match-live-popup">
|
||||||
|
<view class="match-live-popup__header">
|
||||||
|
<text class="match-live-popup__title">{{ lineTitle }}</text>
|
||||||
|
<text class="match-live-popup__desc">{{ lineMetaText }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="match-live-popup__section">
|
||||||
|
<text class="match-live-popup__section-title">直播线路</text>
|
||||||
|
<view v-if="streamOptions.length" class="match-live-popup__options">
|
||||||
|
<view
|
||||||
|
v-for="item in streamOptions"
|
||||||
|
:key="item.key"
|
||||||
|
class="match-live-popup__option"
|
||||||
|
:class="{ 'match-live-popup__option--active': item.key === selectedStreamKey }"
|
||||||
|
@tap="selectOption(item)"
|
||||||
|
>
|
||||||
|
<view class="match-live-popup__option-main">
|
||||||
|
<text class="match-live-popup__option-title">{{ item.label }}</text>
|
||||||
|
<text class="match-live-popup__option-url">{{ item.url }}</text>
|
||||||
|
</view>
|
||||||
|
<text v-if="item.key === selectedStreamKey" class="match-live-popup__option-tag">当前</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="match-live-popup__empty">
|
||||||
|
<text>暂无可用直播线路</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="match-live-popup__actions">
|
||||||
|
<u-button type="primary" shape="circle" @click="openSelected">打开当前线路</u-button>
|
||||||
|
<u-button shape="circle" plain @click="openSource">打开源站</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import type { MatchLiveLineItem, MatchLiveStreamOption } from '@/api/match'
|
||||||
|
|
||||||
|
type PopupStreamOption = MatchLiveStreamOption & {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
line: MatchLiveLineItem | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:show', 'close'])
|
||||||
|
|
||||||
|
const showPopup = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (value: boolean) => emit('update:show', value),
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedStreamKey = ref('')
|
||||||
|
|
||||||
|
const normalizeText = (value: unknown) => String(value || '').trim()
|
||||||
|
|
||||||
|
const normalizeUrl = (value: unknown) => {
|
||||||
|
const text = normalizeText(value)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMetaTime = (value: unknown) => {
|
||||||
|
const text = normalizeText(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
|
||||||
|
const date = new Date(raw)
|
||||||
|
if (!Number.isNaN(date.getTime())) {
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${hour}:${minute}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineTitle = computed(() => {
|
||||||
|
const line = props.line || {}
|
||||||
|
return (
|
||||||
|
normalizeText(line.title) ||
|
||||||
|
normalizeText(line.name) ||
|
||||||
|
normalizeText(line.label) ||
|
||||||
|
normalizeText(line.live_tag) ||
|
||||||
|
normalizeText(line.source_name) ||
|
||||||
|
'直播线路'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const lineMetaText = computed(() => {
|
||||||
|
const line = props.line || {}
|
||||||
|
const parts = [
|
||||||
|
normalizeText(line.status_text) || normalizeText(line.status_desc) || normalizeText(line.status),
|
||||||
|
formatMetaTime(line.updated_at || line.update_time),
|
||||||
|
].filter(Boolean)
|
||||||
|
return parts.length ? parts.join(' · ') : '请选择可用直播线路后打开'
|
||||||
|
})
|
||||||
|
|
||||||
|
const sourceUrl = computed(() => normalizeUrl(props.line?.source_url))
|
||||||
|
|
||||||
|
const buildStreamOption = (option: MatchLiveStreamOption | undefined, index: number) => {
|
||||||
|
if (!option) return null
|
||||||
|
const url =
|
||||||
|
normalizeUrl(option.play_url_m3u8) ||
|
||||||
|
normalizeUrl(option.play_url) ||
|
||||||
|
normalizeUrl(option.url) ||
|
||||||
|
normalizeUrl(option.default_play_url) ||
|
||||||
|
normalizeUrl(option.live_url)
|
||||||
|
if (!url) return null
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
|
key: `${index}-${url}`,
|
||||||
|
label:
|
||||||
|
normalizeText(option.title) ||
|
||||||
|
normalizeText(option.name) ||
|
||||||
|
normalizeText(option.label) ||
|
||||||
|
normalizeText(option.source_name) ||
|
||||||
|
normalizeText(option.quality) ||
|
||||||
|
`线路${index + 1}`,
|
||||||
|
url,
|
||||||
|
} satisfies PopupStreamOption
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamOptions = computed(() => {
|
||||||
|
const line = props.line || {}
|
||||||
|
const options: PopupStreamOption[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
|
||||||
|
const pushOption = (option: MatchLiveStreamOption | undefined, index: number) => {
|
||||||
|
const built = buildStreamOption(option, index)
|
||||||
|
if (!built || seen.has(built.url)) return
|
||||||
|
seen.add(built.url)
|
||||||
|
options.push(built)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(line.stream_options)) {
|
||||||
|
line.stream_options.forEach((item, index) => pushOption(item, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPlayUrl = normalizeUrl(line.default_play_url)
|
||||||
|
if (defaultPlayUrl && !seen.has(defaultPlayUrl)) {
|
||||||
|
pushOption({ label: '默认线路', url: defaultPlayUrl }, options.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const playUrlM3u8 = normalizeUrl(line.play_url_m3u8)
|
||||||
|
if (playUrlM3u8 && !seen.has(playUrlM3u8)) {
|
||||||
|
pushOption({ label: 'M3U8 线路', play_url_m3u8: playUrlM3u8 }, options.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const playUrl = normalizeUrl(line.play_url)
|
||||||
|
if (playUrl && !seen.has(playUrl)) {
|
||||||
|
pushOption({ label: '备用线路', play_url: playUrl }, options.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveUrl = normalizeUrl(line.live_url)
|
||||||
|
if (liveUrl && !seen.has(liveUrl)) {
|
||||||
|
pushOption({ label: '直播地址', live_url: liveUrl }, options.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedStream = computed(() => {
|
||||||
|
return streamOptions.value.find((item) => item.key === selectedStreamKey.value) || streamOptions.value[0] || null
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetSelected = () => {
|
||||||
|
const defaultPlayUrl = normalizeUrl(props.line?.default_play_url)
|
||||||
|
if (defaultPlayUrl) {
|
||||||
|
const matched = streamOptions.value.find((item) => item.url === defaultPlayUrl)
|
||||||
|
if (matched) {
|
||||||
|
selectedStreamKey.value = matched.key
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedStreamKey.value = streamOptions.value[0]?.key || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.line, props.show],
|
||||||
|
() => {
|
||||||
|
resetSelected()
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const navigateToWebview = (url: string) => {
|
||||||
|
uni.navigateTo({ url: `/pages/webview/webview?url=${encodeURIComponent(url)}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectOption = (item: PopupStreamOption) => {
|
||||||
|
selectedStreamKey.value = item.key
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSelected = () => {
|
||||||
|
const url = selectedStream.value?.url || ''
|
||||||
|
if (!url) {
|
||||||
|
uni.showToast({ title: '暂无可用直播链接', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigateToWebview(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openSource = () => {
|
||||||
|
if (!sourceUrl.value) {
|
||||||
|
uni.showToast({ title: '暂无源站链接', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigateToWebview(sourceUrl.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.match-live-popup {
|
||||||
|
padding: 40rpx 28rpx calc(28rpx + env(safe-area-inset-bottom));
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10rpx;
|
||||||
|
margin-bottom: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__section-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 22rpx 24rpx;
|
||||||
|
border: 2rpx solid #e5e7eb;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: #f9fafb;
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
border-color: #185dff;
|
||||||
|
background: rgba(24, 93, 255, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option-main {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option-url {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__option-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #185dff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__empty {
|
||||||
|
padding: 40rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -62,8 +62,31 @@
|
|||||||
</view>
|
</view>
|
||||||
</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 导航栏 -->
|
<!-- 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__card" @tap="openThirdPartyLive">
|
||||||
<view class="live-entrance__left">
|
<view class="live-entrance__left">
|
||||||
<view class="live-entrance__badge">
|
<view class="live-entrance__badge">
|
||||||
@@ -412,6 +435,8 @@
|
|||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
<MatchLivePopup v-model:show="showLivePopup" :line="selectedLiveLine" @close="handleLivePopupClose" />
|
||||||
|
|
||||||
<!-- 分享弹窗 -->
|
<!-- 分享弹窗 -->
|
||||||
<SharePopup v-if="showSharePopup" v-model:show="showSharePopup" page-type="match"
|
<SharePopup v-if="showSharePopup" v-model:show="showSharePopup" page-type="match"
|
||||||
:path="`/pages/match_detail/match_detail?id=${match.id || ''}`"
|
:path="`/pages/match_detail/match_detail?id=${match.id || ''}`"
|
||||||
@@ -423,10 +448,12 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, nextTick, onMounted } from 'vue'
|
import { ref, computed, nextTick, onMounted } from 'vue'
|
||||||
import MatchDetailHeader from '@/components/match-detail-header/match-detail-header.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 SharePopup from '@/components/share-popup/share-popup.vue'
|
||||||
import GlobalPopup from '@/components/global-popup/global-popup.vue'
|
import GlobalPopup from '@/components/global-popup/global-popup.vue'
|
||||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
||||||
import { getMatchDetail, getMatchLiveText, getMatchLineup } from '@/api/match'
|
import { getMatchDetail, getMatchLiveText, getMatchLineup } from '@/api/match'
|
||||||
|
import type { MatchLiveLineItem } from '@/api/match'
|
||||||
import { checkAiUnlock, getMatchPredictSection } from '@/api/ai'
|
import { checkAiUnlock, getMatchPredictSection } from '@/api/ai'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { getLocalMatchDate } from '@/utils/match-time'
|
import { getLocalMatchDate } from '@/utils/match-time'
|
||||||
@@ -447,9 +474,14 @@ const aiInlineLoading = ref(false)
|
|||||||
const aiInline = ref<any>({})
|
const aiInline = ref<any>({})
|
||||||
const showSharePopup = ref(false)
|
const showSharePopup = ref(false)
|
||||||
const liveTextList = ref<any[]>([])
|
const liveTextList = ref<any[]>([])
|
||||||
|
const showLivePopup = ref(false)
|
||||||
|
const selectedLiveLine = ref<MatchLiveLineItem | null>(null)
|
||||||
let liveTextTimer: ReturnType<typeof setInterval> | null = null
|
let liveTextTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
const thirdPartyLiveUrl = computed(() => String(match.value?.live_url || '').trim())
|
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 liveSourceText = computed(() => {
|
||||||
const raw = match.value?.living_tv ?? match.value?.live_source ?? match.value?.livingTv ?? ''
|
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 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 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 techStatsList = computed(() => {
|
||||||
const m = match.value as any
|
const m = match.value as any
|
||||||
// 优先使用 tech_stats JSON(兼容所有运动类型)
|
// 优先使用 tech_stats JSON(兼容所有运动类型)
|
||||||
@@ -540,6 +607,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
const fetchDetail = async (id: number) => {
|
const fetchDetail = async (id: number) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
showLivePopup.value = false
|
||||||
|
selectedLiveLine.value = null
|
||||||
try {
|
try {
|
||||||
const data = await getMatchDetail({ id })
|
const data = await getMatchDetail({ id })
|
||||||
match.value = data || {}
|
match.value = data || {}
|
||||||
@@ -604,6 +673,15 @@ const openThirdPartyLive = () => {
|
|||||||
uni.navigateTo({ url: `/pages/webview/webview?url=${encodeURIComponent(url)}` })
|
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) => {
|
const formatLiveTime = (ts: any) => {
|
||||||
if (!ts) return ''
|
if (!ts) return ''
|
||||||
const s = String(ts)
|
const s = String(ts)
|
||||||
@@ -1560,6 +1638,29 @@ onUnload(() => {
|
|||||||
|
|
||||||
.live-entrance {
|
.live-entrance {
|
||||||
margin: 0 24rpx 24rpx;
|
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 {
|
&__card {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user