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
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, |
|
}, |
|
});
|