diff --git a/app/constants/config.ts b/app/constants/config.ts index 995b1fc..5614a50 100644 --- a/app/constants/config.ts +++ b/app/constants/config.ts @@ -18,6 +18,7 @@ export const STORAGE_KEYS = { AUTH_TOKEN: 'auth_token', USER_INFO: 'user_info', LANGUAGE: 'app_language', + CURRENCY: 'app_currency', }; // 默认请求头 diff --git a/app/locales/en/translation.json b/app/locales/en/translation.json index c20377d..84004e5 100644 --- a/app/locales/en/translation.json +++ b/app/locales/en/translation.json @@ -97,6 +97,7 @@ "my":"My", "categories": "Categories", "chat": { + "tab_label": "Chat", "customer_service": "Customer Service", "product_support": "Product Support", "notifications": "Notifications", @@ -181,6 +182,12 @@ }, "settings": { "title": "Settings", + "country": "Country", + "currency": "Currency", + "language": "Language", + "success": "Settings saved successfully", + "login_required": "Please login first", + "confirm": "Confirm", "profile": "My Profile", "change_password": "Change Password", "change_phone": "Change Phone Number", diff --git a/app/locales/fr/translation.json b/app/locales/fr/translation.json index 794aa61..7e1de0e 100644 --- a/app/locales/fr/translation.json +++ b/app/locales/fr/translation.json @@ -97,6 +97,7 @@ "my":"Mon", "categories":"Catégories", "chat": { + "tab_label": "Chat", "customer_service": "Service Client", "product_support": "Support Produit", "notifications": "Notifications", @@ -114,13 +115,27 @@ "sales":"ventes", "productPicture":"Image du produit", "no_stock": "Pas de stock", - "setting": { - "title": "Sélectionner la langue et la devise", + "settings": { + "title": "Paramètres", "country": "Pays", "currency": "Devise", "language": "Langue", "confirm": "Confirmer", - "success": "Paramètres enregistrés avec succès" + "success": "Paramètres enregistrés avec succès", + "login_required": "Veuillez vous connecter d'abord", + "profile": "Mon profil", + "change_password": "Modifier le mot de passe", + "change_phone": "Changer le numéro de téléphone", + "my_address": "Mon adresse", + "feedback": "Commentaires", + "privacy_policy": "Politique de confidentialité de Brainnel", + "terms_of_use": "Conditions d'utilisation", + "clear_cache": "Vider le cache", + "language_currency": "Sélectionner la langue et la devise", + "add_new_address": "Ajouter une nouvelle adresse", + "address_management": "Gestion des adresses", + "set_default": "Définir comme adresse par défaut", + "delete": "Supprimer" }, "login.now": "Se connecter maintenant", "login.button": "Connexion", @@ -307,22 +322,6 @@ "submit_order": "Soumettre la commande", "select_payment": "Moyens de paiement" }, - "settings": { - "title": "Paramètres", - "profile": "Mon profil", - "change_password": "Modifier le mot de passe", - "change_phone": "Changer le numéro de téléphone", - "my_address": "Mon adresse", - "feedback": "Commentaires", - "privacy_policy": "Politique de confidentialité de Brainnel", - "terms_of_use": "Conditions d'utilisation", - "clear_cache": "Vider le cache", - "language_currency": "Sélectionner la langue et la devise", - "add_new_address": "Ajouter une nouvelle adresse", - "address_management": "Gestion des adresses", - "set_default": "Définir comme adresse par défaut", - "delete": "Supprimer" - }, "login": { "logInOrSignUp": "Se connecter ou s'inscrire", "phoneNumber": "Numéro de téléphone", diff --git a/app/locales/zh/translation.json b/app/locales/zh/translation.json index 78a5c63..89f05ef 100644 --- a/app/locales/zh/translation.json +++ b/app/locales/zh/translation.json @@ -96,6 +96,7 @@ "my": "我的", "categories": "分类", "chat": { + "tab_label": "聊天", "customer_service": "客服聊天", "product_support": "产品咨询", "notifications": "消息通知", @@ -114,13 +115,27 @@ "no_stock": "缺货", "loginRequired": "需要登录", "loginPrompt": "请登录以访问此功能。登录后您将获得更好的体验并能跟踪您的订单。", - "setting": { - "title": "选择语言和货币", + "settings": { + "title": "设置", "country": "国家", "currency": "货币", "language": "语言", "confirm": "确认", - "success": "设置成功" + "success": "设置成功", + "login_required": "请先登录", + "profile": "我的资料", + "change_password": "修改密码", + "change_phone": "更换手机号", + "my_address": "我的地址", + "feedback": "意见反馈", + "privacy_policy": "Brainnel隐私政策", + "terms_of_use": "使用条款", + "clear_cache": "清除缓存", + "language_currency": "选择语言和货币", + "add_new_address": "添加新地址", + "address_management": "地址管理", + "set_default": "设为默认地址", + "delete": "删除" }, "homePage": { "searchPlaceholder": "搜索商品", diff --git a/app/navigation/TabNavigator.tsx b/app/navigation/TabNavigator.tsx index ff14010..9d0ac98 100644 --- a/app/navigation/TabNavigator.tsx +++ b/app/navigation/TabNavigator.tsx @@ -186,7 +186,7 @@ export const TabNavigator = () => { name="Chat" component={ChatScreen} options={{ - tabBarLabel: t('chat'), + tabBarLabel: t('chat.tab_label'), tabBarIcon: ({ color, size }: TabBarIconProps) => ( ), @@ -251,11 +251,11 @@ export const TabNavigator = () => { × - {t('login.loginRequired')} - {t('login.loginPrompt')} + {t('loginRequired')} + {t('loginPrompt')} - {t('login.loginNow')} + {t('loginNow')} diff --git a/app/screens/HomeScreen.tsx b/app/screens/HomeScreen.tsx index aa7ba15..32ac1f2 100644 --- a/app/screens/HomeScreen.tsx +++ b/app/screens/HomeScreen.tsx @@ -657,7 +657,7 @@ export const HomeScreen = () => { - {getSubjectTransLanguage(item)} + {getSubjectTransLanguage(item) || item.subject_trans} @@ -673,7 +673,7 @@ export const HomeScreen = () => { - {item.sold_out || "0"}+ ventes + {item.sold_out || "0"}+ {t("homePage.sales")} ); diff --git a/app/screens/ImageSearchResultScreen.tsx b/app/screens/ImageSearchResultScreen.tsx index 2900576..34b4093 100644 --- a/app/screens/ImageSearchResultScreen.tsx +++ b/app/screens/ImageSearchResultScreen.tsx @@ -1,4 +1,10 @@ -import React, { useState, useEffect, useCallback, useRef, useMemo } from "react"; +import React, { + useState, + useEffect, + useCallback, + useRef, + useMemo, +} from "react"; import { View, Text, @@ -17,7 +23,11 @@ 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 { + 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"; @@ -230,113 +240,152 @@ export const ImageSearchResultScreen = ({ 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"); - + const [activeTab, setActiveTab] = useState<"default" | "volume" | "price">( + "default" + ); + // 获取初始图片URI const imageUri = useMemo(() => { - console.log('获取图片URI', route.params?.image); + console.log("获取图片URI", route.params?.image); return route.params?.image || null; }, [route.params?.image]); - - // 将图片URI转换为FormData - const uriToFormData = async (uri: string) => { + + // 将图片URI转换为base64 + const uriToBase64 = async (uri: string): Promise => { 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"; - + console.log("开始转换图片为base64", uri); + 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; + + // 转换为base64 + const reader = new FileReader(); + const base64Promise = new Promise((resolve, reject) => { + reader.onload = () => { + const result = reader.result as string; + resolve(result); + }; + reader.onerror = reject; + reader.readAsDataURL(imageBlob); + }); + + const base64Data = await base64Promise; + + // 记录图片大小信息 + console.log( + "原始Blob大小:", + imageBlob.size, + "bytes =", + (imageBlob.size / 1024).toFixed(2), + "KB" + ); + console.log("Base64数据长度:", base64Data.length, "字符"); + console.log( + "Base64数据大小:", + (base64Data.length * 0.75).toFixed(0), + "bytes ≈", + ((base64Data.length * 0.75) / 1024).toFixed(2), + "KB" + ); + + // 提取base64数据部分(去掉data:image/jpeg;base64,前缀) + const base64String = base64Data.split(",")[1]; + console.log("纯Base64字符串长度:", base64String.length); + + console.log("图片转换为base64完成"); + return base64String; } catch (error) { - console.error('图片转换出错:', error); + console.error("图片转换base64出错:", error); throw error; } }; - + // 搜索图片 const searchByImage = async (uri: string) => { if (!uri) { - console.log('没有有效的图片URI'); + console.log("没有有效的图片URI"); setLoading(false); setShowSkeleton(false); return; } - + try { - console.log('开始搜索图片:', uri); + console.log("开始搜索图片:", uri); + console.log("用户信息:", userStore.user); setLoading(true); setShowSkeleton(true); - - const formData = await uriToFormData(uri); + + const base64String = await uriToBase64(uri); + + const userId = userStore.user?.user_id || null; const data = { - formData: formData as FormData, - user_id: userStore.user?.user_id, - type: 'formData', + image_base64: base64String, + user_id: userId, }; - - console.log('调用图片搜索API'); + + console.log("调用图片搜索API,用户ID:", userId); const response = await productApi.searchByImage(data); - - console.log('图片搜索完成,返回数据:', JSON.stringify(response)); - + + console.log("图片搜索完成,返回数据:", JSON.stringify(response)); + // 确保我们有有效的产品数组 const productList = Array.isArray(response) ? response : []; - + setProducts(productList); setOriginalProducts(productList); - + // 立即更新状态,无需等待 setLoading(false); setShowSkeleton(false); - } catch (error) { - console.error('图片搜索出错:', error); + } catch (error: any) { + console.error("图片搜索出错:", error); + // 更详细的错误处理 + if (error.code === "ERR_NETWORK") { + console.error("网络连接错误,请检查:"); + console.error("1. 手机是否连接到同一WiFi网络"); + console.error("2. 开发服务器是否正在运行"); + console.error("3. 后端API服务是否正常"); + } setProducts([]); setOriginalProducts([]); setLoading(false); setShowSkeleton(false); } }; - + // 只在组件加载时执行一次搜索 useEffect(() => { - console.log('imageUri', imageUri); - + console.log("imageUri", imageUri); + console.log(imageUri); + // 重置状态,确保每次都是新的开始 imageProcessed.current = false; setLoading(true); setShowSkeleton(true); - + // 如果没有图片URI,立即结束加载状态 if (!imageUri) { - console.log('没有图片URI,结束加载'); + console.log("没有图片URI,结束加载"); setLoading(false); setShowSkeleton(false); return; } - + // 如果已经处理过图片,则跳过 if (imageProcessed.current) { - console.log('已处理过图片,跳过'); + console.log("已处理过图片,跳过"); return; } - - console.log('首次加载,处理图片', imageUri); + + console.log("首次加载,处理图片", imageUri); imageProcessed.current = true; - + // 执行图片搜索 searchByImage(imageUri); }, [imageUri]); - + // 搜索产品的API调用 const searchProducts = useCallback( async (keyword: string, isLoadMore = false) => { @@ -356,7 +405,7 @@ export const ImageSearchResultScreen = ({ language: "en", user_id: userStore.user?.user_id, }; - + const res = await productApi.getSearchProducts(params); if (isLoadMore) { setProducts((prev) => [...prev, ...res.products]); @@ -384,7 +433,7 @@ export const ImageSearchResultScreen = ({ }, [userStore.user] ); - + // 处理搜索提交 const handleSearch = useCallback(() => { if (searchText.trim()) { @@ -398,12 +447,12 @@ export const ImageSearchResultScreen = ({ searchProducts(searchText.trim()); } }, [searchText, searchProducts]); - + // 切换筛选器显示状态 const toggleFilter = useCallback(() => { setIsFilterVisible(!isFilterVisible); }, [isFilterVisible]); - + // 处理点击产品 const handleProductPress = useCallback( (product: Product) => { @@ -414,26 +463,24 @@ export const ImageSearchResultScreen = ({ }, [navigation] ); - + // 返回上一页 const goBack = useCallback(() => { navigation.goBack(); }, [navigation]); - + // 渲染列表为空时的组件 const renderEmptyList = useCallback( () => ( - - {t("noResults")} - + {t("noResults")} {t("tryDifferentImage")} ), [t] ); - + // 渲染产品项 const renderProductItem = useCallback( ({ item }: { item: Product }) => ( @@ -446,29 +493,29 @@ export const ImageSearchResultScreen = ({ ), [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 ( ); }, []); - + // 处理排序 const handleSort = useCallback( (field: "price" | "time", order: "asc" | "desc") => { @@ -510,7 +557,7 @@ export const ImageSearchResultScreen = ({ }, [] ); - + // 处理标签切换 const handleTabChange = useCallback( (tab: "default" | "volume" | "price") => { @@ -546,7 +593,7 @@ export const ImageSearchResultScreen = ({ }, [handleSort, activeTab, sortOrder, originalProducts, scrollToTop] ); - + // 渲染列表底部加载更多 const renderFooter = useCallback(() => { if (!hasMore) @@ -564,7 +611,7 @@ export const ImageSearchResultScreen = ({ ); return ; }, [loadingMore, hasMore, t]); - + return ( @@ -604,7 +651,7 @@ export const ImageSearchResultScreen = ({ )} - + {/* 标签筛选 */} - {t('default')} + {t("default")} - {t('volume')} + {t("volume")} - {t('price')} + {t("price")} {activeTab === "price" && ( @@ -667,7 +716,7 @@ export const ImageSearchResultScreen = ({ - + {/* 搜索结果 */} {/* 搜索结果标题栏和排序选项 */} @@ -739,7 +788,7 @@ export const ImageSearchResultScreen = ({ - {t('time')}: + {t("time")}: { if (res.access_token) { const token = res.token_type + " " + res.access_token; await AsyncStorage.setItem("token", token); - const data = await settingApi.postFirstLogin(221); + const data = await settingApi.postFirstLogin(selectedCountry?.country || 221); setSettings(data); const user = await userApi.getProfile(); setUser(user); diff --git a/app/screens/setting/CountrySetting.tsx b/app/screens/setting/CountrySetting.tsx index 4d99333..b1c6600 100644 --- a/app/screens/setting/CountrySetting.tsx +++ b/app/screens/setting/CountrySetting.tsx @@ -16,10 +16,8 @@ import { useTranslation } from "react-i18next"; import { useGlobalStore } from "../../store/useGlobalStore"; import { userApi } from "../../services/api/userApi"; import useUserStore from "../../store/user"; +import { saveCurrency, saveLanguage } from "../../utils/storage"; - - -// Define CountryList type to match API response type CountryList = { country: number; currency: string; @@ -37,14 +35,14 @@ export const CountrySetting = () => { const navigation = useNavigation>(); const route = useRoute>(); - const [changeType, setChangeType] = useState("country"); + const [changeType, setChangeType] = useState("language"); const [countryList, setCountryList] = useState([]); const [currencyList, setCurrencyList] = useState([]); const [languageList, setLanguageList] = useState([]); const [country, setCountry] = useState(0); const [currency, setCurrency] = useState(""); const [language, setLanguage] = useState(""); - const { setUser } = useUserStore(); + const { user, setUser } = useUserStore(); const getCountry = async () => { const res = await settingApi.getCountryList(); @@ -70,8 +68,16 @@ export const CountrySetting = () => { getLanguage(); }, []); + useEffect(() => { + // 根据用户登录状态设置初始选项卡 + if (user?.user_id && changeType === "language") { + setChangeType("country"); + } else if (!user?.user_id) { + setChangeType("language"); + } + }, [user?.user_id]); + const putSettinghandel = async () => { - // Only include the property that corresponds to the active tab let data = {}; if (changeType === "country") { data = { country: country }; @@ -79,23 +85,40 @@ export const CountrySetting = () => { } else if (changeType === "currency") { data = { currency: currency }; setGlobalCurrency({ currency: currency }); + await saveCurrency(currency); } else if (changeType === "language") { data = { language: language }; setGlobalLanguage({ language: language }); + await saveLanguage(language); } - + Toast.show({ - text1: t('setting.success'), + text1: t('settings.success'), type: "success", visibilityTime: 1000, }); - await settingApi.putSetting(data); - if (changeType === "language" && language) { - await changeLanguage(language); + + // 只有在用户已登录的情况下才调用服务器接口 + if (user?.user_id) { + try { + await settingApi.putSetting(data); + if (changeType === "language" && language) { + await changeLanguage(language); + } + eventBus.emit("refreshSetting"); + const userData = await userApi.getProfile(); + setUser(userData); + } catch (error) { + console.error('保存设置到服务器失败:', error); + // 即使服务器保存失败,本地设置已经保存,不影响用户体验 + } + } else { + // 用户未登录,只保存到本地 + if (changeType === "language" && language) { + await changeLanguage(language); + } + console.log('用户未登录,设置已保存到本地'); } - eventBus.emit("refreshSetting"); - const userData = await userApi.getProfile(); - setUser(userData); }; return ( @@ -108,36 +131,43 @@ export const CountrySetting = () => { > - {t('setting.title')} + {t('settings.title')} - setChangeType("country")} - > - {t('setting.country')} - - setChangeType("currency")} - > - {t('setting.currency')} - + {user?.user_id && ( + setChangeType("country")} + > + {t('settings.country')} + + )} + {user?.user_id && ( + setChangeType("currency")} + > + {t('settings.currency')} + + )} setChangeType("language")} > - {t('setting.language')} + {t('settings.language')} {changeType === "country" && ( @@ -225,7 +255,7 @@ export const CountrySetting = () => { )} - {t('setting.confirm')} + {t('settings.confirm')} @@ -296,6 +326,12 @@ const styles = StyleSheet.create({ borderBottomColor: "#ff611a", color: "#ff611a", }, + changeTypeTextLoggedIn: { + width: "33%", + }, + changeTypeTextFullWidth: { + width: "100%", + }, countryList: { backgroundColor: "white", borderRadius: 10, diff --git a/app/services/api/productApi.ts b/app/services/api/productApi.ts index 4bc33cc..ae08871 100644 --- a/app/services/api/productApi.ts +++ b/app/services/api/productApi.ts @@ -209,8 +209,10 @@ export interface HotTerms { 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); + searchByImage: (data:{image_base64: string,user_id?:number | null}) => { + return apiService.post('/api/search/image_search/?user_id='+data.user_id,{ + image_base64: data.image_base64 + }); } } diff --git a/app/utils/languageUtils.ts b/app/utils/languageUtils.ts index 9874aa0..486696a 100644 --- a/app/utils/languageUtils.ts +++ b/app/utils/languageUtils.ts @@ -3,6 +3,8 @@ import { getCurrentLanguage } from '../i18n'; export const getSubjectTransLanguage = >(data: T): string => { // 获取当前i18n语言 const currentLang = getCurrentLanguage(); + console.log('currentLang', currentLang); + // 特殊处理中文 if (currentLang === 'zh' && 'subject' in data) { @@ -28,28 +30,28 @@ export const getSubjectTransLanguage = >(data: T): -export const getSkuTransLanguage = >(data: T): string => { - // 获取当前i18n语言 - const currentLang = getCurrentLanguage(); +// export const getSkuTransLanguage = >(data: T): string => { +// // 获取当前i18n语言 +// const currentLang = getCurrentLanguage(); - // 特殊处理中文 - if (currentLang === 'zh' && 'value' in data) { - return data.value as string; - } - - // 获取所有subject_trans开头的字段 - const translationFields = Object.keys(data).filter(key => - key.startsWith('value_trans') - ); - - // 查找匹配的字段 - const matchedField = translationFields.find(field => { - // 从字段名中提取语言代码 - const langCode = field.replace('value_trans_', ''); - // 如果没有后缀,则为法语 - return langCode === '' ? currentLang === 'fr' : langCode === currentLang; - }); - - // 返回匹配的翻译值,如果没有匹配则返回法语 - return (data[matchedField || 'value_trans'] as string) || ''; -}; +// // 特殊处理中文 +// if (currentLang === 'zh' && 'value' in data) { +// return data.value as string; +// } + +// // 获取所有subject_trans开头的字段 +// const translationFields = Object.keys(data).filter(key => +// key.startsWith('value_trans') +// ); + +// // 查找匹配的字段 +// const matchedField = translationFields.find(field => { +// // 从字段名中提取语言代码 +// const langCode = field.replace('value_trans_', ''); +// // 如果没有后缀,则为法语 +// return langCode === '' ? currentLang === 'fr' : langCode === currentLang; +// }); + +// // 返回匹配的翻译值,如果没有匹配则返回法语 +// return (data[matchedField || 'value_trans'] as string) || ''; +// }; diff --git a/app/utils/storage.ts b/app/utils/storage.ts new file mode 100644 index 0000000..46b9478 --- /dev/null +++ b/app/utils/storage.ts @@ -0,0 +1,42 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { STORAGE_KEYS } from '../constants/config'; + +// 保存货币设置到本地存储 +export const saveCurrency = async (currency: string) => { + try { + await AsyncStorage.setItem(STORAGE_KEYS.CURRENCY, currency); + } catch (error) { + console.error('保存货币设置失败:', error); + } +}; + +// 从本地存储加载货币设置 +export const loadCurrency = async (): Promise => { + try { + const savedCurrency = await AsyncStorage.getItem(STORAGE_KEYS.CURRENCY); + return savedCurrency; + } catch (error) { + console.error('加载货币设置失败:', error); + return null; + } +}; + +// 保存语言设置到本地存储 +export const saveLanguage = async (language: string) => { + try { + await AsyncStorage.setItem(STORAGE_KEYS.LANGUAGE, language); + } catch (error) { + console.error('保存语言设置失败:', error); + } +}; + +// 从本地存储加载语言设置 +export const loadLanguage = async (): Promise => { + try { + const savedLanguage = await AsyncStorage.getItem(STORAGE_KEYS.LANGUAGE); + return savedLanguage; + } catch (error) { + console.error('加载语言设置失败:', error); + return null; + } +}; \ No newline at end of file