实现收藏页面

This commit is contained in:
yuchenglong
2026-01-16 15:18:42 +08:00
parent d20080eaf3
commit 69e7a87de6
5 changed files with 378 additions and 3 deletions

View File

@@ -1,11 +1,283 @@
import { MatchCard } from "@/components/match-card";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { StyleSheet } from "react-native";
import { IconSymbol, IconSymbolName } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext";
import { fetchFavorites, removeFavorite } from "@/lib/api";
import { FavoriteItem, Match } from "@/types/api";
import { Image } from "expo-image";
import { useRouter } from "expo-router";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
FlatList,
RefreshControl,
ScrollView,
StyleSheet,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
type FilterType = "match" | "team" | "player";
interface FilterTab {
key: FilterType;
label: string;
icon: IconSymbolName;
}
export default function FavoriteScreen() {
const { theme } = useTheme();
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const router = useRouter();
const isDark = theme === "dark";
const iconColor = isDark ? Colors.dark.icon : Colors.light.icon;
const textColor = isDark ? Colors.dark.text : Colors.light.text;
const [activeTab, setActiveTab] = useState<FilterType>("match");
const [favorites, setFavorites] = useState<FavoriteItem[]>([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const tabs: FilterTab[] = [
{
key: "match",
label: t("favorites.filter_match"),
icon: "football-outline",
},
{
key: "team",
label: t("favorites.filter_team"),
icon: "shield-outline",
},
{
key: "player",
label: t("favorites.filter_player"),
icon: "person-outline",
},
];
const loadFavorites = async (isRefresh = false) => {
if (loading) return;
setLoading(true);
try {
const currentPage = isRefresh ? 1 : page;
const res = await fetchFavorites(activeTab, currentPage);
if (isRefresh) {
setFavorites(res.list);
setHasMore(res.list.length >= 20); // Assuming pageSize 20
} else {
setFavorites((prev) => [...prev, ...res.list]);
setHasMore(res.list.length >= 20);
}
if (res.list.length > 0) {
setPage(currentPage + 1);
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
setRefreshing(false);
}
};
useEffect(() => {
setPage(1);
setFavorites([]);
setHasMore(true);
loadFavorites(true);
}, [activeTab]);
const onRefresh = () => {
setRefreshing(true);
setPage(1);
setHasMore(true);
loadFavorites(true);
};
const loadMore = () => {
if (!loading && hasMore) {
loadFavorites(false);
}
};
const handleUnfavorite = async (type: string, typeId: string) => {
try {
await removeFavorite({ type, typeId });
setFavorites((prev) => prev.filter((item) => item.typeId !== typeId));
} catch (error) {
console.error("Remove favorite failed", error);
}
};
const renderMatchItem = (item: FavoriteItem) => {
const m = item.match;
if (!m) return null;
// Use current date as placeholder if not provided, or parse eventTime
// Assuming m contains necessary fields or we map carefully
const mappedMatch: Match = {
id: m.id
? m.id.toString()
: item.matchId
? item.matchId.toString()
: item.typeId,
league: m.leagueName || "",
time: m.eventTime || "",
home: m.eventHomeTeam || "",
away: m.eventAwayTeam || "",
scoreText: m.eventFinalResult || "0 - 0",
fav: true,
leagueId: m.leagueKey,
sportId: 1, // Default
isLive: false, // Provide default
};
return (
<MatchCard
match={mappedMatch}
onFavoriteToggle={() => handleUnfavorite("match", item.typeId)}
/>
);
};
const renderGenericItem = (item: FavoriteItem) => {
let name = "";
let logo = "";
let desc = "";
if (activeTab === "team" && item.team) {
name = item.team.name;
logo = item.team.logo;
} else if (activeTab === "player" && item.player) {
name = item.player.name;
logo = item.player.photo || item.player.avatar;
desc = item.player.teamName;
} else {
name = t("favorites.unknown");
}
return (
<ThemedView
style={[
styles.genericItem,
{ borderColor: isDark ? "#38383A" : "#E5E5EA" },
]}
>
<View style={styles.itemContent}>
<Image
source={{ uri: logo || "https://via.placeholder.com/40" }}
style={{ width: 40, height: 40, borderRadius: 20, marginRight: 12 }}
contentFit="contain"
/>
<View>
<ThemedText>{name}</ThemedText>
{desc ? (
<ThemedText style={{ fontSize: 12, color: "#8E8E93" }}>
{desc}
</ThemedText>
) : null}
</View>
</View>
<TouchableOpacity
onPress={() => handleUnfavorite(activeTab, item.typeId)}
>
<IconSymbol name="star" size={24} color="#FFD700" />
</TouchableOpacity>
</ThemedView>
);
};
return (
<ThemedView style={styles.container}>
<ThemedText type="title">Favorites</ThemedText>
{/* Header */}
<View style={[styles.header, { paddingTop: insets.top + 10 }]}>
<ThemedText type="title" style={{ fontSize: 28 }}>
{t("tabs.fav")}
</ThemedText>
<TouchableOpacity>
<IconSymbol name="create-outline" size={24} color={iconColor} />
</TouchableOpacity>
</View>
{/* Tabs */}
<View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.tabContainer}
>
{tabs.map((tab) => {
const isActive = activeTab === tab.key;
return (
<TouchableOpacity
key={tab.key}
style={[
styles.tabButton,
isActive && {
backgroundColor: isDark ? "#3A3A3C" : "#E5E5EA",
borderColor: isDark ? "#3A3A3C" : "#E5E5EA",
},
!isActive && {
borderColor: isDark ? "#38383A" : "#E5E5EA",
},
]}
onPress={() => setActiveTab(tab.key)}
>
<IconSymbol
name={tab.icon}
size={16}
color={isActive ? textColor : "#8E8E93"}
/>
<ThemedText
style={[
styles.tabText,
{ color: isActive ? textColor : "#8E8E93" },
]}
>
{tab.label}
</ThemedText>
</TouchableOpacity>
);
})}
</ScrollView>
</View>
{/* Content */}
<FlatList
data={favorites}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => {
if (activeTab === "match") return renderMatchItem(item);
return renderGenericItem(item);
}}
contentContainerStyle={styles.list}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
ListFooterComponent={
loading && favorites.length > 0 ? (
<ActivityIndicator style={{ padding: 10 }} />
) : null
}
ListEmptyComponent={
!loading ? (
<View style={styles.empty}>
<ThemedText style={{ color: "#8E8E93" }}>
{t("favorites.no_data")}
</ThemedText>
</View>
) : null
}
/>
</ThemedView>
);
}
@@ -13,7 +285,52 @@ export default function FavoriteScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
paddingBottom: 15,
},
tabContainer: {
flexDirection: "row",
paddingHorizontal: 16,
paddingBottom: 10,
gap: 10,
},
tabButton: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 6,
paddingHorizontal: 12,
borderRadius: 20,
borderWidth: 1,
gap: 6,
},
tabText: {
fontSize: 14,
fontWeight: "600",
},
list: {
padding: 16,
paddingBottom: 100,
},
genericItem: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: 12,
marginBottom: 12,
borderRadius: 12,
borderWidth: 1,
},
itemContent: {
flexDirection: "row",
alignItems: "center",
},
empty: {
padding: 40,
alignItems: "center",
justifyContent: "center",
},
});

