deploy: auto commit repo-root changes 2026-06-21 17:56:21

This commit is contained in:
hajimi
2026-06-21 17:56:21 +08:00
parent bb1a2ebb9e
commit 980b019809
5 changed files with 836 additions and 175 deletions
@@ -31,7 +31,42 @@
<scroll-view scroll-y scroll-with-animation class="worldcup-scroll"
:scroll-into-view="activeTab === 'schedule' ? scheduleScrollIntoView : ''">
<block v-if="activeTab === 'schedule'">
<block v-if="activeTab === 'live'">
<view v-if="liveMatches.length" class="worldcup-live-grid">
<view
v-for="match in liveMatches"
:key="`live-${match.id || match.match_id}`"
class="worldcup-live-card"
@tap="openWorldCupLive(match)"
>
<view class="worldcup-live-card__top">
<text class="worldcup-live-card__badge">直播</text>
<text class="worldcup-live-card__count">{{ getMatchLiveLines(match).length }}条线路</text>
</view>
<view class="worldcup-live-card__teams">
<text class="worldcup-live-card__team">{{ match.home_team }}</text>
<text class="worldcup-live-card__vs">VS</text>
<text class="worldcup-live-card__team">{{ match.away_team }}</text>
</view>
<view class="worldcup-live-card__meta">
<text>{{ matchMetaText(match, 0) }}</text>
<text>{{ formatMatchTime(match.match_time) }}</text>
</view>
<view class="worldcup-live-card__footer">
<text class="worldcup-live-card__line">{{ getPrimaryLiveLineTitle(match) }}</text>
<text class="worldcup-live-card__switch" @tap.stop="openLiveLinePopup(match)">切换线路</text>
</view>
</view>
</view>
<view v-else class="worldcup-empty">
<u-empty text="暂无直播" mode="data" />
</view>
</block>
<block v-else-if="activeTab === 'schedule'">
<view class="worldcup-sortbar" @tap="toggleScheduleSort">
<text class="worldcup-sortbar__label">{{ scheduleSortLabel }}</text>
<text class="worldcup-sortbar__icon"></text>
@@ -193,6 +228,13 @@
</view>
</view>
</scroll-view>
<MatchLivePopup
v-model:show="showLivePopup"
:match-title="livePopupMatchTitle"
:lines="livePopupLines"
@close="handleLivePopupClose"
/>
</view>
</template>
@@ -201,12 +243,18 @@ 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 { getWorldCupRankings, getWorldCupSchedule, getWorldCupStandings } from '@/api/match'
import {
getWorldCupRankings,
getWorldCupSchedule,
getWorldCupStandings,
type MatchLiveLineItem,
} from '@/api/match'
import { getLocalMatchDate } from '@/utils/match-time'
type MainTab = 'schedule' | 'rank' | 'bracket' | 'news'
type MainTab = 'live' | 'schedule' | 'rank' | 'bracket' | 'news'
type RankingMode = 'standings' | 'goals' | 'assists'
type ScheduleSort = 'time' | 'group'
type BracketTabKey = '1/16决赛' | '1/8决赛' | '1/4决赛' | '半决赛' | '决赛'
@@ -224,6 +272,7 @@ const emit = defineEmits<{
}>()
const tabs: { key: MainTab; label: string }[] = [
{ key: 'live', label: '直播' },
{ key: 'schedule', label: '比分和赛程' },
{ key: 'rank', label: '排名' },
{ key: 'bracket', label: '对阵图' },
@@ -240,11 +289,13 @@ const bracketTabs: { key: BracketTabKey; label: string }[] = [
{ key: '决赛', label: '决赛' }
]
const activeTab = ref<MainTab>(tabs[0].key)
const activeTab = ref<MainTab>('schedule')
const rankingMode = ref<RankingMode>('standings')
const scheduleSort = ref<ScheduleSort>('time')
const activeBracketTab = ref<BracketTabKey>(bracketTabs[0].key)
const scheduleScrollIntoView = ref('')
const showLivePopup = ref(false)
const livePopupMatch = ref<any | null>(null)
const statusBarHeight = ref(0)
const scheduleData = ref<any>({
@@ -382,6 +433,66 @@ const resolveBracketTeamIcon = (rawValue: any, fallback = '') => {
return fallback
}
const normalizeLiveText = (value: unknown) => String(value || '').trim()
const getMatchLiveLines = (match: any): MatchLiveLineItem[] => {
return Array.isArray(match?.live_list) ? match.live_list : []
}
const collectLineUrls = (line: MatchLiveLineItem) => {
const candidates: string[] = []
const seen = new Set<string>()
const push = (value: unknown) => {
const url = normalizeLiveText(value)
if (!url || seen.has(url)) return
seen.add(url)
candidates.push(url)
}
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) ||
'点击进入直播'
)
}
const flattenScheduleMatches = computed(() => {
const matches: any[] = []
; (scheduleData.value.schedule_rounds || []).forEach((round: any) => {
@@ -396,6 +507,13 @@ 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[] }> = []
@@ -459,6 +577,14 @@ 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 fetchSchedule = async () => {
scheduleData.value = await getWorldCupSchedule() || { current_round_name: '', bracket_rounds: [], schedule_rounds: [] }
if (activeTab.value === 'schedule') {
@@ -505,7 +631,7 @@ const fetchNews = async () => {
}
const ensureTabData = async (tab: MainTab) => {
if (tab === 'schedule') {
if (tab === 'live' || tab === 'schedule') {
await fetchSchedule()
} else if (tab === 'rank') {
if (rankingMode.value === 'standings') {
@@ -532,6 +658,33 @@ 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')
}
@@ -636,7 +789,9 @@ const initViewState = async () => {
const legacyTab = String(props.initialTab || 'schedule')
await resolveNewsCategoryId(props.initialCid)
if (legacyTab === 'schedule') {
if (legacyTab === 'live') {
activeTab.value = 'live'
} else if (legacyTab === 'schedule') {
activeTab.value = 'schedule'
} else if (legacyTab === 'rank' || legacyTab === 'standings') {
activeTab.value = 'rank'
@@ -876,6 +1031,92 @@ onMounted(() => {
flex-shrink: 0;
}
.worldcup-live-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12rpx;
padding-top: 12rpx;
}
.worldcup-live-card {
display: flex;
flex-direction: column;
gap: 12rpx;
min-width: 0;
padding: 20rpx 18rpx;
border-radius: 22rpx;
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
box-shadow: 0 10rpx 28rpx rgba(37, 99, 235, 0.08);
}
.worldcup-live-card__top,
.worldcup-live-card__meta,
.worldcup-live-card__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10rpx;
}
.worldcup-live-card__badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 68rpx;
height: 36rpx;
padding: 0 12rpx;
border-radius: 999rpx;
background: rgba(53, 82, 255, 0.12);
color: #3552ff;
font-size: 22rpx;
font-weight: 700;
}
.worldcup-live-card__count,
.worldcup-live-card__meta {
font-size: 22rpx;
color: #64748b;
}
.worldcup-live-card__teams {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
min-width: 0;
}
.worldcup-live-card__team {
flex: 1;
min-width: 0;
font-size: 24rpx;
font-weight: 600;
color: #0f172a;
text-align: center;
line-height: 1.4;
}
.worldcup-live-card__vs {
flex-shrink: 0;
font-size: 22rpx;
color: #94a3b8;
}
.worldcup-live-card__line {
flex: 1;
min-width: 0;
font-size: 22rpx;
color: #1e293b;
line-height: 1.4;
}
.worldcup-live-card__switch {
flex-shrink: 0;
color: #3552ff;
font-size: 22rpx;
font-weight: 600;
}
.wc-match-card {
display: flex;
align-items: center;