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