diff --git a/app/match-detail/[id].tsx b/app/match-detail/[id].tsx
index 4b5cf10..c7b9d6c 100644
--- a/app/match-detail/[id].tsx
+++ b/app/match-detail/[id].tsx
@@ -10,6 +10,7 @@ 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 { ScoreHeader } from "@/components/match-detail/score-header";
+import { TennisScoreTable } from "@/components/match-detail/tennis/tennis-score-table";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { Colors } from "@/constants/theme";
@@ -52,18 +53,21 @@ export default function MatchDetailScreen() {
if (data?.match?.sportId) {
const sportId = data.match.sportId;
let validTabs: string[] = [];
-
+
if (sportId === 1) {
// 足球
validTabs = ["info", "stats", "odds", "h2h", "chat"];
} else if (sportId === 2) {
// 篮球
validTabs = ["info", "h2h", "chat"];
+ } else if (sportId === 3) {
+ // 网球
+ validTabs = ["info", "chat"];
} else {
// 默认
validTabs = ["info", "h2h", "chat"];
}
-
+
// 如果当前 activeTab 不在有效标签列表中,重置为第一个
if (!validTabs.includes(activeTab)) {
setActiveTab(validTabs[0]);
@@ -77,11 +81,11 @@ export default function MatchDetailScreen() {
setError(null);
const result = await fetchMatchDetail(id as string);
setData(result);
- console.log("首发阵容" , result.match.players?.away_team);
+ console.log("首发阵容", result.match.players?.away_team);
console.log("红黄牌", result.events);
-
-
-
+
+
+
} catch (err: any) {
setError(err.message || t("detail.fetch_failed"));
} finally {
@@ -130,6 +134,14 @@ export default function MatchDetailScreen() {
>
);
+ } else if (sportId === 3) {
+ // 网球:显示 TennisScoreTable 和 MatchInfoCard
+ return (
+ <>
+
+
+ >
+ );
} else {
// 默认使用足球组件
return (
diff --git a/components/match-detail/match-tabs.tsx b/components/match-detail/match-tabs.tsx
index 9455be1..bcb1928 100644
--- a/components/match-detail/match-tabs.tsx
+++ b/components/match-detail/match-tabs.tsx
@@ -65,6 +65,16 @@ export function MatchTabs({
} else if (sportId === 2) {
// 篮球
return commonTabs;
+ } else if (sportId === 3) {
+ // 网球: 详情、聊天
+ return [
+ {
+ id: "info",
+ label: t("detail.tabs.info"),
+ icon: "document-text-outline",
+ },
+ { id: "chat", label: t("detail.tabs.chat"), icon: "chatbubbles-outline" },
+ ];
}
return commonTabs;
};
diff --git a/components/match-detail/score-header.tsx b/components/match-detail/score-header.tsx
index 918900c..4c68a7e 100644
--- a/components/match-detail/score-header.tsx
+++ b/components/match-detail/score-header.tsx
@@ -1,6 +1,7 @@
import { ThemedText } from "@/components/themed-text";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { addFavorite, checkFavorite, removeFavorite } from "@/lib/api";
+import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
import { storage } from "@/lib/storage";
import { MatchDetailData } from "@/types/api";
import { LinearGradient } from "expo-linear-gradient";
@@ -19,6 +20,7 @@ export function ScoreHeader({ data, isDark, topInset }: ScoreHeaderProps) {
const router = useRouter();
const { t } = useTranslation();
const { match } = data;
+ const isTennis = match.sportId === 3;
const [isFav, setIsFav] = useState(false);
const [favLoading, setFavLoading] = useState(false);
@@ -45,39 +47,45 @@ export function ScoreHeader({ data, isDark, topInset }: ScoreHeaderProps) {
}
}, [match.eventKey]);
- // 检查主队收藏状态
+ // 检查主队/第一球员收藏状态
React.useEffect(() => {
const loadHomeFav = async () => {
const token = await storage.getAccessToken();
if (!token) return;
try {
- const res = await checkFavorite("team", match.homeTeamKey.toString());
- setIsHomeFav(res.isFavorite);
+ const teamKey = isTennis ? match.firstPlayerKey : match.homeTeamKey;
+ if (teamKey) {
+ const res = await checkFavorite("team", teamKey.toString());
+ setIsHomeFav(res.isFavorite);
+ }
} catch (error) {
console.error("Check home team favorite status error:", error);
}
};
- if (match.homeTeamKey) {
+ if (isTennis ? match.firstPlayerKey : match.homeTeamKey) {
loadHomeFav();
}
- }, [match.homeTeamKey]);
+ }, [match.homeTeamKey, match.firstPlayerKey, isTennis]);
- // 检查客队收藏状态
+ // 检查客队/第二球员收藏状态
React.useEffect(() => {
const loadAwayFav = async () => {
const token = await storage.getAccessToken();
if (!token) return;
try {
- const res = await checkFavorite("team", match.awayTeamKey.toString());
- setIsAwayFav(res.isFavorite);
+ const teamKey = isTennis ? match.secondPlayerKey : match.awayTeamKey;
+ if (teamKey) {
+ const res = await checkFavorite("team", teamKey.toString());
+ setIsAwayFav(res.isFavorite);
+ }
} catch (error) {
console.error("Check away team favorite status error:", error);
}
};
- if (match.awayTeamKey) {
+ if (isTennis ? match.secondPlayerKey : match.awayTeamKey) {
loadAwayFav();
}
- }, [match.awayTeamKey]);
+ }, [match.awayTeamKey, match.secondPlayerKey, isTennis]);
const toggleFavorite = async () => {
if (favLoading) return;
@@ -111,7 +119,7 @@ export function ScoreHeader({ data, isDark, topInset }: ScoreHeaderProps) {
const setFav = isHome ? setIsHomeFav : setIsAwayFav;
const loading = isHome ? homeFavLoading : awayFavLoading;
- if (loading) return;
+ if (loading || !teamKey) return;
setLoading(true);
const newFavState = !isTeamFav;
@@ -137,6 +145,36 @@ export function ScoreHeader({ data, isDark, topInset }: ScoreHeaderProps) {
}
};
+ const getScoreDisplay = () => {
+ if (isTennis) {
+ const scores = (match.scores as any) || [];
+ if (scores.length > 0) {
+ const lastSet = scores[scores.length - 1];
+ return `${lastSet.score_first || "0"}-${lastSet.score_second || "0"}`;
+ }
+ return match.eventGameResult && match.eventGameResult !== "-"
+ ? match.eventGameResult
+ : "0-0";
+ }
+ return match.eventFinalResult && match.eventFinalResult !== "-"
+ ? match.eventFinalResult
+ : "0-0";
+ };
+
+ const firstPlayerName = isTennis ? match.eventFirstPlayer : match.eventHomeTeam;
+ const secondPlayerName = isTennis ? match.eventSecondPlayer : match.eventAwayTeam;
+ const firstPlayerLogo = isTennis ? match.eventFirstPlayerLogo : match.homeTeamLogo;
+ const secondPlayerLogo = isTennis ? match.eventSecondPlayerLogo : match.awayTeamLogo;
+ const firstPlayerKey = isTennis ? match.firstPlayerKey : match.homeTeamKey;
+ const secondPlayerKey = isTennis ? match.secondPlayerKey : match.awayTeamKey;
+
+ const hasFirstLogo = firstPlayerLogo && firstPlayerLogo.trim() !== "" && !firstPlayerLogo.includes("placehold");
+ const hasSecondLogo = secondPlayerLogo && secondPlayerLogo.trim() !== "" && !secondPlayerLogo.includes("placehold");
+ const firstGradient = getLogoGradient(firstPlayerName || "");
+ const secondGradient = getLogoGradient(secondPlayerName || "");
+ const firstInitials = getInitials(firstPlayerName || "");
+ const secondInitials = getInitials(secondPlayerName || "");
+
return (
toggleTeamFavorite(match.homeTeamKey, true)}
- disabled={homeFavLoading}
+ onPress={() => {
+ if (firstPlayerKey) {
+ toggleTeamFavorite(firstPlayerKey, true);
+ }
+ }}
+ disabled={homeFavLoading || !firstPlayerKey}
style={styles.starBtnLeft}
>
-
+ {hasFirstLogo ? (
+
+ ) : (
+
+ {firstInitials}
+
+ )}
- {match.eventHomeTeam}
+ {firstPlayerName}
- {match.eventFinalResult && match.eventFinalResult !== "-"
- ? match.eventFinalResult
- : "0-0"}
+ {getScoreDisplay()}
-
+
-
+ {hasSecondLogo ? (
+
+ ) : (
+
+ {secondInitials}
+
+ )}
toggleTeamFavorite(match.awayTeamKey, false)}
- disabled={awayFavLoading}
+ onPress={() => {
+ if (secondPlayerKey) {
+ toggleTeamFavorite(secondPlayerKey, false);
+ }
+ }}
+ disabled={awayFavLoading || !secondPlayerKey}
style={styles.starBtnRight}
>
- {match.eventAwayTeam}
+ {secondPlayerName}
@@ -326,6 +396,19 @@ const styles = StyleSheet.create({
width: 60,
height: 60,
resizeMode: "contain",
+ borderRadius: 30,
+ },
+ teamLogoGradient: {
+ width: 60,
+ height: 60,
+ borderRadius: 30,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ teamLogoText: {
+ fontSize: 20,
+ fontWeight: "700",
+ color: "rgba(255, 255, 255, 0.92)",
},
teamName: {
color: "#FFF",
diff --git a/components/match-detail/tennis/tennis-score-table.tsx b/components/match-detail/tennis/tennis-score-table.tsx
new file mode 100644
index 0000000..886a3c2
--- /dev/null
+++ b/components/match-detail/tennis/tennis-score-table.tsx
@@ -0,0 +1,205 @@
+import { ThemedText } from "@/components/themed-text";
+import { MatchDetailData } from "@/types/api";
+import { Image } from "expo-image";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { StyleSheet, View } from "react-native";
+import { LinearGradient } from "expo-linear-gradient";
+import { getInitials, getLogoGradient } from "@/lib/avatar-utils";
+
+interface TennisScoreTableProps {
+ data: MatchDetailData;
+ isDark: boolean;
+}
+
+export function TennisScoreTable({ data, isDark }: TennisScoreTableProps) {
+ const { t } = useTranslation();
+ const { match } = data;
+ const bgColor = isDark ? "#1C1C1E" : "#FFF";
+ const borderColor = isDark ? "#2C2C2E" : "rgba(0,0,0,0.06)";
+ const headerTextColor = isDark ? "#666" : "#999";
+ const textColor = isDark ? "#FFF" : "#000";
+
+ const isTennis = match.sportId === 3;
+ const scores = (match.scores as any) || [];
+ const firstPlayer = match.eventFirstPlayer || "";
+ const secondPlayer = match.eventSecondPlayer || "";
+ const firstPlayerLogo = match.eventFirstPlayerLogo || "";
+ const secondPlayerLogo = match.eventSecondPlayerLogo || "";
+
+ const hasFirstLogo = firstPlayerLogo && firstPlayerLogo.trim() !== "" && !firstPlayerLogo.includes("placehold");
+ const hasSecondLogo = secondPlayerLogo && secondPlayerLogo.trim() !== "" && !secondPlayerLogo.includes("placehold");
+ const firstGradient = getLogoGradient(firstPlayer);
+ const secondGradient = getLogoGradient(secondPlayer);
+ const firstInitials = getInitials(firstPlayer);
+ const secondInitials = getInitials(secondPlayer);
+
+ const headers = [t("detail.score_table.set") || "Set"];
+ scores.forEach((_, index) => {
+ headers.push(`${index + 1}`);
+ });
+ if (scores.length === 0) {
+ headers.push("1");
+ }
+
+ return (
+
+
+
+
+ {t("detail.score_table.player") || "Player"}
+
+
+ {headers.slice(1).map((header, index) => (
+
+
+ {header}
+
+
+ ))}
+
+
+
+
+ {hasFirstLogo ? (
+
+ ) : (
+
+ {firstInitials}
+
+ )}
+
+ {firstPlayer}
+
+
+ {scores.length > 0 ? (
+ scores.map((score: any, index: number) => (
+
+
+ {score.score_first || "0"}
+
+
+ ))
+ ) : (
+
+ 0
+
+ )}
+
+
+
+
+ {hasSecondLogo ? (
+
+ ) : (
+
+ {secondInitials}
+
+ )}
+
+ {secondPlayer}
+
+
+ {scores.length > 0 ? (
+ scores.map((score: any, index: number) => (
+
+
+ {score.score_second || "0"}
+
+
+ ))
+ ) : (
+
+ 0
+
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ marginHorizontal: 16,
+ marginTop: 12,
+ borderRadius: 16,
+ borderWidth: 1,
+ overflow: "hidden",
+ },
+ header: {
+ flexDirection: "row",
+ borderBottomWidth: 1,
+ paddingVertical: 12,
+ paddingHorizontal: 12,
+ },
+ headerLeft: {
+ flex: 1,
+ minWidth: 120,
+ },
+ headerCell: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ minWidth: 40,
+ },
+ headerText: {
+ fontSize: 12,
+ fontWeight: "600",
+ },
+ row: {
+ flexDirection: "row",
+ borderBottomWidth: 1,
+ paddingVertical: 12,
+ paddingHorizontal: 12,
+ alignItems: "center",
+ },
+ playerCell: {
+ flex: 1,
+ flexDirection: "row",
+ alignItems: "center",
+ minWidth: 120,
+ gap: 8,
+ },
+ playerLogo: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ },
+ playerLogoGradient: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ playerLogoText: {
+ fontSize: 10,
+ fontWeight: "700",
+ color: "rgba(255, 255, 255, 0.92)",
+ },
+ playerName: {
+ flex: 1,
+ fontSize: 13,
+ fontWeight: "600",
+ },
+ scoreCell: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ minWidth: 40,
+ },
+ scoreText: {
+ fontSize: 15,
+ fontWeight: "700",
+ },
+});
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 1949a07..fe0d285 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -115,7 +115,8 @@
"halftime": "Half Time",
"full_time": "90'",
"extra_time": "ET",
- "penalty": "Pen"
+ "penalty": "Pen",
+ "player": "Player"
},
"halftime": "Half: {{score}}",
"empty_stats": "No statistics data",
diff --git a/i18n/locales/hi.json b/i18n/locales/hi.json
index 5a2ea78..1cd85fe 100644
--- a/i18n/locales/hi.json
+++ b/i18n/locales/hi.json
@@ -111,7 +111,8 @@
"halftime": "हाफ टाइम",
"full_time": "90'",
"extra_time": "अतिरिक्त समय",
- "penalty": "पेनल्टी"
+ "penalty": "पेनल्टी",
+ "player": "खिलाड़ी"
},
"halftime": "हाफ टाइम: {{score}}",
"empty_stats": "कोई आँकड़े उपलब्ध नहीं",
diff --git a/i18n/locales/id.json b/i18n/locales/id.json
index 0f39ad3..05fdc9c 100644
--- a/i18n/locales/id.json
+++ b/i18n/locales/id.json
@@ -111,7 +111,8 @@
"halftime": "Babak Pertama",
"full_time": "90'",
"extra_time": "Perpanjangan",
- "penalty": "Adu Penalti"
+ "penalty": "Adu Penalti",
+ "player": "Pemain"
},
"halftime": "Babak Pertama: {{score}}",
"empty_stats": "Tidak ada data statistik",
diff --git a/i18n/locales/ms.json b/i18n/locales/ms.json
index 5e03504..fb7df10 100644
--- a/i18n/locales/ms.json
+++ b/i18n/locales/ms.json
@@ -111,7 +111,8 @@
"halftime": "Separuh Masa",
"full_time": "90'",
"extra_time": "Masa Tambahan",
- "penalty": "Sepakan Penalti"
+ "penalty": "Sepakan Penalti",
+ "player": "Pemain"
},
"halftime": "Separuh masa: {{score}}",
"empty_stats": "Tiada data statistik",
diff --git a/i18n/locales/th.json b/i18n/locales/th.json
index f6e641f..7b16919 100644
--- a/i18n/locales/th.json
+++ b/i18n/locales/th.json
@@ -111,7 +111,8 @@
"halftime": "ครึ่งแรก",
"full_time": "90'",
"extra_time": "ต่อเวลา",
- "penalty": "จุดโทษ"
+ "penalty": "จุดโทษ",
+ "player": "ผู้เล่น"
},
"halftime": "ครึ่งแรก: {{score}}",
"empty_stats": "ไม่มีข้อมูลสถิติ",
diff --git a/i18n/locales/vi.json b/i18n/locales/vi.json
index af81a1e..e79cbbb 100644
--- a/i18n/locales/vi.json
+++ b/i18n/locales/vi.json
@@ -111,7 +111,8 @@
"halftime": "Hiệp 1",
"full_time": "90'",
"extra_time": "Hiệp phụ",
- "penalty": "Luân lưu"
+ "penalty": "Luân lưu",
+ "player": "Cầu thủ"
},
"halftime": "Hiệp 1: {{score}}",
"empty_stats": "Không có dữ liệu thống kê",
diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json
index dd30207..478afcf 100644
--- a/i18n/locales/zh.json
+++ b/i18n/locales/zh.json
@@ -115,7 +115,8 @@
"halftime": "半场",
"full_time": "90分钟",
"extra_time": "加时",
- "penalty": "点球"
+ "penalty": "点球",
+ "player": "球员"
},
"halftime": "半场: {{score}}",
"empty_stats": "暂无统计数据",
diff --git a/temp.json b/temp.json
new file mode 100644
index 0000000..d3a226b
--- /dev/null
+++ b/temp.json
@@ -0,0 +1,72 @@
+{
+ "code": 0,
+ "message": "ok",
+ "data": {
+ "match": {
+ "ID": 250468,
+ "CreatedAt": "2026-01-21T05:45:15.704+08:00",
+ "UpdatedAt": "2026-01-21T05:45:15.704+08:00",
+ "DeletedAt": null,
+ "eventKey": "12097737",
+ "eventDate": "2026-01-22",
+ "eventTime": "03:00",
+ "eventHomeTeam": "",
+ "homeTeamKey": "",
+ "homeTeamLogo": "",
+ "eventAwayTeam": "",
+ "awayTeamKey": "",
+ "awayTeamLogo": "",
+ "eventHalftimeResult": "",
+ "eventFinalResult": "-",
+ "eventFtResult": "",
+ "eventPenaltyResult": "",
+ "eventStatus": "1",
+ "countryName": "Challenger Men Singles",
+ "leagueName": "Phan Thiet",
+ "leagueKey": "13614",
+ "leagueRound": "Phan Thiet - 1/8-finals",
+ "leagueSeason": "2026",
+ "eventLive": "0",
+ "eventStadium": "",
+ "eventReferee": "",
+ "eventCountryKey": "281",
+ "leagueLogo": "",
+ "countryLogo": "",
+ "eventHomeFormation": "",
+ "eventAwayFormation": "",
+ "fkStageKey": "",
+ "stageName": "",
+ "leagueGroup": "",
+ "sportId": 3,
+ "eventQuarter": "",
+ "eventSet": "",
+ "eventType": "",
+ "eventToss": "",
+ "eventManOfMatch": "",
+ "eventFirstPlayer": "O. Jasika",
+ "firstPlayerKey": "1261",
+ "eventSecondPlayer": "I. Simakin",
+ "secondPlayerKey": "1238",
+ "eventFirstPlayerLogo": "https://apiv2.allsportsapi.com/logo-tennis/1261_o-jasika.jpg",
+ "eventSecondPlayerLogo": "https://apiv2.allsportsapi.com/logo-tennis/1238_i-simakin.jpg",
+ "eventGameResult": "-",
+ "eventServe": "",
+ "eventWinner": "",
+ "events": null,
+ "players": null,
+ "scores": [
+ {
+ "score_set": "1",
+ "score_first": "0",
+ "score_second": "0"
+ }
+ ],
+ "stats": null,
+ "pointByPoint": null,
+ "scorecard": null,
+ "comments": null,
+ "wickets": null,
+ "extra": null
+ }
+ }
+}
\ No newline at end of file
diff --git a/types/api.ts b/types/api.ts
index d57047a..1ffb97d 100644
--- a/types/api.ts
+++ b/types/api.ts
@@ -103,11 +103,11 @@ export interface LiveScoreMatch {
substitutes?: {
time: string;
home_scorer:
- | { in: string; out: string; in_id: number; out_id: number }
- | any[];
+ | { in: string; out: string; in_id: number; out_id: number }
+ | any[];
away_scorer:
- | { in: string; out: string; in_id: number; out_id: number }
- | any[];
+ | { in: string; out: string; in_id: number; out_id: number }
+ | any[];
info: string;
info_time: string;
score: string;
@@ -305,6 +305,14 @@ export interface MatchDetailData {
eventType: string;
eventToss: string;
eventManOfMatch: string;
+ eventFirstPlayer?: string;
+ eventSecondPlayer?: string;
+ eventFirstPlayerLogo?: string;
+ eventSecondPlayerLogo?: string;
+ firstPlayerKey?: string;
+ secondPlayerKey?: string;
+ eventGameResult?: string;
+ scores?: any[];
events?: MatchEvents;
players?: {
home_team?: Player[];