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

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