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.

481 lines
13 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';
// 获取屏幕宽度
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} />;
});
// 产品类型定义
interface Product {
id: string;
title: string;
price: number;
image: string;
currency: string;
sales: number;
description: string;
specifications: Record<string, string>;
}
// 路由参数类型
type ProductDetailRouteParams = {
offer_id: string;
searchKeyword?: string;
price:number;
};
export const ProductDetailScreen = () => {
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);
// 获取产品详情
useEffect(() => {
fetchProductDetails();
}, []);
// 模拟获取产品详情的API调用
const fetchProductDetails = async () => {
if (!route.params?.offer_id) return;
setLoading(true);
try {
const res = await productApi.getProductDetail(route.params.offer_id);
console.log(res);
res.price = route.params.price;
// 模拟返回的数据
const searchKeyword = route.params.searchKeyword || 'Product';
console.log(searchKeyword);
setProduct(res);
} catch (error) {
console.error('Error fetching product details:', error);
// 在实际应用中应该显示错误消息
} finally {
setLoading(false);
}
};
// 返回上一页
const goBack = useCallback(() => {
navigation.goBack();
}, [navigation]);
// 返回到搜索结果页
const goBackToSearch = useCallback(() => {
if (route.params?.searchKeyword) {
navigation.navigate('SearchResult', { keyword: route.params.searchKeyword });
} else {
navigation.goBack();
}
}, [navigation, route.params?.searchKeyword]);
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>
<Text style={styles.headerTitle}>Product Details</Text>
<View style={styles.headerRight} />
</View>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0066FF" />
<Text style={styles.loadingText}>Loading product details...</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>
<Text style={styles.headerTitle}>Product Details</Text>
<View style={styles.headerRight} />
</View>
<View style={styles.errorContainer}>
<IconComponent name="alert-circle-outline" size={48} color="#ff6600" />
<Text style={styles.errorText}>Product not found</Text>
<TouchableOpacity style={styles.backToSearchButton} onPress={goBackToSearch}>
<Text style={styles.backToSearchText}>Back to Search Results</Text>
</TouchableOpacity>
</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}>
{product.subject}
</Text>
<View style={styles.headerRight} />
</View>
<ScrollView style={styles.scrollContainer} showsVerticalScrollIndicator={false}>
{/* 产品图片轮播 */}
<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}>
<Text style={styles.imagePlaceholderText}>Product Image</Text>
</View>
)}
</View>
{/* 产品基本信息 */}
<View style={styles.infoSection}>
<Text style={styles.productTitle}>{product.subject}</Text>
<Text style={styles.productPrice}>
{product.price}
</Text>
<Text style={styles.productSales}>{product.subject_trans}</Text>
{/* 如果是从搜索结果跳转来的,显示匹配的关键词 */}
{route.params?.searchKeyword && (
<View style={styles.keywordContainer}>
<Text style={styles.keywordLabel}>Matched Search: </Text>
<Text style={styles.keywordValue}>{route.params.searchKeyword}</Text>
</View>
)}
</View>
{/* 产品描述 */}
{/* <View style={styles.section}>
<Text style={styles.sectionTitle}>Description</Text>
<Text style={styles.descriptionText}>{product.description}</Text>
</View> */}
{/* 产品规格 */}
{/* <View style={styles.section}>
<Text style={styles.sectionTitle}>Specifications</Text>
{Object.entries(product.specifications).map(([key, value], index) => (
<View key={index} style={styles.specRow}>
<Text style={styles.specKey}>{key}</Text>
<Text style={styles.specValue}>{value}</Text>
</View>
))}
</View> */}
{/* 返回搜索结果按钮 */}
<TouchableOpacity style={styles.backToSearchButton} onPress={goBackToSearch}>
<Text style={styles.backToSearchText}>Back to Search Results</Text>
</TouchableOpacity>
{/* 底部空间,确保内容不被底部操作栏遮挡 */}
<View style={styles.bottomSpace} />
</ScrollView>
{/* 底部操作栏 */}
<View style={styles.bottomBar}>
<TouchableOpacity style={styles.addToCartButton}>
<IconComponent name="cart-outline" size={20} color="#fff" />
<Text style={styles.addToCartText}>Add to Cart</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.buyNowButton}>
<Text style={styles.buyNowText}>Buy Now</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 15,
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
backgroundColor: '#fff',
},
backButton: {
padding: 5,
},
headerTitle: {
flex: 1,
fontSize: 16,
fontWeight: 'bold',
color: '#333',
textAlign: 'center',
marginHorizontal: 10,
},
headerRight: {
width: 24,
},
scrollContainer: {
flex: 1,
},
imageContainer: {
height: 300,
backgroundColor: '#f9f9f9',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
},
productImage: {
width: '100%',
height: '100%',
},
imagePlaceholder: {
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
},
imagePlaceholderText: {
fontSize: 16,
color: '#999',
},
infoSection: {
padding: 15,
borderBottomWidth: 8,
borderBottomColor: '#f5f5f5',
},
productTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
},
productPrice: {
fontSize: 24,
fontWeight: 'bold',
color: '#ff6600',
marginBottom: 5,
},
productSales: {
fontSize: 14,
color: '#999',
marginBottom: 10,
},
keywordContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f0f8ff',
paddingVertical: 5,
paddingHorizontal: 10,
borderRadius: 5,
marginTop: 5,
},
keywordLabel: {
fontSize: 14,
color: '#666',
},
keywordValue: {
fontSize: 14,
fontWeight: 'bold',
color: '#0066ff',
},
section: {
padding: 15,
borderBottomWidth: 8,
borderBottomColor: '#f5f5f5',
},
sectionTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
},
descriptionText: {
fontSize: 14,
color: '#666',
lineHeight: 20,
},
specRow: {
flexDirection: 'row',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
specKey: {
flex: 1,
fontSize: 14,
color: '#666',
},
specValue: {
flex: 2,
fontSize: 14,
color: '#333',
},
backToSearchButton: {
alignItems: 'center',
paddingVertical: 15,
margin: 15,
},
backToSearchText: {
fontSize: 14,
color: '#0066ff',
},
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,
},
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,
},
paginationContainer: {
position: 'absolute',
bottom: 15,
flexDirection: 'row',
width: '100%',
justifyContent: 'center',
alignItems: 'center',
},
paginationDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: 'rgba(0, 0, 0, 0.2)',
marginHorizontal: 4,
},
paginationDotActive: {
backgroundColor: '#ff6600',
width: 10,
height: 10,
borderRadius: 5,
},
});