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