联赛筛选

This commit is contained in:
xianyi
2026-01-19 14:50:47 +08:00
parent 67ce779fee
commit c522f2ba01
4 changed files with 481 additions and 96 deletions

View File

@@ -1,6 +1,6 @@
import { HomeHeader } from "@/components/home-header";
import { MatchCard } from "@/components/match-card";
import { MatchCardLeague } from "@/components/match-card-league";
import { MatchesByLeague } from "@/components/matches-by-league";
import { SelectionModal } from "@/components/selection-modal";
import { CalendarModal } from "@/components/simple-calendar";
import { ThemedText } from "@/components/themed-text";
@@ -341,7 +341,7 @@ export default function HomeScreen() {
let merged: Match[] = list.map((m) => ({
...m,
date: (m as any).date || selectedStr,
sport: (m as any).sport ?? sportId,
sportId: (m as any).sportId ?? sportId,
}));
if (shouldMergeLive) {
@@ -374,7 +374,7 @@ export default function HomeScreen() {
away: item.event_away_team,
scoreText: item.event_final_result || "0 - 0",
fav: false,
sport: sportId,
sportId: sportId,
isLive: true,
};
});
@@ -556,17 +556,13 @@ export default function HomeScreen() {
<ActivityIndicator size="large" color={Colors[theme].tint} />
<ThemedText style={{ marginTop: 10 }}>{t("home.loading")}</ThemedText>
</View>
) : (
) : filterMode === "time" ? (
<FlatList
data={matches}
keyExtractor={(item) => item.id}
renderItem={({ item }) =>
filterMode === "time" ? (
<MatchCard match={item} onFavoriteToggle={handleFavoriteToggle} />
) : (
<MatchCardLeague match={item} onFavoriteToggle={handleFavoriteToggle} />
)
}
renderItem={({ item }) => (
<MatchCard match={item} onFavoriteToggle={handleFavoriteToggle} />
)}
contentContainerStyle={styles.listContent}
ListEmptyComponent={
<View style={styles.center}>
@@ -574,6 +570,11 @@ export default function HomeScreen() {
</View>
}
/>
) : (
<MatchesByLeague
matches={matches}
onFavoriteToggle={handleFavoriteToggle}
/>
)}
{/* Modals - 条件渲染,只在可见时渲染 */}

View File

