From 08d05df8be11bc8e7a4ecc5ccec7e944c935c991 Mon Sep 17 00:00:00 2001 From: yuchenglong Date: Tue, 20 Jan 2026 10:12:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8D=A1=E7=89=8C=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=AE=A1=E7=90=86=E5=92=8C=E7=95=8C=E9=9D=A2=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/profile.tsx | 41 +++++++++++- components/match-card.tsx | 129 ++++++++++++++++++++++++++++++++++-- context/AppStateContext.tsx | 16 ++++- i18n/locales/en.json | 4 ++ i18n/locales/zh.json | 4 ++ lib/api.ts | 63 +++++++++--------- lib/storage.ts | 23 +++++++ 7 files changed, 242 insertions(+), 38 deletions(-) diff --git a/app/profile.tsx b/app/profile.tsx index b0b5a28..8b31215 100644 --- a/app/profile.tsx +++ b/app/profile.tsx @@ -37,7 +37,7 @@ const BOOKMAKERS = [ export default function ProfileScreen() { const { theme, toggleTheme, setTheme, isSystemTheme, useSystemTheme } = useTheme(); - const { state, updateOddsSettings } = useAppState(); + const { state, updateOddsSettings, updateCardsSettings } = useAppState(); const { t, i18n } = useTranslation(); const router = useRouter(); const isDark = theme === "dark"; @@ -55,6 +55,13 @@ export default function ProfileScreen() { }); }; + const toggleCards = () => { + updateCardsSettings({ + ...state.cardsSettings, + enabled: !state.cardsSettings.enabled, + }); + }; + const selectBookmaker = (name: string) => { const current = state.oddsSettings.selectedBookmakers; let next: string[]; @@ -429,6 +436,38 @@ export default function ProfileScreen() { + + {t("settings.cards_title")} + + + + + + + {t("settings.cards_show")} + + + + + {state.cardsSettings.enabled + ? t("settings.cards_enabled") + : t("settings.cards_disabled")} + + + + + + {/* 登录 (match.odds || []); + const [liveDetail, setLiveDetail] = useState(null); // console.log("MatchCard render:", JSON.stringify(match)); const oddsSettings = state.oddsSettings; + const cardsSettings = state.cardsSettings; useEffect(() => { if ( @@ -52,6 +59,20 @@ export function MatchCard({ match.odds, ]); + // Fetch live score detail for cards info + useEffect(() => { + if (cardsSettings.enabled && isLive && match.leagueKey) { + fetchLiveScore(match.sportId || 1, Number(match.leagueKey)) + .then((matches) => { + const detail = matches.find((m) => String(m.event_key) === match.id); + if (detail) { + setLiveDetail(detail); + } + }) + .catch((err) => console.log("Fetch live detail for cards error:", err)); + } + }, [cardsSettings.enabled, match.id, match.leagueKey, match.sportId]); + // 当外部传入的 match.fav 改变时,更新内部状态 useEffect(() => { setIsFav(match.fav); @@ -90,10 +111,53 @@ export function MatchCard({ if (m) return { home: m[1], away: m[2], hasScore: true }; if (s && s !== "-" && s !== "0 - 0") return { home: s, away: "", hasScore: true }; - if (s === "0 - 0") return { home: "0", away: "0", hasScore: true }; + if (s === "0 - 0" || s === "0-0") + return { home: "0", away: "0", hasScore: true }; return { home: "", away: "", hasScore: false }; }, [match.scoreText]); + const cardsCount = React.useMemo(() => { + if (!liveDetail?.cards || !cardsSettings.enabled) { + return { homeYellow: 0, homeRed: 0, awayYellow: 0, awayRed: 0 }; + } + + let homeYellow = 0, + homeRed = 0, + awayYellow = 0, + awayRed = 0; + + liveDetail.cards.forEach((card) => { + const cardType = (card.card || "").toLowerCase(); + const isYellow = cardType.includes("yellow"); + const isRed = cardType.includes("red"); + if (!isYellow && !isRed) return; + + const info = (card.info || "").toLowerCase(); + const sideFromInfo = info.includes("home") + ? "home" + : info.includes("away") + ? "away" + : null; + const sideFromFault = card.home_fault + ? "home" + : card.away_fault + ? "away" + : null; + const side = sideFromInfo || sideFromFault; + if (!side) return; + + if (side === "home") { + if (isYellow) homeYellow++; + if (isRed) homeRed++; + } else { + if (isYellow) awayYellow++; + if (isRed) awayRed++; + } + }); + + return { homeYellow, homeRed, awayYellow, awayRed }; + }, [liveDetail, cardsSettings.enabled]); + const handlePress = () => { if (onPress) { onPress(match); @@ -191,6 +255,30 @@ export function MatchCard({ ); }; + const renderCardsInline = (yellow: number, red: number) => { + if (!cardsSettings.enabled || !liveDetail) return null; + if (yellow <= 0 && red <= 0) return null; + + return ( + + {yellow > 0 && ( + + + {yellow} + + + )} + {red > 0 && ( + + {red} + + )} + + ); + }; + return ( {match.homeTeamName || match.home} + {renderCardsInline(cardsCount.homeYellow, cardsCount.homeRed)} {renderOddsRow(oddsSettings.selectedBookmakers[0], true)} @@ -265,6 +354,7 @@ export function MatchCard({ > {match.awayTeamName || match.away} + {renderCardsInline(cardsCount.awayYellow, cardsCount.awayRed)} {renderOddsRow(oddsSettings.selectedBookmakers[1], false)} @@ -368,7 +458,7 @@ const styles = StyleSheet.create({ flexDirection: "row", alignItems: "center", flex: 1, - marginRight: 6, + minWidth: 0, }, teamLogo: { width: 22, @@ -379,6 +469,7 @@ const styles = StyleSheet.create({ fontWeight: "600", marginLeft: 6, flex: 1, + minWidth: 0, }, bookmakerOddsRow: { flexDirection: "row", @@ -427,4 +518,34 @@ const styles = StyleSheet.create({ width: 36, height: 54, }, + cardsInline: { + flexDirection: "row", + alignItems: "center", + gap: 4, + marginLeft: 6, + flexShrink: 0, + }, + cardBadge: { + minWidth: 16, + height: 16, + borderRadius: 3, + alignItems: "center", + justifyContent: "center", + paddingHorizontal: 3, + }, + cardBadgeYellow: { + backgroundColor: "#FFC400", + }, + cardBadgeRed: { + backgroundColor: "#FF3B30", + }, + cardBadgeText: { + fontSize: 10, + fontWeight: "900", + lineHeight: 12, + color: "#fff", + }, + cardBadgeTextDark: { + color: "#000", + }, }); diff --git a/context/AppStateContext.tsx b/context/AppStateContext.tsx index 097b06f..08d30e4 100644 --- a/context/AppStateContext.tsx +++ b/context/AppStateContext.tsx @@ -1,4 +1,4 @@ -import { OddsSettings, storage } from "@/lib/storage"; +import { CardsSettings, OddsSettings, storage } from "@/lib/storage"; import React, { createContext, ReactNode, @@ -13,6 +13,7 @@ interface AppState { selectedLeagueKey: string | null; timezone: string; oddsSettings: OddsSettings; + cardsSettings: CardsSettings; } interface AppStateContextType { @@ -22,6 +23,7 @@ interface AppStateContextType { updateLeagueKey: (leagueKey: string | null) => void; updateTimezone: (timezone: string) => void; updateOddsSettings: (settings: OddsSettings) => void; + updateCardsSettings: (settings: CardsSettings) => void; } const AppStateContext = createContext( @@ -35,13 +37,17 @@ export function AppStateProvider({ children }: { children: ReactNode }) { selectedLeagueKey: null, timezone: "UTC", oddsSettings: { enabled: false, selectedBookmakers: [] }, + cardsSettings: { enabled: false }, }); useEffect(() => { - // Initial load of odds settings + // Initial load of odds settings and cards settings storage.getOddsSettings().then((settings) => { setState((prev) => ({ ...prev, oddsSettings: settings })); }); + storage.getCardsSettings().then((settings) => { + setState((prev) => ({ ...prev, cardsSettings: settings })); + }); }, []); const updateSportId = (sportId: number | null) => { @@ -65,6 +71,11 @@ export function AppStateProvider({ children }: { children: ReactNode }) { storage.setOddsSettings(settings); }; + const updateCardsSettings = (settings: CardsSettings) => { + setState((prev) => ({ ...prev, cardsSettings: settings })); + storage.setCardsSettings(settings); + }; + return ( {children} diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 486c7b1..8f6d80e 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -32,6 +32,10 @@ "odds_unselected": "Unselected", "odds_modal_title": "Select Bookmakers (Max 2)", "odds_confirm": "Confirm", + "cards_title": "Cards Settings", + "cards_show": "Show Cards", + "cards_enabled": "On", + "cards_disabled": "Off", "login": "Login", "click_to_login": "Click to login", "logout": "Logout", diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json index d55c902..87b72ff 100644 --- a/i18n/locales/zh.json +++ b/i18n/locales/zh.json @@ -32,6 +32,10 @@ "odds_unselected": "未选择", "odds_modal_title": "选择赔率公司 (最多2项)", "odds_confirm": "确定", + "cards_title": "卡牌设置", + "cards_show": "显示红黄牌", + "cards_enabled": "开启", + "cards_disabled": "关闭", "login": "登录", "click_to_login": "点击登录", "logout": "登出", diff --git a/lib/api.ts b/lib/api.ts index 0f630b4..e3e47d2 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -41,12 +41,12 @@ apiClient.interceptors.request.use(async (config) => { }); const refreshTokenApi = async ( - request: RefreshTokenRequest + request: RefreshTokenRequest, ): Promise => { try { const response = await apiClient.post>( API_ENDPOINTS.REFRESH_TOKEN, - request + request, ); if (response.data.code === 0) { @@ -81,13 +81,13 @@ apiClient.interceptors.response.use( } } return Promise.reject(error); - } + }, ); export const fetchSports = async (): Promise => { try { const response = await apiClient.get>>( - API_ENDPOINTS.SPORTS + API_ENDPOINTS.SPORTS, ); if (response.data.code === 0) { return response.data.data.list; @@ -103,7 +103,7 @@ export const fetchSports = async (): Promise => { export const fetchCountries = async (): Promise => { try { const response = await apiClient.get>>( - API_ENDPOINTS.COUNTRIES + API_ENDPOINTS.COUNTRIES, ); if (response.data.code === 0) { return response.data.data.list; @@ -127,7 +127,7 @@ export const fetchLeagues = async (params: { try { const response = await apiClient.get>>( API_ENDPOINTS.LEAGUES, - { params } + { params }, ); if (response.data.code === 0) { return response.data.data; @@ -179,7 +179,7 @@ export const fetchTodayMatches = async (options: { const response = await apiClient.get>>( API_ENDPOINTS.MATCHES_TODAY, - { params } + { params }, ); if (response.data.code === 0) { return response.data.data; @@ -192,11 +192,11 @@ export const fetchTodayMatches = async (options: { }; export const fetchMatchDetail = async ( - id: string + id: string, ): Promise => { try { const response = await apiClient.get>( - API_ENDPOINTS.MATCH_DETAIL(id) + API_ENDPOINTS.MATCH_DETAIL(id), ); if (response.data.code === 0) { return response.data.data; @@ -211,7 +211,7 @@ export const fetchMatchDetail = async ( export const fetchLiveScore = async ( sportId: number, leagueId?: number, - timezone?: string + timezone?: string, ): Promise => { // console.log("Fetching live scores with params:", { // sportId, @@ -220,9 +220,9 @@ export const fetchLiveScore = async ( // }); try { const params: { sport_id: number; league_id?: number; timezone?: string } = - { - sport_id: sportId, - }; + { + sport_id: sportId, + }; if (leagueId) { params.league_id = leagueId; @@ -234,10 +234,11 @@ export const fetchLiveScore = async ( const response = await apiClient.get>( API_ENDPOINTS.LIVESCORE, - { params } + { params }, ); if (response.data.code === 0) { + // console.log("Live score data:", JSON.stringify(response.data.data)); return response.data.data; } @@ -251,7 +252,7 @@ export const fetchLiveScore = async ( export const fetchUpcomingMatches = async ( sportId: number, leagueKey: string, - limit: number = 50 + limit: number = 50, ): Promise => { try { const response = await apiClient.get< @@ -278,7 +279,7 @@ export const fetchUpcomingMatches = async ( // 获取实时赔率(足球/网球使用 LiveOdds,篮球/板球使用 Odds) export const fetchOdds = async ( sportId: number, - matchId: number + matchId: number, ): Promise => { try { const response = await apiClient.get>( @@ -288,7 +289,7 @@ export const fetchOdds = async ( sport_id: sportId, match_id: matchId, }, - } + }, ); if (response.data.code === 0) { @@ -305,7 +306,7 @@ export const fetchOdds = async ( // 搜索联赛、球队或球员 export const fetchSearch = async ( query: string, - sportId?: number + sportId?: number, ): Promise => { try { const params: { q: string; sportId?: number } = { q: query }; @@ -315,7 +316,7 @@ export const fetchSearch = async ( const response = await apiClient.get>( API_ENDPOINTS.SEARCH, - { params } + { params }, ); if (response.data.code === 0) { @@ -338,7 +339,7 @@ export const fetchH2H = async ( firstPlayerId?: number; secondPlayerId?: number; timezone?: string; - } + }, ): Promise => { try { const params: { @@ -370,7 +371,7 @@ export const fetchH2H = async ( const response = await apiClient.get>( API_ENDPOINTS.H2H, - { params } + { params }, ); if (response.data.code === 0) { @@ -385,12 +386,12 @@ export const fetchH2H = async ( }; export const appleSignIn = async ( - request: AppleSignInRequest + request: AppleSignInRequest, ): Promise => { try { const response = await apiClient.post>( API_ENDPOINTS.APPLE_SIGNIN, - request + request, ); if (response.data.code === 0) { @@ -407,7 +408,7 @@ export const appleSignIn = async ( export const logout = async (): Promise => { try { const response = await apiClient.post>( - API_ENDPOINTS.LOGOUT + API_ENDPOINTS.LOGOUT, ); if (response.data.code === 0) { @@ -426,7 +427,7 @@ export const refreshToken = refreshTokenApi; export const fetchUserProfile = async (): Promise => { try { const response = await apiClient.get>( - API_ENDPOINTS.USER_PROFILE + API_ENDPOINTS.USER_PROFILE, ); if (response.data.code === 0) { @@ -450,7 +451,7 @@ export const addFavorite = async (request: FavoriteRequest): Promise => { // console.log("Adding favorite with request:", request); const response = await apiClient.post>( API_ENDPOINTS.FAVORITES, - request + request, ); if (response.data.code === 0) { return response.data.data; @@ -475,7 +476,7 @@ export const removeFavorite = async (request: { console.log("Removing favorite with request:", request); const response = await apiClient.delete>( API_ENDPOINTS.FAVORITES, - { data: request } + { data: request }, ); if (response.data.code === 0) { return response.data.data; @@ -489,14 +490,14 @@ export const removeFavorite = async (request: { export const checkFavorite = async ( type: string, - typeId: string + typeId: string, ): Promise => { try { const response = await apiClient.get>( API_ENDPOINTS.CHECK_FAVORITE, { params: { type, typeId }, - } + }, ); if (response.data.code === 0) { return response.data.data; @@ -511,14 +512,14 @@ export const checkFavorite = async ( export const fetchFavorites = async ( type: string, page: number = 1, - pageSize: number = 20 + pageSize: number = 20, ): Promise => { try { const response = await apiClient.get>( API_ENDPOINTS.FAVORITES, { params: { type, page, pageSize }, - } + }, ); if (response.data.code === 0) { console.log("Fetched favorites:", JSON.stringify(response.data.data)); diff --git a/lib/storage.ts b/lib/storage.ts index ae6ce72..5dda9f2 100644 --- a/lib/storage.ts +++ b/lib/storage.ts @@ -6,6 +6,7 @@ const STORAGE_KEYS = { REFRESH_TOKEN: "refresh_token", USER: "user", ODDS_SETTINGS: "odds_settings", + CARDS_SETTINGS: "cards_settings", }; export interface OddsSettings { @@ -13,6 +14,10 @@ export interface OddsSettings { selectedBookmakers: string[]; } +export interface CardsSettings { + enabled: boolean; +} + export const storage = { async setAccessToken(token: string): Promise { await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, token); @@ -61,12 +66,30 @@ export const storage = { } }, + async setCardsSettings(settings: CardsSettings): Promise { + await AsyncStorage.setItem( + STORAGE_KEYS.CARDS_SETTINGS, + JSON.stringify(settings), + ); + }, + + async getCardsSettings(): Promise { + const settingsStr = await AsyncStorage.getItem(STORAGE_KEYS.CARDS_SETTINGS); + if (!settingsStr) return { enabled: false }; + try { + return JSON.parse(settingsStr) as CardsSettings; + } catch { + return { enabled: false }; + } + }, + async clear(): Promise { await AsyncStorage.multiRemove([ STORAGE_KEYS.ACCESS_TOKEN, STORAGE_KEYS.REFRESH_TOKEN, STORAGE_KEYS.USER, STORAGE_KEYS.ODDS_SETTINGS, + STORAGE_KEYS.CARDS_SETTINGS, ]); }, };