deploy: auto commit repo-root changes 2026-07-03 18:56:25

This commit is contained in:
hajimi
2026-07-03 18:56:25 +08:00
parent affb0a4002
commit a88764162d
82 changed files with 299 additions and 83 deletions
@@ -37,8 +37,8 @@
@tap="openWorldCupLiveCard(live)">
<view class="worldcup-live-card__player-wrap">
<video :id="buildWorldCupLiveVideoId(live)" class="worldcup-live-card__player"
:src="resolveWorldCupLivePlayUrl(live)"
:poster="live.home_icon || live.away_icon || ''" :autoplay="true" :muted="true"
:src="resolveWorldCupLiveVideoSrc(live)"
:poster="live.home_icon || live.away_icon || ''" :muted="false" preload="none"
:controls="!hideLiveCardControlsOnAndroid"
:show-play-btn="!hideLiveCardControlsOnAndroid"
:show-fullscreen-btn="!hideLiveCardControlsOnAndroid" object-fit="cover" />
@@ -225,7 +225,10 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
// #ifdef H5
import Hls from 'hls.js'
// #endif
import RankingNav from '../pages/worldcup/components/ranking/RankingNav.vue'
import StandingsPanel from '../pages/worldcup/components/ranking/StandingsPanel.vue'
import PlayerRankingPanel from '../pages/worldcup/components/ranking/PlayerRankingPanel.vue'
@@ -284,6 +287,12 @@ const statusBarHeight = ref(0)
const hideLiveCardControlsOnAndroid = ref(false)
const liveCards = ref<WorldCupLiveCardItem[]>([])
const liveCardsLoaded = ref(false)
const activeWorldCupLiveKey = ref('')
const h5NativeHlsSupported = ref(false)
// #ifdef H5
let worldCupHlsPlayer: Hls | null = null
// #endif
const scheduleData = ref<any>({
current_round_name: '',
@@ -426,10 +435,18 @@ const liveLineTitle = (live: WorldCupLiveCardItem) => {
return normalizeLiveText(live.title) || '直播线路'
}
const buildWorldCupLiveVideoId = (live: WorldCupLiveCardItem) => {
return `worldcup-live-${live.live_id || live.id || 0}`
const buildWorldCupLiveKey = (live: WorldCupLiveCardItem) => {
return String(live.live_id || live.id || 0)
}
const buildWorldCupLiveVideoId = (live: WorldCupLiveCardItem) => {
return `worldcup-live-${buildWorldCupLiveKey(live)}`
}
const isHlsLiveUrl = (url: string) => /\.m3u8(\?|$)/i.test(url)
const isFlvLiveUrl = (url: string) => /\.flv(\?|$)/i.test(url)
const resolveWorldCupLivePlayUrl = (live: WorldCupLiveCardItem) => {
const urls: string[] = [
live.play_url,
@@ -452,6 +469,19 @@ const resolveWorldCupLivePlayUrl = (live: WorldCupLiveCardItem) => {
return urls.find((url) => /\.(m3u8|mp4|flv)(\?|$)/i.test(url)) || urls[0] || ''
}
const resolveWorldCupLiveVideoSrc = (live: WorldCupLiveCardItem) => {
const url = resolveWorldCupLivePlayUrl(live)
if (!url) return ''
// #ifdef H5
if (activeWorldCupLiveKey.value !== buildWorldCupLiveKey(live)) return ''
if (isFlvLiveUrl(url)) return ''
if (isHlsLiveUrl(url) && !h5NativeHlsSupported.value) return ''
// #endif
return url
}
const liveStatusText = (live: WorldCupLiveCardItem) => {
if ((live.status ?? 0) === 1) {
return normalizeLiveText(live.current_minute) || '进行中'
@@ -524,6 +554,19 @@ const activeBracketPairs = computed(() => {
return pairs
})
const detectH5NativeHlsSupported = () => {
// #ifdef H5
const video = document.createElement('video')
return Boolean(
video.canPlayType('application/vnd.apple.mpegurl') ||
video.canPlayType('application/x-mpegURL')
)
// #endif
// #ifndef H5
return false
// #endif
}
const initLayout = () => {
const info = uni.getSystemInfoSync()
hideLiveCardControlsOnAndroid.value = String(info.platform || '').toLowerCase() === 'android'
@@ -532,6 +575,7 @@ const initLayout = () => {
// #endif
// #ifdef H5
statusBarHeight.value = 0
h5NativeHlsSupported.value = detectH5NativeHlsSupported()
// #endif
}
@@ -629,12 +673,169 @@ const goMatchDetail = (match: any) => {
uni.navigateTo({ url: `/pages/match_detail/match_detail?id=${id}` })
}
const destroyWorldCupH5Player = () => {
// #ifdef H5
if (worldCupHlsPlayer) {
worldCupHlsPlayer.destroy()
worldCupHlsPlayer = null
}
// #endif
}
const getH5WorldCupLiveVideoElement = (videoId: string): HTMLVideoElement | null => {
// #ifdef H5
const node = document.getElementById(videoId)
if (!node) return null
if (node instanceof HTMLVideoElement) return node
return node.querySelector('video')
// #endif
// #ifndef H5
return null
// #endif
}
const requestH5WorldCupLiveFullscreen = (video: HTMLVideoElement, videoId: string) => {
// #ifdef H5
try {
const h5Video = video as HTMLVideoElement & {
webkitEnterFullscreen?: () => void
webkitRequestFullscreen?: () => Promise<void> | void
msRequestFullscreen?: () => Promise<void> | void
}
if (typeof h5Video.webkitEnterFullscreen === 'function') {
h5Video.webkitEnterFullscreen()
return
}
const request = h5Video.requestFullscreen ||
h5Video.webkitRequestFullscreen ||
h5Video.msRequestFullscreen
if (typeof request === 'function') {
const result = request.call(h5Video)
if (result && typeof result.catch === 'function') {
void result.catch(() => undefined)
}
return
}
uni.createVideoContext(videoId)?.requestFullScreen?.({ direction: 90 })
} catch (error) {
console.warn('进入世界杯 H5 直播全屏失败', error)
}
// #endif
}
const playH5WorldCupVideo = async (video: HTMLVideoElement) => {
// #ifdef H5
try {
video.muted = false
await video.play()
} catch (error) {
video.muted = true
await video.play()
}
// #endif
}
const loadH5WorldCupHlsSource = (video: HTMLVideoElement, url: string) => {
// #ifdef H5
return new Promise<void>((resolve, reject) => {
if (!Hls.isSupported()) {
reject(new Error('HLS is not supported'))
return
}
destroyWorldCupH5Player()
const hls = new Hls({
enableWorker: true,
lowLatencyMode: true,
liveSyncDurationCount: 3,
})
worldCupHlsPlayer = hls
let settled = false
const resolveOnce = () => {
if (settled) return
settled = true
resolve()
}
const rejectOnce = (error: Error) => {
if (settled) return
settled = true
reject(error)
}
hls.on(Hls.Events.MANIFEST_PARSED, resolveOnce)
hls.on(Hls.Events.ERROR, (_event, data) => {
if (data?.fatal) {
rejectOnce(new Error(data.details || 'HLS fatal error'))
}
})
hls.attachMedia(video)
hls.loadSource(url)
})
// #endif
// #ifndef H5
return Promise.resolve()
// #endif
}
const openH5WorldCupLiveCard = async (live: WorldCupLiveCardItem, playUrl: string) => {
// #ifdef H5
if (isFlvLiveUrl(playUrl)) {
uni.showToast({ title: 'H5暂不支持FLV直播流', icon: 'none' })
return
}
activeWorldCupLiveKey.value = buildWorldCupLiveKey(live)
const videoId = buildWorldCupLiveVideoId(live)
const video = getH5WorldCupLiveVideoElement(videoId)
if (!video) {
uni.showToast({ title: '未找到直播播放器', icon: 'none' })
return
}
requestH5WorldCupLiveFullscreen(video, videoId)
try {
video.pause()
if (isHlsLiveUrl(playUrl) && !h5NativeHlsSupported.value) {
video.removeAttribute('src')
video.load()
await loadH5WorldCupHlsSource(video, playUrl)
} else {
destroyWorldCupH5Player()
if (video.currentSrc !== playUrl && video.src !== playUrl) {
video.src = playUrl
video.load()
}
}
await playH5WorldCupVideo(video)
} catch (error) {
console.warn('播放世界杯 H5 直播失败', error)
destroyWorldCupH5Player()
activeWorldCupLiveKey.value = ''
uni.showToast({ title: '当前浏览器无法播放该直播流', icon: 'none' })
}
// #endif
}
const openWorldCupLiveCard = (live: WorldCupLiveCardItem) => {
if (!resolveWorldCupLivePlayUrl(live)) {
const playUrl = resolveWorldCupLivePlayUrl(live)
if (!playUrl) {
uni.showToast({ title: '暂无直播链接', icon: 'none' })
return
}
// #ifdef H5
void openH5WorldCupLiveCard(live, playUrl)
return
// #endif
try {
const videoContext = uni.createVideoContext(buildWorldCupLiveVideoId(live))
videoContext?.requestFullScreen?.({
@@ -771,6 +972,10 @@ const initViewState = async () => {
}
watch(activeTab, (tab) => {
if (tab !== 'live') {
activeWorldCupLiveKey.value = ''
destroyWorldCupH5Player()
}
ensureTabData(tab)
}, { immediate: true })
@@ -789,6 +994,10 @@ watch(scheduleSort, () => {
onMounted(() => {
initViewState()
})
onBeforeUnmount(() => {
destroyWorldCupH5Player()
})
</script>
<style lang="scss" scoped>