@@ -1,12 +1,11 @@
import { ThemedText } from "@/components/themed-text";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext";
import { addFavorite, removeFavorite } from "@/lib/api";
import { Match } from "@/types/api";
import { useRouter } from "expo-router";
import React, { useState } from "react";
import { Pressable, StyleSheet, TouchableOpacity, View } from "react-native";
import { Image, Pressable, StyleSheet, TouchableOpacity, View } from "react-native";
interface MatchCardLeagueProps {
match: Match;
@@ -24,15 +23,17 @@ export function MatchCardLeague({
const [isFav, setIsFav] = useState(match.fav);
const [loading, setLoading] = useState(false);
// 当外部传入的 match.fav 改变时,更新内部状态
React.useEffect(() => {
setIsFav(match.fav);
}, [match.fav]);
const isDark = theme === "dark";
const iconColor = isDark ? Colors.dark.icon : Colors.light.icon;
// 截图中的卡片背景通常非常深,接近纯黑
const cardBg = isDark ? "#1C1C1E" : "#FFFFFF";
const borderColor = isDark ? "#38383A" : "#E5E5EA";
const textColor = isDark ? "#FFFFFF" : "#000000";
// 赢家的高亮颜色 (截图中的橙黄色)
const winnerColor = "#FF9500";
const loserColor = isDark ? "#FFFFFF" : "#000000";
const handlePress = () => {
if (onPress) {
@@ -71,109 +72,210 @@ export function MatchCardLeague({
}
};
// --- 数据解析与样式逻辑 ---
// 假设 match 对象中有 homeScore 和 awayScore (数字或字符串)
// 如果 API 只有 "1 - 0" 这种 scoreText你需要在这里拆分
// 这里为了演示,假设 match 对象已经扩展了这些字段,或者我们从 scoreText 简单解析
let homeScore = 0;
let awayScore = 0;
// 简单的解析逻辑 demo (根据你的实际数据结构调整)
if (match.scoreText && match.scoreText.includes("-")) {
const parts = match.scoreText.split("-");
homeScore = parseInt(parts[0].trim()) || 0;
awayScore = parseInt(parts[1].trim()) || 0;
}
// 如果 match 对象里直接有 match.homeScore 最好:
// homeScore = match.homeScore;
// awayScore = match.awayScore;
// 判断文字颜色和背景样式
let homeColor = loserColor;
let awayColor = loserColor;
let homeScoreBg = "#2C2C2E"; // 默认深灰背景
let awayScoreBg = "#2C2C2E";
let homeBorderColor = "transparent";
let awayBorderColor = "transparent";
if (homeScore > awayScore) {
homeColor = "#000000"; // 赢家黑色文字
homeScoreBg = winnerColor; // 金色背景
homeBorderColor = winnerColor;
} else if (awayScore > homeScore) {
awayColor = "#000000"; // 赢家黑色文字
awayScoreBg = winnerColor; // 金色背景
awayBorderColor = winnerColor;
}
// 如果相等,保持默认深灰背景
return (
<Pressable
onPress={handlePress}
style={({ pressed }) => [
styles.card,
{ backgroundColor: cardBg, borderColor, opacity: pressed ? 0.7 : 1 },
{ backgroundColor: cardBg, opacity: pressed ? 0.8 : 1 },
]}
>
<View style={styles.header}>
<View
style={[
styles.leagueBadge,
{ backgroundColor: isDark ? "#2C2C2E" : "#F2F2F7" },
]}
>
<ThemedText style={styles.leagueText} numberOfLines={1}>
{match.league}
</ThemedText>
</View>
<ThemedText style={styles.timeText}>{match.time}</ThemedText>
</View>
<View style={styles.teamsContainer}>
<ThemedText type="defaultSemiBold" style={styles.teamsText}>
{match.home} vs2 {match.away}
{/* 1. 左侧:时间/状态 */}
<View style={styles.leftColumn}>
{/* 如果有状态字段 match.meta (如 'FT', 'AET'), 优先显示,否则显示时间 */}
<ThemedText style={styles.statusText}>
{(match.meta || match.time || "").toUpperCase()}
</ThemedText>
<View style={styles.scoreContainer}>
<ThemedText type="defaultSemiBold" style={styles.scoreText}>
{match.scoreText}
</View>
{/* 2. 中间:球队信息 (上下排布) */}
<View style={styles.teamsColumn}>
{/* 主队行 */}
<View style={styles.teamRow}>
<Image
source={{ uri: (match as any).homeLogo || match.homeTeamLogo || "https://placehold.co/24x24/png" }}
style={styles.teamLogo}
/>
<ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}>
{match.home || match.homeTeamName}
</ThemedText>
</View>
{/* 客队行 */}
<View style={[styles.teamRow, { marginTop: 6 }]}>
<Image
source={{ uri: (match as any).awayLogo || match.awayTeamLogo || "https://placehold.co/24x24/png" }}
style={styles.teamLogo}
/>
<ThemedText style={[styles.teamName, { color: textColor }]} numberOfLines={1}>
{match.away || match.awayTeamName}
</ThemedText>
<TouchableOpacity
onPress={(e) => {
e.stopPropagation();
toggleFavorite();
}}
disabled={loading}
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
>
<IconSymbol
name={isFav ? "star" : "star-outline"}
size={24}
color={isFav ? "#FFD700" : iconColor}
/>
</TouchableOpacity>
</View>
</View>
{match.meta && (
<ThemedText style={styles.metaText}>{match.meta}</ThemedText>
)}
{/* 3. 右侧:比分与铃铛 */}
<View style={styles.rightWrapper}>
{/* 比分列 (上下排布) */}
<View style={styles.scoresColumn}>
<View style={[
styles.scoreBox,
{
backgroundColor: homeScoreBg,
borderColor: homeBorderColor,
borderWidth: homeBorderColor !== "transparent" ? 1.5 : 0,
}
]}>
<ThemedText style={[styles.scoreText, { color: homeColor }]}>
{homeScore}
</ThemedText>
</View>
<View style={[
styles.scoreBox,
{
backgroundColor: awayScoreBg,
borderColor: awayBorderColor,
borderWidth: awayBorderColor !== "transparent" ? 1.5 : 0,
marginTop: 6,
}
]}>
<ThemedText style={[styles.scoreText, { color: awayColor }]}>
{awayScore}
</ThemedText>
</View>
</View>
{/* 收藏按钮 */}
<TouchableOpacity
onPress={(e) => {
e.stopPropagation();
toggleFavorite();
}}
disabled={loading}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
style={styles.favoriteButton}
>
<IconSymbol
name={isFav ? "star" : "star-outline"}
size={20}
color={isFav ? "#FFD700" : "#545458"}
/>
</TouchableOpacity>
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
card: {
padding: 12,
marginBottom: 12,
flexDirection: "row",
paddingVertical: 14,
paddingHorizontal: 16,
marginHorizontal: 0,
marginBottom: 0,
borderRadius: 12,
borderWidth: 1,
},
header: {
flexDirection: "row",
alignItems: "center",
marginBottom: 8,
minHeight: 68,
},
leagueBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
marginRight: 8,
maxWidth: "70%",
// 左侧时间列
leftColumn: {
width: 52,
justifyContent: "center",
alignItems: "flex-start",
marginRight: 12,
},
leagueText: {
statusText: {
fontSize: 12,
opacity: 0.7,
color: "#8E8E93", // 次要文本颜色 (Grey)
fontWeight: "600",
},
timeText: {
fontSize: 12,
opacity: 0.5,
},
teamsContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 4,
},
teamsText: {
fontSize: 16,
// 中间球队列
teamsColumn: {
flex: 1,
marginRight: 8,
justifyContent: "center",
paddingRight: 8,
},
scoreContainer: {
teamRow: {
flexDirection: "row",
alignItems: "center",
gap: 12,
},
teamLogo: {
width: 22,
height: 22,
borderRadius: 11, // 圆形图标
marginRight: 10,
backgroundColor: "#3A3A3C", // 图片加载占位
},
teamName: {
fontSize: 16,
fontWeight: "500",
flex: 1,
},
// 右侧整体包装
rightWrapper: {
flexDirection: "row",
alignItems: "center",
},
// 比分列
scoresColumn: {
alignItems: "flex-end", // 数字右对齐
justifyContent: "center",
marginRight: 12,
},
scoreBox: {
minWidth: 32,
height: 28,
borderRadius: 6,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: 8,
},
scoreText: {
fontSize: 16,
fontWeight: "700",
lineHeight: 20,
},
metaText: {
fontSize: 12,
opacity: 0.5,
marginTop: 4,
},
});
// 收藏按钮
favoriteButton: {
paddingHorizontal: 4,
paddingVertical: 4,
justifyContent: 'center',
alignItems: 'center',
}
});

View File

@@ -0,0 +1,268 @@
import { MatchCardLeague } from "@/components/match-card-league";
import { ThemedText } from "@/components/themed-text";
import { useTheme } from "@/context/ThemeContext";
import { Match } from "@/types/api";
import React, { useState } from "react";
import {
Image,
LayoutAnimation,
Platform,
ScrollView,
StyleSheet,
TouchableOpacity,
UIManager,
View,
} from "react-native";
// 开启 Android 上的 LayoutAnimation
if (
Platform.OS === "android" &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
interface MatchesByLeagueProps {
matches: Match[];
onFavoriteToggle?: (matchId: string, isFav: boolean) => void;
/**
* 是否支持折叠收起
* @default true
*/
enableCollapsible?: boolean;
}
export function MatchesByLeague({
matches,
onFavoriteToggle,
enableCollapsible = true,
}: MatchesByLeagueProps) {
const { theme } = useTheme();
const isDark = theme === "dark";
// 数据分组逻辑
const matchesByLeague = React.useMemo(() => {
const grouped: Record<string, Match[]> = {};
matches.forEach((match) => {
const league = match.league || match.leagueName || "其他";
if (!grouped[league]) {
grouped[league] = [];
}
grouped[league].push(match);
});
return grouped;
}, [matches]);
const leagueNames = Object.keys(matchesByLeague);
// 状态:记录哪些联赛被折叠了 (Key 为联赛名称),默认全部收起
const [collapsedSections, setCollapsedSections] = useState<
Record<string, boolean>
>({});
// 当联赛列表变化时,确保所有联赛默认都是收起状态
React.useEffect(() => {
setCollapsedSections((prev) => {
const updated: Record<string, boolean> = {};
leagueNames.forEach((name) => {
updated[name] = prev[name] !== undefined ? prev[name] : true;
});
return updated;
});
}, [leagueNames.join(",")]);
// 切换折叠状态
const toggleCollapse = (leagueName: string) => {
if (!enableCollapsible) return;
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCollapsedSections((prev) => ({
...prev,
[leagueName]: !prev[leagueName],
}));
};
if (leagueNames.length === 0) {
return (
<View style={styles.emptyContainer}>
<ThemedText></ThemedText>
</View>
);
}
return (
<ScrollView
style={[
styles.container,
{ backgroundColor: isDark ? "#000000" : "#F2F2F7" }
]}
contentContainerStyle={{ paddingBottom: 40 }}
>
{leagueNames.map((leagueName) => {
const leagueMatches = matchesByLeague[leagueName];
// 取该组第一场比赛的数据作为头部信息的来源(图标、国家等)
const firstMatch = leagueMatches[0];
const isCollapsed = collapsedSections[leagueName];
return (
<View key={leagueName} style={styles.leagueSection}>
{/* 联赛头部 */}
<TouchableOpacity
activeOpacity={enableCollapsible ? 0.7 : 1}
onPress={() => toggleCollapse(leagueName)}
style={styles.leagueHeaderWrapper}
>
<View style={styles.leagueHeaderLeft}>
{/* 联赛 Logo */}
<Image
source={{
uri: firstMatch.leagueLogo || (firstMatch as any).leagueLogoUrl || "https://placehold.co/40x40/png",
}}
style={styles.leagueLogo}
/>
<View style={styles.leagueInfoText}>
{/* 联赛名称 */}
<ThemedText style={styles.leagueTitle}>{leagueName}</ThemedText>
{/* 国家信息行 */}
<View style={styles.countryRow}>
<Image
source={{ uri: firstMatch.countryLogo || (firstMatch as any).countryFlagUrl || "https://placehold.co/20x20/png" }}
style={styles.countryFlag}
/>
<ThemedText style={styles.countryName}>
{firstMatch.countryName || (firstMatch as any).countryName || "International"}
</ThemedText>
</View>
</View>
</View>
<View style={styles.leagueHeaderRight}>
{/* 比赛数量 */}
<ThemedText style={styles.matchCount}>
{leagueMatches.length}
</ThemedText>
{/* 折叠箭头 (仅当支持折叠时显示) */}
{enableCollapsible && (
<ThemedText style={styles.chevron}>
{isCollapsed ? "⌄" : "⌃"}
</ThemedText>
)}
</View>
</TouchableOpacity>
{/* 比赛列表内容 (根据状态显示/隐藏) */}
{!isCollapsed && (
<View style={styles.matchListContainer}>
{leagueMatches.map((match, index) => (
<View
key={match.id}
style={[
styles.matchCardWrapper,
index < leagueMatches.length - 1 && styles.matchCardDivider
]}
>
<MatchCardLeague
match={match}
onFavoriteToggle={onFavoriteToggle}
/>
</View>
))}
</View>
)}
</View>
);
})}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
leagueSection: {
marginBottom: 16,
},
leagueHeaderWrapper: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: "transparent",
},
leagueHeaderLeft: {
flexDirection: "row",
alignItems: "center",
flex: 1,
},
leagueLogo: {
width: 36,
height: 36,
borderRadius: 6,
marginRight: 12,
backgroundColor: "#3A3A3C",
},
leagueInfoText: {
justifyContent: "center",
},
leagueTitle: {
fontSize: 17,
fontWeight: "600",
color: "#FFFFFF",
marginBottom: 3,
},
countryRow: {
flexDirection: "row",
alignItems: "center",
},
countryFlag: {
width: 16,
height: 12,
marginRight: 5,
borderRadius: 2,
},
countryName: {
fontSize: 13,
color: "#8E8E93",
fontWeight: "500",
},
leagueHeaderRight: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
matchCount: {
fontSize: 15,
color: "#8E8E93",
fontWeight: "600",
},
chevron: {
fontSize: 16,
color: "#8E8E93",
fontWeight: '600',
},
matchListContainer: {
backgroundColor: "#1C1C1E",
borderRadius: 12,
marginHorizontal: 16,
overflow: "hidden",
},
matchCardWrapper: {
// 卡片包装器
},
matchCardDivider: {
borderBottomWidth: 0.5,
borderBottomColor: "#3A3A3C",
},
emptyContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingTop: 100,
},
});

View File

@@ -12,12 +12,26 @@ export interface Match {
id: string;
league: string;
time: string;
date: string;
date?: string;
home: string;
away: string;
scoreText: string;
fav: boolean;
// sport: string;
sport?: string;
homeTeamName?: string;
homeTeamKey?: string;
homeTeamLogo?: string;
awayTeamName?: string;
awayTeamKey?: string;
awayTeamLogo?: string;
leagueName?: string;
leagueKey?: string;
leagueLogo?: string;
countryName?: string;
countryKey?: string;
countryLogo?: string;
events?: string;
players?: string;
sportId?: number;
leagueId?: number;
isLive?: boolean;
@@ -73,11 +87,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;