From 8dc87c9b29daf4ba80a0dca08b7ef1b7df734b7f Mon Sep 17 00:00:00 2001 From: xianyi Date: Wed, 14 Jan 2026 15:14:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=B7=BB=E5=8A=A0=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E4=B8=8E=E6=97=B6=E5=8C=BA&=E8=B5=94=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/index.tsx | 11 ++- app/(tabs)/live.tsx | 12 +++- app/match-detail/[id].tsx | 9 +-- components/match-detail/odds/odds-view.tsx | 84 ++++++++++++++++++++++ constants/api.ts | 1 + lib/api.ts | 40 ++++++++++- types/api.ts | 5 ++ 7 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 components/match-detail/odds/odds-view.tsx diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 322dc9d..57220ad 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -34,6 +34,15 @@ export default function HomeScreen() { const [loadingLeagues, setLoadingLeagues] = useState(false); const [now, setNow] = useState(() => new Date()); + const deviceTimeZone = useMemo(() => { + try { + console.log("deviceTimeZone", Intl.DateTimeFormat().resolvedOptions().timeZone); + return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; + } catch { + return "UTC"; + } + }, []); + // Selection States // 默认足球 const [selectedSportId, setSelectedSportId] = useState(1); @@ -158,7 +167,7 @@ export default function HomeScreen() { const loadMatches = async (sportId: number) => { setLoading(true); try { - const list = await fetchTodayMatches(sportId, selectedDate); + const list = await fetchTodayMatches(sportId, selectedDate, deviceTimeZone); setMatches(list); } catch (e) { console.error(e); diff --git a/app/(tabs)/live.tsx b/app/(tabs)/live.tsx index 2b802e2..100774e 100644 --- a/app/(tabs)/live.tsx +++ b/app/(tabs)/live.tsx @@ -10,7 +10,7 @@ import { Colors } from "@/constants/theme"; import { useTheme } from "@/context/ThemeContext"; import { fetchLeagues, fetchSports, fetchTodayMatches } from "@/lib/api"; import { League, Match, Sport } from "@/types/api"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, @@ -32,6 +32,14 @@ export default function HomeScreen() { const [matches, setMatches] = useState([]); const [loading, setLoading] = useState(true); + const deviceTimeZone = useMemo(() => { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; + } catch { + return "UTC"; + } + }, []); + // Selection States // 默认足球 const [selectedSportId, setSelectedSportId] = useState(1); @@ -131,7 +139,7 @@ export default function HomeScreen() { setLoading(true); try { // Pass selectedDate if API supported it - const list = await fetchTodayMatches(sportId); + const list = await fetchTodayMatches(sportId, undefined, deviceTimeZone); setMatches(list); } catch (e) { console.error(e); diff --git a/app/match-detail/[id].tsx b/app/match-detail/[id].tsx index c9e9784..d6ce0f1 100644 --- a/app/match-detail/[id].tsx +++ b/app/match-detail/[id].tsx @@ -7,6 +7,7 @@ import { SubstitutesCard } from "@/components/match-detail/football/substitutes- import { LeagueInfo } from "@/components/match-detail/league-info"; import { MatchInfoCard } from "@/components/match-detail/match-info-card"; import { MatchTabs } from "@/components/match-detail/match-tabs"; +import { OddsView } from "@/components/match-detail/odds/odds-view"; import { ScoreHeader } from "@/components/match-detail/score-header"; import { ThemedText } from "@/components/themed-text"; import { ThemedView } from "@/components/themed-view"; @@ -160,13 +161,7 @@ export default function MatchDetailScreen() { ); } case "odds": - return ( - - - {t("detail.empty_odds")} - - - ); + return ; case "h2h": return ( diff --git a/components/match-detail/odds/odds-view.tsx b/components/match-detail/odds/odds-view.tsx new file mode 100644 index 0000000..af8bc4e --- /dev/null +++ b/components/match-detail/odds/odds-view.tsx @@ -0,0 +1,84 @@ +import { ThemedText } from "@/components/themed-text"; +import { Colors } from "@/constants/theme"; +import { useTheme } from "@/context/ThemeContext"; +import { fetchOdds } from "@/lib/api"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { ActivityIndicator, StyleSheet, View } from "react-native"; + +interface OddsViewProps { + sportId: number; + matchId: number; +} + +export function OddsView({ sportId, matchId }: OddsViewProps) { + const { theme } = useTheme(); + const { t } = useTranslation(); + const isDark = theme === "dark"; + const bgColor = isDark ? "#1C1C1E" : "#FFF"; + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + + const run = async () => { + try { + setLoading(true); + setError(null); + const data = await fetchOdds(sportId, matchId); + if (cancelled) return; + console.log("odds", { sportId, matchId, data }); + } catch (e: any) { + if (cancelled) return; + setError(e?.message || "fetchOdds failed"); + } finally { + if (!cancelled) setLoading(false); + } + }; + + run(); + return () => { + cancelled = true; + }; + }, [sportId, matchId]); + + return ( + + {loading ? ( + + + {t("detail.loading")} + + ) : error ? ( + {error} + ) : ( + {t("detail.empty_odds")} + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + margin: 16, + marginTop: 0, + borderRadius: 12, + padding: 16, + elevation: 2, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + center: { + flexDirection: "row", + alignItems: "center", + gap: 10, + }, + text: { + opacity: 0.7, + }, +}); + diff --git a/constants/api.ts b/constants/api.ts index 3cfef29..3c0de18 100644 --- a/constants/api.ts +++ b/constants/api.ts @@ -10,4 +10,5 @@ export const API_ENDPOINTS = { MATCHES_TODAY: "/v1/api/matches/today", UPCOMING_MATCHES: "/v1/api/matches/upcoming", MATCH_DETAIL: (id: string) => `/v1/api/matches/${id}`, + ODDS: "/v1/api/odds", }; diff --git a/lib/api.ts b/lib/api.ts index e0e2df2..ed03dff 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -6,6 +6,7 @@ import { League, Match, MatchDetailData, + OddsData, Sport, UpcomingMatch, } from "@/types/api"; @@ -76,11 +77,12 @@ export const fetchLeagues = async ( export const fetchTodayMatches = async ( sportId: number, - date?: Date | string + date?: Date | string, + timezone?: string ): Promise => { try { - const params: { sport_id: number; date?: string } = { - sport_id: sportId, + const params: { sportId: number; date?: string; timezone?: string } = { + sportId, }; // 如果提供了日期,格式化为 YYYY-MM-DD 格式 @@ -97,6 +99,11 @@ export const fetchTodayMatches = async ( params.date = dateStr; } + // 如果提供了时区,传给后端;不传则由后端使用本地时区 + if (timezone) { + params.timezone = timezone; + } + const response = await apiClient.get>>( API_ENDPOINTS.MATCHES_TODAY, { @@ -159,3 +166,30 @@ export const fetchUpcomingMatches = async ( throw error; } }; + +// 获取实时赔率(足球/网球使用 LiveOdds,篮球/板球使用 Odds) +export const fetchOdds = async ( + sportId: number, + matchId: number +): Promise => { + try { + const response = await apiClient.get>( + API_ENDPOINTS.ODDS, + { + params: { + sport_id: sportId, + match_id: matchId, + }, + } + ); + + if (response.data.code === 0) { + return response.data.data; + } + + throw new Error(response.data.message); + } catch (error) { + console.error("Fetch odds error:", error); + throw error; + } +}; diff --git a/types/api.ts b/types/api.ts index 2097573..f01211a 100644 --- a/types/api.ts +++ b/types/api.ts @@ -195,3 +195,8 @@ export interface MatchDetailData { }; }; } + +// 实时赔率(根据 sportId 可能是 LiveOdds 或 Odds 结构,暂时使用宽泛类型) +export interface OddsData { + [key: string]: any; +}