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转换为base64
const uriToBase64 = async (uri: string): Promise => {
try {
console.log("开始转换图片为base64", uri);
const imageUri = Platform.OS === "ios" ? uri.replace("file://", "") : uri;
const imageFetchResponse = await fetch(imageUri);
const imageBlob = await imageFetchResponse.blob();
// 转换为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("图片转换base64出错:", error);
throw error;
}
};
// 搜索图片
const searchByImage = async (uri: string) => {
if (!uri) {
console.log("没有有效的图片URI");
setLoading(false);
setShowSkeleton(false);
return;
}
try {
console.log("开始搜索图片:", uri);
console.log("用户信息:", userStore.user);
setLoading(true);
setShowSkeleton(true);
const base64String = await uriToBase64(uri);
const userId = userStore.user?.user_id || null;
const data = {
image_base64: base64String,
user_id: userId,
};
console.log("调用图片搜索API,用户ID:", userId);
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: 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);
// 重置状态,确保每次都是新的开始
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,
},
});