View File

@@ -7,6 +7,14 @@
"finished": "Finished",
"fav": "Favorites"
},
"favorites": {
"filter_match": "Matches",
"filter_team": "Teams",
"filter_player": "Players",
"filter_league": "Leagues",
"no_data": "No favorites found",
"unknown": "Unknown"
},
"settings": {
"title": "Settings",
"theme": "Theme",

View File

@@ -7,6 +7,14 @@
"finished": "已完成",
"fav": "收藏"
},
"favorites": {
"filter_match": "比赛",
"filter_team": "球队",
"filter_player": "球员",
"filter_league": "联赛",
"no_data": "暂无收藏",
"unknown": "未知"
},
"settings": {
"title": "设置",
"theme": "主题",

View File

@@ -6,6 +6,7 @@ import {
AppleSignInResponse,
Country,
FavoriteCheckResponse,
FavoriteListResponse,
FavoriteRequest,
H2HData,
League,
@@ -483,3 +484,25 @@ export const checkFavorite = async (
throw error;
}
};
export const fetchFavorites = async (
type: string,
page: number = 1,
pageSize: number = 20
): Promise<FavoriteListResponse> => {
try {
const response = await apiClient.get<ApiResponse<FavoriteListResponse>>(
API_ENDPOINTS.FAVORITES,
{
params: { type, page, pageSize },
}
);
if (response.data.code === 0) {
return response.data.data;
}
throw new Error(response.data.message);
} catch (error) {
console.error("Fetch favorites error:", error);
throw error;
}
};

View File

@@ -490,3 +490,22 @@ export interface UserProfile {
nickname: string;
avatar: string;
}
export interface FavoriteItem {
id: number;
type: string; // "match" | "team" | "player" | "league"
typeId: string;
matchId?: number;
teamId?: number;
playerId?: number;
match?: any;
team?: any;
player?: any;
notify: boolean;
createdAt: string;
}
export interface FavoriteListResponse {
list: FavoriteItem[];
total: number;
}