实现直播详情页赔率自动更新功能,优化赔率数据结构

This commit is contained in:
yuchenglong
2026-01-15 09:44:40 +08:00
parent 622fd2b327
commit 236701c302
3 changed files with 155 additions and 29 deletions

View File

@@ -40,11 +40,26 @@ export default function LiveDetailScreen() {
useEffect(() => {
loadLiveDetail();
// 设置每 15 秒更新一次直播比分
const timer = setInterval(() => {
refreshLiveDetail();
}, 15000);
return () => clearInterval(timer);
}, [id, league_id]);
const loadLiveDetail = async () => {
try {
setLoading(true);
await refreshLiveDetail();
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const refreshLiveDetail = async () => {
try {
// Fetch live scores for the league
const sportId = parseInt(sport_id || "1");
const leagueId = parseInt(league_id || "0");
@@ -59,9 +74,7 @@ export default function LiveDetailScreen() {
}
}
} catch (err) {
console.error(err);
} finally {
setLoading(false);
console.error("Refresh live detail error:", err);
}
};
@@ -85,15 +98,18 @@ export default function LiveDetailScreen() {
}
const renderTabContent = () => {
const numericSportId = parseInt(sport_id || "1");
switch (activeTab) {
case "stats":
return <StatsCard match={match} isDark={isDark} />;
case "odds":
return <OddsCard match={match} isDark={isDark} />;
return (
<OddsCard match={match} isDark={isDark} sportId={numericSportId} />
);
case "detail":
return (
<>
<OddsCard match={match} isDark={isDark} />
<OddsCard match={match} isDark={isDark} sportId={numericSportId} />
<StatsCard match={match} isDark={isDark} />
<EventsTimeline match={match} isDark={isDark} />
<OtherInfoCard match={match} isDark={isDark} />

View File

@@ -1,23 +1,116 @@
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { LiveScoreMatch } from "@/types/api";
import React from "react";
import { fetchOdds } from "@/lib/api";
import { LiveScoreMatch, OddsItem } from "@/types/api";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native";
import { ActivityIndicator, StyleSheet, View } from "react-native";
interface OddsCardProps {
match: LiveScoreMatch;
isDark: boolean;
sportId: number;
}
export function OddsCard({ match, isDark }: OddsCardProps) {
export function OddsCard({ match, isDark, sportId }: OddsCardProps) {
const { t } = useTranslation();
const [odds, setOdds] = useState<OddsItem | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadOdds();
// 设置每 30 秒自动更新一次
const interval = setInterval(loadOdds, 30000);
return () => clearInterval(interval);
}, [match.event_key, sportId]);
const loadOdds = async () => {
try {
const data = await fetchOdds(sportId, match.event_key);
const matchOdds = data[match.event_key.toString()];
if (matchOdds && matchOdds.data && matchOdds.data.length > 0) {
// 优先选择包含常见赔率项的博彩公司,这里暂取第一个
setOdds(matchOdds.data[0]);
}
} catch (error) {
console.error("Load odds error:", error);
} finally {
setLoading(false);
}
};
// 提取队名缩写或前3个字母
const homeAbbr =
match.event_home_team?.substring(0, 3).toUpperCase() || "HOME";
const awayAbbr =
match.event_away_team?.substring(0, 3).toUpperCase() || "AWAY";
// 获取显示的盘口和赔率
const renderOddsContent = () => {
if (loading && !odds) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="small" color="#FF9800" />
</View>
);
}
// 尝试寻找亚洲盘口 (AH) 或者 主客和 (1X2)
// 这里为了演示演示,优先寻找 AH 相关数据
const ahKey1 = Object.keys(odds || {}).find(
(k) => k.startsWith("ah") && k.endsWith("_1")
);
const ahKey2 = ahKey1 ? ahKey1.replace("_1", "_2") : null;
if (odds && ahKey1 && ahKey2) {
const handicapValue = ahKey1.replace("ah", "").replace("_1", "");
return (
<View style={styles.row}>
<View style={[styles.item, isDark && styles.darkItem]}>
<ThemedText style={styles.team}>{homeAbbr}</ThemedText>
<ThemedText style={styles.odds}>{odds[ahKey1] || "-"}</ThemedText>
</View>
<View style={[styles.item, isDark && styles.darkItem]}>
<ThemedText style={styles.team}>HDP</ThemedText>
<ThemedText style={styles.odds}>{handicapValue}</ThemedText>
</View>
<View style={[styles.item, isDark && styles.darkItem]}>
<ThemedText style={styles.team}>{awayAbbr}</ThemedText>
<ThemedText style={styles.odds}>{odds[ahKey2] || "-"}</ThemedText>
</View>
</View>
);
}
// 如果没有 AH显示 1X2
if (odds && odds.odd_1 && odds.odd_2) {
return (
<View style={styles.row}>
<View style={[styles.item, isDark && styles.darkItem]}>
<ThemedText style={styles.team}>{homeAbbr}</ThemedText>
<ThemedText style={styles.odds}>{odds.odd_1}</ThemedText>
</View>
<View style={[styles.item, isDark && styles.darkItem]}>
<ThemedText style={styles.team}>DRAW</ThemedText>
<ThemedText style={styles.odds}>{odds.odd_x || "-"}</ThemedText>
</View>
<View style={[styles.item, isDark && styles.darkItem]}>
<ThemedText style={styles.team}>{awayAbbr}</ThemedText>
<ThemedText style={styles.odds}>{odds.odd_2}</ThemedText>
</View>
</View>
);
}
return (
<View style={styles.emptyContainer}>
<ThemedText style={{ color: "#999" }}>
{t("detail.empty_odds")}
</ThemedText>
</View>
);
};
return (
<ThemedView style={[styles.container, isDark && styles.darkContainer]}>
<View style={styles.header}>
@@ -25,24 +118,13 @@ export function OddsCard({ match, isDark }: OddsCardProps) {
{t("detail.odds_card.title")}
</ThemedText>
<View style={styles.badge}>
<ThemedText style={styles.badgeText}>bet365</ThemedText>
<ThemedText style={styles.badgeText}>
{odds?.odd_bookmakers || "bet365"}
</ThemedText>
</View>
</View>
<View style={styles.row}>
<View style={[styles.item, isDark && styles.darkItem]}>
<ThemedText style={styles.team}>{homeAbbr}</ThemedText>
<ThemedText style={styles.odds}>-0.93</ThemedText>
</View>
<View style={[styles.item, isDark && styles.darkItem]}>
<ThemedText style={styles.team}>HDP</ThemedText>
<ThemedText style={styles.odds}>0/0.5</ThemedText>
</View>
<View style={[styles.item, isDark && styles.darkItem]}>
<ThemedText style={styles.team}>{awayAbbr}</ThemedText>
<ThemedText style={styles.odds}>0.72</ThemedText>
</View>
</View>
{renderOddsContent()}
<ThemedText style={styles.disclaimer}>
{t("detail.odds_card.disclaimer")}
@@ -105,12 +187,12 @@ const styles = StyleSheet.create({
backgroundColor: "rgba(255,255,255,0.05)",
},
team: {
fontSize: 16,
fontSize: 14,
fontWeight: "500",
},
odds: {
fontSize: 16,
fontWeight: "600",
fontSize: 14,
fontWeight: "700",
color: "#FF9800",
},
disclaimer: {
@@ -119,4 +201,14 @@ const styles = StyleSheet.create({
marginTop: 20,
textAlign: "center",
},
loadingContainer: {
height: 60,
justifyContent: "center",
alignItems: "center",
},
emptyContainer: {
height: 60,
justifyContent: "center",
alignItems: "center",
},
});

View File

@@ -265,7 +265,25 @@ export interface MatchDetailData {
};
}
// 实时赔率(根据 sportId 可能是 LiveOdds 或 Odds 结构,暂时使用宽泛类型)
export interface OddsData {
// 实时赔率数据项结构
export interface OddsItem {
odd_bookmakers: string;
odd_1?: number;
odd_x?: number;
odd_2?: number;
odd_12?: number;
odd_1x?: number;
odd_x2?: number;
"ah+1_1"?: number;
"ah+1_2"?: number;
"o+2.5"?: number;
"u+2.5"?: number;
[key: string]: any;
}
// 实时赔率响应数据结构
export interface OddsData {
[match_id: string]: {
data: OddsItem[];
};
}