Browse Source

图搜

main
Mac 3 weeks ago
parent
commit
57c45f77e1
  1. 15
      app/locales/en/translation.json
  2. 16
      app/navigation/AppNavigator.tsx
  3. 2
      app/navigation/screens.ts
  4. 3
      app/navigation/types.ts
  5. 70
      app/screens/ChatScreen.tsx
  6. 276
      app/screens/HomeScreen.tsx
  7. 1145
      app/screens/ImageSearchResultScreen.tsx
  8. 48
      app/screens/SearchResultScreen.tsx
  9. 11
      app/screens/SearchScreen.tsx
  10. 2
      app/screens/productStatus/Status.tsx
  11. 75
      app/services/api/chat.ts
  12. 12
      app/services/api/productApi.ts
  13. 42
      package-lock.json
  14. 16
      yarn.lock

15
app/locales/en/translation.json

@ -98,10 +98,10 @@
"cart":"Cart",
"categories": "Categories",
"chat": "Chat",
"customerServiceChat": "Customer Service Chat",
"productChat": "Product Chat",
"notificationChat": "Notification Chat",
"inputMessage": "Input message",
"customerServiceChat": "Customer Service",
"productChat": "Product Support",
"notificationChat": "Notifications",
"inputMessage": "Type a message...",
"send": "Send",
"searchProducts": "Search products",
"recommendations": "Recommendations",
@ -316,5 +316,10 @@
"address_management": "Address Management",
"set_default": "Set Default Address",
"delete": "Delete"
}
},
"typingMessage": "Typing",
"defaultResponse": "Thank you for your message. Our team will get back to you shortly.",
"errorResponse": "Sorry, there was an error processing your request. Please try again later.",
"loginRequired": "Login Required",
"pleaseLoginToChat": "Please login to continue with the chat service."
}

16
app/navigation/AppNavigator.tsx

@ -47,12 +47,9 @@ export const AppNavigator = () => {
name="Search"
component={Screens.SearchScreen}
options={{
presentation: "fullScreenModal",
animation: "fade",
animationDuration: 200,
animation: "slide_from_right",
gestureEnabled: true,
gestureDirection: "vertical",
contentStyle: { backgroundColor: "#ffffff" },
gestureDirection: "horizontal",
}}
/>
<Stack.Screen
@ -64,6 +61,15 @@ export const AppNavigator = () => {
gestureDirection: "horizontal",
}}
/>
<Stack.Screen
name="ImageSearchResultScreen"
component={Screens.ImageSearchResultScreen}
options={{
animation: "slide_from_right",
gestureEnabled: true,
gestureDirection: "horizontal",
}}
/>
<Stack.Screen
name="ProductDetail"
component={Screens.ProductDetailScreen}

2
app/navigation/screens.ts

