|
|
@ -1,4 +1,4 @@ |
|
|
|
import React from "react"; |
|
|
|
import React, { useState, useEffect, useCallback } from "react"; |
|
|
|
import { |
|
|
|
import { |
|
|
|
View, |
|
|
|
View, |
|
|
|
Text, |
|
|
|
Text, |
|
|
@ -9,34 +9,52 @@ import { |
|
|
|
SafeAreaView, |
|
|
|
SafeAreaView, |
|
|
|
StatusBar, |
|
|
|
StatusBar, |
|
|
|
Platform, |
|
|
|
Platform, |
|
|
|
|
|
|
|
ActivityIndicator, |
|
|
|
|
|
|
|
RefreshControl, |
|
|
|
|
|
|
|
Alert, |
|
|
|
} from "react-native"; |
|
|
|
} from "react-native"; |
|
|
|
import widthUtils from "../../utils/widthUtils"; |
|
|
|
import widthUtils from "../../utils/widthUtils"; |
|
|
|
import fontSize from "../../utils/fontsizeUtils"; |
|
|
|
import fontSize from "../../utils/fontsizeUtils"; |
|
|
|
import BackIcon from "../../components/BackIcon"; |
|
|
|
import BackIcon from "../../components/BackIcon"; |
|
|
|
import { useNavigation } from "@react-navigation/native"; |
|
|
|
import { useNavigation } from "@react-navigation/native"; |
|
|
|
|
|
|
|
import { productApi } from "../../services/api/productApi"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 收藏商品项组件
|
|
|
|
const FavoriteItem = ({ |
|
|
|
const FavoriteItem = ({ |
|
|
|
image, |
|
|
|
favoriteItem, |
|
|
|
title, |
|
|
|
onDelete, |
|
|
|
price, |
|
|
|
|
|
|
|
}: { |
|
|
|
}: { |
|
|
|
image: string; |
|
|
|
favoriteItem: any; |
|
|
|
title: string; |
|
|
|
onDelete: (favoriteId: number) => void; |
|
|
|
price: number; |
|
|
|
|
|
|
|
}) => { |
|
|
|
}) => { |
|
|
|
|
|
|
|
const product = favoriteItem.product; |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<View style={styles.item}> |
|
|
|
<View style={styles.item}> |
|
|
|
<Image source={{ uri: image }} style={styles.image} /> |
|
|
|
<Image
|
|
|
|
|
|
|
|
source={{ uri: product.product_image_urls[0] || 'https://via.placeholder.com/100' }}
|
|
|
|
|
|
|
|
style={styles.image}
|
|
|
|
|
|
|
|
/> |
|
|
|
<View style={styles.info}> |
|
|
|
<View style={styles.info}> |
|
|
|
<Text style={styles.title} numberOfLines={2}> |
|
|
|
<Text style={styles.title} numberOfLines={2}> |
|
|
|
{title} |
|
|
|
{product.subject_trans || product.subject} |
|
|
|
|
|
|
|
</Text> |
|
|
|
|
|
|
|
<Text style={styles.price}> |
|
|
|
|
|
|
|
{product.currency}{product.min_price} |
|
|
|
|
|
|
|
{product.vip_discount > 0 && ( |
|
|
|
|
|
|
|
<Text style={styles.originalPrice}> |
|
|
|
|
|
|
|
{product.currency}{product.original_min_price} |
|
|
|
|
|
|
|
</Text> |
|
|
|
|
|
|
|
)} |
|
|
|
</Text> |
|
|
|
</Text> |
|
|
|
<Text style={styles.price}>¥{price}</Text> |
|
|
|
|
|
|
|
<View style={styles.actions}> |
|
|
|
<View style={styles.actions}> |
|
|
|
<TouchableOpacity style={[styles.btn, styles.cart]}> |
|
|
|
<TouchableOpacity style={[styles.btn, styles.cart]}> |
|
|
|
<Text style={styles.cartText}>加入购物车</Text> |
|
|
|
<Text style={styles.cartText}>加入购物车</Text> |
|
|
|
</TouchableOpacity> |
|
|
|
</TouchableOpacity> |
|
|
|
<TouchableOpacity style={[styles.btn, styles.delete]}> |
|
|
|
<TouchableOpacity
|
|
|
|
|
|
|
|
style={[styles.btn, styles.delete]} |
|
|
|
|
|
|
|
onPress={() => onDelete(favoriteItem.favorite_id)} |
|
|
|
|
|
|
|
> |
|
|
|
<Text style={styles.deleteText}>删除</Text> |
|
|
|
<Text style={styles.deleteText}>删除</Text> |
|
|
|
</TouchableOpacity> |
|
|
|
</TouchableOpacity> |
|
|
|
</View> |
|
|
|
</View> |
|
|
@ -47,57 +65,149 @@ const FavoriteItem = ({ |
|
|
|
|
|
|
|
|
|
|
|
export const Collection = () => { |
|
|
|
export const Collection = () => { |
|
|
|
const navigation = useNavigation(); |
|
|
|
const navigation = useNavigation(); |
|
|
|
const data = [ |
|
|
|
|
|
|
|
{ |
|
|
|
// 状态管理
|
|
|
|
id: "1", |
|
|
|
const [favorites, setFavorites] = useState<any[]>([]); |
|
|
|
image: "https://img.alicdn.com/imgextra/i1/1234567890/O1CN01Item1.jpg", |
|
|
|
const [loading, setLoading] = useState(true); |
|
|
|
title: "韩版宽松休闲卫衣女春秋款薄款学生ins潮", |
|
|
|
const [loadingMore, setLoadingMore] = useState(false); |
|
|
|
price: 89.0, |
|
|
|
const [refreshing, setRefreshing] = useState(false); |
|
|
|
}, |
|
|
|
const [hasMore, setHasMore] = useState(true); |
|
|
|
{ |
|
|
|
const [currentPage, setCurrentPage] = useState(1); |
|
|
|
id: "2", |
|
|
|
const [total, setTotal] = useState(0); |
|
|
|
image: "https://img.alicdn.com/imgextra/i2/2234567890/O1CN01Item2.jpg", |
|
|
|
|
|
|
|
title: "小米无线蓝牙耳机半入耳式 轻巧便携", |
|
|
|
const PAGE_SIZE = 10; |
|
|
|
price: 129.0, |
|
|
|
|
|
|
|
}, |
|
|
|
// 获取收藏列表
|
|
|
|
{ |
|
|
|
const fetchFavorites = useCallback(async (page: number = 1, isRefresh: boolean = false) => { |
|
|
|
id: "3", |
|
|
|
try { |
|
|
|
image: "https://img.alicdn.com/imgextra/i3/3234567890/O1CN01Item3.jpg", |
|
|
|
if (!isRefresh && page === 1) { |
|
|
|
title: "华为MateBook X Pro 2020款 13.9英寸全面屏笔记本电脑", |
|
|
|
setLoading(true); |
|
|
|
price: 10999.0, |
|
|
|
} else if (!isRefresh && page > 1) { |
|
|
|
}, |
|
|
|
setLoadingMore(true); |
|
|
|
{ |
|
|
|
} |
|
|
|
id: "4", |
|
|
|
|
|
|
|
image: "https://img.alicdn.com/imgextra/i2/2234567890/O1CN01Item2.jpg", |
|
|
|
const response = await productApi.getCollectProductList(page, PAGE_SIZE); |
|
|
|
title: "小米无线蓝牙耳机半入耳式 轻巧便携", |
|
|
|
|
|
|
|
price: 129.0, |
|
|
|
if (isRefresh || page === 1) { |
|
|
|
}, |
|
|
|
setFavorites(response.items); |
|
|
|
{ |
|
|
|
setCurrentPage(1); |
|
|
|
id: "5", |
|
|
|
} else { |
|
|
|
image: "https://img.alicdn.com/imgextra/i2/2234567890/O1CN01Item2.jpg", |
|
|
|
// 避免重复添加数据
|
|
|
|
title: "小米无线蓝牙耳机半入耳式 轻巧便携", |
|
|
|
setFavorites(prev => { |
|
|
|
price: 129.0, |
|
|
|
const newItems = response.items.filter( |
|
|
|
}, |
|
|
|
newItem => !prev.some(existingItem => existingItem.favorite_id === newItem.favorite_id) |
|
|
|
{ |
|
|
|
); |
|
|
|
id: "6", |
|
|
|
return [...prev, ...newItems]; |
|
|
|
image: "https://img.alicdn.com/imgextra/i2/2234567890/O1CN01Item2.jpg", |
|
|
|
}); |
|
|
|
title: "小米无线蓝牙耳机半入耳式 轻巧便携", |
|
|
|
} |
|
|
|
price: 129.0, |
|
|
|
|
|
|
|
}, |
|
|
|
setTotal(response.total); |
|
|
|
{ |
|
|
|
setHasMore(response.items.length === PAGE_SIZE && favorites.length + response.items.length < response.total); |
|
|
|
id: "7", |
|
|
|
|
|
|
|
image: "https://img.alicdn.com/imgextra/i2/2234567890/O1CN01Item2.jpg", |
|
|
|
if (!isRefresh && page > 1) { |
|
|
|
title: "小米无线蓝牙耳机半入耳式 轻巧便携", |
|
|
|
setCurrentPage(page); |
|
|
|
price: 129.0, |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
{ |
|
|
|
} catch (error) { |
|
|
|
id: "82", |
|
|
|
console.error("获取收藏列表失败:", error); |
|
|
|
image: "https://img.alicdn.com/imgextra/i2/2234567890/O1CN01Item2.jpg", |
|
|
|
Alert.alert("错误", "获取收藏列表失败,请重试"); |
|
|
|
title: "小米无线蓝牙耳机半入耳式 轻巧便携", |
|
|
|
} finally { |
|
|
|
price: 129.0, |
|
|
|
setLoading(false); |
|
|
|
}, |
|
|
|
setLoadingMore(false); |
|
|
|
// 可以继续添加更多商品
|
|
|
|
setRefreshing(false); |
|
|
|
]; |
|
|
|
} |
|
|
|
|
|
|
|
}, [favorites.length]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化加载
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
|
|
fetchFavorites(1); |
|
|
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 下拉刷新
|
|
|
|
|
|
|
|
const handleRefresh = useCallback(() => { |
|
|
|
|
|
|
|
if (refreshing) return; |
|
|
|
|
|
|
|
setRefreshing(true); |
|
|
|
|
|
|
|
fetchFavorites(1, true); |
|
|
|
|
|
|
|
}, [refreshing, fetchFavorites]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 触底加载更多
|
|
|
|
|
|
|
|
const handleLoadMore = useCallback(() => { |
|
|
|
|
|
|
|
if (!hasMore || loadingMore || loading) return; |
|
|
|
|
|
|
|
fetchFavorites(currentPage + 1); |
|
|
|
|
|
|
|
}, [hasMore, loadingMore, loading, currentPage, fetchFavorites]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 删除收藏
|
|
|
|
|
|
|
|
const handleDelete = useCallback(async (favoriteId: number) => { |
|
|
|
|
|
|
|
Alert.alert( |
|
|
|
|
|
|
|
"确认删除", |
|
|
|
|
|
|
|
"确定要删除这个收藏吗?", |
|
|
|
|
|
|
|
[ |
|
|
|
|
|
|
|
{ text: "取消", style: "cancel" }, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
text: "删除", |
|
|
|
|
|
|
|
style: "destructive", |
|
|
|
|
|
|
|
onPress: async () => { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
// 这里需要调用删除收藏的API,如果API没有提供,先在前端删除
|
|
|
|
|
|
|
|
setFavorites(prev => prev.filter(item => item.favorite_id !== favoriteId)); |
|
|
|
|
|
|
|
setTotal(prev => prev - 1); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
|
|
console.error("删除收藏失败:", error); |
|
|
|
|
|
|
|
Alert.alert("错误", "删除失败,请重试"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动事件处理
|
|
|
|
|
|
|
|
const handleScroll = useCallback((event: any) => { |
|
|
|
|
|
|
|
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent; |
|
|
|
|
|
|
|
const paddingToBottom = 20; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom) { |
|
|
|
|
|
|
|
handleLoadMore(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, [handleLoadMore]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染加载指示器
|
|
|
|
|
|
|
|
const renderLoadingIndicator = () => ( |
|
|
|
|
|
|
|
<View style={styles.loadingContainer}> |
|
|
|
|
|
|
|
<ActivityIndicator size="large" color="#ff5100" /> |
|
|
|
|
|
|
|
<Text style={styles.loadingText}>加载中...</Text> |
|
|
|
|
|
|
|
</View> |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染底部加载更多指示器
|
|
|
|
|
|
|
|
const renderFooter = () => { |
|
|
|
|
|
|
|
if (loadingMore) { |
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
|
|
<View style={styles.footerContainer}> |
|
|
|
|
|
|
|
<ActivityIndicator size="small" color="#ff5100" /> |
|
|
|
|
|
|
|
<Text style={styles.footerText}>加载更多...</Text> |
|
|
|
|
|
|
|
</View> |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasMore && favorites.length > 0) { |
|
|
|
|
|
|
|
return ( |
|
|
|
|
|
|
|
<View style={styles.footerContainer}> |
|
|
|
|
|
|
|
<Text style={styles.footerText}>没有更多数据了</Text> |
|
|
|
|
|
|
|
</View> |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return null; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染空状态
|
|
|
|
|
|
|
|
const renderEmptyState = () => ( |
|
|
|
|
|
|
|
<View style={styles.emptyContainer}> |
|
|
|
|
|
|
|
<Text style={styles.emptyText}>暂无收藏商品</Text> |
|
|
|
|
|
|
|
<Text style={styles.emptySubtext}>去逛逛,收藏喜欢的商品吧</Text> |
|
|
|
|
|
|
|
</View> |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<SafeAreaView style={styles.safeArea}> |
|
|
|
<SafeAreaView style={styles.safeArea}> |
|
|
@ -110,14 +220,43 @@ export const Collection = () => { |
|
|
|
> |
|
|
|
> |
|
|
|
<BackIcon size={fontSize(22)} /> |
|
|
|
<BackIcon size={fontSize(22)} /> |
|
|
|
</TouchableOpacity> |
|
|
|
</TouchableOpacity> |
|
|
|
<Text style={styles.titles}>我的收藏</Text> |
|
|
|
<Text style={styles.titles}>我的收藏 ({total})</Text> |
|
|
|
<View style={styles.placeholder} /> |
|
|
|
<View style={styles.placeholder} /> |
|
|
|
</View> |
|
|
|
</View> |
|
|
|
<ScrollView style={styles.container} showsVerticalScrollIndicator={false}> |
|
|
|
|
|
|
|
{data.map((item) => ( |
|
|
|
{loading ? ( |
|
|
|
<FavoriteItem key={item.id} {...item} /> |
|
|
|
renderLoadingIndicator() |
|
|
|
))} |
|
|
|
) : ( |
|
|
|
</ScrollView> |
|
|
|
<ScrollView
|
|
|
|
|
|
|
|
style={styles.container}
|
|
|
|
|
|
|
|
showsVerticalScrollIndicator={false} |
|
|
|
|
|
|
|
refreshControl={ |
|
|
|
|
|
|
|
<RefreshControl |
|
|
|
|
|
|
|
refreshing={refreshing} |
|
|
|
|
|
|
|
onRefresh={handleRefresh} |
|
|
|
|
|
|
|
colors={["#ff5100"]} |
|
|
|
|
|
|
|
tintColor="#ff5100" |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
onScroll={handleScroll} |
|
|
|
|
|
|
|
scrollEventThrottle={16} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
{favorites.length === 0 ? ( |
|
|
|
|
|
|
|
renderEmptyState() |
|
|
|
|
|
|
|
) : ( |
|
|
|
|
|
|
|
<> |
|
|
|
|
|
|
|
{favorites.map((item) => ( |
|
|
|
|
|
|
|
<FavoriteItem
|
|
|
|
|
|
|
|
key={item.favorite_id}
|
|
|
|
|
|
|
|
favoriteItem={item} |
|
|
|
|
|
|
|
onDelete={handleDelete} |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
))} |
|
|
|
|
|
|
|
{renderFooter()} |
|
|
|
|
|
|
|
</> |
|
|
|
|
|
|
|
)} |
|
|
|
|
|
|
|
</ScrollView> |
|
|
|
|
|
|
|
)} |
|
|
|
</View> |
|
|
|
</View> |
|
|
|
</SafeAreaView> |
|
|
|
</SafeAreaView> |
|
|
|
); |
|
|
|
); |
|
|
@ -172,6 +311,8 @@ const styles = StyleSheet.create({ |
|
|
|
backgroundColor: "#fff", |
|
|
|
backgroundColor: "#fff", |
|
|
|
borderBottomWidth: 1, |
|
|
|
borderBottomWidth: 1, |
|
|
|
borderBottomColor: "#eee", |
|
|
|
borderBottomColor: "#eee", |
|
|
|
|
|
|
|
marginBottom: 8, |
|
|
|
|
|
|
|
borderRadius: 8, |
|
|
|
}, |
|
|
|
}, |
|
|
|
image: { |
|
|
|
image: { |
|
|
|
width: widthUtils(100, 100).width, |
|
|
|
width: widthUtils(100, 100).width, |
|
|
@ -193,29 +334,75 @@ const styles = StyleSheet.create({ |
|
|
|
color: "#f40", |
|
|
|
color: "#f40", |
|
|
|
fontSize: fontSize(16), |
|
|
|
fontSize: fontSize(16), |
|
|
|
marginBottom: 10, |
|
|
|
marginBottom: 10, |
|
|
|
|
|
|
|
fontWeight: "600", |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
originalPrice: { |
|
|
|
|
|
|
|
color: "#999", |
|
|
|
|
|
|
|
fontSize: fontSize(14), |
|
|
|
|
|
|
|
textDecorationLine: "line-through", |
|
|
|
|
|
|
|
marginLeft: 8, |
|
|
|
}, |
|
|
|
}, |
|
|
|
actions: { |
|
|
|
actions: { |
|
|
|
flexDirection: "row", |
|
|
|
flexDirection: "row", |
|
|
|
gap: 8, |
|
|
|
gap: 8, |
|
|
|
}, |
|
|
|
}, |
|
|
|
btn: { |
|
|
|
btn: { |
|
|
|
paddingVertical: 4, |
|
|
|
flex: 1, |
|
|
|
paddingHorizontal: 10, |
|
|
|
paddingVertical: 6, |
|
|
|
borderRadius: 3, |
|
|
|
paddingHorizontal: 12, |
|
|
|
borderWidth: 1, |
|
|
|
borderRadius: 4, |
|
|
|
|
|
|
|
alignItems: "center", |
|
|
|
}, |
|
|
|
}, |
|
|
|
cart: { |
|
|
|
cart: { |
|
|
|
borderColor: "#ff5000", |
|
|
|
backgroundColor: "#ff5100", |
|
|
|
}, |
|
|
|
}, |
|
|
|
delete: { |
|
|
|
delete: { |
|
|
|
borderColor: "#ccc", |
|
|
|
backgroundColor: "#f5f5f5", |
|
|
|
|
|
|
|
borderWidth: 1, |
|
|
|
|
|
|
|
borderColor: "#ddd", |
|
|
|
}, |
|
|
|
}, |
|
|
|
cartText: { |
|
|
|
cartText: { |
|
|
|
fontSize: fontSize(12), |
|
|
|
color: "#fff", |
|
|
|
color: "#ff5000", |
|
|
|
fontSize: fontSize(14), |
|
|
|
}, |
|
|
|
}, |
|
|
|
deleteText: { |
|
|
|
deleteText: { |
|
|
|
|
|
|
|
color: "#666", |
|
|
|
|
|
|
|
fontSize: fontSize(14), |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
loadingContainer: { |
|
|
|
|
|
|
|
flex: 1, |
|
|
|
|
|
|
|
justifyContent: "center", |
|
|
|
|
|
|
|
alignItems: "center", |
|
|
|
|
|
|
|
paddingTop: 100, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
loadingText: { |
|
|
|
|
|
|
|
marginTop: 10, |
|
|
|
|
|
|
|
color: "#666", |
|
|
|
|
|
|
|
fontSize: fontSize(14), |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
footerContainer: { |
|
|
|
|
|
|
|
paddingVertical: 20, |
|
|
|
|
|
|
|
alignItems: "center", |
|
|
|
|
|
|
|
justifyContent: "center", |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
footerText: { |
|
|
|
|
|
|
|
color: "#999", |
|
|
|
fontSize: fontSize(12), |
|
|
|
fontSize: fontSize(12), |
|
|
|
|
|
|
|
marginTop: 5, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
emptyContainer: { |
|
|
|
|
|
|
|
flex: 1, |
|
|
|
|
|
|
|
justifyContent: "center", |
|
|
|
|
|
|
|
alignItems: "center", |
|
|
|
|
|
|
|
paddingTop: 100, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
emptyText: { |
|
|
|
|
|
|
|
fontSize: fontSize(16), |
|
|
|
|
|
|
|
color: "#666", |
|
|
|
|
|
|
|
marginBottom: 8, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
emptySubtext: { |
|
|
|
|
|
|
|
fontSize: fontSize(14), |
|
|
|
color: "#999", |
|
|
|
color: "#999", |
|
|
|
}, |
|
|
|
}, |
|
|
|
}); |
|
|
|
}); |
|
|
|