实现直播详情页赔率自动更新功能,优化赔率数据结构
This commit is contained in:
@@ -40,11 +40,26 @@ export default function LiveDetailScreen() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadLiveDetail();
|
loadLiveDetail();
|
||||||
|
// 设置每 15 秒更新一次直播比分
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
refreshLiveDetail();
|
||||||
|
}, 15000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
}, [id, league_id]);
|
}, [id, league_id]);
|
||||||
|
|
||||||
const loadLiveDetail = async () => {
|
const loadLiveDetail = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
await refreshLiveDetail();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshLiveDetail = async () => {
|
||||||
|
try {
|
||||||
// Fetch live scores for the league
|
// Fetch live scores for the league
|
||||||
const sportId = parseInt(sport_id || "1");
|
const sportId = parseInt(sport_id || "1");
|
||||||
const leagueId = parseInt(league_id || "0");
|
const leagueId = parseInt(league_id || "0");
|
||||||
@@ -59,9 +74,7 @@ export default function LiveDetailScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error("Refresh live detail error:", err);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,15 +98,18 @@ export default function LiveDetailScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderTabContent = () => {
|
const renderTabContent = () => {
|
||||||
|
const numericSportId = parseInt(sport_id || "1");
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case "stats":
|
case "stats":
|
||||||
return <StatsCard match={match} isDark={isDark} />;
|
return <StatsCard match={match} isDark={isDark} />;
|
||||||
case "odds":
|
case "odds":
|
||||||
return <OddsCard match={match} isDark={isDark} />;
|
return (
|
||||||
|
<OddsCard match={match} isDark={isDark} sportId={numericSportId} />
|
||||||
|
);
|
||||||
case "detail":
|
case "detail":
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OddsCard match={match} isDark={isDark} />
|
<OddsCard match={match} isDark={isDark} sportId={numericSportId} />
|
||||||
<StatsCard match={match} isDark={isDark} />
|
<StatsCard match={match} isDark={isDark} />
|
||||||
<EventsTimeline match={match} isDark={isDark} />
|
<EventsTimeline match={match} isDark={isDark} />
|
||||||
<OtherInfoCard match={match} isDark={isDark} />
|
<OtherInfoCard match={match} isDark={isDark} />
|
||||||
|
|||||||
@@ -1,23 +1,116 @@
|
|||||||
import { ThemedText } from "@/components/themed-text";
|
import { ThemedText } from "@/components/themed-text";
|
||||||
import { ThemedView } from "@/components/themed-view";
|
import { ThemedView } from "@/components/themed-view";
|
||||||
import { LiveScoreMatch } from "@/types/api";
|
import { fetchOdds } from "@/lib/api";
|
||||||
import React from "react";
|
import { LiveScoreMatch, OddsItem } from "@/types/api";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { StyleSheet, View } from "react-native";
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
||||||
|
|
||||||
interface OddsCardProps {
|
interface OddsCardProps {
|
||||||
match: LiveScoreMatch;
|
match: LiveScoreMatch;
|
||||||
isDark: boolean;
|
isDark: boolean;
|
||||||
|
sportId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OddsCard({ match, isDark }: OddsCardProps) {
|
export function OddsCard({ match, isDark, sportId }: OddsCardProps) {
|
||||||
const { t } = useTranslation();
|
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个字母
|
// 提取队名缩写或前3个字母
|
||||||
const homeAbbr =
|
const homeAbbr =
|
||||||
match.event_home_team?.substring(0, 3).toUpperCase() || "HOME";
|
match.event_home_team?.substring(0, 3).toUpperCase() || "HOME";
|
||||||
const awayAbbr =
|
const awayAbbr =
|
||||||
match.event_away_team?.substring(0, 3).toUpperCase() || "AWAY";
|
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 (
|
return (
|
||||||
<ThemedView style={[styles.container, isDark && styles.darkContainer]}>
|
<ThemedView style={[styles.container, isDark && styles.darkContainer]}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@@ -25,24 +118,13 @@ export function OddsCard({ match, isDark }: OddsCardProps) {
|
|||||||
{t("detail.odds_card.title")}
|
{t("detail.odds_card.title")}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<View style={styles.badge}>
|
<View style={styles.badge}>
|
||||||
<ThemedText style={styles.badgeText}>bet365</ThemedText>
|
<ThemedText style={styles.badgeText}>
|
||||||
|
{odds?.odd_bookmakers || "bet365"}
|
||||||
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.row}>
|
{renderOddsContent()}
|
||||||
<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>
|
|
||||||
|
|
||||||
<ThemedText style={styles.disclaimer}>
|
<ThemedText style={styles.disclaimer}>
|
||||||
{t("detail.odds_card.disclaimer")}
|
{t("detail.odds_card.disclaimer")}
|
||||||
@@ -105,12 +187,12 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: "rgba(255,255,255,0.05)",
|
backgroundColor: "rgba(255,255,255,0.05)",
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
odds: {
|
odds: {
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: "600",
|
fontWeight: "700",
|
||||||
color: "#FF9800",
|
color: "#FF9800",
|
||||||
},
|
},
|
||||||
disclaimer: {
|
disclaimer: {
|
||||||
@@ -119,4 +201,14 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: 20,
|
marginTop: 20,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
height: 60,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
emptyContainer: {
|
||||||
|
height: 60,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
22
types/api.ts
22
types/api.ts
@@ -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;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 实时赔率响应数据结构
|
||||||
|
export interface OddsData {
|
||||||
|
[match_id: string]: {
|
||||||
|
data: OddsItem[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user