From 57c45f77e14059dd9dc0eb6075ecbec79ebfa70a Mon Sep 17 00:00:00 2001 From: Mac Date: Tue, 20 May 2025 19:08:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E6=90=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/locales/en/translation.json | 15 +- app/navigation/AppNavigator.tsx | 16 +- app/navigation/screens.ts | 2 + app/navigation/types.ts | 3 +- app/screens/ChatScreen.tsx | 70 +- app/screens/HomeScreen.tsx | 276 ++++-- app/screens/ImageSearchResultScreen.tsx | 1145 +++++++++++++++++++++++ app/screens/SearchResultScreen.tsx | 48 +- app/screens/SearchScreen.tsx | 11 +- app/screens/productStatus/Status.tsx | 2 +- app/services/api/chat.ts | 75 +- app/services/api/productApi.ts | 12 +- package-lock.json | 42 +- yarn.lock | 16 +- 14 files changed, 1445 insertions(+), 288 deletions(-) create mode 100644 app/screens/ImageSearchResultScreen.tsx diff --git a/app/locales/en/translation.json b/app/locales/en/translation.json index 08ca8bf..b7d9765 100644 --- a/app/locales/en/translation.json +++ b/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." } \ No newline at end of file diff --git a/app/navigation/AppNavigator.tsx b/app/navigation/AppNavigator.tsx index f584af1..f16ae40 100644 --- a/app/navigation/AppNavigator.tsx +++ b/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", }} /> { gestureDirection: "horizontal", }} /> + { 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 diff --git a/app/screens/HomeScreen.tsx b/app/screens/HomeScreen.tsx index db56941..f6e5545 100644 --- a/app/screens/HomeScreen.tsx +++ b/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( {/* Show placeholder while image is loading */} {!isLoaded && !hasError && ( - + )} - + {/* Show error state if image failed to load */} {hasError && ( - + 加载失败 @@ -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(() => { ]} /> - + { )} - + {userStore.user?.user_id && ( @@ -514,12 +526,12 @@ export const HomeScreen = () => { ); - + // 渲染骨架屏网格 const renderSkeletonGrid = useCallback(() => { // 创建骨架屏数组 const skeletonArray = Array(8).fill(null); - + return ( { numColumns={2} columnWrapperStyle={styles.productCardGroup} scrollEnabled={false} - contentContainerStyle={{paddingBottom: 15}} + contentContainerStyle={{ paddingBottom: 15 }} /> ); @@ -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 = () => { > 搜索商品 - setShowImagePickerModal(true)} > @@ -815,7 +877,7 @@ export const HomeScreen = () => { ) : null} ); - + return ( @@ -855,7 +917,7 @@ export const HomeScreen = () => { } /> )} - + {/* Categories Modal */} { - + {/* Image Picker Modal */} { transparent={true} onRequestClose={() => setShowImagePickerModal(false)} > - setShowImagePickerModal(false)} @@ -929,35 +991,43 @@ export const HomeScreen = () => { {!galleryUsed ? ( // 正常状态,显示相机选项 - - + 拍照 ) : ( // 已使用相册状态,显示重置选项 - - + 重置相机功能 )} - + - - 从相册选择 - - setShowImagePickerModal(false)} > @@ -974,15 +1044,15 @@ export const HomeScreen = () => { const styles = StyleSheet.create({ 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({ 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({ // 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", }, -}); \ No newline at end of file +}); diff --git a/app/screens/ImageSearchResultScreen.tsx b/app/screens/ImageSearchResultScreen.tsx new file mode 100644 index 0000000..2900576 --- /dev/null +++ b/app/screens/ImageSearchResultScreen.tsx @@ -0,0 +1,1145 @@ +import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; +import { + View, + Text, + StyleSheet, + FlatList, + Image, + TouchableOpacity, + TextInput, + SafeAreaView, + StatusBar, + ActivityIndicator, + Platform, + ScrollView, +} 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, type Product, ProductParams } from "../services/api/productApi"; +import { useTranslation } from "react-i18next"; +import isSmallScreen from "../utils/isSmallScreen"; +import { Svg, Path } from "react-native-svg"; +import SearchIcon from "../components/SearchIcon"; +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 }) => { + const Icon = Ionicons as any; + return ; + } +); + +// 路由参数类型 +type ImageSearchRouteParams = { + image?: string; +}; + +// 组件Props类型 +type ImageSearchResultScreenProps = { + route: RouteProp, string>; + navigation: NativeStackNavigationProp; +}; + +// 懒加载图片组件 - 改进版本 +const LazyImage = React.memo( + ({ + uri, + style, + resizeMode, + }: { + uri: string; + style: any; + resizeMode: any; + }) => { + 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 ( + + {/* Show placeholder while image is loading */} + {!isLoaded && !hasError && ( + + )} + + {/* Show error state if image failed to load */} + {hasError && ( + + + + 加载失败 + + + )} + + {/* Actual image */} + + + ); + } +); + +// 产品骨架屏组件 - 用于加载状态 +const ProductSkeleton = React.memo(() => ( + + + + + + + + + +)); + +// 产品项组件 - 使用React.memo优化渲染 +const ProductItem = React.memo( + ({ + product, + onPress, + t, + userStore, + }: { + product: Product; + onPress: (product: Product) => void; + t: any; + userStore: any; + }) => ( + onPress(product)} + activeOpacity={0.7} + key={product.offer_id} + > + + {product.product_image_urls[0] ? ( + + ) : ( + {t("productPicture")} + )} + + {userStore.user?.user_id && ( + + VIP + {userStore.user?.vip_level} + + )} + + {/* 产品分类 */} + + + {getSubjectTransLanguage(product)} + + {/* 价格信息 */} + + + {userStore.user?.user_id && ( + + {product.original_min_price || "0"} + {product.currency || "FCFA"} + + )} + + + {product.min_price || "0"} + + + {product.currency || "FCFA"} + + + + + {/* 销售量 */} + + {product.sold_out} {product.sold_out === 0 ? "" : "+"} {t("sales")} + + + + ) +); + +export const ImageSearchResultScreen = ({ + route, + navigation, +}: ImageSearchResultScreenProps) => { + const { t } = useTranslation(); + const [searchText, setSearchText] = useState(""); + const [products, setProducts] = useState([]); + const [originalProducts, setOriginalProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [showSkeleton, setShowSkeleton] = useState(true); + const userStore = useUserStore(); + const flatListRef = useRef(null); + const [showBackToTop, setShowBackToTop] = useState(false); + const imageProcessed = useRef(false); + const [hasMore, setHasMore] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [isFilterVisible, setIsFilterVisible] = useState(false); + const [sortOrder, setSortOrder] = useState<"asc" | "desc" | null>(null); + const [sortField, setSortField] = useState<"price" | "time">("price"); + const [activeTab, setActiveTab] = useState<"default" | "volume" | "price">("default"); + + // 获取初始图片URI + const imageUri = useMemo(() => { + console.log('获取图片URI', route.params?.image); + return route.params?.image || null; + }, [route.params?.image]); + + // 将图片URI转换为FormData + const uriToFormData = async (uri: string) => { + try { + console.log('开始转换图片', uri); + const formData = new FormData(); + + const filename = uri.split("/").pop() || "image.jpg"; + const match = /\.(\w+)$/.exec(filename); + const type = match ? `image/${match[1]}` : "image/jpeg"; + + const imageUri = Platform.OS === "ios" ? uri.replace("file://", "") : uri; + + const imageFetchResponse = await fetch(imageUri); + const imageBlob = await imageFetchResponse.blob(); + + formData.append("image", imageBlob, filename); + + console.log('图片转换完成'); + return formData; + } catch (error) { + console.error('图片转换出错:', error); + throw error; + } + }; + + // 搜索图片 + const searchByImage = async (uri: string) => { + if (!uri) { + console.log('没有有效的图片URI'); + setLoading(false); + setShowSkeleton(false); + return; + } + + try { + console.log('开始搜索图片:', uri); + setLoading(true); + setShowSkeleton(true); + + const formData = await uriToFormData(uri); + const data = { + formData: formData as FormData, + user_id: userStore.user?.user_id, + type: 'formData', + }; + + console.log('调用图片搜索API'); + const response = await productApi.searchByImage(data); + + console.log('图片搜索完成,返回数据:', JSON.stringify(response)); + + // 确保我们有有效的产品数组 + const productList = Array.isArray(response) ? response : []; + + setProducts(productList); + setOriginalProducts(productList); + + // 立即更新状态,无需等待 + setLoading(false); + setShowSkeleton(false); + } catch (error) { + console.error('图片搜索出错:', error); + setProducts([]); + setOriginalProducts([]); + setLoading(false); + setShowSkeleton(false); + } + }; + + // 只在组件加载时执行一次搜索 + useEffect(() => { + console.log('imageUri', imageUri); + + // 重置状态,确保每次都是新的开始 + imageProcessed.current = false; + setLoading(true); + setShowSkeleton(true); + + // 如果没有图片URI,立即结束加载状态 + if (!imageUri) { + console.log('没有图片URI,结束加载'); + setLoading(false); + setShowSkeleton(false); + return; + } + + // 如果已经处理过图片,则跳过 + if (imageProcessed.current) { + console.log('已处理过图片,跳过'); + return; + } + + console.log('首次加载,处理图片', imageUri); + imageProcessed.current = true; + + // 执行图片搜索 + searchByImage(imageUri); + }, [imageUri]); + + // 搜索产品的API调用 + const searchProducts = useCallback( + async (keyword: string, isLoadMore = false) => { + if (!isLoadMore) { + setLoading(true); + setShowSkeleton(true); + } else { + setLoadingMore(true); + } + try { + const params: ProductParams = { + keyword: keyword, + page: 1, + page_size: 20, + sort_order: "desc", + sort_by: "default", + language: "en", + user_id: userStore.user?.user_id, + }; + + const res = await productApi.getSearchProducts(params); + if (isLoadMore) { + setProducts((prev) => [...prev, ...res.products]); + } else { + setProducts(res.products); + // 保存原始排序的数据,以便默认排序时恢复 + setOriginalProducts(res.products); + } + // 如果返回的数据少于页面大小,说明没有更多数据了 + setHasMore(res.products.length === params.page_size); + } catch (error) { + console.error("Error searching products:", error); + // 发生错误时,设置hasMore为false,防止继续加载 + setHasMore(false); + // 如果不是加载更多,清空产品列表 + if (!isLoadMore) { + setProducts([]); + setOriginalProducts([]); + } + } finally { + setLoading(false); + setLoadingMore(false); + setShowSkeleton(false); + } + }, + [userStore.user] + ); + + // 处理搜索提交 + const handleSearch = useCallback(() => { + if (searchText.trim()) { + // 重置排序状态 + setSortField("price"); + setSortOrder(null); + // 重置到默认标签 + setActiveTab("default"); + // Show skeleton for new search + setShowSkeleton(true); + searchProducts(searchText.trim()); + } + }, [searchText, searchProducts]); + + // 切换筛选器显示状态 + const toggleFilter = useCallback(() => { + setIsFilterVisible(!isFilterVisible); + }, [isFilterVisible]); + + // 处理点击产品 + const handleProductPress = useCallback( + (product: Product) => { + navigation.navigate("ProductDetail", { + offer_id: product.offer_id, + price: product.min_price, + }); + }, + [navigation] + ); + + // 返回上一页 + const goBack = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + // 渲染列表为空时的组件 + const renderEmptyList = useCallback( + () => ( + + + + {t("noResults")} + + {t("tryDifferentImage")} + + ), + [t] + ); + + // 渲染产品项 + const renderProductItem = useCallback( + ({ item }: { item: Product }) => ( + + ), + [handleProductPress, t, userStore] + ); + + // 创建产品列表项的key提取器 + const keyExtractor = useCallback( + (item: Product, index: number) => `${item.offer_id}-${index}`, + [] + ); + + // 处理滚动事件 + 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 renderSkeletonGrid = useCallback(() => { + const skeletonArray = Array(8).fill(null); + + return ( + + } + keyExtractor={(_, index) => `skeleton-${index}`} + numColumns={2} + scrollEnabled={false} + contentContainerStyle={styles.productGrid} + /> + + ); + }, []); + + // 处理排序 + 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; + const priceB = b.min_price || 0; + return order === "asc" ? priceA - priceB : priceB - priceA; + }); + } else if (field === "time") { + sortedProducts.sort((a, b) => { + // 假设产品有create_time字段,如果没有可以使用其他时间相关字段 + const timeA = new Date(a.create_date || 0).getTime(); + const timeB = new Date(b.create_date || 0).getTime(); + return order === "asc" ? timeA - timeB : timeB - timeA; + }); + } + return sortedProducts; + }); + }, + [] + ); + + // 处理标签切换 + const handleTabChange = useCallback( + (tab: "default" | "volume" | "price") => { + // 如果点击的是已经激活的价格标签,则切换排序顺序 + if (tab === "price" && activeTab === "price") { + // 如果当前是价格升序,则切换为降序;如果是降序或未设置,则切换为升序 + const newOrder = sortOrder === "asc" ? "desc" : "asc"; + handleSort("price", newOrder); + scrollToTop(); + } else { + setActiveTab(tab); + // 根据标签类型设置排序规则 + if (tab === "price") { + // 默认价格从低到高 + handleSort("price", "asc"); + scrollToTop(); + } else if (tab === "volume") { + // 按销量排序 + const sortedProducts = [...originalProducts]; + sortedProducts.sort((a, b) => { + const volumeA = a.sold_out || 0; + const volumeB = b.sold_out || 0; + return volumeB - volumeA; // 从高到低排序 + }); + setProducts(sortedProducts); + scrollToTop(); + } else { + // 默认排序 - 恢复到原始数据顺序 + setProducts([...originalProducts]); + scrollToTop(); + } + } + }, + [handleSort, activeTab, sortOrder, originalProducts, scrollToTop] + ); + + // 渲染列表底部加载更多 + const renderFooter = useCallback(() => { + if (!hasMore) + return ( + + {t("noMoreData")} + + ); + if (loadingMore) + return ( + + + {t("loadingMore")} + + ); + return ; + }, [loadingMore, hasMore, t]); + + return ( + + + + + {/* 搜索栏 */} + + + + + + + + + + + + {searchText.length > 0 && ( + setSearchText("")} + hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }} + > + + + )} + + + + {/* 标签筛选 */} + + handleTabChange("default")} + > + + {t('default')} + + + handleTabChange("volume")} + > + + {t('volume')} + + + handleTabChange("price")} + > + + + {t('price')} + + {activeTab === "price" && ( + + + + )} + + + + + {/* 搜索结果 */} + + {/* 搜索结果标题栏和排序选项 */} + {isFilterVisible && ( + + + + {t("price")}: + + handleSort("price", "asc")} + > + + {t("lowToHigh")} + + {sortField === "price" && sortOrder === "asc" && ( + + )} + + handleSort("price", "desc")} + > + + {t("highToLow")} + + {sortField === "price" && sortOrder === "desc" && ( + + )} + + + + + + {t('time')}: + + handleSort("time", "asc")} + > + + {t("oldest")} + + {sortField === "time" && sortOrder === "asc" && ( + + )} + + handleSort("time", "desc")} + > + + {t("newest")} + + {sortField === "time" && sortOrder === "desc" && ( + + )} + + + + + + )} + + {/* 加载指示器或产品列表 */} + {loading && showSkeleton ? ( + renderSkeletonGrid() + ) : ( + <> + + {showBackToTop && ( + + + + )} + + )} + + + + + ); +}; + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: "#fff", + }, + safeAreaContent: { + flex: 1, + paddingTop: Platform.OS === "android" ? 0 : 0, + }, + container: { + flex: 1, + backgroundColor: "#fff", + }, + searchHeader: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 12, + paddingVertical: 8, + backgroundColor: "#fff", + borderBottomWidth: 1, + borderBottomColor: "#f0f0f0", + }, + backButton: { + padding: 4, + }, + searchBar: { + flex: 1, + flexDirection: "row", + alignItems: "center", + backgroundColor: "#f5f5f5", + borderRadius: 20, + paddingHorizontal: 8, + height: widthUtils(40, 40).height, + marginHorizontal: 8, + }, + searchInput: { + flex: 1, + marginLeft: 4, + fontSize: isSmallScreen ? 14 : 16, + color: "#333", + height: widthUtils(40, 40).height, + paddingRight: 30, + }, + clearButton: { + position: "absolute", + right: 10, + top: '50%', + marginTop: -10, + width: 20, + height: 20, + alignItems: 'center', + justifyContent: 'center', + zIndex: 20, + }, + titleContainer: { + flex: 1, + alignItems: "center", + }, + headerTitle: { + fontSize: fontSize(18), + fontWeight: "600", + color: "#333", + }, + tabContainer: { + flexDirection: "row", + backgroundColor: "#fff", + borderBottomWidth: 1, + borderBottomColor: "#f0f0f0", + }, + tabButton: { + flex: 1, + alignItems: "center", + justifyContent: "center", + paddingVertical: 12, + position: "relative", + }, + tabButtonContent: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + }, + tabIcon: { + marginLeft: 4, + }, + tabText: { + fontSize: fontSize(16), + color: "#000", + }, + activeTabText: { + color: "#0933a1", + fontWeight: "bold", + }, + activeTabButton: { + // borderBottomColor: "#0933a1", + }, + resultsContainer: { + flex: 1, + }, + resultsHeader: { + backgroundColor: "#fff", + borderBottomWidth: 1, + borderBottomColor: "#f0f0f0", + paddingVertical: 8, + }, + productGrid: { + padding: 8, + }, + productCard: { + flex: 1, + margin: 8, + backgroundColor: "#fff", + borderRadius: 8, + overflow: "hidden", + }, + productImageContainer: { + height: widthUtils(190, 190).height, + backgroundColor: "#f9f9f9", + alignItems: "center", + justifyContent: "center", + }, + productImage: { + width: "100%", + height: "100%", + }, + placeholderText: { + color: "#999", + fontSize: fontSize(14), + }, + productInfo: { + padding: 8, + }, + categoryText: { + fontSize: isSmallScreen ? 12 : 14, + color: "#000000", + fontWeight: "600", + marginBottom: 4, + fontFamily: "PingFang SC", + letterSpacing: 0, + }, + beautyProductInfoRow: { + flexDirection: "row", + alignItems: "center", + }, + flexRowCentered: {}, + priceContainer: { + flexDirection: "row", + }, + highlightedText: { + fontWeight: "700", + fontSize: fontSize(24), + color: "#ff5100", + }, + highlightedText1: { + fontWeight: "700", + fontSize: fontSize(14), + color: "#ff5100", + }, + priceLabel1: { + fontSize: fontSize(12), + fontWeight: "600", + color: "#9a9a9a", + textDecorationLine: "line-through", + }, + productSales: { + fontSize: fontSize(14), + fontWeight: "600", + fontFamily: "PingFang SC", + color: "#7c7c7c", + }, + sortScrollView: { + flexGrow: 0, + }, + sortGroup: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 16, + }, + sortLabel: { + fontSize: fontSize(16), + color: "#666", + marginRight: 8, + }, + sortButtons: { + flexDirection: "row", + alignItems: "center", + }, + sortButton: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 4, + paddingHorizontal: 8, + borderRadius: 4, + marginLeft: 4, + borderWidth: 1, + borderColor: "#e0e0e0", + }, + sortButtonActive: { + borderColor: "#ff6600", + backgroundColor: "#fff8f5", + }, + sortButtonText: { + fontSize: fontSize(14), + color: "#666", + }, + sortButtonTextActive: { + color: "#ff6600", + fontWeight: "bold", + }, + sortDivider: { + width: 1, + height: widthUtils(20, 20).height, + backgroundColor: "#e0e0e0", + marginHorizontal: 16, + }, + footerContainer: { + padding: 16, + alignItems: "center", + flexDirection: "row", + justifyContent: "center", + }, + footerText: { + fontSize: fontSize(14), + color: "#666", + marginLeft: 8, + }, + footerSpace: { + height: widthUtils(20, 20).height, + }, + backToTopButton: { + position: "absolute", + bottom: 20, + right: 20, + width: widthUtils(20, 20).width, + height: widthUtils(20, 20).height, + borderRadius: 22, + backgroundColor: "#0066FF", + justifyContent: "center", + alignItems: "center", + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + emptyContainer: { + flex: 1, + minHeight: 300, + justifyContent: "center", + alignItems: "center", + padding: 16, + }, + emptyText: { + fontSize: fontSize(14), + fontWeight: "bold", + color: "#333", + marginTop: 16, + marginBottom: 8, + }, + emptySubtext: { + fontSize: fontSize(14), + color: "#999", + textAlign: "center", + }, + imagePlaceholder: { + backgroundColor: "#EAEAEA", + justifyContent: "center", + alignItems: "center", + borderRadius: 8, + }, + vipIcon: { + position: "absolute", + top: 0, + right: 0, + justifyContent: "center", + alignItems: "center", + backgroundColor: "#3b3b3b", + borderRadius: 10, + flexDirection: "row", + width: widthUtils(30, 66).width, + height: widthUtils(30, 66).height, + }, + vipButtonText: { + fontStyle: "italic", + fontWeight: "900", + fontSize: fontSize(18), + color: "#f1c355", + }, + vipLabelBold: { + fontStyle: "italic", + fontWeight: "900", + fontSize: fontSize(18), + color: "#f1c355", + }, + skeletonText: { + backgroundColor: "#EAEAEA", + borderRadius: 4, + }, +}); \ No newline at end of file diff --git a/app/screens/SearchResultScreen.tsx b/app/screens/SearchResultScreen.tsx index 44e5452..fe3fe00 100644 --- a/app/screens/SearchResultScreen.tsx +++ b/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 ; } ); - // 路由参数类型 type SearchResultRouteParams = { keyword: string; }; - // 组件Props类型 type SearchResultScreenProps = { route: RouteProp, string>; navigation: NativeStackNavigationProp; }; - // 懒加载图片组件 - 改进版本 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 ( {/* Show placeholder while image is loading */} @@ -92,7 +85,6 @@ const LazyImage = React.memo( )} - {/* Actual image */} ( @@ -118,7 +109,6 @@ const ProductSkeleton = React.memo(() => ( )); - // 产品项组件 - 使用React.memo优化渲染 const ProductItem = React.memo( ({ @@ -148,7 +138,6 @@ const ProductItem = React.memo( ) : ( {t('productPicture')} )} - {userStore.user?.user_id && ( VIP @@ -189,7 +178,6 @@ const ProductItem = React.memo( ) ); - 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({ 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 {t("noMoreData")} ); - if (loadingMore) return ( @@ -416,22 +384,18 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp {t("loadingMore")} ); - return ; }, [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 ); }, []); - return ( @@ -527,7 +488,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp )} - {/* 标签筛选 */} - {/* 搜索结果 */} {/* 搜索结果标题栏和排序选项 */} @@ -660,9 +619,7 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp - - {t('time')}: @@ -725,7 +682,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp )} - {/* 加载指示器或产品列表 */} {loading && showSkeleton ? ( renderSkeletonGrid() @@ -768,7 +724,6 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp ); }; - 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, }, -}); +}); \ No newline at end of file diff --git a/app/screens/SearchScreen.tsx b/app/screens/SearchScreen.tsx index ed31955..b46a524 100644 --- a/app/screens/SearchScreen.tsx +++ b/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 ( @@ -256,7 +255,7 @@ export const SearchScreen = () => { handleTagPress(tag)} showDeleteButton={true} onDelete={removeSearchHistoryItem} /> @@ -276,7 +275,7 @@ export const SearchScreen = () => { handleTagPress(tag)} /> ))} diff --git a/app/screens/productStatus/Status.tsx b/app/screens/productStatus/Status.tsx index c48e257..de29b8f 100644 --- a/app/screens/productStatus/Status.tsx +++ b/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: { diff --git a/app/services/api/chat.ts b/app/services/api/chat.ts index 9f59cb3..45bdf3b 100644 --- a/app/services/api/chat.ts +++ b/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 { - 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 { + return apiService.post('https://api.brainnel.com/app_chat/chat/',newMessage); }, - - // Get chat history - async getChatHistory(sessionId: string): Promise { - try { - const response = await chatApi.get('/history', { params: { sessionId } }); - return response.data; - } catch (error) { - throw error; - } - } }; -export default chatApi; diff --git a/app/services/api/productApi.ts b/app/services/api/productApi.ts index b6baf32..f8e9172 100644 --- a/app/services/api/productApi.ts +++ b/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(url); + }, + // 图片搜索 + searchByImage: (data:{formData: FormData,user_id?:number}) => { + return apiService.upload('/api/search/image_search/?user_id='+data.user_id,data.formData); } } diff --git a/package-lock.json b/package-lock.json index 61462f2..73d190a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/yarn.lock b/yarn.lock index a993b4e..39695a2 100644 --- a/yarn.lock +++ b/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"