接口添加时间与时区&赔率
This commit is contained in:
@@ -34,6 +34,15 @@ export default function HomeScreen() {
|
|||||||
const [loadingLeagues, setLoadingLeagues] = useState(false);
|
const [loadingLeagues, setLoadingLeagues] = useState(false);
|
||||||
const [now, setNow] = useState(() => new Date());
|
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
|
// Selection States
|
||||||
// 默认足球
|
// 默认足球
|
||||||
const [selectedSportId, setSelectedSportId] = useState<number | null>(1);
|
const [selectedSportId, setSelectedSportId] = useState<number | null>(1);
|
||||||
@@ -158,7 +167,7 @@ export default function HomeScreen() {
|
|||||||
const loadMatches = async (sportId: number) => {
|
const loadMatches = async (sportId: number) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const list = await fetchTodayMatches(sportId, selectedDate);
|
const list = await fetchTodayMatches(sportId, selectedDate, deviceTimeZone);
|
||||||
setMatches(list);
|
setMatches(list);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Colors } from "@/constants/theme";
|
|||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
import { fetchLeagues, fetchSports, fetchTodayMatches } from "@/lib/api";
|
import { fetchLeagues, fetchSports, fetchTodayMatches } from "@/lib/api";
|
||||||
import { League, Match, Sport } from "@/types/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 { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -32,6 +32,14 @@ export default function HomeScreen() {
|
|||||||
const [matches, setMatches] = useState<Match[]>([]);
|
const [matches, setMatches] = useState<Match[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const deviceTimeZone = useMemo(() => {
|
||||||
|
try {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
||||||
|
} catch {
|
||||||
|
return "UTC";
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Selection States
|
// Selection States
|
||||||
// 默认足球
|
// 默认足球
|
||||||
const [selectedSportId, setSelectedSportId] = useState<number | null>(1);
|
const [selectedSportId, setSelectedSportId] = useState<number | null>(1);
|
||||||
@@ -131,7 +139,7 @@ export default function HomeScreen() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Pass selectedDate if API supported it
|
// Pass selectedDate if API supported it
|
||||||
const list = await fetchTodayMatches(sportId);
|
const list = await fetchTodayMatches(sportId, undefined, deviceTimeZone);
|
||||||
setMatches(list);
|
setMatches(list);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { SubstitutesCard } from "@/components/match-detail/football/substitutes-
|
|||||||
import { LeagueInfo } from "@/components/match-detail/league-info";
|
import { LeagueInfo } from "@/components/match-detail/league-info";
|
||||||
import { MatchInfoCard } from "@/components/match-detail/match-info-card";
|
import { MatchInfoCard } from "@/components/match-detail/match-info-card";
|
||||||
import { MatchTabs } from "@/components/match-detail/match-tabs";
|
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 { ScoreHeader } from "@/components/match-detail/score-header";
|
||||||
import { ThemedText } from "@/components/themed-text";
|
import { ThemedText } from "@/components/themed-text";
|
||||||
import { ThemedView } from "@/components/themed-view";
|
import { ThemedView } from "@/components/themed-view";
|
||||||
@@ -160,13 +161,7 @@ export default function MatchDetailScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "odds":
|
case "odds":
|
||||||
return (
|
return <OddsView sportId={sportId} matchId={data.match.ID} />;
|
||||||
<View style={styles.emptyContent}>
|
|
||||||
<ThemedText style={styles.emptyText}>
|
|
||||||
{t("detail.empty_odds")}
|
|
||||||
</ThemedText>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
case "h2h":
|
case "h2h":
|
||||||
return (
|
return (
|
||||||
<View style={styles.emptyContent}>
|
<View style={styles.emptyContent}>
|
||||||
|
|||||||
84
components/match-detail/odds/odds-view.tsx
Normal file
84
components/match-detail/odds/odds-view.tsx
Normal file
@@ -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<string | null>(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 (
|
||||||
|
<View style={[styles.container, { backgroundColor: bgColor }]}>
|
||||||
|
{loading ? (
|
||||||
|
<View style={styles.center}>
|
||||||
|
<ActivityIndicator size="small" color={Colors[theme].tint} />
|
||||||
|
<ThemedText style={styles.text}>{t("detail.loading")}</ThemedText>
|
||||||
|
</View>
|
||||||
|
) : error ? (
|
||||||
|
<ThemedText style={styles.text}>{error}</ThemedText>
|
||||||
|
) : (
|
||||||
|
<ThemedText style={styles.text}>{t("detail.empty_odds")}</ThemedText>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
@@ -10,4 +10,5 @@ export const API_ENDPOINTS = {
|
|||||||
MATCHES_TODAY: "/v1/api/matches/today",
|
MATCHES_TODAY: "/v1/api/matches/today",
|
||||||
UPCOMING_MATCHES: "/v1/api/matches/upcoming",
|
UPCOMING_MATCHES: "/v1/api/matches/upcoming",
|
||||||
MATCH_DETAIL: (id: string) => `/v1/api/matches/${id}`,
|
MATCH_DETAIL: (id: string) => `/v1/api/matches/${id}`,
|
||||||
|
ODDS: "/v1/api/odds",
|
||||||
};
|
};
|
||||||
|
|||||||
40
lib/api.ts
40
lib/api.ts
@@ -6,6 +6,7 @@ import {
|
|||||||
League,
|
League,
|
||||||
Match,
|
Match,
|
||||||
MatchDetailData,
|
MatchDetailData,
|
||||||
|
OddsData,
|
||||||
Sport,
|
Sport,
|
||||||
UpcomingMatch,
|
UpcomingMatch,
|
||||||
} from "@/types/api";
|
} from "@/types/api";
|
||||||
@@ -76,11 +77,12 @@ export const fetchLeagues = async (
|
|||||||
|
|
||||||
export const fetchTodayMatches = async (
|
export const fetchTodayMatches = async (
|
||||||
sportId: number,
|
sportId: number,
|
||||||
date?: Date | string
|
date?: Date | string,
|
||||||
|
timezone?: string
|
||||||
): Promise<Match[]> => {
|
): Promise<Match[]> => {
|
||||||
try {
|
try {
|
||||||
const params: { sport_id: number; date?: string } = {
|
const params: { sportId: number; date?: string; timezone?: string } = {
|
||||||
sport_id: sportId,
|
sportId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果提供了日期,格式化为 YYYY-MM-DD 格式
|
// 如果提供了日期,格式化为 YYYY-MM-DD 格式
|
||||||
@@ -97,6 +99,11 @@ export const fetchTodayMatches = async (
|
|||||||
params.date = dateStr;
|
params.date = dateStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果提供了时区,传给后端;不传则由后端使用本地时区
|
||||||
|
if (timezone) {
|
||||||
|
params.timezone = timezone;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await apiClient.get<ApiResponse<ApiListResponse<Match>>>(
|
const response = await apiClient.get<ApiResponse<ApiListResponse<Match>>>(
|
||||||
API_ENDPOINTS.MATCHES_TODAY,
|
API_ENDPOINTS.MATCHES_TODAY,
|
||||||
{
|
{
|
||||||
@@ -159,3 +166,30 @@ export const fetchUpcomingMatches = async (
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取实时赔率(足球/网球使用 LiveOdds,篮球/板球使用 Odds)
|
||||||
|
export const fetchOdds = async (
|
||||||
|
sportId: number,
|
||||||
|
matchId: number
|
||||||
|
): Promise<OddsData> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<ApiResponse<OddsData>>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -195,3 +195,8 @@ export interface MatchDetailData {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 实时赔率(根据 sportId 可能是 LiveOdds 或 Odds 结构,暂时使用宽泛类型)
|
||||||
|
export interface OddsData {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user