From a279083252e17d05cb5c42242e015931ea652025 Mon Sep 17 00:00:00 2001 From: xianyi Date: Thu, 22 Jan 2026 14:05:39 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BD=91=E7=90=83=E8=AF=A6=E6=83=85=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/match-detail/[id].tsx | 24 +- components/match-detail/match-tabs.tsx | 10 + components/match-detail/score-header.tsx | 141 +++++++++--- .../tennis/tennis-score-table.tsx | 205 ++++++++++++++++++ i18n/locales/en.json | 3 +- i18n/locales/hi.json | 3 +- i18n/locales/id.json | 3 +- i18n/locales/ms.json | 3 +- i18n/locales/th.json | 3 +- i18n/locales/vi.json | 3 +- i18n/locales/zh.json | 3 +- temp.json | 72 ++++++ types/api.ts | 16 +- 13 files changed, 443 insertions(+), 46 deletions(-) create mode 100644 components/match-detail/tennis/tennis-score-table.tsx create mode 100644 temp.json 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[];