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.
333 lines
8.8 KiB
333 lines
8.8 KiB
import React, { useState, useEffect, useCallback } from 'react'; |
|
import { |
|
View, |
|
Text, |
|
StyleSheet, |
|
FlatList, |
|
Image, |
|
TouchableOpacity, |
|
TextInput, |
|
SafeAreaView, |
|
StatusBar, |
|
ActivityIndicator |
|
} 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, ProductParams, type Product } from '../services/api/productApi'; |
|
|
|
// 图标组件 - 使用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 SearchResultRouteParams = { |
|
keyword: string; |
|
}; |
|
|
|
// 产品项组件 - 使用React.memo优化渲染 |
|
const ProductItem = React.memo(({ |
|
product, |
|
onPress |
|
}: { |
|
product: Product; |
|
onPress: (product: Product) => void; |
|
}) => ( |
|
<TouchableOpacity |
|
style={styles.productCard} |
|
onPress={() => onPress(product)} |
|
activeOpacity={0.7} |
|
> |
|
<View style={styles.productImageContainer}> |
|
{product.product_image_urls[0] ? ( |
|
<Image source={{ uri: product.product_image_urls[0] }} style={styles.productImage} resizeMode="cover" /> |
|
) : ( |
|
<Text style={styles.placeholderText}>product picture</Text> |
|
)} |
|
</View> |
|
<View style={styles.productInfo}> |
|
<Text style={styles.productPrice}> |
|
{product.subject}{product.min_price.toFixed(2)} |
|
</Text> |
|
<Text style={styles.productTitle} numberOfLines={2}> |
|
{product.subject_trans} |
|
</Text> |
|
<Text style={styles.productSales}>Monthly Sales: {product.min_price}</Text> |
|
</View> |
|
</TouchableOpacity> |
|
)); |
|
|
|
export const SearchResultScreen = () => { |
|
const navigation = useNavigation<NativeStackNavigationProp<any>>(); |
|
const route = useRoute<RouteProp<Record<string, SearchResultRouteParams>, string>>(); |
|
const [searchText, setSearchText] = useState(''); |
|
const [products, setProducts] = useState<Product[]>([]); |
|
const [loading, setLoading] = useState(true); |
|
|
|
// 初始化搜索关键字 |
|
useEffect(() => { |
|
if (route.params?.keyword) { |
|
setSearchText(route.params.keyword); |
|
searchProducts(route.params.keyword); |
|
} |
|
}, [route.params?.keyword]); |
|
|
|
// 模拟搜索产品的API调用 |
|
const searchProducts = useCallback(async (keyword: string) => { |
|
setLoading(true); |
|
try { |
|
// 这里实际项目中应替换为真实的API调用 |
|
// await apiService.search(keyword) |
|
const res = await productApi.getSearchProducts({ |
|
keyword, |
|
page: 1, |
|
page_size: 20, |
|
sort_order: 'desc' |
|
}) |
|
|
|
setProducts(res.products); |
|
} catch (error) { |
|
console.error('Error searching products:', error); |
|
// 在实际应用中应该显示错误消息 |
|
} finally { |
|
setLoading(false); |
|
} |
|
}, []); |
|
|
|
// 处理搜索提交 |
|
const handleSearch = useCallback(() => { |
|
if (searchText.trim()) { |
|
searchProducts(searchText.trim()); |
|
} |
|
}, [searchText, searchProducts]); |
|
|
|
// 处理点击产品 |
|
const handleProductPress = useCallback((product: Product) => { |
|
// 导航到产品详情页,并传递产品信息 |
|
navigation.navigate('ProductDetail', { |
|
offer_id: product.offer_id, |
|
searchKeyword: searchText, |
|
price: product.min_price |
|
}); |
|
}, [navigation, searchText]); |
|
|
|
// 返回上一页 |
|
const goBack = useCallback(() => { |
|
navigation.goBack(); |
|
}, [navigation]); |
|
|
|
// 渲染列表为空时的组件 |
|
const renderEmptyList = useCallback(() => ( |
|
<View style={styles.emptyContainer}> |
|
<IconComponent name="search-outline" size={48} color="#ccc" /> |
|
<Text style={styles.emptyText}>No results found for "{searchText}"</Text> |
|
<Text style={styles.emptySubtext}>Try using different keywords or check your spelling</Text> |
|
</View> |
|
), [searchText]); |
|
|
|
// 渲染产品项 |
|
const renderProductItem = useCallback(({ item }: { item: Product }) => ( |
|
<ProductItem product={item} onPress={handleProductPress} /> |
|
), [handleProductPress]); |
|
|
|
return ( |
|
<SafeAreaView style={styles.safeArea}> |
|
<StatusBar barStyle="dark-content" backgroundColor="#fff" /> |
|
<View style={styles.container}> |
|
{/* 搜索栏 */} |
|
<View style={styles.searchHeader}> |
|
<TouchableOpacity style={styles.backButton} onPress={goBack}> |
|
<IconComponent name="arrow-back" size={24} color="#333" /> |
|
</TouchableOpacity> |
|
<View style={styles.searchBar}> |
|
<IconComponent name="search-outline" size={20} color="#999" /> |
|
<TextInput |
|
style={styles.searchInput} |
|
placeholder="搜索商品" |
|
placeholderTextColor="#999" |
|
value={searchText} |
|
onChangeText={setSearchText} |
|
returnKeyType="search" |
|
onSubmitEditing={handleSearch} |
|
/> |
|
{searchText.length > 0 && ( |
|
<TouchableOpacity onPress={() => setSearchText('')}> |
|
<IconComponent name="close-circle" size={20} color="#999" /> |
|
</TouchableOpacity> |
|
)} |
|
</View> |
|
</View> |
|
|
|
{/* 搜索结果 */} |
|
<View style={styles.resultsContainer}> |
|
{/* 搜索结果标题栏 */} |
|
<View style={styles.resultsHeader}> |
|
<Text style={styles.resultsTitle}> |
|
{loading ? 'Searching...' : `Results for "${searchText}"`} |
|
</Text> |
|
<Text style={styles.resultsCount}> |
|
{loading ? '' : `${products.length} items found`} |
|
</Text> |
|
</View> |
|
|
|
{/* 加载指示器 */} |
|
{loading ? ( |
|
<View style={styles.loadingContainer}> |
|
<ActivityIndicator size="large" color="#0066FF" /> |
|
</View> |
|
) : ( |
|
<FlatList |
|
data={products} |
|
renderItem={renderProductItem} |
|
keyExtractor={(item) => String(item.offer_id)} |
|
numColumns={2} |
|
contentContainerStyle={styles.productGrid} |
|
ListEmptyComponent={renderEmptyList} |
|
showsVerticalScrollIndicator={false} |
|
initialNumToRender={8} |
|
maxToRenderPerBatch={10} |
|
windowSize={5} |
|
removeClippedSubviews={true} |
|
/> |
|
)} |
|
</View> |
|
</View> |
|
</SafeAreaView> |
|
); |
|
}; |
|
|
|
const styles = StyleSheet.create({ |
|
safeArea: { |
|
flex: 1, |
|
backgroundColor: '#ffffff', |
|
}, |
|
container: { |
|
flex: 1, |
|
backgroundColor: '#f5f5f5', |
|
}, |
|
searchHeader: { |
|
flexDirection: 'row', |
|
alignItems: 'center', |
|
paddingHorizontal: 15, |
|
paddingVertical: 10, |
|
backgroundColor: '#fff', |
|
borderBottomWidth: 1, |
|
borderBottomColor: '#f0f0f0', |
|
}, |
|
backButton: { |
|
marginRight: 10, |
|
padding: 5, |
|
}, |
|
searchBar: { |
|
flex: 1, |
|
flexDirection: 'row', |
|
alignItems: 'center', |
|
backgroundColor: '#f5f5f5', |
|
borderRadius: 20, |
|
paddingHorizontal: 15, |
|
height: 40, |
|
}, |
|
searchInput: { |
|
flex: 1, |
|
marginLeft: 8, |
|
fontSize: 16, |
|
color: '#333', |
|
height: 40, |
|
}, |
|
resultsContainer: { |
|
flex: 1, |
|
}, |
|
resultsHeader: { |
|
flexDirection: 'row', |
|
justifyContent: 'space-between', |
|
alignItems: 'center', |
|
padding: 15, |
|
backgroundColor: '#fff', |
|
borderBottomWidth: 1, |
|
borderBottomColor: '#f0f0f0', |
|
}, |
|
resultsTitle: { |
|
fontSize: 16, |
|
fontWeight: 'bold', |
|
color: '#333', |
|
}, |
|
resultsCount: { |
|
fontSize: 14, |
|
color: '#999', |
|
}, |
|
loadingContainer: { |
|
flex: 1, |
|
justifyContent: 'center', |
|
alignItems: 'center', |
|
}, |
|
productGrid: { |
|
padding: 8, |
|
}, |
|
productCard: { |
|
flex: 1, |
|
margin: 8, |
|
backgroundColor: '#fff', |
|
borderRadius: 8, |
|
overflow: 'hidden', |
|
elevation: 2, // Android shadow |
|
shadowColor: '#000', // iOS shadow |
|
shadowOffset: { width: 0, height: 1 }, |
|
shadowOpacity: 0.1, |
|
shadowRadius: 2, |
|
}, |
|
productImageContainer: { |
|
height: 150, |
|
backgroundColor: '#f9f9f9', |
|
alignItems: 'center', |
|
justifyContent: 'center', |
|
}, |
|
productImage: { |
|
width: '100%', |
|
height: '100%', |
|
}, |
|
placeholderText: { |
|
color: '#999', |
|
fontSize: 16, |
|
}, |
|
productInfo: { |
|
padding: 10, |
|
}, |
|
productPrice: { |
|
fontSize: 18, |
|
fontWeight: 'bold', |
|
color: '#ff6600', |
|
marginBottom: 5, |
|
}, |
|
productTitle: { |
|
fontSize: 14, |
|
color: '#333', |
|
marginBottom: 5, |
|
}, |
|
productSales: { |
|
fontSize: 12, |
|
color: '#999', |
|
}, |
|
emptyContainer: { |
|
flex: 1, |
|
minHeight: 300, |
|
justifyContent: 'center', |
|
alignItems: 'center', |
|
padding: 20, |
|
}, |
|
emptyText: { |
|
fontSize: 16, |
|
fontWeight: 'bold', |
|
color: '#333', |
|
marginTop: 15, |
|
marginBottom: 8, |
|
}, |
|
emptySubtext: { |
|
fontSize: 14, |
|
color: '#999', |
|
textAlign: 'center', |
|
}, |
|
});
|