diff --git a/qa/frontend/test_worldcup_live_bottom_padding_static.py b/qa/frontend/test_worldcup_live_bottom_padding_static.py new file mode 100644 index 0000000..f72477b --- /dev/null +++ b/qa/frontend/test_worldcup_live_bottom_padding_static.py @@ -0,0 +1,48 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +PANEL = ROOT / "uniapp" / "src" / "packages_match" / "components" / "WorldCupPanel.vue" + + +def extract_style_block(source: str, selector: str) -> str: + start = source.find(selector) + if start < 0: + raise AssertionError(f"missing selector: {selector}") + + brace = source.find("{", start) + if brace < 0: + raise AssertionError(f"missing style body for: {selector}") + + depth = 0 + for pos in range(brace, len(source)): + char = source[pos] + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + return source[brace + 1:pos] + + raise AssertionError(f"unterminated style body for: {selector}") + + +def main() -> int: + panel = PANEL.read_text(encoding="utf-8") + scroll_style = extract_style_block(panel, ".worldcup-scroll") + + required_tokens = [ + "140rpx", + "constant(safe-area-inset-bottom)", + "env(safe-area-inset-bottom)", + ] + for token in required_tokens: + if token not in scroll_style: + raise AssertionError(f"{PANEL} .worldcup-scroll bottom padding should reserve tabbar space: {token}") + + print("worldcup live bottom padding static checks passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/uniapp/src/packages_match/components/WorldCupPanel.vue b/uniapp/src/packages_match/components/WorldCupPanel.vue index cf87c93..deac5b9 100644 --- a/uniapp/src/packages_match/components/WorldCupPanel.vue +++ b/uniapp/src/packages_match/components/WorldCupPanel.vue @@ -34,11 +34,12 @@ + @tap="openWorldCupLiveCard(live)"> - @@ -425,6 +426,32 @@ const liveLineTitle = (live: WorldCupLiveCardItem) => { return normalizeLiveText(live.title) || '直播线路' } +const buildWorldCupLiveVideoId = (live: WorldCupLiveCardItem) => { + return `worldcup-live-${live.live_id || live.id || 0}` +} + +const resolveWorldCupLivePlayUrl = (live: WorldCupLiveCardItem) => { + const urls: string[] = [ + live.play_url, + live.default_play_url, + ].map(normalizeLiveText).filter(Boolean) + + if (Array.isArray(live.stream_options)) { + live.stream_options.forEach((option) => { + urls.push( + normalizeLiveText(option?.url), + normalizeLiveText(option?.play_url), + normalizeLiveText(option?.play_url_m3u8), + normalizeLiveText(option?.play_url_hd_m3u8), + normalizeLiveText(option?.play_url_flv), + normalizeLiveText(option?.play_url_hd_flv) + ) + }) + } + + return urls.find((url) => /\.(m3u8|mp4|flv)(\?|$)/i.test(url)) || urls[0] || '' +} + const liveStatusText = (live: WorldCupLiveCardItem) => { if ((live.status ?? 0) === 1) { return normalizeLiveText(live.current_minute) || '进行中' @@ -602,6 +629,24 @@ const goMatchDetail = (match: any) => { uni.navigateTo({ url: `/pages/match_detail/match_detail?id=${id}` }) } +const openWorldCupLiveCard = (live: WorldCupLiveCardItem) => { + if (!resolveWorldCupLivePlayUrl(live)) { + uni.showToast({ title: '暂无直播链接', icon: 'none' }) + return + } + + try { + const videoContext = uni.createVideoContext(buildWorldCupLiveVideoId(live)) + videoContext?.requestFullScreen?.({ + direction: 90, + }) + videoContext?.play?.() + } catch (error) { + console.warn('进入世界杯直播全屏失败', error) + uni.showToast({ title: '当前设备暂不支持全屏', icon: 'none' }) + } +} + const getWorldCupDisplayDate = (timestamp: number) => { return getLocalMatchDate(timestamp, WORLD_CUP_MATCH_CONTEXT) } @@ -888,6 +933,8 @@ onMounted(() => { height: 0; min-height: 0; padding: 0 14rpx 18rpx; + padding-bottom: calc(140rpx + constant(safe-area-inset-bottom)); + padding-bottom: calc(140rpx + env(safe-area-inset-bottom)); box-sizing: border-box; background: #fff; }