You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

801 lines
23 KiB

import React, { useEffect, useState, useCallback, useRef } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
Image,
TouchableOpacity,
SafeAreaView,
StatusBar,
ActivityIndicator,
Dimensions,
FlatList
} from 'react-native';
import Ionicons from '@expo/vector-icons/Ionicons';
import { useNavigation, useRoute } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RouteProp } from '@react-navigation/native';
import { productApi, ProductDetailParams } from '../services/api/productApi';
2 months ago
import { useTranslation } from 'react-i18next';
// 获取屏幕宽度
const { width: screenWidth } = Dimensions.get('window');
// 图标组件 - 使用React.memo优化渲染
const IconComponent = React.memo(({ name, size, color }: { name: string; size: number; color: string }) => {
const Icon = Ionicons as any;
return <Icon name={name} size={size} color={color} />;
});
// 路由参数类型
type ProductDetailRouteParams = {
offer_id: string;
searchKeyword?: string;
2 months ago
price: number;
};
2 months ago
// 颜色选项组件
const ColorOption = React.memo(({ color, selected, onSelect }: { color: string; selected: boolean; onSelect: () => void }) => (
<TouchableOpacity
style={[styles.colorOption, selected && styles.colorOptionSelected]}
onPress={onSelect}
>
<View style={[styles.colorCircle, { backgroundColor: color }]} />
<Text style={styles.colorName}>{color}</Text>
</TouchableOpacity>
));
// 尺寸选项组件
const SizeOption = React.memo(({ size, selected, onSelect }: { size: string; selected: boolean; onSelect: () => void }) => (
<TouchableOpacity
style={[styles.sizeOption, selected && styles.sizeOptionSelected]}
onPress={onSelect}
>
<Text style={[styles.sizeText, selected && styles.sizeTextSelected]}>{size}</Text>
</TouchableOpacity>
));
// 相关商品组件
const RelatedProductItem = React.memo(({ product, onPress }: { product: any; onPress: () => void }) => (
<TouchableOpacity style={styles.relatedProductItem} onPress={onPress}>
<Image source={{ uri: product.image }} style={styles.relatedProductImage} />
<Text style={styles.relatedProductPrice}>${product.price}</Text>
</TouchableOpacity>
));
export const ProductDetailScreen = () => {
2 months ago
const { t } = useTranslation();
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const route = useRoute<RouteProp<Record<string, ProductDetailRouteParams>, string>>();
const [product, setProduct] = useState<ProductDetailParams | null>(null);
const [loading, setLoading] = useState(true);
const [activeImageIndex, setActiveImageIndex] = useState(0);
2 months ago
const [selectedColor, setSelectedColor] = useState('Black');
const [selectedSize, setSelectedSize] = useState('M');
const [relatedProducts, setRelatedProducts] = useState([
{ id: 1, name: 'Related Product 1', price: 19.99, image: 'https://via.placeholder.com/150' },
{ id: 2, name: 'Related Product 2', price: 24.99, image: 'https://via.placeholder.com/150' },
{ id: 3, name: 'Related Product 3', price: 14.99, image: 'https://via.placeholder.com/150' },
{ id: 4, name: 'Related Product 4', price: 34.99, image: 'https://via.placeholder.com/150' },
]);
const [page, setPage] = useState(1);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMoreProducts, setHasMoreProducts] = useState(true);
// 颜色和尺寸选项
const colorOptions = ['Black', 'White', 'Blue', 'Red'];
const sizeOptions = ['S', 'M', 'L', 'XL', 'XXL'];
// 获取产品详情
useEffect(() => {
fetchProductDetails();
}, []);
2 months ago
// 获取产品详情的API调用
const fetchProductDetails = async () => {
if (!route.params?.offer_id) return;
setLoading(true);
try {
2 months ago
const res = await productApi.getProductDetail(route.params.offer_id);
res.price = route.params.price;
setProduct(res);
} catch (error) {
console.error('Error fetching product details:', error);
} finally {
setLoading(false);
}
};
// 返回上一页
const goBack = useCallback(() => {
navigation.goBack();
}, [navigation]);
2 months ago
// 处理相关商品点击
const handleRelatedProductPress = useCallback((product: any) => {
// 这里可以导航到对应的商品详情页
console.log('Navigate to related product:', product.id);
}, []);
// 处理添加到购物车
const handleAddToCart = useCallback(() => {
// 添加到购物车的逻辑
console.log('Add to cart with color:', selectedColor, 'and size:', selectedSize);
}, [selectedColor, selectedSize]);
// 处理立即购买
const handleBuyNow = useCallback(() => {
// 立即购买的逻辑
console.log('Buy now with color:', selectedColor, 'and size:', selectedSize);
}, [selectedColor, selectedSize]);
// 获取相关商品
const fetchRelatedProducts = useCallback(async (pageNumber = 1) => {
if (!hasMoreProducts && pageNumber > 1) return;
try {
setLoadingMore(true);
// 模拟网络请求延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟新数据
const newProducts = Array(4).fill(0).map((_, index) => ({
id: pageNumber * 10 + index,
name: `Related Product ${pageNumber * 10 + index}`,
price: Number((Math.random() * 30 + 10).toFixed(2)),
image: 'https://via.placeholder.com/150'
}));
if (pageNumber === 1) {
setRelatedProducts(newProducts);
} else {
setRelatedProducts(prev => [...prev, ...newProducts]);
}
// 模拟数据加载完毕的情况
if (pageNumber >= 3) {
setHasMoreProducts(false);
} else {
setPage(pageNumber);
}
} catch (error) {
console.error('Error fetching related products:', error);
} finally {
setLoadingMore(false);
}
2 months ago
}, [hasMoreProducts]);
// 初始加载相关商品
useEffect(() => {
fetchRelatedProducts(1);
}, [fetchRelatedProducts]);
// 处理滚动到底部加载更多
const handleLoadMore = useCallback(() => {
if (!loadingMore && hasMoreProducts) {
fetchRelatedProducts(page + 1);
}
}, [fetchRelatedProducts, loadingMore, hasMoreProducts, page]);
if (loading) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={goBack}>
<IconComponent name="arrow-back" size={24} color="#333" />
</TouchableOpacity>
2 months ago
<Text style={styles.headerTitle}>{t('productDetail')}</Text>
<View style={styles.headerRight}>
<IconComponent name="ellipsis-vertical" size={20} color="#333" />
</View>
</View>
<View style={styles.loadingContainer}>
2 months ago
<ActivityIndicator size="large" color="#ff6600" />
<Text style={styles.loadingText}>{t('loadingProductInfo')}</Text>
</View>
</SafeAreaView>
);
}
if (!product) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={goBack}>
<IconComponent name="arrow-back" size={24} color="#333" />
</TouchableOpacity>
2 months ago
<Text style={styles.headerTitle}>{t('productDetail')}</Text>
<View style={styles.headerRight}>
<IconComponent name="ellipsis-vertical" size={20} color="#333" />
</View>
</View>
<View style={styles.errorContainer}>
<IconComponent name="alert-circle-outline" size={48} color="#ff6600" />
2 months ago
<Text style={styles.errorText}>{t('productNotAvailable')}</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
{/* 头部导航 */}
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={goBack}>
<IconComponent name="arrow-back" size={24} color="#333" />
</TouchableOpacity>
<Text style={styles.headerTitle} numberOfLines={1}>
2 months ago
{t('productDetail')}
</Text>
2 months ago
<View style={styles.headerRight}>
<TouchableOpacity style={styles.headerIconButton}>
<IconComponent name="ellipsis-vertical" size={20} color="#333" />
</TouchableOpacity>
</View>
</View>
2 months ago
<ScrollView
style={styles.scrollContainer}
showsVerticalScrollIndicator={false}
onScroll={({ nativeEvent }) => {
const { layoutMeasurement, contentOffset, contentSize } = nativeEvent;
const paddingToBottom = 20;
if (layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom) {
handleLoadMore();
}
}}
scrollEventThrottle={400}
>
{/* 产品图片轮播 */}
<View style={styles.imageContainer}>
{product.product_image_urls && product.product_image_urls.length > 0 ? (
<>
<FlatList
data={product.product_image_urls}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
keyExtractor={(_, index) => `image-${index}`}
onScroll={(event) => {
const slideSize = event.nativeEvent.layoutMeasurement.width;
const index = Math.floor(event.nativeEvent.contentOffset.x / slideSize);
setActiveImageIndex(index);
}}
scrollEventThrottle={16}
renderItem={({ item }) => (
<View style={[styles.imageContainer, { width: screenWidth }]}>
<Image
source={{ uri: item }}
style={{ width: '100%', height: '100%' }}
resizeMode="contain"
/>
</View>
)}
/>
{/* 指示器 */}
{product.product_image_urls.length > 1 && (
<View style={styles.paginationContainer}>
{product.product_image_urls.map((_, index) => (
<View
key={`dot-${index}`}
style={[
styles.paginationDot,
index === activeImageIndex ? styles.paginationDotActive : {}
]}
/>
))}
</View>
)}
</>
) : (
<View style={styles.imagePlaceholder}>
2 months ago
<Text style={styles.imagePlaceholderText}>{t('productDetail')}</Text>
</View>
)}
</View>
{/* 产品基本信息 */}
<View style={styles.infoSection}>
2 months ago
<View style={styles.priceContainer}>
<Text style={styles.productPrice}>
${product.price?.toFixed(2) || '29.99'}
</Text>
<Text style={styles.originalPrice}>${(product.price * 1.4).toFixed(2) || '49.99'}</Text>
<View style={styles.discountBadge}>
<Text style={styles.discountText}>-40%</Text>
</View>
<View style={styles.vipBadge}>
<Text style={styles.vipText}>VIP</Text>
</View>
</View>
<Text style={styles.productTitle}>{product.subject || 'Product Name'}</Text>
<Text style={styles.productSales}>{t('monthlySales')} {product.sold_out || 2458}</Text>
2 months ago
{/* 颜色选择 */}
<View style={styles.optionSection}>
<Text style={styles.optionTitle}>{t('color')}</Text>
<View style={styles.colorOptions}>
{colorOptions.map((color, index) => (
<ColorOption
key={`color-${index}`}
color={color}
selected={selectedColor === color}
onSelect={() => setSelectedColor(color)}
/>
))}
<TouchableOpacity style={styles.moreColorsButton}>
<Text style={styles.moreColorsText}>+2</Text>
</TouchableOpacity>
</View>
2 months ago
</View>
{/* 尺寸选择 */}
<View style={styles.optionSection}>
<Text style={styles.optionTitle}>{t('size')}</Text>
<View style={styles.sizeOptions}>
{sizeOptions.map((size, index) => (
<SizeOption
key={`size-${index}`}
size={size}
selected={selectedSize === size}
onSelect={() => setSelectedSize(size)}
/>
))}
</View>
</View>
</View>
2 months ago
{/* 相关商品 */}
<View style={styles.relatedProductsSection}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{t('moreFromStore')}</Text>
<TouchableOpacity style={styles.viewAllButton}>
<Text style={styles.viewAllText}>{t('viewAll')}</Text>
<IconComponent name="chevron-forward" size={16} color="#0066ff" />
</TouchableOpacity>
</View>
<View style={styles.relatedProductsContainer}>
{relatedProducts.map((product, index) => (
<RelatedProductItem
key={`related-${product.id}`}
product={product}
onPress={() => handleRelatedProductPress(product)}
/>
))}
</View>
{loadingMore && (
<View style={styles.loadMoreIndicator}>
<ActivityIndicator size="small" color="#ff6600" />
<Text style={styles.loadMoreText}>{t('loadingMoreProducts')}</Text>
</View>
2 months ago
)}
{!hasMoreProducts && relatedProducts.length > 0 && (
<Text style={styles.noMoreProductsText}>{t('noMoreProducts')}</Text>
)}
</View>
2 months ago
{/* 商品详情 */}
<View style={styles.detailsSection}>
<Text style={styles.sectionTitle}>{t('productDetails')}</Text>
<View style={styles.productDetails}>
<View style={styles.productDetailItem}>
<View style={styles.detailPlaceholder}>
<Text style={styles.detailPlaceholderText}>{t('productDetails')} 1</Text>
</View>
</View>
<View style={styles.productDetailItem}>
<View style={styles.detailPlaceholder}>
<Text style={styles.detailPlaceholderText}>{t('productDetails')} 2</Text>
</View>
</View>
<View style={styles.productDetailItem}>
<View style={styles.detailPlaceholder}>
<Text style={styles.detailPlaceholderText}>{t('productDetails')} 3</Text>
</View>
</View>
</View>
</View>
{/* 底部空间,确保内容不被底部操作栏遮挡 */}
<View style={styles.bottomSpace} />
</ScrollView>
{/* 底部操作栏 */}
<View style={styles.bottomBar}>
2 months ago
<TouchableOpacity style={styles.chatButton}>
<IconComponent name="chatbubble-outline" size={22} color="#666" />
<Text style={styles.chatButtonText}>{t('customerService')}</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.addToCartButton} onPress={handleAddToCart}>
<IconComponent name="cart-outline" size={20} color="#fff" />
2 months ago
<Text style={styles.addToCartText}>{t('addToCart')}</Text>
</TouchableOpacity>
2 months ago
<TouchableOpacity style={styles.buyNowButton} onPress={handleBuyNow}>
<Text style={styles.buyNowText}>{t('buyNow')}</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
2 months ago
backgroundColor: '#f5f5f5',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 15,
paddingVertical: 10,
2 months ago
backgroundColor: '#fff',
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
backButton: {
padding: 5,
},
headerTitle: {
flex: 1,
fontSize: 16,
fontWeight: 'bold',
color: '#333',
textAlign: 'center',
marginHorizontal: 10,
},
headerRight: {
2 months ago
flexDirection: 'row',
alignItems: 'center',
},
headerIconButton: {
padding: 5,
},
scrollContainer: {
flex: 1,
},
imageContainer: {
height: 300,
backgroundColor: '#f9f9f9',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
},
imagePlaceholder: {
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
},
imagePlaceholderText: {
fontSize: 16,
color: '#999',
},
2 months ago
paginationContainer: {
position: 'absolute',
bottom: 15,
flexDirection: 'row',
width: '100%',
justifyContent: 'center',
alignItems: 'center',
},
paginationDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(255, 255, 255, 0.4)',
marginHorizontal: 4,
},
paginationDotActive: {
backgroundColor: '#fff',
width: 10,
height: 10,
borderRadius: 5,
},
infoSection: {
padding: 15,
2 months ago
backgroundColor: '#fff',
marginBottom: 10,
},
2 months ago
priceContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
productPrice: {
fontSize: 24,
fontWeight: 'bold',
color: '#ff6600',
2 months ago
},
originalPrice: {
fontSize: 16,
color: '#999',
textDecorationLine: 'line-through',
marginLeft: 8,
},
discountBadge: {
backgroundColor: '#ff6600',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 3,
marginLeft: 8,
},
discountText: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold',
},
vipBadge: {
backgroundColor: '#f1c40f',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 3,
marginLeft: 6,
},
vipText: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold',
},
productTitle: {
fontSize: 16,
color: '#333',
marginBottom: 8,
},
productSales: {
2 months ago
fontSize: 12,
color: '#999',
2 months ago
marginBottom: 15,
},
optionSection: {
marginTop: 15,
},
optionTitle: {
fontSize: 14,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
},
2 months ago
colorOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
},
colorOption: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 10,
2 months ago
paddingVertical: 6,
borderRadius: 4,
marginRight: 10,
marginBottom: 10,
borderWidth: 1,
borderColor: '#e0e0e0',
},
2 months ago
colorOptionSelected: {
borderColor: '#ff6600',
backgroundColor: '#fff8f5',
},
2 months ago
colorCircle: {
width: 16,
height: 16,
borderRadius: 8,
marginRight: 6,
},
colorName: {
fontSize: 12,
color: '#333',
},
moreColorsButton: {
width: 35,
height: 35,
borderRadius: 4,
backgroundColor: '#f5f5f5',
justifyContent: 'center',
alignItems: 'center',
marginRight: 10,
marginBottom: 10,
},
moreColorsText: {
fontSize: 12,
color: '#333',
},
sizeOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
},
sizeOption: {
paddingHorizontal: 15,
paddingVertical: 6,
borderRadius: 4,
marginRight: 10,
marginBottom: 10,
borderWidth: 1,
borderColor: '#e0e0e0',
},
sizeOptionSelected: {
borderColor: '#ff6600',
backgroundColor: '#fff8f5',
},
sizeText: {
fontSize: 12,
color: '#333',
},
sizeTextSelected: {
color: '#ff6600',
fontWeight: 'bold',
},
2 months ago
relatedProductsSection: {
backgroundColor: '#fff',
padding: 15,
2 months ago
marginBottom: 10,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 15,
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
},
2 months ago
viewAllButton: {
flexDirection: 'row',
alignItems: 'center',
},
viewAllText: {
fontSize: 12,
color: '#0066ff',
},
2 months ago
relatedProductsContainer: {
flexDirection: 'row',
2 months ago
flexWrap: 'wrap',
justifyContent: 'space-between',
marginTop: 10,
},
2 months ago
relatedProductItem: {
width: screenWidth / 2 - 25,
marginBottom: 15,
},
relatedProductImage: {
width: '100%',
height: 120,
borderRadius: 4,
marginBottom: 5,
},
relatedProductPrice: {
fontSize: 14,
2 months ago
fontWeight: 'bold',
color: '#ff6600',
},
loadMoreIndicator: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 10,
},
loadMoreText: {
fontSize: 12,
color: '#666',
2 months ago
marginLeft: 8,
},
2 months ago
noMoreProductsText: {
textAlign: 'center',
fontSize: 12,
color: '#999',
paddingVertical: 10,
},
detailsSection: {
backgroundColor: '#fff',
padding: 15,
},
productDetails: {
marginTop: 10,
},
productDetailItem: {
marginBottom: 10,
},
productDetailImage: {
width: '100%',
height: 200,
borderRadius: 4,
},
2 months ago
detailPlaceholder: {
width: '100%',
height: 200,
borderRadius: 4,
backgroundColor: '#f0f0f0',
justifyContent: 'center',
alignItems: 'center',
},
2 months ago
detailPlaceholderText: {
fontSize: 16,
color: '#999',
},
bottomSpace: {
height: 60,
},
bottomBar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flexDirection: 'row',
height: 60,
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#f0f0f0',
paddingHorizontal: 15,
paddingVertical: 10,
},
2 months ago
chatButton: {
alignItems: 'center',
justifyContent: 'center',
marginRight: 15,
},
chatButtonText: {
fontSize: 10,
color: '#666',
marginTop: 2,
},
addToCartButton: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#ff9500',
borderRadius: 25,
marginRight: 10,
},
addToCartText: {
color: '#fff',
fontWeight: 'bold',
marginLeft: 5,
},
buyNowButton: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#ff6600',
borderRadius: 25,
},
buyNowText: {
color: '#fff',
fontWeight: 'bold',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
fontSize: 16,
color: '#666',
marginTop: 10,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
errorText: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
marginTop: 15,
marginBottom: 20,
},
});