diff --git a/server/app/api/controller/MatchController.php b/server/app/api/controller/MatchController.php index 7be6b1c..3cba9da 100644 --- a/server/app/api/controller/MatchController.php +++ b/server/app/api/controller/MatchController.php @@ -17,7 +17,7 @@ use app\common\service\match\MatchLiveService; class MatchController extends BaseApiController { - public array $notNeedLogin = ['lists', 'detail', 'leagues', 'sportTypes', 'liveText', 'lineup', 'worldCupStandings', 'worldCupRankings', 'worldCupSchedule']; + public array $notNeedLogin = ['lists', 'detail', 'leagues', 'sportTypes', 'liveText', 'lineup', 'worldCupStandings', 'worldCupRankings', 'worldCupSchedule', 'worldCupLiveCards']; protected int $worldCupSeasonId = 26123; protected string $worldCupLeagueName = '世界杯'; @@ -428,20 +428,10 @@ class MatchController extends BaseApiController ->order('id asc') ->select() ->toArray(); - $liveMap = MatchLiveService::getLiveMapForApi(array_column($rows, 'id')); $scheduleMap = []; foreach ($rows as $row) { $roundName = $row['round_name'] ?: '未分轮次'; - $liveList = $liveMap[(int) $row['id']] ?? []; - $defaultLiveUrl = ''; - foreach ($liveList as $line) { - $candidate = trim((string) ($line['default_play_url'] ?? '')); - if ($candidate !== '') { - $defaultLiveUrl = $candidate; - break; - } - } if (!isset($scheduleMap[$roundName])) { $scheduleMap[$roundName] = [ 'round_name' => $roundName, @@ -466,8 +456,6 @@ class MatchController extends BaseApiController 'match_time' => (int) $row['match_time'], 'current_minute' => $row['current_minute'], 'half_score' => $row['half_score'], - 'live_list' => $liveList, - 'default_live_url' => $defaultLiveUrl, ]; } @@ -515,4 +503,12 @@ class MatchController extends BaseApiController 'schedule_rounds' => $scheduleRounds, ]); } + + public function worldCupLiveCards() + { + return $this->data([ + 'season_id' => $this->worldCupSeasonId, + 'list' => MatchLiveService::getWorldCupLiveCards(), + ]); + } } diff --git a/server/app/common/service/match/MatchLiveService.php b/server/app/common/service/match/MatchLiveService.php index 8e7a208..b449198 100644 --- a/server/app/common/service/match/MatchLiveService.php +++ b/server/app/common/service/match/MatchLiveService.php @@ -96,4 +96,89 @@ class MatchLiveService return ''; } + + public static function getWorldCupLiveCards(): array + { + $rows = MatchLive::alias('live') + ->join('match m', 'm.id = live.match_id') + ->where(['m.is_show' => 1]) + ->where([['m.status', '<>', 2]]) + ->where(function ($query) { + $query->where('m.league_name', '世界杯') + ->whereOr('m.competition_id', 61); + }) + ->field([ + 'live.id as live_id', + 'live.match_id', + 'live.title', + 'live.source_url', + 'live.play_url_m3u8', + 'live.play_url_hd_m3u8', + 'live.play_url_flv', + 'live.play_url_hd_flv', + 'live.fetch_status', + 'live.last_fetch_at', + 'live.update_time as live_update_time', + 'm.id', + 'm.match_id as third_match_id', + 'm.competition_id', + 'm.league_name', + 'm.round_name', + 'm.stage', + 'm.home_team', + 'm.home_icon', + 'm.home_score', + 'm.away_team', + 'm.away_icon', + 'm.away_score', + 'm.status', + 'm.match_time', + 'm.current_minute', + 'm.half_score', + ]) + ->order('m.match_time', 'asc') + ->order('live.create_time', 'asc') + ->order('live.id', 'asc') + ->select() + ->toArray(); + + $cards = []; + foreach ($rows as $row) { + $row['stream_options'] = self::buildStreamOptions($row); + $row['default_play_url'] = self::resolveDefaultPlayUrl($row); + if ($row['default_play_url'] === '') { + continue; + } + + $cards[] = [ + 'live_id' => (int) $row['live_id'], + 'match_id' => (int) $row['match_id'], + 'title' => (string) ($row['title'] ?? ''), + 'source_url' => (string) ($row['source_url'] ?? ''), + 'play_url' => (string) $row['default_play_url'], + 'default_play_url' => (string) $row['default_play_url'], + 'stream_options' => $row['stream_options'], + 'fetch_status' => $row['fetch_status'] ?? '', + 'last_fetch_at' => $row['last_fetch_at'] ?? '', + 'update_time' => $row['live_update_time'] ?? '', + 'id' => (int) $row['id'], + 'competition_id' => (int) ($row['competition_id'] ?? 0), + 'league_name' => (string) ($row['league_name'] ?? ''), + 'round_name' => (string) ($row['round_name'] ?? ''), + 'stage' => (string) ($row['stage'] ?? ''), + 'home_team' => (string) ($row['home_team'] ?? ''), + 'home_icon' => (string) ($row['home_icon'] ?? ''), + 'home_score' => (int) ($row['home_score'] ?? 0), + 'away_team' => (string) ($row['away_team'] ?? ''), + 'away_icon' => (string) ($row['away_icon'] ?? ''), + 'away_score' => (int) ($row['away_score'] ?? 0), + 'status' => (int) ($row['status'] ?? 0), + 'match_time' => (int) ($row['match_time'] ?? 0), + 'current_minute' => $row['current_minute'] ?? '', + 'half_score' => (string) ($row['half_score'] ?? ''), + ]; + } + + return $cards; + } } diff --git a/uniapp/src/api/match.ts b/uniapp/src/api/match.ts index 667038e..49089a3 100644 --- a/uniapp/src/api/match.ts +++ b/uniapp/src/api/match.ts @@ -49,6 +49,35 @@ export interface MatchLiveLineItem { [key: string]: any } +export interface WorldCupLiveCardItem { + live_id: number + match_id: number + title?: string + source_url?: string + play_url: string + default_play_url?: string + stream_options?: MatchLiveStreamOption[] + fetch_status?: string | number + last_fetch_at?: string | number + update_time?: string | number + id: number + competition_id?: number + league_name?: string + round_name?: string + stage?: string + home_team?: string + home_icon?: string + home_score?: number + away_team?: string + away_icon?: string + away_score?: number + status?: number + match_time?: number + current_minute?: string + half_score?: string + [key: string]: any +} + export function getMatchList(data?: Record) { return request.get({ url: '/match/lists', data: data }, { withToken: false }) } @@ -84,3 +113,7 @@ export function getWorldCupRankings(data: { type: 'goals' | 'assists' }) { export function getWorldCupSchedule() { return request.get({ url: '/match/worldCupSchedule' }, { withToken: false }) } + +export function getWorldCupLiveCards() { + return request.get({ url: '/match/worldCupLiveCards' }, { withToken: false }) +} diff --git a/uniapp/src/packages_match/components/WorldCupPanel.vue b/uniapp/src/packages_match/components/WorldCupPanel.vue index 81beb09..1874254 100644 --- a/uniapp/src/packages_match/components/WorldCupPanel.vue +++ b/uniapp/src/packages_match/components/WorldCupPanel.vue @@ -32,32 +32,47 @@ - + 直播 - {{ getMatchLiveLines(match).length }}条线路 + {{ liveStatusText(live) }} + + + + - {{ match.home_team }} + {{ live.home_team }} VS - {{ match.away_team }} + {{ live.away_team }} - {{ matchMetaText(match, 0) }} - {{ formatMatchTime(match.match_time) }} + {{ matchMetaText(live, 0) }} + {{ formatMatchTime(live.match_time) }} - {{ getPrimaryLiveLineTitle(match) }} - 切换线路 + {{ liveLineTitle(live) }} + 进入详情 @@ -228,13 +243,6 @@ - - @@ -243,14 +251,14 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue' 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' -import MatchLivePopup from '@/components/match-live-popup/match-live-popup.vue' import TranslatedArticleCard from '@/components/translated-article-card/translated-article-card.vue' import { getArticleCate, getArticleList } from '@/api/news' import { + getWorldCupLiveCards, getWorldCupRankings, getWorldCupSchedule, getWorldCupStandings, - type MatchLiveLineItem, + type WorldCupLiveCardItem, } from '@/api/match' import { getLocalMatchDate } from '@/utils/match-time' @@ -294,9 +302,9 @@ const rankingMode = ref('standings') const scheduleSort = ref('time') const activeBracketTab = ref(bracketTabs[0].key) const scheduleScrollIntoView = ref('') -const showLivePopup = ref(false) -const livePopupMatch = ref(null) const statusBarHeight = ref(0) +const liveCards = ref([]) +const liveCardsLoaded = ref(false) const scheduleData = ref({ current_round_name: '', @@ -435,62 +443,15 @@ const resolveBracketTeamIcon = (rawValue: any, fallback = '') => { const normalizeLiveText = (value: unknown) => String(value || '').trim() -const getMatchLiveLines = (match: any): MatchLiveLineItem[] => { - return Array.isArray(match?.live_list) ? match.live_list : [] +const liveLineTitle = (live: WorldCupLiveCardItem) => { + return normalizeLiveText(live.title) || '直播线路' } -const collectLineUrls = (line: MatchLiveLineItem) => { - const candidates: string[] = [] - const seen = new Set() - const push = (value: unknown) => { - const url = normalizeLiveText(value) - if (!url || seen.has(url)) return - seen.add(url) - candidates.push(url) +const liveStatusText = (live: WorldCupLiveCardItem) => { + if ((live.status ?? 0) === 1) { + return normalizeLiveText(live.current_minute) || '进行中' } - - push(line.default_play_url) - if (Array.isArray(line.stream_options)) { - line.stream_options.forEach((option) => { - push(option.default_play_url) - push(option.play_url_m3u8) - push(option.play_url_hd_m3u8) - push(option.play_url_flv) - push(option.play_url_hd_flv) - push(option.play_url) - push(option.url) - push(option.live_url) - }) - } - push(line.play_url_m3u8) - push(line.play_url_hd_m3u8) - push(line.play_url_flv) - push(line.play_url_hd_flv) - push(line.play_url) - - return candidates -} - -const getLineDirectUrl = (line: MatchLiveLineItem) => collectLineUrls(line)[0] || '' - -const getLineSourceUrl = (line: MatchLiveLineItem) => normalizeLiveText(line.source_url || line.live_url) - -const getPrimaryLiveLine = (match: any) => { - return getMatchLiveLines(match).find((line) => getLineDirectUrl(line) || getLineSourceUrl(line)) || null -} - -const getPrimaryLiveLineTitle = (match: any) => { - const line = getPrimaryLiveLine(match) - if (!line) return '点击进入直播' - - return ( - normalizeLiveText(line.title) || - normalizeLiveText(line.name) || - normalizeLiveText(line.label) || - normalizeLiveText(line.live_tag) || - normalizeLiveText(line.source_name) || - '点击进入直播' - ) + return '未开赛' } const flattenScheduleMatches = computed(() => { @@ -507,13 +468,6 @@ const flattenScheduleMatches = computed(() => { return matches.sort((a, b) => (a.match_time || 0) - (b.match_time || 0)) }) -const liveMatches = computed(() => { - return flattenScheduleMatches.value.filter((match) => { - if ((match?.status ?? 0) === 2) return false - return !!getPrimaryLiveLine(match) - }) -}) - const scheduleGroups = computed(() => { const groups: Array<{ key: string; label: string; items: any[] }> = [] @@ -577,13 +531,12 @@ const initLayout = () => { const scheduleGroupAnchorId = (key: string) => `worldcup-schedule-group-${key}` -const livePopupLines = computed(() => getMatchLiveLines(livePopupMatch.value)) - -const livePopupMatchTitle = computed(() => { - const match = livePopupMatch.value - if (!match) return '切换线路' - return `${match.home_team || ''} VS ${match.away_team || ''}`.trim() || '切换线路' -}) +const fetchWorldCupLiveCards = async () => { + if (liveCardsLoaded.value) return + const data = await getWorldCupLiveCards() + liveCards.value = Array.isArray(data?.list) ? data.list : [] + liveCardsLoaded.value = true +} const fetchSchedule = async () => { scheduleData.value = await getWorldCupSchedule() || { current_round_name: '', bracket_rounds: [], schedule_rounds: [] } @@ -631,7 +584,9 @@ const fetchNews = async () => { } const ensureTabData = async (tab: MainTab) => { - if (tab === 'live' || tab === 'schedule') { + if (tab === 'live') { + await fetchWorldCupLiveCards() + } else if (tab === 'schedule') { await fetchSchedule() } else if (tab === 'rank') { if (rankingMode.value === 'standings') { @@ -658,33 +613,6 @@ const toggleScheduleSort = () => { scheduleSort.value = scheduleSort.value === 'time' ? 'group' : 'time' } -const navigateToWebview = (url: string) => { - uni.navigateTo({ url: `/pages/webview/webview?url=${encodeURIComponent(url)}` }) -} - -const openWorldCupLive = (match: any) => { - const primaryLine = getPrimaryLiveLine(match) - const url = primaryLine ? (getLineDirectUrl(primaryLine) || getLineSourceUrl(primaryLine)) : '' - if (!url) { - uni.showToast({ title: '暂无可用直播线路', icon: 'none' }) - return - } - navigateToWebview(url) -} - -const openLiveLinePopup = (match: any) => { - if (!getMatchLiveLines(match).length) { - uni.showToast({ title: '暂无直播线路', icon: 'none' }) - return - } - livePopupMatch.value = match - showLivePopup.value = true -} - -const handleLivePopupClose = () => { - livePopupMatch.value = null -} - const goBack = () => { emit('back') } @@ -1049,6 +977,20 @@ onMounted(() => { box-shadow: 0 10rpx 28rpx rgba(37, 99, 235, 0.08); } +.worldcup-live-card__player-wrap { + width: 100%; + height: 210rpx; + border-radius: 18rpx; + overflow: hidden; + background: #0f172a; +} + +.worldcup-live-card__player { + width: 100%; + height: 100%; + display: block; +} + .worldcup-live-card__top, .worldcup-live-card__meta, .worldcup-live-card__footer {