@ -5,6 +5,7 @@ import { EmailLoginScreen } from "../screens/EmailLoginScreen";
import { TabNavigator } from "./TabNavigator";
import { SearchScreen } from "../screens/SearchScreen";
import { SearchResultScreen } from "../screens/SearchResultScreen";
import { ImageSearchResultScreen } from "../screens/ImageSearchResultScreen";
import { ProductDetailScreen } from "../screens/ProductDetailScreen";
import { BalanceScreen } from "../screens/BalanceScreen";
import { ShippingDetailsSection } from "../screens/banner/ShippingDetailsSection";
@ -45,6 +46,7 @@ export {
TabNavigator,
SearchScreen,
SearchResultScreen,
ImageSearchResultScreen,
ProductDetailScreen,
BalanceScreen,
ShippingDetailsSection,

3
app/navigation/types.ts

@ -7,7 +7,8 @@ export type RootStackParamList = {
EmailLogin: undefined;
MainTabs: undefined;
Search: undefined;
SearchResult: { keyword: string };
SearchResult: { keyword: string, formData?: FormData };
ImageSearchResultScreen: { image?: string, type?: number };
ProductDetail: { productId: string; searchKeyword?: string };
Balance: undefined;
ShippingDetailsSection: undefined;

70
app/screens/ChatScreen.tsx

@ -150,9 +150,77 @@ export const ChatScreen = () => {
text: newMessage.text
};
chatService.sendMessage(chatServiceMessage);
// Add user message to the chat UI
setMessages([...messages, newMessage]);
setInputText("");
// Add simulated response with loading indicator
const simulatedId = `simulated-${Date.now()}`;
const simulatedResponse: Message = {
mimetype: "text/plain",
userWs: "system",
app_id: "system",
country: country,
body: "",
text: `${t('typingMessage')}...`,
type: "chat",
isMe: false,
timestamp: new Date(),
id: simulatedId,
};
// Add simulated message after a short delay to make it feel more natural
setTimeout(() => {
setMessages(prevMessages => [...prevMessages, simulatedResponse]);
}, 800);
// Send actual message to API
chatService.sendMessage(chatServiceMessage)
.then(response => {
// When real response arrives, replace simulated message
setMessages(prevMessages => {
// Filter out the simulated message and add real response
const filtered = prevMessages.filter(msg => msg.id !== simulatedId);
// Create the real response message object
const realResponse: Message = {
mimetype: "text/plain",
userWs: "system",
app_id: "system",
country: country,
body: "",
text: response?.text || t('defaultResponse'),
type: "chat",
isMe: false,
timestamp: new Date(),
id: `real-${Date.now()}`,
};
return [...filtered, realResponse];
});
})
.catch(error => {
// In case of error, replace simulated message with error message
console.error('Chat API error:', error);
setMessages(prevMessages => {
const filtered = prevMessages.filter(msg => msg.id !== simulatedId);
const errorResponse: Message = {
mimetype: "text/plain",
userWs: "system",
app_id: "system",
country: country,
body: "",
text: t('errorResponse'),
type: "chat",
isMe: false,
timestamp: new Date(),
id: `error-${Date.now()}`,
};
return [...filtered, errorResponse];
});
});
};
// Generate a unique key for each message

276
app/screens/HomeScreen.tsx

@ -39,8 +39,8 @@ import CloseIcon from "../components/CloseIcon";
import CheckmarkIcon from "../components/CheckmarkIcon";
import { getSubjectTransLanguage } from "../utils/languageUtils";
import useUserStore from "../store/user";
import * as ImagePicker from 'expo-image-picker';
import * as FileSystem from 'expo-file-system';
import * as ImagePicker from "expo-image-picker";
import * as FileSystem from "expo-file-system";
// 为图标定义类型
type IconProps = {
name: string;
@ -88,16 +88,28 @@ const LazyImage = React.memo(
<View style={[style, { overflow: "hidden" }]}>
{/* Show placeholder while image is loading */}
{!isLoaded && !hasError && (
<View style={[style, styles.imagePlaceholder, { position: 'absolute', zIndex: 1 }]} />
<View
style={[
style,
styles.imagePlaceholder,
{ position: "absolute", zIndex: 1 },
]}
/>
)}
{/* Show error state if image failed to load */}
{hasError && (
<View
style={[style, styles.imagePlaceholder, { position: 'absolute', zIndex: 1 }]}
style={[
style,
styles.imagePlaceholder,
{ position: "absolute", zIndex: 1 },
]}
>
<IconComponent name="image-outline" size={24} color="#999" />
<Text style={{ fontSize: fontSize(12), color: "#999", marginTop: 4 }}>
<Text
style={{ fontSize: fontSize(12), color: "#999", marginTop: 4 }}
>
</Text>
</View>
@ -120,7 +132,7 @@ const LazyImage = React.memo(
const ProductSkeleton = React.memo(() => {
// 创建动画值
const shimmerAnim = useRef(new Animated.Value(0)).current;
// 设置动画效果
useEffect(() => {
const shimmerAnimation = Animated.loop(
@ -130,14 +142,14 @@ const ProductSkeleton = React.memo(() => {
useNativeDriver: true,
})
);
shimmerAnimation.start();
return () => {
shimmerAnimation.stop();
};
}, []);
// 定义动画插值
const shimmerTranslate = shimmerAnim.interpolate({
inputRange: [0, 1],
@ -167,7 +179,7 @@ const ProductSkeleton = React.memo(() => {
]}
/>
</View>
<View style={[styles.skeletonTitle, { width: '60%' }]}>
<View style={[styles.skeletonTitle, { width: "60%" }]}>
<Animated.View
style={[
styles.shimmer,
@ -470,7 +482,7 @@ export const HomeScreen = () => {
<IconComponent name="image-outline" size={24} color="#999" />
</View>
)}
{userStore.user?.user_id && (
<View style={styles.vipButtonContainer}>
<TouchableOpacity style={styles.vipButton}>
@ -514,12 +526,12 @@ export const HomeScreen = () => {
</View>
</TouchableOpacity>
);
// 渲染骨架屏网格
const renderSkeletonGrid = useCallback(() => {
// 创建骨架屏数组
const skeletonArray = Array(8).fill(null);
return (
<View style={styles.skeletonContainer}>
<FlatList
@ -529,7 +541,7 @@ export const HomeScreen = () => {
numColumns={2}
columnWrapperStyle={styles.productCardGroup}
scrollEnabled={false}
contentContainerStyle={{paddingBottom: 15}}
contentContainerStyle={{ paddingBottom: 15 }}
/>
</View>
);
@ -538,33 +550,78 @@ export const HomeScreen = () => {
// 清理expo-image-picker临时文件
const cleanupImagePickerCache = async () => {
try {
// Skip cache cleanup on web platform
if (Platform.OS === 'web') {
console.log('Cache cleanup skipped on web platform');
setGalleryUsed(false);
return;
}
// 相册选择后清理临时缓存
const cacheDir = `${FileSystem.cacheDirectory}ImagePicker`;
await FileSystem.deleteAsync(cacheDir, { idempotent: true });
console.log('已清理ImagePicker缓存');
console.log("已清理ImagePicker缓存");
// 立即重置状态,无需用户干预
setGalleryUsed(false);
} catch (error) {
console.log('清理缓存错误', error);
console.log("清理缓存错误", error);
// Even if cleanup fails, reset the state
setGalleryUsed(false);
}
};
// 将图片URI转换为FormData
const uriToFormData = async (uri: string) => {
try {
// 创建FormData对象
const formData = new FormData();
// 获取文件名
const filename = uri.split("/").pop() || "image.jpg";
// 判断文件类型(mime type)
const match = /\.(\w+)$/.exec(filename);
const type = match ? `image/${match[1]}` : "image/jpeg";
// 处理iOS路径前缀
const imageUri = Platform.OS === "ios" ? uri.replace("file://", "") : uri;
// 将图片转换为Blob
const imageFetchResponse = await fetch(imageUri);
const imageBlob = await imageFetchResponse.blob();
// 添加图片到FormData
formData.append("image", imageBlob, filename);
console.log("FormData 详情:");
console.log("- 图片URI:", uri);
console.log("- 文件名:", filename);
console.log("- 文件类型:", type);
return formData;
} catch (error) {
console.error("创建FormData错误:", error);
throw error;
}
};
// 处理从相册选择
const handleChooseFromGallery = useCallback(async () => {
console.log('handleChooseFromGallery');
console.log("handleChooseFromGallery");
setShowImagePickerModal(false);
// 等待模态窗关闭后再执行
setTimeout(async () => {
try {
// 请求相册权限
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permissionResult.status !== 'granted') {
console.log('相册权限被拒绝');
const permissionResult =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permissionResult.status !== "granted") {
console.log("相册权限被拒绝");
return;
}
// 打开相册
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
@ -572,70 +629,75 @@ export const HomeScreen = () => {
aspect: [4, 3],
quality: 1,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
console.log('相册选择成功:', result.assets[0].uri);
// 这里可以添加后续处理代码,如图片上传等
// 相册选择完成后,立即清理缓存并重置状态
console.log("相册选择成功:", result.assets[0].uri);
await cleanupImagePickerCache();
navigation.navigate("ImageSearchResultScreen", {
image: result.assets[0].uri,
type: 1,
});
}
} catch (error: any) {
console.error('相册错误:', error);
console.error("相册错误:", error);
// 出错时也清理缓存
await cleanupImagePickerCache();
}
}, 500);
}, []);
}, [userStore.user]);
// 处理相机拍照 - 简化版本,不再需要处理galleryUsed
const handleTakePhoto = useCallback(async () => {
console.log('handleTakePhoto');
console.log("handleTakePhoto");
setShowImagePickerModal(false);
// 等待模态窗关闭后再执行
setTimeout(async () => {
try {
const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
if (permissionResult.status !== 'granted') {
console.log('相机权限被拒绝');
const permissionResult =
await ImagePicker.requestCameraPermissionsAsync();
if (permissionResult.status !== "granted") {
console.log("相机权限被拒绝");
return;
}
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
console.log(result);
if (!result.canceled && result.assets && result.assets.length > 0) {
console.log('拍照成功:', result.assets[0].uri);
// 这里可以添加后续处理代码,如图片上传等
console.log("拍照成功:", result.assets[0].uri);
// 使用后清理缓存
await cleanupImagePickerCache();
// 将图片URI转换为FormData
navigation.navigate("ImageSearchResultScreen", {
image: result.assets[0].uri,
type: 1,
});
}
// 使用后清理缓存
await cleanupImagePickerCache();
} catch (error: any) {
console.error('相机错误:', error);
console.error("相机错误:", error);
// 出错时也清理缓存
await cleanupImagePickerCache();
}
}, 500);
}, []);
}, [userStore.user]);
// 重置应用状态函数
const resetAppState = useCallback(() => {
// 重置标记
setGalleryUsed(false);
// 清理缓存
cleanupImagePickerCache();
// 提示用户
Alert.alert('已重置', '现在您可以使用相机功能了');
Alert.alert("已重置", "现在您可以使用相机功能了");
}, []);
// 渲染列表头部内容
@ -695,7 +757,7 @@ export const HomeScreen = () => {
>
<IconComponent name="search-outline" size={20} color="#999" />
<Text style={styles.searchPlaceholder}></Text>
<TouchableOpacity
<TouchableOpacity
style={styles.cameraButton}
onPress={() => setShowImagePickerModal(true)}
>
@ -815,7 +877,7 @@ export const HomeScreen = () => {
) : null}
</>
);
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
@ -855,7 +917,7 @@ export const HomeScreen = () => {
}
/>
)}
{/* Categories Modal */}
<Modal
visible={showCategoryModal}
@ -913,7 +975,7 @@ export const HomeScreen = () => {
</View>
</View>
</Modal>
{/* Image Picker Modal */}
<Modal
visible={showImagePickerModal}
@ -921,7 +983,7 @@ export const HomeScreen = () => {
transparent={true}
onRequestClose={() => setShowImagePickerModal(false)}
>
<TouchableOpacity
<TouchableOpacity
style={styles.imagePickerOverlay}
activeOpacity={1}
onPress={() => setShowImagePickerModal(false)}
@ -929,35 +991,43 @@ export const HomeScreen = () => {
<View style={styles.imagePickerContent}>
{!galleryUsed ? (
// 正常状态,显示相机选项
<TouchableOpacity
<TouchableOpacity
style={styles.imagePickerOption}
onPress={handleTakePhoto}
>
<IconComponent name="camera-outline" size={24} color="#333" />
<IconComponent
name="camera-outline"
size={24}
color="#333"
/>
<Text style={styles.imagePickerText}></Text>
</TouchableOpacity>
) : (
// 已使用相册状态,显示重置选项
<TouchableOpacity
<TouchableOpacity
style={styles.imagePickerOption}
onPress={resetAppState}
>
<IconComponent name="refresh-outline" size={24} color="#333" />
<IconComponent
name="refresh-outline"
size={24}
color="#333"
/>
<Text style={styles.imagePickerText}></Text>
</TouchableOpacity>
)}
<View style={styles.imagePickerDivider} />
<TouchableOpacity
<TouchableOpacity
style={styles.imagePickerOption}
onPress={handleChooseFromGallery}
>
<IconComponent name="images-outline" size={24} color="#333" />
<Text style={styles.imagePickerText}></Text>
</TouchableOpacity>
<TouchableOpacity
<TouchableOpacity
style={styles.imagePickerCancelButton}
onPress={() => setShowImagePickerModal(false)}
>
@ -974,15 +1044,15 @@ export const HomeScreen = () => {
const styles = StyleSheet.create<StylesType>({
safeArea: {
flex: 1,
backgroundColor: '#fff',
backgroundColor: "#fff",
},
safeAreaContent: {
flex: 1,
paddingTop: Platform.OS === 'android' ? 0 : 0,
paddingTop: Platform.OS === "android" ? 0 : 0,
},
container: {
flex: 1,
backgroundColor: '#fff',
backgroundColor: "#fff",
},
swpImg: {
width: "100%",
@ -1359,53 +1429,53 @@ const styles = StyleSheet.create<StylesType>({
paddingTop: 0,
},
skeletonImage: {
width: '100%',
paddingBottom: '100%',
width: "100%",
paddingBottom: "100%",
borderRadius: 10,
backgroundColor: '#e1e1e1',
overflow: 'hidden',
position: 'relative',
backgroundColor: "#e1e1e1",
overflow: "hidden",
position: "relative",
},
skeletonTitle: {
height: 16,
borderRadius: 4,
marginTop: 8,
marginBottom: 4,
width: '100%',
backgroundColor: '#e1e1e1',
overflow: 'hidden',
position: 'relative',
width: "100%",
backgroundColor: "#e1e1e1",
overflow: "hidden",
position: "relative",
},
skeletonPrice: {
height: 24,
width: 80,
borderRadius: 4,
marginTop: 8,
backgroundColor: '#e1e1e1',
overflow: 'hidden',
position: 'relative',
backgroundColor: "#e1e1e1",
overflow: "hidden",
position: "relative",
},
skeletonSales: {
height: 14,
width: '40%',
width: "40%",
borderRadius: 4,
marginTop: 8,
backgroundColor: '#e1e1e1',
overflow: 'hidden',
position: 'relative',
backgroundColor: "#e1e1e1",
overflow: "hidden",
position: "relative",
},
shimmer: {
width: '30%',
height: '100%',
backgroundColor: 'rgba(255, 255, 255, 0.3)',
position: 'absolute',
width: "30%",
height: "100%",
backgroundColor: "rgba(255, 255, 255, 0.3)",
position: "absolute",
top: 0,
left: 0,
},
imagePlaceholder: {
backgroundColor: '#EAEAEA',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: "#EAEAEA",
justifyContent: "center",
alignItems: "center",
borderRadius: 8,
},
productImage: {
@ -1416,40 +1486,40 @@ const styles = StyleSheet.create<StylesType>({
// Image Picker Modal Styles
imagePickerOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
backgroundColor: "rgba(0, 0, 0, 0.5)",
justifyContent: "flex-end",
},
imagePickerContent: {
backgroundColor: '#fff',
backgroundColor: "#fff",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingTop: 20,
},
imagePickerOption: {
flexDirection: 'row',
alignItems: 'center',
flexDirection: "row",
alignItems: "center",
paddingVertical: 16,
paddingHorizontal: 20,
},
imagePickerText: {
fontSize: fontSize(16),
marginLeft: 12,
color: '#333',
color: "#333",
},
imagePickerDivider: {
height: 1,
backgroundColor: '#f0f0f0',
backgroundColor: "#f0f0f0",
marginHorizontal: 20,
},
imagePickerCancelButton: {
alignItems: 'center',
alignItems: "center",
paddingVertical: 16,
marginTop: 8,
borderTopWidth: 1,
borderTopColor: '#f0f0f0',
borderTopColor: "#f0f0f0",
},
imagePickerCancelText: {
fontSize: fontSize(16),
color: '#999',
color: "#999",
},
});
});

1145
app/screens/ImageSearchResultScreen.tsx

File diff suppressed because it is too large Load Diff

48
app/screens/SearchResultScreen.tsx

@ -31,7 +31,6 @@ import widthUtils from "../utils/widthUtils";
import fontSize from "../utils/fontsizeUtils";
import useUserStore from "../store/user";
import { getSubjectTransLanguage } from "../utils/languageUtils";
// 图标组件 - 使用React.memo优化渲染
const IconComponent = React.memo(
({ name, size, color }: { name: string; size: number; color: string }) => {
@ -39,18 +38,15 @@ const IconComponent = React.memo(
return <Icon name={name} size={size} color={color} />;
}
);
// 路由参数类型
type SearchResultRouteParams = {
keyword: string;
};
// 组件Props类型
type SearchResultScreenProps = {
route: RouteProp<Record<string, SearchResultRouteParams>, string>;
navigation: NativeStackNavigationProp<any>;
};
// 懒加载图片组件 - 改进版本
const LazyImage = React.memo(
({
@ -64,16 +60,13 @@ const LazyImage = React.memo(
}) => {
const [isLoaded, setIsLoaded] = useState(false);
const [hasError, setHasError] = useState(false);
const onLoad = useCallback(() => {
setIsLoaded(true);
}, []);
const onError = useCallback(() => {
setHasError(true);
setIsLoaded(true); // Also mark as loaded on error to remove placeholder
}, []);
return (
<View style={[style, { overflow: "hidden" }]}>
{/* Show placeholder while image is loading */}
@ -92,7 +85,6 @@ const LazyImage = React.memo(
</Text>
</View>
)}
{/* Actual image */}
<Image
source={{ uri }}
@ -105,7 +97,6 @@ const LazyImage = React.memo(
);
}
);
// 产品骨架屏组件 - 用于加载状态
const ProductSkeleton = React.memo(() => (
<View style={styles.productCard}>
@ -118,7 +109,6 @@ const ProductSkeleton = React.memo(() => (
</View>
</View>
));
// 产品项组件 - 使用React.memo优化渲染
const ProductItem = React.memo(
({
@ -148,7 +138,6 @@ const ProductItem = React.memo(
) : (
<Text style={styles.placeholderText}>{t('productPicture')}</Text>
)}
{userStore.user?.user_id && (
<TouchableOpacity style={styles.vipIcon}>
<Text style={styles.vipButtonText}>VIP</Text>
@ -189,7 +178,6 @@ const ProductItem = React.memo(
</TouchableOpacity>
)
);
export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProps) => {
const { t } = useTranslation();
const [searchText, setSearchText] = useState("");
@ -208,7 +196,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
);
const userStore = useUserStore();
const [showSkeleton, setShowSkeleton] = useState(true);
const [searchParams, setSearchParams] = useState<ProductParams>({
keyword: route.params?.keyword || "",
page: 1,
@ -219,7 +206,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
language: "en",
user_id: userStore.user.user_id,
});
// 初始化搜索关键字
useEffect(() => {
if (route.params?.keyword) {
@ -233,7 +219,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
searchProducts(newParams);
}
}, [route.params?.keyword]);
// 搜索产品的API调用
const searchProducts = useCallback(
async (params: ProductParams, isLoadMore = false) => {
@ -277,7 +262,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
},
[]
);
// 处理搜索提交
const handleSearch = useCallback(() => {
if (searchText.trim()) {
@ -288,7 +272,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
setActiveTab("default");
// Show skeleton for new search
setShowSkeleton(true);
const newParams = {
...searchParams,
keyword: searchText.trim(),
@ -298,12 +281,10 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
searchProducts(newParams);
}
}, [searchText, searchParams, searchProducts]);
// 切换筛选器显示状态
const toggleFilter = useCallback(() => {
setIsFilterVisible(!isFilterVisible);
}, [isFilterVisible]);
// 处理点击产品
const handleProductPress = useCallback(
(product: Product) => {
@ -316,12 +297,10 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
},
[navigation, searchText]
);
// 返回上一页
const goBack = useCallback(() => {
navigation.goBack();
}, [navigation]);
// 渲染列表为空时的组件
const renderEmptyList = useCallback(
() => (
@ -335,7 +314,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
),
[searchText, t]
);
// 渲染产品项
const renderProductItem = useCallback(
({ item }: { item: Product }) => (
@ -348,23 +326,19 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
),
[handleProductPress, t, userStore]
);
// 创建产品列表项的key提取器
const keyExtractor = useCallback(
(item: Product, index: number) => `${item.offer_id}-${index}`,
[]
);
// 处理排序
const handleSort = useCallback(
(field: "price" | "time", order: "asc" | "desc") => {
setSortField(field);
setSortOrder(order);
// 本地排序,不发送API请求
setProducts((prevProducts) => {
const sortedProducts = [...prevProducts];
if (field === "price") {
sortedProducts.sort((a, b) => {
const priceA = a.min_price || 0;
@ -379,19 +353,15 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
return order === "asc" ? timeA - timeB : timeB - timeA;
});
}
return sortedProducts;
});
},
[]
);
// 处理加载更多
const handleLoadMore = useCallback(() => {
if (loading || !hasMore || loadingMore) return;
setLoadingMore(true);
const newParams = {
...searchParams,
page: searchParams.page + 1,
@ -399,7 +369,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
setSearchParams(newParams);
searchProducts(newParams, true);
}, [loading, hasMore, loadingMore, searchParams, searchProducts]);
// 渲染底部加载指示器
const renderFooter = useCallback(() => {
if (!hasMore)
@ -408,7 +377,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
<Text style={styles.footerText}>{t("noMoreData")}</Text>
</View>
);
if (loadingMore)
return (
<View style={styles.footerContainer}>
@ -416,22 +384,18 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
<Text style={styles.footerText}>{t("loadingMore")}</Text>
</View>
);
return <View style={styles.footerSpace} />;
}, [loadingMore, hasMore, t]);
// 处理滚动事件
const handleScroll = useCallback((event: any) => {
const offsetY = event.nativeEvent.contentOffset.y;
// 当滚动超过屏幕高度的一半时显示回到顶部按钮
setShowBackToTop(offsetY > 300);
}, []);
// 回到顶部
const scrollToTop = useCallback(() => {
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
}, []);
// 处理标签切换
const handleTabChange = useCallback(
(tab: "default" | "volume" | "price") => {
@ -443,7 +407,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
scrollToTop();
} else {
setActiveTab(tab);
// 根据标签类型设置排序规则
if (tab === "price") {
// 默认价格从低到高
@ -468,7 +431,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
},
[handleSort, activeTab, sortOrder, originalProducts, scrollToTop]
);
// 渲染骨架屏网格
const renderSkeletonGrid = useCallback(() => {
// Create an array of items for the skeleton grid
@ -487,7 +449,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
</View>
);
}, []);
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
@ -527,7 +488,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
)}
</View>
</View>
{/* 标签筛选 */}
<View style={styles.tabContainer}>
<TouchableOpacity
@ -590,7 +550,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
</View>
</TouchableOpacity>
</View>
{/* 搜索结果 */}
<View style={styles.resultsContainer}>
{/* 搜索结果标题栏和排序选项 */}
@ -660,9 +619,7 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
</TouchableOpacity>
</View>
</View>
<View style={styles.sortDivider} />
<View style={styles.sortGroup}>
<Text style={styles.sortLabel}>{t('time')}:</Text>
<View style={styles.sortButtons}>
@ -725,7 +682,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
</ScrollView>
</View>
)}
{/* 加载指示器或产品列表 */}
{loading && showSkeleton ? (
renderSkeletonGrid()
@ -768,7 +724,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
@ -1075,7 +1030,6 @@ const styles = StyleSheet.create({
width: widthUtils(30, 66).width,
height: widthUtils(30, 66).height,
},
vipButtonText: {
fontStyle: "italic",
fontWeight: "900",
@ -1117,4 +1071,4 @@ const styles = StyleSheet.create({
backgroundColor: '#EAEAEA',
borderRadius: 4,
},
});
});

11
app/screens/SearchScreen.tsx

@ -193,14 +193,13 @@ export const SearchScreen = () => {
}, [searchText, saveSearchHistory, navigation]);
// 点击搜索标签
const handleTagPress = useCallback((tag: string) => {
console.log('tag',tag);
const handleTagPress = (tag: string) => {
setSearchText(tag);
saveSearchHistory(tag);
logSearch(searchText.trim(),navigation.getState().routes[navigation.getState().index - 1]?.name as string);
logSearch(tag,navigation.getState().routes[navigation.getState().index - 1]?.name as string);
// 导航到搜索结果页面,并传递搜索关键词
navigation.navigate('SearchResult', { keyword: tag });
}, [saveSearchHistory, navigation]);
}
return (
<SafeAreaView style={styles.safeArea}>
@ -256,7 +255,7 @@ export const SearchScreen = () => {
<SearchTagItem
key={`recent-${index}`}
tag={tag}
onPress={handleTagPress}
onPress={() => handleTagPress(tag)}
showDeleteButton={true}
onDelete={removeSearchHistoryItem}
/>
@ -276,7 +275,7 @@ export const SearchScreen = () => {
<SearchTagItem
key={`trending-${index}`}
tag={tag}
onPress={handleTagPress}
onPress={() => handleTagPress(tag)}
/>
))}
</View>

2
app/screens/productStatus/Status.tsx

@ -326,8 +326,8 @@ const styles = StyleSheet.create({
width: "100%",
},
statusItem: {
width: widthUtils(100, 100).width,
padding: 16,
paddingHorizontal: 20,
backgroundColor: "white",
},
statusItemText: {

75
app/services/api/chat.ts

@ -1,67 +1,18 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
// Chat API base URL
const CHAT_API_BASE_URL = 'https://6454c61f-3a39-43f7-afb1-d342c903b84e-00-21kfz12hqvw76.sisko.replit.dev';
// Create axios instance for chat
const chatApi: AxiosInstance = axios.create({
baseURL: CHAT_API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
// Request interceptor
chatApi.interceptors.request.use(
async (config) => {
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor
chatApi.interceptors.response.use(
(response) => {
return response;
},
(error: AxiosError) => {
return Promise.reject(error);
}
);
import apiService from './apiClient';
export interface ChatMessage {
type: string;
mimetype: string;
userWs: string;
app_id: string;
country: string;
body: string;
text: string;
}
// API methods
export const chatService = {
// Send message
async sendMessage(newMessage: {
type: string,
mimetype: string,
userWs: string,
app_id: string,
country: string,
body: string,
text: string
}): Promise<any> {
try {
const response = await chatApi.post('/chat', { newMessage });
return response.data;
} catch (error) {
throw error;
}
// Send message with retry mechanism
async sendMessage(newMessage:ChatMessage): Promise<any> {
return apiService.post('https://api.brainnel.com/app_chat/chat/',newMessage);
},
// Get chat history
async getChatHistory(sessionId: string): Promise<any> {
try {
const response = await chatApi.get('/history', { params: { sessionId } });
return response.data;
} catch (error) {
throw error;
}
}
};
export default chatApi;

12
app/services/api/productApi.ts

@ -37,7 +37,13 @@ export type Products = Product[]
sort_order?:string;
sort_by:string,
language:string,
user_id?:number
user_id?:number,
type?: number,
image?: string
}
export interface ImageSearchParams {
page_size?: number;
}
export interface SkuAttribute {
@ -193,6 +199,10 @@ export type Products = Product[]
getSimilarProducts: (offer_id: string, user_id?: number) => {
const url = user_id ? `/api/products/${offer_id}/similar/?limit=5&user_id=${user_id}` : `/api/products/${offer_id}/similar/?limit=5`;
return apiService.get<Similars>(url);
},
// 图片搜索
searchByImage: (data:{formData: FormData,user_id?:number}) => {
return apiService.upload<Products>('/api/search/image_search/?user_id='+data.user_id,data.formData);
}
}

42
package-lock.json generated

@ -14829,8 +14829,6 @@
},
"node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"inBundle": true,
"license": "MIT",
"dependencies": {
@ -15052,8 +15050,6 @@
},
"node_modules/npm/node_modules/@npmcli/metavuln-calculator/node_modules/pacote": {
"version": "20.0.0",
"resolved": "https://registry.npmmirror.com/pacote/-/pacote-20.0.0.tgz",
"integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==",
"inBundle": true,
"license": "ISC",
"dependencies": {
@ -15400,8 +15396,6 @@
},
"node_modules/npm/node_modules/cacache/node_modules/tar": {
"version": "7.4.3",
"resolved": "https://registry.npmmirror.com/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"inBundle": true,
"license": "ISC",
"dependencies": {
@ -15427,9 +15421,7 @@
}
},
"node_modules/npm/node_modules/chalk": {
"version": "5.4.1",
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"version": "5.3.0",
"inBundle": true,
"license": "MIT",
"engines": {
@ -15550,8 +15542,6 @@
},
"node_modules/npm/node_modules/cross-spawn/node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"inBundle": true,
"license": "ISC",
"dependencies": {
@ -16165,8 +16155,6 @@
},
"node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"inBundle": true,
"license": "MIT",
"engines": {
@ -16255,8 +16243,6 @@
},
"node_modules/npm/node_modules/minipass-flush/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"inBundle": true,
"license": "ISC",
"dependencies": {
@ -16281,8 +16267,6 @@
},
"node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"inBundle": true,
"license": "ISC",
"dependencies": {
@ -16307,8 +16291,6 @@
},
"node_modules/npm/node_modules/minipass-sized/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"inBundle": true,
"license": "ISC",
"dependencies": {
@ -16334,8 +16316,6 @@
},
"node_modules/npm/node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"inBundle": true,
"license": "ISC",
"dependencies": {
@ -16438,8 +16418,6 @@
},
"node_modules/npm/node_modules/node-gyp/node_modules/tar": {
"version": "7.4.3",
"resolved": "https://registry.npmmirror.com/tar/-/tar-7.4.3.tgz",
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
"inBundle": true,
"license": "ISC",
"dependencies": {
@ -17066,8 +17044,6 @@
},
"node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"inBundle": true,
"license": "MIT",
"dependencies": {
@ -17209,8 +17185,6 @@
},
"node_modules/npm/node_modules/tar/node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"inBundle": true,
"license": "ISC",
"dependencies": {
@ -17222,8 +17196,6 @@
},
"node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"inBundle": true,
"license": "ISC",
"dependencies": {
@ -17235,8 +17207,6 @@
},
"node_modules/npm/node_modules/tar/node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"inBundle": true,
"license": "ISC",
"engines": {
@ -17284,8 +17254,6 @@
},
"node_modules/npm/node_modules/tuf-js/node_modules/@tufjs/models": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/@tufjs/models/-/models-3.0.1.tgz",
"integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==",
"inBundle": true,
"license": "MIT",
"dependencies": {
@ -17342,8 +17310,6 @@
},
"node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"inBundle": true,
"license": "MIT",
"dependencies": {
@ -17386,8 +17352,6 @@
},
"node_modules/npm/node_modules/which/node_modules/isexe": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-3.1.1.tgz",
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
"inBundle": true,
"license": "ISC",
"engines": {
@ -17433,8 +17397,6 @@
},
"node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"inBundle": true,
"license": "MIT",
"dependencies": {
@ -17469,8 +17431,6 @@
},
"node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"inBundle": true,
"license": "MIT",
"dependencies": {

16
yarn.lock

@ -2658,8 +2658,6 @@
"@tufjs/models@3.0.1":
version "3.0.1"
resolved "https://registry.npmmirror.com/@tufjs/models/-/models-3.0.1.tgz"
integrity sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==
dependencies:
"@tufjs/canonical-json" "2.0.0"
minimatch "^9.0.5"
@ -4161,9 +4159,7 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
supports-color "^7.1.0"
chalk@^5.3.0:
version "5.4.1"
resolved "https://registry.npmmirror.com/chalk/-/chalk-5.4.1.tgz"
integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==
version "5.3.0"
char-regex@^1.0.2:
version "1.0.2"
@ -7314,8 +7310,6 @@ isexe@^2.0.0:
isexe@^3.1.1:
version "3.1.1"
resolved "https://registry.npmmirror.com/isexe/-/isexe-3.1.1.tgz"
integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==
isobject@^3.0.1:
version "3.0.1"
@ -8624,8 +8618,6 @@ natural-compare@^1.4.0:
negotiator@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz"
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
negotiator@~0.6.4:
version "0.6.4"
@ -9196,8 +9188,6 @@ pacote@^19.0.0, pacote@^19.0.1:
pacote@^20.0.0:
version "20.0.0"
resolved "https://registry.npmmirror.com/pacote/-/pacote-20.0.0.tgz"
integrity sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==
dependencies:
"@npmcli/git" "^6.0.0"
"@npmcli/installed-package-contents" "^3.0.0"
@ -11069,8 +11059,6 @@ spdx-exceptions@^2.1.0:
spdx-expression-parse@^3.0.0:
version "3.0.1"
resolved "https://registry.npmmirror.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz"
integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
dependencies:
spdx-exceptions "^2.1.0"
spdx-license-ids "^3.0.0"
@ -11528,8 +11516,6 @@ tar@^6.1.11, tar@^6.2.1:
tar@^7.4.3:
version "7.4.3"
resolved "https://registry.npmmirror.com/tar/-/tar-7.4.3.tgz"
integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==
dependencies:
"@isaacs/fs-minipass" "^4.0.0"
chownr "^3.0.0"

Loading…
Cancel
Save