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.
362 lines
11 KiB
362 lines
11 KiB
2 months ago
|
import React, { useState, useEffect, useCallback } from 'react';
|
||
|
import {
|
||
|
View,
|
||
|
Text,
|
||
|
StyleSheet,
|
||
|
TextInput,
|
||
|
TouchableOpacity,
|
||
|
FlatList,
|
||
|
Keyboard,
|
||
|
Platform,
|
||
|
SafeAreaView,
|
||
|
StatusBar
|
||
|
} from 'react-native';
|
||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||
|
import Ionicons from '@expo/vector-icons/Ionicons';
|
||
|
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||
|
|
||
|
// 图标组件 - 使用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} />;
|
||
|
});
|
||
|
|
||
|
// 搜索历史存储键
|
||
|
const SEARCH_HISTORY_KEY = 'search_history';
|
||
|
|
||
|
// 历史记录项组件 - 使用React.memo优化渲染
|
||
|
const HistoryItem = React.memo(({
|
||
|
item,
|
||
|
onPress,
|
||
|
onRemove
|
||
|
}: {
|
||
|
item: string;
|
||
|
onPress: (item: string) => void;
|
||
|
onRemove: (item: string) => void
|
||
|
}) => (
|
||
|
<View style={styles.historyItemContainer}>
|
||
|
<TouchableOpacity
|
||
|
style={styles.historyItem}
|
||
|
onPress={() => onPress(item)}
|
||
|
>
|
||
|
<IconComponent name="time-outline" size={18} color="#999" />
|
||
|
<Text style={styles.historyItemText}>{item}</Text>
|
||
|
</TouchableOpacity>
|
||
|
<TouchableOpacity onPress={() => onRemove(item)}>
|
||
|
<IconComponent name="close" size={18} color="#999" />
|
||
|
</TouchableOpacity>
|
||
|
</View>
|
||
|
));
|
||
|
|
||
|
// 热门标签组件 - 使用React.memo优化渲染
|
||
|
const HotTagItem = React.memo(({
|
||
|
tag,
|
||
|
onPress
|
||
|
}: {
|
||
|
tag: string;
|
||
|
onPress: (tag: string) => void
|
||
|
}) => (
|
||
|
<TouchableOpacity
|
||
|
style={styles.hotSearchTag}
|
||
|
onPress={() => onPress(tag)}
|
||
|
>
|
||
|
<Text style={styles.hotSearchTagText}>{tag}</Text>
|
||
|
</TouchableOpacity>
|
||
|
));
|
||
|
|
||
|
export const SearchScreen = () => {
|
||
|
const [searchText, setSearchText] = useState('');
|
||
|
const [searchHistory, setSearchHistory] = useState<string[]>([]);
|
||
|
const [isLoading, setIsLoading] = useState(true);
|
||
|
const navigation = useNavigation<NativeStackNavigationProp<any>>();
|
||
|
|
||
|
// 预设热门搜索标签
|
||
|
const hotSearchTags = ['手机', '耳机', '电脑', '平板', '手表', '相机', '家电', '食品'];
|
||
|
|
||
|
// 只在页面聚焦时加载历史记录,而不是每次组件挂载
|
||
|
useFocusEffect(
|
||
|
useCallback(() => {
|
||
|
loadSearchHistory();
|
||
|
}, [])
|
||
|
);
|
||
|
|
||
|
// 从AsyncStorage加载搜索历史 - 优化异步操作
|
||
|
const loadSearchHistory = async () => {
|
||
|
try {
|
||
|
setIsLoading(true);
|
||
|
const historyJson = await AsyncStorage.getItem(SEARCH_HISTORY_KEY);
|
||
|
if (historyJson) {
|
||
|
const history = JSON.parse(historyJson);
|
||
|
setSearchHistory(history);
|
||
|
}
|
||
|
} catch (error) {
|
||
|
console.error('Failed to load search history:', error);
|
||
|
} finally {
|
||
|
setIsLoading(false);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// 保存搜索历史 - 使用useCallback优化函数引用
|
||
|
const saveSearchHistory = useCallback(async (searchTerm: string) => {
|
||
|
try {
|
||
|
// 如果搜索词为空,不保存
|
||
|
if (!searchTerm.trim()) return;
|
||
|
|
||
|
// 创建新的历史记录,将新搜索词放在最前面
|
||
|
const newHistory = [searchTerm, ...searchHistory.filter(item => item !== searchTerm)];
|
||
|
|
||
|
// 只保留最近10条
|
||
|
const trimmedHistory = newHistory.length > 10 ? newHistory.slice(0, 10) : newHistory;
|
||
|
|
||
|
// 先更新UI状态,再进行异步存储操作
|
||
|
setSearchHistory(trimmedHistory);
|
||
|
|
||
|
// 异步存储操作不阻塞UI
|
||
|
AsyncStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(trimmedHistory))
|
||
|
.catch(error => console.error('Failed to save search history:', error));
|
||
|
} catch (error) {
|
||
|
console.error('Failed to process search history:', error);
|
||
|
}
|
||
|
}, [searchHistory]);
|
||
|
|
||
|
// 清除指定的搜索历史 - 使用useCallback优化函数引用
|
||
|
const removeSearchHistoryItem = useCallback(async (searchTerm: string) => {
|
||
|
try {
|
||
|
const newHistory = searchHistory.filter(item => item !== searchTerm);
|
||
|
|
||
|
// 先更新UI状态,再进行异步存储操作
|
||
|
setSearchHistory(newHistory);
|
||
|
|
||
|
// 异步存储操作不阻塞UI
|
||
|
AsyncStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(newHistory))
|
||
|
.catch(error => console.error('Failed to remove search history item:', error));
|
||
|
} catch (error) {
|
||
|
console.error('Failed to remove search history item:', error);
|
||
|
}
|
||
|
}, [searchHistory]);
|
||
|
|
||
|
// 清除所有搜索历史 - 使用useCallback优化函数引用
|
||
|
const clearAllSearchHistory = useCallback(async () => {
|
||
|
try {
|
||
|
// 先更新UI状态,再进行异步存储操作
|
||
|
setSearchHistory([]);
|
||
|
|
||
|
// 异步存储操作不阻塞UI
|
||
|
AsyncStorage.removeItem(SEARCH_HISTORY_KEY)
|
||
|
.catch(error => console.error('Failed to clear search history:', error));
|
||
|
} catch (error) {
|
||
|
console.error('Failed to clear search history:', error);
|
||
|
}
|
||
|
}, []);
|
||
|
|
||
|
// 处理搜索提交 - 使用useCallback优化函数引用
|
||
|
const handleSearch = useCallback(() => {
|
||
|
if (searchText.trim()) {
|
||
|
saveSearchHistory(searchText.trim());
|
||
|
Keyboard.dismiss();
|
||
|
|
||
|
// 导航到搜索结果页面,并传递搜索关键词
|
||
|
navigation.navigate('SearchResult', { keyword: searchText.trim() });
|
||
|
}
|
||
|
}, [searchText, saveSearchHistory, navigation]);
|
||
|
|
||
|
// 点击历史记录项 - 使用useCallback优化函数引用
|
||
|
const handleHistoryItemPress = useCallback((item: string) => {
|
||
|
setSearchText(item);
|
||
|
saveSearchHistory(item);
|
||
|
|
||
|
// 导航到搜索结果页面,并传递搜索关键词
|
||
|
navigation.navigate('SearchResult', { keyword: item });
|
||
|
}, [saveSearchHistory, navigation]);
|
||
|
|
||
|
// 渲染历史记录项 - 使用useCallback优化函数引用
|
||
|
const renderHistoryItem = useCallback(({ item }: { item: string }) => (
|
||
|
<HistoryItem
|
||
|
item={item}
|
||
|
onPress={handleHistoryItemPress}
|
||
|
onRemove={removeSearchHistoryItem}
|
||
|
/>
|
||
|
), [handleHistoryItemPress, removeSearchHistoryItem]);
|
||
|
|
||
|
// 处理热门标签点击 - 使用useCallback优化函数引用
|
||
|
const handleHotTagPress = useCallback((tag: string) => {
|
||
|
setSearchText(tag);
|
||
|
saveSearchHistory(tag);
|
||
|
|
||
|
// 导航到搜索结果页面,并传递搜索关键词
|
||
|
navigation.navigate('SearchResult', { keyword: tag });
|
||
|
}, [saveSearchHistory, navigation]);
|
||
|
|
||
|
return (
|
||
|
<SafeAreaView style={styles.safeArea}>
|
||
|
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
||
|
<View style={styles.container}>
|
||
|
{/* 搜索栏 */}
|
||
|
<View style={styles.searchHeader}>
|
||
|
<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}
|
||
|
autoFocus={true}
|
||
|
/>
|
||
|
{searchText.length > 0 && (
|
||
|
<TouchableOpacity onPress={() => setSearchText('')}>
|
||
|
<IconComponent name="close-circle" size={20} color="#999" />
|
||
|
</TouchableOpacity>
|
||
|
)}
|
||
|
</View>
|
||
|
<TouchableOpacity style={styles.cancelButton} onPress={() => navigation.goBack()}>
|
||
|
<Text style={styles.cancelButtonText}>取消</Text>
|
||
|
</TouchableOpacity>
|
||
|
</View>
|
||
|
|
||
|
{/* 历史搜索记录 */}
|
||
|
{!isLoading && searchHistory.length > 0 && (
|
||
|
<View style={styles.historyContainer}>
|
||
|
<View style={styles.historyHeader}>
|
||
|
<Text style={styles.historyTitle}>历史搜索</Text>
|
||
|
<TouchableOpacity onPress={clearAllSearchHistory}>
|
||
|
<IconComponent name="trash-outline" size={20} color="#999" />
|
||
|
</TouchableOpacity>
|
||
|
</View>
|
||
|
<FlatList
|
||
|
data={searchHistory}
|
||
|
keyExtractor={(item, index) => `history-${index}`}
|
||
|
renderItem={renderHistoryItem}
|
||
|
style={styles.historyList}
|
||
|
keyboardShouldPersistTaps="handled"
|
||
|
initialNumToRender={5}
|
||
|
maxToRenderPerBatch={10}
|
||
|
removeClippedSubviews={true}
|
||
|
windowSize={5}
|
||
|
/>
|
||
|
</View>
|
||
|
)}
|
||
|
|
||
|
{/* 热门搜索 */}
|
||
|
<View style={styles.hotSearchContainer}>
|
||
|
<Text style={styles.hotSearchTitle}>热门搜索</Text>
|
||
|
<View style={styles.hotSearchTags}>
|
||
|
{hotSearchTags.map((tag, index) => (
|
||
|
<HotTagItem
|
||
|
key={`hot-${index}`}
|
||
|
tag={tag}
|
||
|
onPress={handleHotTagPress}
|
||
|
/>
|
||
|
))}
|
||
|
</View>
|
||
|
</View>
|
||
|
</View>
|
||
|
</SafeAreaView>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
const styles = StyleSheet.create({
|
||
|
safeArea: {
|
||
|
flex: 1,
|
||
|
backgroundColor: '#ffffff',
|
||
|
},
|
||
|
container: {
|
||
|
flex: 1,
|
||
|
backgroundColor: '#ffffff',
|
||
|
},
|
||
|
searchHeader: {
|
||
|
flexDirection: 'row',
|
||
|
alignItems: 'center',
|
||
|
paddingHorizontal: 15,
|
||
|
paddingVertical: 10,
|
||
|
borderBottomWidth: 1,
|
||
|
borderBottomColor: '#f0f0f0',
|
||
|
},
|
||
|
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,
|
||
|
},
|
||
|
cancelButton: {
|
||
|
marginLeft: 10,
|
||
|
padding: 5,
|
||
|
},
|
||
|
cancelButtonText: {
|
||
|
fontSize: 16,
|
||
|
color: '#333',
|
||
|
},
|
||
|
historyContainer: {
|
||
|
padding: 15,
|
||
|
},
|
||
|
historyHeader: {
|
||
|
flexDirection: 'row',
|
||
|
justifyContent: 'space-between',
|
||
|
alignItems: 'center',
|
||
|
marginBottom: 10,
|
||
|
},
|
||
|
historyTitle: {
|
||
|
fontSize: 16,
|
||
|
fontWeight: 'bold',
|
||
|
color: '#333',
|
||
|
},
|
||
|
historyList: {
|
||
|
marginTop: 5,
|
||
|
},
|
||
|
historyItemContainer: {
|
||
|
flexDirection: 'row',
|
||
|
justifyContent: 'space-between',
|
||
|
alignItems: 'center',
|
||
|
paddingVertical: 10,
|
||
|
borderBottomWidth: 1,
|
||
|
borderBottomColor: '#f0f0f0',
|
||
|
},
|
||
|
historyItem: {
|
||
|
flex: 1,
|
||
|
flexDirection: 'row',
|
||
|
alignItems: 'center',
|
||
|
},
|
||
|
historyItemText: {
|
||
|
fontSize: 14,
|
||
|
color: '#333',
|
||
|
marginLeft: 10,
|
||
|
},
|
||
|
hotSearchContainer: {
|
||
|
padding: 15,
|
||
|
},
|
||
|
hotSearchTitle: {
|
||
|
fontSize: 16,
|
||
|
fontWeight: 'bold',
|
||
|
color: '#333',
|
||
|
marginBottom: 10,
|
||
|
},
|
||
|
hotSearchTags: {
|
||
|
flexDirection: 'row',
|
||
|
flexWrap: 'wrap',
|
||
|
},
|
||
|
hotSearchTag: {
|
||
|
backgroundColor: '#f5f5f5',
|
||
|
paddingHorizontal: 12,
|
||
|
paddingVertical: 6,
|
||
|
borderRadius: 15,
|
||
|
marginRight: 10,
|
||
|
marginBottom: 10,
|
||
|
},
|
||
|
hotSearchTagText: {
|
||
|
fontSize: 14,
|
||
|
color: '#333',
|
||
|
},
|
||
|
});
|