Browse Source

随机关键词、搜索页修改

main
unknown 2 weeks ago
parent
commit
d920e972f9
  1. 81
      app/navigation/AppNavigator.tsx
  2. 451
      app/screens/HomeScreen.tsx
  3. 386
      app/screens/SearchResultScreen.tsx
  4. 8
      app/services/api/productApi.ts
  5. 955
      yarn.lock

81
app/navigation/AppNavigator.tsx

@ -1,5 +1,5 @@
import React from 'react'; import React, { useRef } from 'react';
import { NavigationContainer } from "@react-navigation/native"; import { NavigationContainer, NavigationState, PartialState } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { RootStackParamList } from './types'; import { RootStackParamList } from './types';
import * as Screens from './screens'; import * as Screens from './screens';
@ -7,9 +7,84 @@ import Toast from "react-native-toast-message";
const Stack = createNativeStackNavigator<RootStackParamList>(); const Stack = createNativeStackNavigator<RootStackParamList>();
// 获取当前路由信息的工具函数
// [DEBUG-ROUTER-LOGGER] 路由跟踪函数 - 生产环境可删除
const getActiveRouteName = (state: NavigationState | PartialState<NavigationState> | undefined): string => {
if (!state || !state.routes) return '';
const route = state.routes[state.index || 0];
// 检查是否存在嵌套导航
if (route.state && route.state.routes) {
return getActiveRouteName(route.state);
}
return route.name;
};
// 获取路由的完整路径
// [DEBUG-ROUTER-LOGGER] 路由跟踪函数 - 生产环境可删除
const getRoutePath = (state: NavigationState | PartialState<NavigationState> | undefined): string[] => {
if (!state || !state.routes) return [];
const route = state.routes[state.index || 0];
const currentPath = [route.name];
// 检查是否存在嵌套导航
if (route.state && route.state.routes) {
return [...currentPath, ...getRoutePath(route.state)];
}
return currentPath;
};
export const AppNavigator = () => { export const AppNavigator = () => {
// [DEBUG-ROUTER-LOGGER] 路由跟踪引用 - 生产环境可删除
const navigationRef = useRef<any>(null);
const routeNameRef = useRef<string | undefined>();
return ( return (
<NavigationContainer> <NavigationContainer
ref={navigationRef}
onReady={() => {
// [DEBUG-ROUTER-LOGGER] 初始路由日志 - 生产环境可删除 - 开始
routeNameRef.current = getActiveRouteName(navigationRef.current?.getRootState());
console.log('[DEBUG-ROUTER] 初始路由:', routeNameRef.current);
// 打印组件信息
const componentInfo = Screens[routeNameRef.current as keyof typeof Screens];
console.log('[DEBUG-ROUTER] 组件信息:', componentInfo ? componentInfo.name || '未命名组件' : '未找到组件');
// [DEBUG-ROUTER-LOGGER] 初始路由日志 - 生产环境可删除 - 结束
}}
onStateChange={(state) => {
// [DEBUG-ROUTER-LOGGER] 路由变化日志 - 生产环境可删除 - 开始
const previousRouteName = routeNameRef.current;
const currentRouteName = getActiveRouteName(state);
if (previousRouteName !== currentRouteName) {
// 记录路由变化
console.log(`[DEBUG-ROUTER] 路由变化: ${previousRouteName} -> ${currentRouteName}`);
// 打印完整路径
const fullPath = getRoutePath(state);
console.log('[DEBUG-ROUTER] 路由完整路径:', fullPath.join(' -> '));
// 打印组件信息
const componentInfo = Screens[currentRouteName as keyof typeof Screens];
console.log('[DEBUG-ROUTER] 组件信息:', componentInfo ? componentInfo.name || '未命名组件' : '未找到组件');
// 打印路由参数信息
const currentRoute = state?.routes?.[state.index || 0];
if (currentRoute && currentRoute.params) {
console.log('[DEBUG-ROUTER] 路由参数:', JSON.stringify(currentRoute.params, null, 2));
}
// 更新当前路由名称引用
routeNameRef.current = currentRouteName;
}
// [DEBUG-ROUTER-LOGGER] 路由变化日志 - 生产环境可删除 - 结束
}}
>
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{
headerShown: false, headerShown: false,

451
app/screens/HomeScreen.tsx

@ -1,4 +1,4 @@
import React, { useCallback, useState, useRef, useEffect } from "react"; import React, { useCallback, useState, useRef, useEffect, useMemo } from "react";
import { import {
View, View,
Text, Text,
@ -42,6 +42,7 @@ import useUserStore from "../store/user";
import * as ImagePicker from "expo-image-picker"; import * as ImagePicker from "expo-image-picker";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
import { useGlobalStore } from "../store/useGlobalStore"; import { useGlobalStore } from "../store/useGlobalStore";
import { getCurrentLanguage } from '../i18n';
// 为图标定义类型 // 为图标定义类型
type IconProps = { type IconProps = {
name: string; name: string;
@ -316,27 +317,11 @@ export const HomeScreen = () => {
const [showCategoryModal, setShowCategoryModal] = useState(false); const [showCategoryModal, setShowCategoryModal] = useState(false);
const [showImagePickerModal, setShowImagePickerModal] = useState(false); const [showImagePickerModal, setShowImagePickerModal] = useState(false);
const [selectedCategory, setSelectedCategory] = useState("Bijoux"); const [selectedCategory, setSelectedCategory] = useState("Bijoux");
const [selectedHorizontalCategory, setSelectedHorizontalCategory] = const [selectedHorizontalCategory, setSelectedHorizontalCategory] = useState("Tous");
useState("Tous");
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(true); // 添加加载状态
const horizontalScrollRef = useRef<ScrollView>(null);
const userStore = useUserStore(); const userStore = useUserStore();
const [searchParams, setSearchParams] = useState<ProductParams>({ const { country, currency } = useGlobalStore();
keyword: "pen",
page: 1,
page_size: 10,
sort_order: "desc",
sort_by: "default",
language: "en",
});
const [products, setProducts] = useState<Product[]>([]);
const [hasMore, setHasMore] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [totalProducts, setTotalProducts] = useState(0);
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
const [currentPage, setCurrentPage] = useState(1); // 添加当前页码状态
const flatListRef = useRef<FlatList>(null); const flatListRef = useRef<FlatList>(null);
const horizontalScrollRef = useRef<ScrollView>(null);
const data = [ const data = [
{ {
imgUrl: require("../../assets/img/banner en (5)_compressed.png"), imgUrl: require("../../assets/img/banner en (5)_compressed.png"),
@ -351,95 +336,221 @@ export const HomeScreen = () => {
add: "CompanyScreen", add: "CompanyScreen",
}, },
]; ];
const [galleryUsed, setGalleryUsed] = useState(false); // 标记是否使用过相册 const [galleryUsed, setGalleryUsed] = useState(false);
const [loadingPlaceholders, setLoadingPlaceholders] = useState(0); // 添加占位符数量状态 const [hotTerms, setHotTerms] = useState<string[]>([]);
const getProductData = async (isLoadMore = false) => { const [isLoadingHotTerms, setIsLoadingHotTerms] = useState(false);
if (isLoadMore) {
setIsLoadingMore(true); // 直接在组件中实现分页加载逻辑
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [loadingPlaceholders, setLoadingPlaceholders] = useState(0);
const [totalItems, setTotalItems] = useState(0);
const [params, setParams] = useState<ProductParams>({
keyword: "pen", // 初始关键词,将在获取热门关键词后更新
sort_order: "desc",
sort_by: "default",
language: getCurrentLanguage(),
page: 1,
page_size: 10,
...(userStore.user?.user_id ? { user_id: userStore.user.user_id } : {}),
});
// 获取热门关键词并初始化产品列表
useEffect(() => {
const initApp = async () => {
try {
// 获取热门关键词
const response = await productApi.getHotTerms();
const terms = response.terms || [];
setHotTerms(terms);
// 如果获取到了热门关键词,使用随机关键词
if (terms.length > 0) {
const randomIndex = Math.floor(Math.random() * terms.length);
const randomKeyword = terms[randomIndex];
// 更新参数
setParams(prev => ({
...prev,
keyword: randomKeyword
}));
// 获取第一页数据
await fetchInitialProducts(randomKeyword);
} else { } else {
setLoading(true); // 如果没有热门关键词,使用默认关键词"pen"
await fetchInitialProducts("pen");
} }
} catch (error) {
console.error("初始化失败:", error);
// 出错时使用默认关键词
await fetchInitialProducts("pen");
}
};
initApp();
}, []);
// 获取随机关键词
const getRandomKeyword = useCallback(() => {
if (hotTerms.length === 0) return "pen";
const randomIndex = Math.floor(Math.random() * hotTerms.length);
const keyword = hotTerms[randomIndex];
console.log("获取随机关键词:", keyword);
return keyword;
}, [hotTerms]);
// 获取初始产品数据(第一页及额外的三页)
const fetchInitialProducts = useCallback(async (keyword: string) => {
setLoading(true);
try { try {
const currentParams = { // 第一页请求参数
...searchParams, const initialParams = {
page: isLoadMore ? currentPage + 1 : 1, ...params,
...(userStore.user?.user_id ? { user_id: userStore.user.user_id } : {}), keyword,
page: 1,
page_size: 10
}; };
const res = await productApi.getSearchProducts(currentParams);
setTotalProducts(res.total || 0); // 获取第一页数据
const firstPageRes = await productApi.getSearchProducts(initialParams);
setProducts(firstPageRes.products);
setTotalItems(firstPageRes.total || 0);
if (isLoadMore) { if (hotTerms.length > 0) {
setProducts(prev => [...prev, ...res.products]); // 存储已使用的关键词,避免重复
setCurrentPage(prev => prev + 1); const usedKeywords = new Set([keyword]);
} else {
setProducts(res.products); // 创建获取唯一关键词的函数
setCurrentPage(1); const getUniqueKeyword = () => {
// 如果热门关键词数量不足,或者已经用完所有关键词,返回随机关键词
if (hotTerms.length <= usedKeywords.size || hotTerms.length <= 1) {
return hotTerms[Math.floor(Math.random() * hotTerms.length)];
} }
const currentTotal = isLoadMore ? products.length + res.products.length : res.products.length; // 尝试获取未使用过的关键词
setHasMore(currentTotal < (res.total || 0)); let attempts = 0;
while (attempts < 10) { // 最多尝试10次
const randomIndex = Math.floor(Math.random() * hotTerms.length);
const candidateKeyword = hotTerms[randomIndex];
if (!usedKeywords.has(candidateKeyword)) {
usedKeywords.add(candidateKeyword);
return candidateKeyword;
}
if (!isLoadMore && !initialLoadComplete) { attempts++;
setInitialLoadComplete(true); }
const remainingParams = {
...currentParams, // 如果无法找到唯一关键词,返回随机关键词
page: 2, return hotTerms[Math.floor(Math.random() * hotTerms.length)];
page_size: 30 };
// 使用不同关键词加载额外的3页数据
const remainingRequests = Array.from({ length: 3 }, async (_, index) => {
// 获取唯一的随机关键词
const pageKeyword = getUniqueKeyword();
const pageParams = {
...params,
keyword: pageKeyword,
page: index + 2,
page_size: 10
}; };
const remainingRes = await productApi.getSearchProducts(remainingParams); return productApi.getSearchProducts(pageParams);
setProducts(prev => {
const newProducts = [...prev, ...remainingRes.products];
setHasMore(newProducts.length < (res.total || 0));
return newProducts;
}); });
setCurrentPage(2);
}
// 并行获取额外数据
const additionalResults = await Promise.all(remainingRequests);
const additionalProducts = additionalResults.flatMap(result => result.products);
// 合并所有产品
setProducts(prev => [...prev, ...additionalProducts]);
setCurrentPage(4);
setHasMore(firstPageRes.products.length + additionalProducts.length < (firstPageRes.total || 0));
} else {
// 如果没有热门关键词,只使用第一页数据
setCurrentPage(1);
setHasMore(firstPageRes.products.length < (firstPageRes.total || 0));
}
} catch (error) { } catch (error) {
console.error("Error fetching products:", error); console.error("获取产品数据失败:", error);
} finally { } finally {
if (isLoadMore) {
setIsLoadingMore(false);
setLoadingPlaceholders(0); // 清除占位符
} else {
setTimeout(() => {
setLoading(false); setLoading(false);
}, 300);
}
} }
}, [params, hotTerms]);
// 加载更多产品
const handleLoadMore = useCallback(() => {
if (!hasMore || loadingMore || hotTerms.length === 0) return;
setLoadingMore(true);
setLoadingPlaceholders(10);
// 使用新的随机关键词
const newKeyword = getRandomKeyword();
// 准备请求参数
const loadMoreParams = {
...params,
keyword: newKeyword,
page: currentPage + 1,
page_size: 10
}; };
// 获取下一页数据
productApi.getSearchProducts(loadMoreParams)
.then(res => {
setProducts(prev => [...prev, ...res.products]);
setCurrentPage(prev => prev + 1);
setHasMore((products.length + res.products.length) < (res.total || 0));
})
.catch(error => {
console.error("加载更多失败:", error);
})
.finally(() => {
setLoadingMore(false);
setLoadingPlaceholders(0);
});
}, [hasMore, loadingMore, hotTerms, getRandomKeyword, params, currentPage, products.length]);
// 刷新产品列表
const handleRefresh = useCallback(async () => {
if (hotTerms.length === 0) return;
setRefreshing(true);
try {
// 使用新的随机关键词
const refreshKeyword = getRandomKeyword();
console.log("刷新,使用关键词:", refreshKeyword);
// 重新获取初始数据
await fetchInitialProducts(refreshKeyword);
} catch (error) {
console.error("刷新失败:", error);
} finally {
setRefreshing(false);
}
}, [hotTerms, getRandomKeyword, fetchInitialProducts]);
const handleProductPress = useCallback( const handleProductPress = useCallback(
(item: Product) => { (item: Product) => {
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {
navigation.navigate("ProductDetail", { navigation.navigate("ProductDetail", {
offer_id: item.offer_id, offer_id: item.offer_id,
searchKeyword: searchParams.keyword, searchKeyword: params.keyword,
price: item.min_price, price: item.min_price,
}); });
}); });
}, },
[navigation] [navigation]
); );
const onRefresh = useCallback(async () => {
setRefreshing(true);
setInitialLoadComplete(false); // 重置初始加载标记
setCurrentPage(1); // 重置页码
setSearchParams(prev => ({ ...prev, page_size: 10 })); // 只重置每页数量
try {
await getProductData();
} catch (error) {
console.error("Error fetching products:", error);
} finally {
setRefreshing(false);
}
}, [searchParams]);
const { country, currency, language } = useGlobalStore();
useEffect(() => {
console.log("userStore.user", userStore.user);
getProductData();
}, [userStore.user, country, currency, language]);
const categories = [ const categories = [
"Tous", "Tous",
"Bijoux", "Bijoux",
@ -453,6 +564,7 @@ export const HomeScreen = () => {
"Hygiène et Soins pour le corps", "Hygiène et Soins pour le corps",
"Maquillage", "Maquillage",
]; ];
const defaultSubcategories: SubcategoryItem[] = [ const defaultSubcategories: SubcategoryItem[] = [
{ id: 1, title: "Jewelry", icon: "diamond-outline" }, { id: 1, title: "Jewelry", icon: "diamond-outline" },
{ id: 2, title: "Earrings", icon: "ear-outline" }, { id: 2, title: "Earrings", icon: "ear-outline" },
@ -461,6 +573,7 @@ export const HomeScreen = () => {
{ id: 5, title: "Earrings", icon: "ear-outline" }, { id: 5, title: "Earrings", icon: "ear-outline" },
{ id: 6, title: "Bracelet", icon: "watch-outline" }, { id: 6, title: "Bracelet", icon: "watch-outline" },
]; ];
const categoryContent: CategoryContentType = { const categoryContent: CategoryContentType = {
Tous: [], Tous: [],
Bijoux: defaultSubcategories, Bijoux: defaultSubcategories,
@ -474,38 +587,37 @@ export const HomeScreen = () => {
"Hygiène et Soins pour le corps": defaultSubcategories, "Hygiène et Soins pour le corps": defaultSubcategories,
Maquillage: defaultSubcategories, Maquillage: defaultSubcategories,
}; };
useEffect(() => { useEffect(() => {
// Ensure the content is available when category changes
if (!categoryContent[selectedHorizontalCategory]) { if (!categoryContent[selectedHorizontalCategory]) {
setSelectedHorizontalCategory("Tous"); setSelectedHorizontalCategory("Tous");
} }
}, [selectedHorizontalCategory]); }, [selectedHorizontalCategory]);
// 导航到搜索页面 - 使用useCallback优化函数引用
const navigateToSearch = useCallback(() => { const navigateToSearch = useCallback(() => {
// 使用InteractionManager延迟执行导航操作,确保当前交互和动画已完成
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {
navigation.navigate("Search"); navigation.navigate("Search");
}); });
}, [navigation]); }, [navigation]);
const navigateToShippingDetails = useCallback(() => { const navigateToShippingDetails = useCallback(() => {
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {
navigation.navigate("ShippingDetailsSection"); navigation.navigate("ShippingDetailsSection");
}); });
}, [navigation]); }, [navigation]);
const navigateToInquiry = useCallback(() => { const navigateToInquiry = useCallback(() => {
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {
navigation.navigate("InquiryScreen"); navigation.navigate("InquiryScreen");
}); });
}, [navigation]); }, [navigation]);
const scrollToCategory = (category: string) => { const scrollToCategory = (category: string) => {
const categoryIndex = categories.findIndex((c) => c === category); const categoryIndex = categories.findIndex((c) => c === category);
if (categoryIndex !== -1 && horizontalScrollRef.current) { if (categoryIndex !== -1 && horizontalScrollRef.current) {
const firstFourKeys = Object.keys(categoryContent).slice( const firstFourKeys = Object.keys(categoryContent).slice(0, categoryIndex - 1);
0,
categoryIndex - 1
);
let str = ""; let str = "";
firstFourKeys.forEach((key, index) => { firstFourKeys.forEach((key) => {
str += key; str += key;
}); });
horizontalScrollRef.current.scrollTo({ horizontalScrollRef.current.scrollTo({
@ -514,7 +626,7 @@ export const HomeScreen = () => {
}); });
} }
}; };
// 渲染产品列表项
const renderProductItem = ({ item }: { item: Product }) => ( const renderProductItem = ({ item }: { item: Product }) => (
<TouchableOpacity <TouchableOpacity
onPress={() => handleProductPress(item)} onPress={() => handleProductPress(item)}
@ -538,19 +650,13 @@ export const HomeScreen = () => {
<View style={styles.vipButtonContainer}> <View style={styles.vipButtonContainer}>
<TouchableOpacity style={styles.vipButton}> <TouchableOpacity style={styles.vipButton}>
<Text style={styles.vipButtonText}>VIP</Text> <Text style={styles.vipButtonText}>VIP</Text>
<Text style={styles.vipLabelBold}> <Text style={styles.vipLabelBold}>{userStore.user?.vip_level}</Text>
{userStore.user?.vip_level}
</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
</View> </View>
<View style={styles.beautyProductCard}> <View style={styles.beautyProductCard}>
<Text <Text style={styles.beautyProductTitle} numberOfLines={2} ellipsizeMode="tail">
style={styles.beautyProductTitle}
numberOfLines={2}
ellipsizeMode="tail"
>
{getSubjectTransLanguage(item)} {getSubjectTransLanguage(item)}
</Text> </Text>
<View style={styles.beautyProductInfoRow}> <View style={styles.beautyProductInfoRow}>
@ -562,27 +668,18 @@ export const HomeScreen = () => {
</Text> </Text>
)} )}
<View style={styles.priceContainer}> <View style={styles.priceContainer}>
<Text style={styles.highlightedText}> <Text style={styles.highlightedText}>{item.min_price || "0"}</Text>
{item.min_price || "0"} <Text style={styles.highlightedText1}>{item.currency || "FCFA"}</Text>
</Text>
<Text style={styles.highlightedText1}>
{item.currency || "FCFA"}
</Text>
</View> </View>
</View> </View>
</View> </View>
<Text style={styles.beautySalesInfo}> <Text style={styles.beautySalesInfo}>{item.sold_out || "0"}+ ventes</Text>
{item.sold_out || "0"}+ ventes
</Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
); );
// 渲染骨架屏网格
const renderSkeletonGrid = useCallback(() => { const renderSkeletonGrid = useCallback(() => {
// 创建骨架屏数组
const skeletonArray = Array(8).fill(null); const skeletonArray = Array(8).fill(null);
return ( return (
<View style={styles.skeletonContainer}> <View style={styles.skeletonContainer}>
<FlatList <FlatList
@ -598,82 +695,34 @@ export const HomeScreen = () => {
); );
}, []); }, []);
// 清理expo-image-picker临时文件
const cleanupImagePickerCache = async () => { const cleanupImagePickerCache = async () => {
try { try {
// Skip cache cleanup on web platform
if (Platform.OS === 'web') { if (Platform.OS === 'web') {
console.log('Cache cleanup skipped on web platform'); console.log('Cache cleanup skipped on web platform');
setGalleryUsed(false); setGalleryUsed(false);
return; return;
} }
// 相册选择后清理临时缓存
const cacheDir = `${FileSystem.cacheDirectory}ImagePicker`; const cacheDir = `${FileSystem.cacheDirectory}ImagePicker`;
await FileSystem.deleteAsync(cacheDir, { idempotent: true }); await FileSystem.deleteAsync(cacheDir, { idempotent: true });
console.log("已清理ImagePicker缓存"); console.log("已清理ImagePicker缓存");
// 立即重置状态,无需用户干预
setGalleryUsed(false); setGalleryUsed(false);
} catch (error) { } catch (error) {
console.log("清理缓存错误", error); console.log("清理缓存错误", error);
// Even if cleanup fails, reset the state
setGalleryUsed(false); 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 () => { const handleChooseFromGallery = useCallback(async () => {
console.log("handleChooseFromGallery");
setShowImagePickerModal(false); setShowImagePickerModal(false);
// 等待模态窗关闭后再执行
setTimeout(async () => { setTimeout(async () => {
try { try {
// 请求相册权限 const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
const permissionResult =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permissionResult.status !== "granted") { if (permissionResult.status !== "granted") {
console.log("相册权限被拒绝"); console.log("相册权限被拒绝");
return; return;
} }
// 打开相册
const result = await ImagePicker.launchImageLibraryAsync({ const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images, mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true, allowsEditing: true,
@ -682,32 +731,24 @@ export const HomeScreen = () => {
}); });
if (!result.canceled && result.assets && result.assets.length > 0) { if (!result.canceled && result.assets && result.assets.length > 0) {
console.log("相册选择成功:", result.assets[0].uri);
await cleanupImagePickerCache(); await cleanupImagePickerCache();
navigation.navigate("ImageSearchResultScreen", { navigation.navigate("ImageSearchResultScreen", {
image: result.assets[0].uri, image: result.assets[0].uri,
type: 1, type: 1,
}); });
} }
} catch (error: any) { } catch (error) {
console.error("相册错误:", error); console.error("相册错误:", error);
// 出错时也清理缓存
await cleanupImagePickerCache(); await cleanupImagePickerCache();
} }
}, 500); }, 500);
}, [userStore.user]); }, []);
// 处理相机拍照 - 简化版本,不再需要处理galleryUsed
const handleTakePhoto = useCallback(async () => { const handleTakePhoto = useCallback(async () => {
console.log("handleTakePhoto");
setShowImagePickerModal(false); setShowImagePickerModal(false);
// 等待模态窗关闭后再执行
setTimeout(async () => { setTimeout(async () => {
try { try {
const permissionResult = const permissionResult = await ImagePicker.requestCameraPermissionsAsync();
await ImagePicker.requestCameraPermissionsAsync();
if (permissionResult.status !== "granted") { if (permissionResult.status !== "granted") {
console.log("相机权限被拒绝"); console.log("相机权限被拒绝");
return; return;
@ -721,68 +762,34 @@ export const HomeScreen = () => {
}); });
if (!result.canceled && result.assets && result.assets.length > 0) { if (!result.canceled && result.assets && result.assets.length > 0) {
console.log("拍照成功:", result.assets[0].uri);
// 使用后清理缓存
await cleanupImagePickerCache(); await cleanupImagePickerCache();
// 将图片URI转换为FormData
navigation.navigate("ImageSearchResultScreen", { navigation.navigate("ImageSearchResultScreen", {
image: result.assets[0].uri, image: result.assets[0].uri,
type: 1, type: 1,
}); });
} }
} catch (error: any) { } catch (error) {
console.error("相机错误:", error); console.error("相机错误:", error);
// 出错时也清理缓存
await cleanupImagePickerCache(); await cleanupImagePickerCache();
} }
}, 500); }, 500);
}, [userStore.user]); }, []);
// 重置应用状态函数
const resetAppState = useCallback(() => { const resetAppState = useCallback(() => {
// 重置标记
setGalleryUsed(false); setGalleryUsed(false);
// 清理缓存
cleanupImagePickerCache(); cleanupImagePickerCache();
// 提示用户
Alert.alert("已重置", "现在您可以使用相机功能了"); Alert.alert("已重置", "现在您可以使用相机功能了");
}, []); }, []);
// 修改加载更多函数
const loadMore = () => {
if (!hasMore || isLoadingMore) {
console.log('不加载更多:', { hasMore, isLoadingMore });
return;
}
console.log('开始加载更多, 当前页码:', currentPage);
setLoadingPlaceholders(10); // 设置10个占位符
getProductData(true);
};
// 修改滚动到底部处理函数
const handleEndReached = () => {
console.log('触发加载更多');
loadMore();
};
// 修改渲染函数
const renderItem = ({ item, index }: { item: Product; index: number }) => { const renderItem = ({ item, index }: { item: Product; index: number }) => {
// 如果是占位符
if (index >= products.length && index < products.length + loadingPlaceholders) { if (index >= products.length && index < products.length + loadingPlaceholders) {
return <ProductSkeleton />; return <ProductSkeleton />;
} }
return renderProductItem({ item }); return renderProductItem({ item });
}; };
// 渲染列表头部内容
const renderHeader = () => ( const renderHeader = () => (
<> <>
{/* 轮播图 */}
<View style={styles.swiperContainer}> <View style={styles.swiperContainer}>
<Carousel <Carousel
loop loop
@ -815,19 +822,6 @@ export const HomeScreen = () => {
</TouchableOpacity> </TouchableOpacity>
)} )}
/> />
{/* 自定义指示器 */}
{/* <View style={styles.indicatorContainer}>
{data.map((_, index) => (
<View
key={index}
style={[
styles.indicator,
activeIndex === index ? styles.activeIndicator : styles.inactiveIndicator,
]}
/>
))}
</View> */}
{/* 搜索栏 - 定位在轮播图上方 */}
<View style={styles.searchOverlay}> <View style={styles.searchOverlay}>
<TouchableOpacity <TouchableOpacity
style={styles.searchBar} style={styles.searchBar}
@ -845,9 +839,7 @@ export const HomeScreen = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
{/* 内容区域 */}
<View style={styles.bannerContainer}> <View style={styles.bannerContainer}>
{/* 左侧区域 - 上下两个 */}
<View style={styles.leftContainer}> <View style={styles.leftContainer}>
<TouchableOpacity <TouchableOpacity
style={styles.leftTopItem} style={styles.leftTopItem}
@ -868,7 +860,6 @@ export const HomeScreen = () => {
/> />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* 右侧区域 - 一个 */}
<TouchableOpacity <TouchableOpacity
style={styles.rightContainer} style={styles.rightContainer}
onPress={navigateToInquiry} onPress={navigateToInquiry}
@ -894,16 +885,14 @@ export const HomeScreen = () => {
key={index} key={index}
style={[ style={[
styles.categoryItem, styles.categoryItem,
selectedHorizontalCategory === category && selectedHorizontalCategory === category && styles.categoryItemActive,
styles.categoryItemActive,
]} ]}
onPress={() => setSelectedHorizontalCategory(category)} onPress={() => setSelectedHorizontalCategory(category)}
> >
<Text <Text
style={[ style={[
styles.categoryText, styles.categoryText,
selectedHorizontalCategory === category && selectedHorizontalCategory === category && styles.categoryTextActive,
styles.categoryTextActive,
]} ]}
> >
{category} {category}
@ -924,7 +913,6 @@ export const HomeScreen = () => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
{/* Subcategory Content */}
{selectedHorizontalCategory && {selectedHorizontalCategory &&
categoryContent[selectedHorizontalCategory] && categoryContent[selectedHorizontalCategory] &&
categoryContent[selectedHorizontalCategory].length > 0 ? ( categoryContent[selectedHorizontalCategory].length > 0 ? (
@ -983,7 +971,7 @@ export const HomeScreen = () => {
backgroundColor: "transparent", backgroundColor: "transparent",
}} }}
ListHeaderComponent={renderHeader} ListHeaderComponent={renderHeader}
onEndReached={handleEndReached} onEndReached={handleLoadMore}
onEndReachedThreshold={3} onEndReachedThreshold={3}
ListFooterComponent={() => ( ListFooterComponent={() => (
!hasMore && !loadingPlaceholders ? ( !hasMore && !loadingPlaceholders ? (
@ -995,7 +983,7 @@ export const HomeScreen = () => {
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={refreshing} refreshing={refreshing}
onRefresh={onRefresh} onRefresh={handleRefresh}
colors={["#ff5100"]} colors={["#ff5100"]}
tintColor="#ff5100" tintColor="#ff5100"
progressBackgroundColor="transparent" progressBackgroundColor="transparent"
@ -1004,7 +992,6 @@ export const HomeScreen = () => {
/> />
)} )}
{/* Categories Modal */}
<Modal <Modal
visible={showCategoryModal} visible={showCategoryModal}
animationType="slide" animationType="slide"
@ -1046,8 +1033,7 @@ export const HomeScreen = () => {
<Text <Text
style={[ style={[
styles.categoryModalText, styles.categoryModalText,
selectedCategory === category && selectedCategory === category && styles.selectedCategoryText,
styles.selectedCategoryText,
]} ]}
> >
{category} {category}
@ -1062,7 +1048,6 @@ export const HomeScreen = () => {
</View> </View>
</Modal> </Modal>
{/* Image Picker Modal */}
<Modal <Modal
visible={showImagePickerModal} visible={showImagePickerModal}
animationType="slide" animationType="slide"
@ -1076,7 +1061,6 @@ export const HomeScreen = () => {
> >
<View style={styles.imagePickerContent}> <View style={styles.imagePickerContent}>
{!galleryUsed ? ( {!galleryUsed ? (
// 正常状态,显示相机选项
<TouchableOpacity <TouchableOpacity
style={styles.imagePickerOption} style={styles.imagePickerOption}
onPress={handleTakePhoto} onPress={handleTakePhoto}
@ -1089,7 +1073,6 @@ export const HomeScreen = () => {
<Text style={styles.imagePickerText}></Text> <Text style={styles.imagePickerText}></Text>
</TouchableOpacity> </TouchableOpacity>
) : ( ) : (
// 已使用相册状态,显示重置选项
<TouchableOpacity <TouchableOpacity
style={styles.imagePickerOption} style={styles.imagePickerOption}
onPress={resetAppState} onPress={resetAppState}

386
app/screens/SearchResultScreen.tsx

@ -13,6 +13,8 @@ import {
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
ScrollView, ScrollView,
RefreshControl,
Animated,
} from "react-native"; } from "react-native";
import Ionicons from "@expo/vector-icons/Ionicons"; import Ionicons from "@expo/vector-icons/Ionicons";
import { useNavigation, useRoute } from "@react-navigation/native"; import { useNavigation, useRoute } from "@react-navigation/native";
@ -99,17 +101,90 @@ const LazyImage = React.memo(
} }
); );
// 产品骨架屏组件 - 用于加载状态 // 产品骨架屏组件 - 用于加载状态
const ProductSkeleton = React.memo(() => ( const ProductSkeleton = React.memo(() => {
// 创建动画值
const shimmerAnim = useRef(new Animated.Value(0)).current;
// 设置动画效果
useEffect(() => {
const shimmerAnimation = Animated.loop(
Animated.timing(shimmerAnim, {
toValue: 1,
duration: 1500,
useNativeDriver: true,
})
);
shimmerAnimation.start();
return () => {
shimmerAnimation.stop();
};
}, []);
// 定义动画插值
const shimmerTranslate = shimmerAnim.interpolate({
inputRange: [0, 1],
outputRange: [-200, 200],
});
return (
<View style={styles.productCard}> <View style={styles.productCard}>
<View style={[styles.productImageContainer, styles.imagePlaceholder]} /> <View style={[styles.productImageContainer, styles.imagePlaceholder]}>
<Animated.View
style={[
styles.shimmer,
{
transform: [{ translateX: shimmerTranslate }],
},
]}
/>
</View>
<View style={styles.productInfo}> <View style={styles.productInfo}>
<View style={[styles.skeletonText, { width: '90%', height: 16, marginBottom: 8 }]} /> <View style={[styles.skeletonText, { width: '90%', height: 16, marginBottom: 8 }]}>
<View style={[styles.skeletonText, { width: '70%', height: 16, marginBottom: 8 }]} /> <Animated.View
<View style={[styles.skeletonText, { width: '40%', height: 24, marginBottom: 4 }]} /> style={[
<View style={[styles.skeletonText, { width: '30%', height: 12 }]} /> styles.shimmer,
{
transform: [{ translateX: shimmerTranslate }],
},
]}
/>
</View>
<View style={[styles.skeletonText, { width: '70%', height: 16, marginBottom: 8 }]}>
<Animated.View
style={[
styles.shimmer,
{
transform: [{ translateX: shimmerTranslate }],
},
]}
/>
</View> </View>
<View style={[styles.skeletonText, { width: '40%', height: 24, marginBottom: 4 }]}>
<Animated.View
style={[
styles.shimmer,
{
transform: [{ translateX: shimmerTranslate }],
},
]}
/>
</View> </View>
)); <View style={[styles.skeletonText, { width: '30%', height: 12 }]}>
<Animated.View
style={[
styles.shimmer,
{
transform: [{ translateX: shimmerTranslate }],
},
]}
/>
</View>
</View>
</View>
);
});
// 产品项组件 - 使用React.memo优化渲染 // 产品项组件 - 使用React.memo优化渲染
const ProductItem = React.memo( const ProductItem = React.memo(
({ ({
@ -127,10 +202,9 @@ const ProductItem = React.memo(
style={styles.productCard} style={styles.productCard}
onPress={() => onPress(product)} onPress={() => onPress(product)}
activeOpacity={0.7} activeOpacity={0.7}
key={product.offer_id}
> >
<View style={styles.productImageContainer}> <View style={styles.productImageContainer}>
{product.product_image_urls[0] ? ( {product.product_image_urls && product.product_image_urls[0] ? (
<LazyImage <LazyImage
uri={product.product_image_urls[0]} uri={product.product_image_urls[0]}
style={styles.productImage} style={styles.productImage}
@ -207,6 +281,9 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
language: "en", language: "en",
user_id: userStore.user.user_id, user_id: userStore.user.user_id,
}); });
const [currentPage, setCurrentPage] = useState(1);
const [refreshing, setRefreshing] = useState(false);
const lastLoadTime = useRef(0);
// 初始化搜索关键字 // 初始化搜索关键字
useEffect(() => { useEffect(() => {
if (route.params?.keyword) { if (route.params?.keyword) {
@ -217,7 +294,28 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
keyword: route.params.keyword, keyword: route.params.keyword,
}; };
setSearchParams(newParams); setSearchParams(newParams);
searchProducts(newParams); // 直接调用API,避免依赖循环
const fetchData = async () => {
try {
setLoading(true);
const res = await productApi.getSearchProducts(newParams);
setProducts(res.products);
setOriginalProducts(res.products);
setCurrentPage(1);
setHasMore(res.products.length === newParams.page_size);
} catch (error) {
console.error("Error fetching products:", error);
setProducts([]);
setOriginalProducts([]);
setHasMore(false);
} finally {
setLoading(false);
setTimeout(() => {
setShowSkeleton(false);
}, 300);
}
};
fetchData();
} }
if (route.params?.category_id) { if (route.params?.category_id) {
setShowSkeleton(true); setShowSkeleton(true);
@ -226,29 +324,79 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
category_id: route.params.category_id, category_id: route.params.category_id,
}; };
setSearchParams(newParams); setSearchParams(newParams);
searchProducts(newParams) // 直接调用API,避免依赖循环
const fetchData = async () => {
try {
setLoading(true);
const res = await productApi.getSearchProducts(newParams);
setProducts(res.products);
setOriginalProducts(res.products);
setCurrentPage(1);
setHasMore(res.products.length === newParams.page_size);
} catch (error) {
console.error("Error fetching products:", error);
setProducts([]);
setOriginalProducts([]);
setHasMore(false);
} finally {
setLoading(false);
setTimeout(() => {
setShowSkeleton(false);
}, 300);
}
};
fetchData();
} }
}, [route.params?.keyword, route.params?.category_id]); }, [route.params?.keyword, route.params?.category_id]);
// 搜索产品的API调用 // 搜索产品的API调用
const searchProducts = useCallback( const searchProducts = useCallback(
async (params: ProductParams, isLoadMore = false) => { async (params: ProductParams, isLoadMore = false) => {
// 防止重复请求 - 只在内部状态已经是加载中时阻止请求
if (isLoadMore && loadingMore) {
console.log('阻止重复加载更多请求');
return;
}
if (!isLoadMore && loading) {
console.log('阻止重复初始加载请求');
return;
}
console.log('发起请求:', isLoadMore ? '加载更多' : '初始加载', params);
if (!isLoadMore) { if (!isLoadMore) {
setLoading(true); setLoading(true);
setShowSkeleton(true); setShowSkeleton(true);
} else { } else {
setLoadingMore(true); setLoadingMore(true);
} }
try { try {
const res = await productApi.getSearchProducts(params); const res = await productApi.getSearchProducts(params);
console.log('请求成功, 获取商品数:', res.products.length);
if (isLoadMore) { if (isLoadMore) {
setProducts((prev) => [...prev, ...res.products]); // 使用回调方式更新,确保获取最新状态
setProducts(prev => {
// 过滤掉重复商品,避免闪烁
const newProducts = res.products.filter(
newProduct => !prev.some(
existingProduct => existingProduct.offer_id === newProduct.offer_id
)
);
return [...prev, ...newProducts];
});
setCurrentPage(prev => prev + 1);
} else { } else {
setProducts(res.products); setProducts(res.products);
// 保存原始排序的数据,以便默认排序时恢复 // 保存原始排序的数据,以便默认排序时恢复
setOriginalProducts(res.products); setOriginalProducts(res.products);
setCurrentPage(1);
} }
// 如果返回的数据少于页面大小,说明没有更多数据了 // 如果返回的数据少于页面大小,说明没有更多数据了
setHasMore(res.products.length === params.page_size); setHasMore(res.products.length === params.page_size);
return res;
} catch (error) { } catch (error) {
console.error("Error searching products:", error); console.error("Error searching products:", error);
// 发生错误时,设置hasMore为false,防止继续加载 // 发生错误时,设置hasMore为false,防止继续加载
@ -258,19 +406,23 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
setProducts([]); setProducts([]);
setOriginalProducts([]); setOriginalProducts([]);
} }
throw error;
} finally { } finally {
setLoading(false); if (isLoadMore) {
// 延迟清除加载状态,让视觉过渡更平滑
setTimeout(() => {
setLoadingMore(false); setLoadingMore(false);
}, 300);
// Add a short delay before hiding skeletons for smoother transition } else {
if (!isLoadMore) { setLoading(false);
// 添加延迟以使骨架屏过渡更平滑
setTimeout(() => { setTimeout(() => {
setShowSkeleton(false); setShowSkeleton(false);
}, 300); }, 300);
} }
} }
}, },
[] [loading, loadingMore]
); );
// 处理搜索提交 // 处理搜索提交
const handleSearch = useCallback(() => { const handleSearch = useCallback(() => {
@ -324,23 +476,34 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
), ),
[searchText, t] [searchText, t]
); );
// 渲染产品项 // 渲染商品项
const renderProductItem = useCallback( const renderItem = useCallback(({ item, index }: { item: any; index: number }) => {
({ item }: { item: Product }) => ( // 处理骨架屏项
if (item && item.isLoadingSkeleton) {
return <ProductSkeleton />;
}
// 处理空白占位项
if (!item) {
// 显示透明的占位视图
return <View style={[styles.productCard, { backgroundColor: 'transparent', elevation: 0, borderWidth: 0 }]} />;
}
// 渲染正常商品项
return (
<ProductItem <ProductItem
product={item} product={item}
onPress={handleProductPress} onPress={handleProductPress}
t={t} t={t}
userStore={userStore} userStore={userStore}
/> />
),
[handleProductPress, t, userStore]
); );
}, [handleProductPress, t, userStore]);
// 创建产品列表项的key提取器 // 创建产品列表项的key提取器
const keyExtractor = useCallback( const keyExtractor = useCallback((item: any, index: number) => {
(item: Product, index: number) => `${item.offer_id}-${index}`, if (!item) return `empty-${index}`;
[] return `${item.offer_id || 'item'}-${index}`;
); }, []);
// 处理排序 // 处理排序
const handleSort = useCallback( const handleSort = useCallback(
(field: "price" | "time", order: "asc" | "desc") => { (field: "price" | "time", order: "asc" | "desc") => {
@ -370,35 +533,121 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
); );
// 处理加载更多 // 处理加载更多
const handleLoadMore = useCallback(() => { const handleLoadMore = useCallback(() => {
if (loading || !hasMore || loadingMore) return; // 简化条件判断,直接仿照HomeScreen的处理方式
if (!hasMore || loadingMore) {
return;
}
// 添加时间防抖,避免频繁触发
const now = Date.now();
if (now - lastLoadTime.current < 500) { // 500ms内不重复触发
return;
}
lastLoadTime.current = now;
// 标记为加载中
setLoadingMore(true); setLoadingMore(true);
const newParams = {
// 请求参数
const loadMoreParams = {
...searchParams, ...searchParams,
page: searchParams.page + 1, page: currentPage + 1,
}; };
setSearchParams(newParams);
searchProducts(newParams, true); // 获取下一页数据
}, [loading, hasMore, loadingMore, searchParams, searchProducts]); productApi.getSearchProducts(loadMoreParams)
.then(res => {
// 使用回调更新,确保获取最新状态
setProducts(prev => {
// 过滤掉重复商品
const newProducts = res.products.filter(
newProduct => !prev.some(
existingProduct => existingProduct.offer_id === newProduct.offer_id
)
);
return [...prev, ...newProducts];
});
setCurrentPage(prev => prev + 1);
setHasMore(res.products.length === loadMoreParams.page_size);
})
.catch(error => {
console.error("加载更多失败:", error);
})
.finally(() => {
// 延迟结束加载状态,给用户更好的体验
setTimeout(() => {
setLoadingMore(false);
}, 300);
});
}, [hasMore, loadingMore, searchParams, currentPage]);
// 处理下拉刷新
const handleRefresh = useCallback(() => {
if (refreshing) return;
setRefreshing(true);
const refreshParams = {
...searchParams,
page: 1,
};
// 直接调用API避免状态冲突
productApi.getSearchProducts(refreshParams)
.then(res => {
// 保留原有商品列表,先显示再更新
setProducts(res.products);
setOriginalProducts(res.products);
setCurrentPage(1);
setHasMore(res.products.length === refreshParams.page_size);
})
.catch(error => {
console.error("刷新失败:", error);
// 不清空产品列表,保持用户体验
})
.finally(() => {
// 延迟结束刷新状态,让过渡更平滑
setTimeout(() => {
setRefreshing(false);
}, 300);
});
}, [refreshing, searchParams]);
// 渲染底部加载指示器 // 渲染底部加载指示器
const renderFooter = useCallback(() => { const renderFooter = useCallback(() => {
if (!hasMore) // 加载中状态显示骨架屏
if (loadingMore) {
return ( return (
<View style={styles.footerContainer}> <View style={{
<Text style={styles.footerText}>{t("noMoreData")}</Text> flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 8,
width: '100%'
}}>
<View style={{ width: '48%' }}>
<ProductSkeleton />
</View>
<View style={{ width: '48%' }}>
<ProductSkeleton />
</View>
</View> </View>
); );
if (loadingMore) }
// 没有更多数据时显示提示
if (!hasMore) {
return ( return (
<View style={styles.footerContainer}> <View style={styles.footerContainer}>
<ActivityIndicator size="small" color="#0066FF" /> <Text style={styles.footerText}>{t("noMoreData")}</Text>
<Text style={styles.footerText}>{t("loadingMore")}</Text>
</View> </View>
); );
}
return <View style={styles.footerSpace} />; return <View style={styles.footerSpace} />;
}, [loadingMore, hasMore, t]); }, [hasMore, loadingMore, t]);
// 处理滚动事件 // 处理滚动事件
const handleScroll = useCallback((event: any) => { const handleScroll = useCallback((event: any) => {
const offsetY = event.nativeEvent.contentOffset.y; const offsetY = event.nativeEvent.contentOffset.y;
// 当滚动超过屏幕高度的一半时显示回到顶部按钮 // 当滚动超过屏幕高度的一半时显示回到顶部按钮
setShowBackToTop(offsetY > 300); setShowBackToTop(offsetY > 300);
}, []); }, []);
@ -443,7 +692,7 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
); );
// 渲染骨架屏网格 // 渲染骨架屏网格
const renderSkeletonGrid = useCallback(() => { const renderSkeletonGrid = useCallback(() => {
// Create an array of items for the skeleton grid // 创建一个骨架屏数组
const skeletonArray = Array(8).fill(null); const skeletonArray = Array(8).fill(null);
return ( return (
@ -459,6 +708,20 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
</View> </View>
); );
}, []); }, []);
// 确保产品列表包含偶数条目,防止最后一个产品占满整行
const ensureEvenItems = useCallback((): (Product | any)[] => {
// 如果商品数量为奇数
if (products.length % 2 !== 0) {
// 加载更多时使用骨架屏替代空白占位符
if (loadingMore) {
return [...products, { isLoadingSkeleton: true, tempId: 'loadingPlaceholder' }];
}
// 非加载状态时使用空白占位符
return [...products, null];
}
return products;
}, [products, loadingMore]);
return ( return (
<SafeAreaView style={styles.safeArea}> <SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" /> <StatusBar barStyle="dark-content" backgroundColor="#fff" />
@ -699,23 +962,36 @@ export const SearchResultScreen = ({ route, navigation }: SearchResultScreenProp
<> <>
<FlatList <FlatList
ref={flatListRef} ref={flatListRef}
data={products} data={ensureEvenItems() as any}
renderItem={renderProductItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
numColumns={2} numColumns={2}
contentContainerStyle={styles.productGrid} columnWrapperStyle={styles.productColumnWrapper}
contentContainerStyle={{
paddingBottom: 15,
backgroundColor: "transparent",
}}
ListEmptyComponent={renderEmptyList} ListEmptyComponent={renderEmptyList}
ListFooterComponent={renderFooter} ListFooterComponent={renderFooter}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
initialNumToRender={4} initialNumToRender={8}
maxToRenderPerBatch={8} maxToRenderPerBatch={10}
windowSize={3} windowSize={5}
removeClippedSubviews={Platform.OS !== "web"} removeClippedSubviews={Platform.OS !== "web"}
updateCellsBatchingPeriod={50} updateCellsBatchingPeriod={50}
onEndReached={handleLoadMore} onEndReached={handleLoadMore}
onEndReachedThreshold={0.5} onEndReachedThreshold={3}
onScroll={handleScroll} onScroll={handleScroll}
scrollEventThrottle={16} scrollEventThrottle={160}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
colors={["#0066FF"]}
tintColor="#0066FF"
progressBackgroundColor="transparent"
/>
}
/> />
{showBackToTop && ( {showBackToTop && (
<TouchableOpacity <TouchableOpacity
@ -1080,5 +1356,19 @@ const styles = StyleSheet.create({
skeletonText: { skeletonText: {
backgroundColor: '#EAEAEA', backgroundColor: '#EAEAEA',
borderRadius: 4, borderRadius: 4,
overflow: "hidden",
position: "relative",
},
shimmer: {
width: "30%",
height: "100%",
backgroundColor: "rgba(255, 255, 255, 0.3)",
position: "absolute",
top: 0,
left: 0,
},
productColumnWrapper: {
justifyContent: 'space-between',
paddingHorizontal: 8,
}, },
}); });

8
app/services/api/productApi.ts

@ -184,12 +184,20 @@ export type Products = Product[]
export type Similars = similar[] export type Similars = similar[]
export interface HotTerms {
terms: string[];
}
// 搜索商品 // 搜索商品
export const productApi = { export const productApi = {
// 搜索商品 // 搜索商品
getSearchProducts: (params: ProductParams) => { getSearchProducts: (params: ProductParams) => {
return apiService.get<products>('/api/search/', params); return apiService.get<products>('/api/search/', params);
}, },
// 获取热门搜索词
getHotTerms: () => {
return apiService.get<HotTerms>('/api/search/hot-terms/');
},
// 获取商品详情 // 获取商品详情
getProductDetail: (offer_id: string, user_id?: number) => { getProductDetail: (offer_id: string, user_id?: number) => {
const url = user_id ? `/api/products/${offer_id}/?user_id=${user_id}` : `/api/products/${offer_id}/`; const url = user_id ? `/api/products/${offer_id}/?user_id=${user_id}` : `/api/products/${offer_id}/`;

955
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save