|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
|
|
import {
|
|
|
|
View,
|
|
|
|
Text,
|
|
|
|
StyleSheet,
|
|
|
|
FlatList,
|
|
|
|
Image,
|
|
|
|
TouchableOpacity,
|
|
|
|
TextInput,
|
|
|
|
SafeAreaView,
|
|
|
|
StatusBar,
|
|
|
|
ActivityIndicator,
|
|
|
|
KeyboardAvoidingView,
|
|
|
|
Platform,
|
|
|
|
Keyboard,
|
|
|
|
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,
|
|
|
|
ProductParams,
|
|
|
|
type Product,
|
|
|
|
} from "../services/api/productApi";
|
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
|
|
|
// 图标组件 - 使用React.memo优化渲染
|
|
|
|
const IconComponent = React.memo(
|
|
|
|
({ name, size, color }: { name: string; size: number; color: string }) => {
|
|
|
|
const Icon = Ionicons as any;
|
|
|
|
return <Icon name={name} size={size} color={color} />;
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
// 路由参数类型
|
|
|
|
type SearchResultRouteParams = {
|
|
|
|
keyword: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
// 产品项组件 - 使用React.memo优化渲染
|
|
|
|
const ProductItem = React.memo(
|
|
|
|
({
|
|
|
|
product,
|
|
|
|
onPress,
|
|
|
|
t,
|
|
|
|
}: {
|
|
|
|
product: Product;
|
|
|
|
onPress: (product: Product) => void;
|
|
|
|
t: any;
|
|
|
|
}) => (
|
|
|
|
<TouchableOpacity
|
|
|
|
style={styles.productCard}
|
|
|
|
onPress={() => onPress(product)}
|
|
|
|
activeOpacity={0.7}
|
|
|
|
>
|
|
|
|
<View style={styles.productImageContainer}>
|
|
|
|
{product.product_image_urls[0] ? (
|
|
|
|
<Image
|
|
|
|
source={{ uri: product.product_image_urls[0] }}
|
|
|
|
style={styles.productImage}
|
|
|
|
resizeMode="cover"
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<Text style={styles.placeholderText}>product picture</Text>
|
|
|
|
)}
|
|
|
|
</View>
|
|
|
|
{/* 价格 */}
|
|
|
|
<View style={styles.productInfo}>
|
|
|
|
<Text style={styles.productPrice}>
|
|
|
|
{product?.min_price?.toFixed(2)}
|
|
|
|
</Text>
|
|
|
|
{/* 产品标题 */}
|
|
|
|
<Text style={styles.productTitle} numberOfLines={2}>
|
|
|
|
{product.subject}
|
|
|
|
</Text>
|
|
|
|
{/* 销售量 */}
|
|
|
|
<Text style={styles.productSales}>
|
|
|
|
{t('monthlySales')}: {product.sold_out}
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
</TouchableOpacity>
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
export const SearchResultScreen = () => {
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const navigation = useNavigation<NativeStackNavigationProp<any>>();
|
|
|
|
const route =
|
|
|
|
useRoute<RouteProp<Record<string, SearchResultRouteParams>, string>>();
|
|
|
|
const [searchText, setSearchText] = useState("");
|
|
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [hasMore, setHasMore] = useState(true);
|
|
|
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
|
|
const [minPrice, setMinPrice] = useState<string>("");
|
|
|
|
const [maxPrice, setMaxPrice] = useState<string>("");
|
|
|
|
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
|
|
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc" | null>(null);
|
|
|
|
const [sortField, setSortField] = useState<"price" | "time">("price");
|
|
|
|
const [showBackToTop, setShowBackToTop] = useState(false);
|
|
|
|
const flatListRef = React.useRef<FlatList>(null);
|
|
|
|
|
|
|
|
const [searchParams, setSearchParams] = useState<ProductParams>({
|
|
|
|
keyword: route.params?.keyword || "",
|
|
|
|
page: 1,
|
|
|
|
page_size: 20,
|
|
|
|
sort_order: "desc",
|
|
|
|
max_price: null,
|
|
|
|
min_price: null,
|
|
|
|
category_id: null,
|
|
|
|
sort_by: "create_date",
|
|
|
|
});
|
|
|
|
|
|
|
|
// 初始化搜索关键字
|
|
|
|
useEffect(() => {
|
|
|
|
if (route.params?.keyword) {
|
|
|
|
setSearchText(route.params.keyword);
|
|
|
|
const newParams = {
|
|
|
|
...searchParams,
|
|
|
|
keyword: route.params.keyword,
|
|
|
|
};
|
|
|
|
setSearchParams(newParams);
|
|
|
|
searchProducts(newParams);
|
|
|
|
}
|
|
|
|
}, [route.params?.keyword]);
|
|
|
|
|
|
|
|
// 搜索产品的API调用
|
|
|
|
const searchProducts = useCallback(
|
|
|
|
async (params: ProductParams, isLoadMore = false) => {
|
|
|
|
if (!isLoadMore) setLoading(true);
|
|
|
|
try {
|
|
|
|
const res = await productApi.getSearchProducts(params);
|
|
|
|
if (isLoadMore) {
|
|
|
|
setProducts((prev) => [...prev, ...res.products]);
|
|
|
|
} else {
|
|
|
|
setProducts(res.products);
|
|
|
|
}
|
|
|
|
// 如果返回的数据少于页面大小,说明没有更多数据了
|
|
|
|
setHasMore(res.products.length === params.page_size);
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error searching products:", error);
|
|
|
|
} finally {
|
|
|
|
setLoading(false);
|
|
|
|
setLoadingMore(false);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[]
|
|
|
|
);
|
|
|
|
|
|
|
|
// 处理搜索提交
|
|
|
|
const handleSearch = useCallback(() => {
|
|
|
|
if (searchText.trim()) {
|
|
|
|
// 重置排序状态
|
|
|
|
setSortField("price");
|
|
|
|
setSortOrder(null);
|
|
|
|
|
|
|
|
const newParams = {
|
|
|
|
...searchParams,
|
|
|
|
keyword: searchText.trim(),
|
|
|
|
page: 1, // 重置到第一页
|
|
|
|
};
|
|
|
|
setSearchParams(newParams);
|
|
|
|
searchProducts(newParams);
|
|
|
|
}
|
|
|
|
}, [searchText, searchParams, searchProducts]);
|
|
|
|
|
|
|
|
// 处理价格筛选
|
|
|
|
const handlePriceFilter = useCallback(() => {
|
|
|
|
Keyboard.dismiss();
|
|
|
|
const newParams = { ...searchParams };
|
|
|
|
|
|
|
|
if (minPrice.trim()) {
|
|
|
|
newParams.min_price = parseFloat(minPrice);
|
|
|
|
} else {
|
|
|
|
newParams.min_price = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (maxPrice.trim()) {
|
|
|
|
newParams.max_price = parseFloat(maxPrice);
|
|
|
|
} else {
|
|
|
|
newParams.max_price = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
newParams.page = 1; // 重置到第一页
|
|
|
|
setSearchParams(newParams);
|
|
|
|
searchProducts(newParams);
|
|
|
|
console.log(newParams);
|
|
|
|
}, [minPrice, maxPrice, searchParams, searchProducts]);
|
|
|
|
|
|
|
|
// 重置价格筛选
|
|
|
|
const resetPriceFilter = useCallback(() => {
|
|
|
|
setMinPrice("");
|
|
|
|
setMaxPrice("");
|
|
|
|
const newParams = {
|
|
|
|
...searchParams,
|
|
|
|
min_price: null,
|
|
|
|
max_price: null,
|
|
|
|
page: 1,
|
|
|
|
};
|
|
|
|
setSearchParams(newParams);
|
|
|
|
searchProducts(newParams);
|
|
|
|
}, [searchParams, searchProducts]);
|
|
|
|
|
|
|
|
// 切换筛选器显示状态
|
|
|
|
const toggleFilter = useCallback(() => {
|
|
|
|
setIsFilterVisible(!isFilterVisible);
|
|
|
|
}, [isFilterVisible]);
|
|
|
|
|
|
|
|
// 处理点击产品
|
|
|
|
const handleProductPress = useCallback(
|
|
|
|
(product: Product) => {
|
|
|
|
// 导航到产品详情页,并传递产品信息
|
|
|
|
navigation.navigate("ProductDetail", {
|
|
|
|
offer_id: product.offer_id,
|
|
|
|
searchKeyword: searchText,
|
|
|
|
price: product.min_price,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[navigation, searchText]
|
|
|
|
);
|
|
|
|
|
|
|
|
// 返回上一页
|
|
|
|
const goBack = useCallback(() => {
|
|
|
|
navigation.goBack();
|
|
|
|
}, [navigation]);
|
|
|
|
|
|
|
|
// 渲染列表为空时的组件
|
|
|
|
const renderEmptyList = useCallback(
|
|
|
|
() => (
|
|
|
|
<View style={styles.emptyContainer}>
|
|
|
|
<IconComponent name="search-outline" size={48} color="#ccc" />
|
|
|
|
<Text style={styles.emptyText}>
|
|
|
|
{t('noResults')} "{searchText}"
|
|
|
|
</Text>
|
|
|
|
<Text style={styles.emptySubtext}>
|
|
|
|
{t('tryDifferentKeywords')}
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
),
|
|
|
|
[searchText, t]
|
|
|
|
);
|
|
|
|
|
|
|
|
// 渲染产品项
|
|
|
|
const renderProductItem = useCallback(
|
|
|
|
({ item }: { item: Product }) => (
|
|
|
|
<ProductItem product={item} onPress={handleProductPress} t={t} />
|
|
|
|
),
|
|
|
|
[handleProductPress, t]
|
|
|
|
);
|
|
|
|
|
|
|
|
// 处理排序
|
|
|
|
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 handleLoadMore = useCallback(() => {
|
|
|
|
if (loading || !hasMore || loadingMore) return;
|
|
|
|
|
|
|
|
setLoadingMore(true);
|
|
|
|
|
|
|
|
const newParams = {
|
|
|
|
...searchParams,
|
|
|
|
page: searchParams.page + 1,
|
|
|
|
};
|
|
|
|
setSearchParams(newParams);
|
|
|
|
searchProducts(newParams, true);
|
|
|
|
}, [loading, hasMore, loadingMore, searchParams, searchProducts]);
|
|
|
|
|
|
|
|
// 渲染底部加载指示器
|
|
|
|
const renderFooter = useCallback(() => {
|
|
|
|
if (!hasMore)
|
|
|
|
return (
|
|
|
|
<View style={styles.footerContainer}>
|
|
|
|
<Text style={styles.footerText}>{t('noMoreData')}</Text>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
|
|
|
|
if (loadingMore)
|
|
|
|
return (
|
|
|
|
<View style={styles.footerContainer}>
|
|
|
|
<ActivityIndicator size="small" color="#0066FF" />
|
|
|
|
<Text style={styles.footerText}>{t('loadingMore')}</Text>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
|
|
|
|
return <View style={styles.footerSpace} />;
|
|
|
|
}, [loadingMore, hasMore, t]);
|
|
|
|
|
|
|
|
// 处理滚动事件
|
|
|
|
const handleScroll = useCallback((event: any) => {
|
|
|
|
const offsetY = event.nativeEvent.contentOffset.y;
|
|
|
|
// 当滚动超过屏幕高度的一半时显示回到顶部按钮
|
|
|
|
setShowBackToTop(offsetY > 300);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
// 回到顶部
|
|
|
|
const scrollToTop = useCallback(() => {
|
|
|
|
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<SafeAreaView style={styles.safeArea}>
|
|
|
|
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
|
|
|
<KeyboardAvoidingView
|
|
|
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
|
|
style={styles.container}
|
|
|
|
>
|
|
|
|
{/* 搜索栏 */}
|
|
|
|
<View style={styles.searchHeader}>
|
|
|
|
<TouchableOpacity style={styles.backButton} onPress={goBack}>
|
|
|
|
<IconComponent name="arrow-back" size={24} color="#333" />
|
|
|
|
</TouchableOpacity>
|
|
|
|
<View style={styles.searchBar}>
|
|
|
|
<IconComponent name="search-outline" size={20} color="#999" />
|
|
|
|
<TextInput
|
|
|
|
style={styles.searchInput}
|
|
|
|
placeholder={t('searchProducts')}
|
|
|
|
placeholderTextColor="#999"
|
|
|
|
value={searchText}
|
|
|
|
onChangeText={setSearchText}
|
|
|
|
returnKeyType="search"
|
|
|
|
onSubmitEditing={handleSearch}
|
|
|
|
/>
|
|
|
|
{searchText.length > 0 && (
|
|
|
|
<TouchableOpacity onPress={() => setSearchText("")}>
|
|
|
|
<IconComponent name="close-circle" size={20} color="#999" />
|
|
|
|
</TouchableOpacity>
|
|
|
|
)}
|
|
|
|
</View>
|
|
|
|
<TouchableOpacity style={styles.filterButton} onPress={toggleFilter}>
|
|
|
|
<IconComponent
|
|
|
|
name={isFilterVisible ? "options" : "options-outline"}
|
|
|
|
size={24}
|
|
|
|
color="#333"
|
|
|
|
/>
|
|
|
|
</TouchableOpacity>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
{/* 搜索结果 */}
|
|
|
|
<View style={styles.resultsContainer}>
|
|
|
|
{/* 价格筛选器 */}
|
|
|
|
{isFilterVisible && (
|
|
|
|
<View style={styles.filterContainer}>
|
|
|
|
<View style={styles.priceFilterRow}>
|
|
|
|
<Text style={styles.filterLabel}>{t('priceRange')}:</Text>
|
|
|
|
<View style={styles.priceInputContainer}>
|
|
|
|
<TextInput
|
|
|
|
style={styles.priceInput}
|
|
|
|
placeholder={t('minPrice')}
|
|
|
|
placeholderTextColor="#999"
|
|
|
|
value={minPrice}
|
|
|
|
onChangeText={setMinPrice}
|
|
|
|
keyboardType="numeric"
|
|
|
|
returnKeyType="done"
|
|
|
|
/>
|
|
|
|
<Text style={styles.priceDivider}>-</Text>
|
|
|
|
<TextInput
|
|
|
|
style={styles.priceInput}
|
|
|
|
placeholder={t('maxPrice')}
|
|
|
|
placeholderTextColor="#999"
|
|
|
|
value={maxPrice}
|
|
|
|
onChangeText={setMaxPrice}
|
|
|
|
keyboardType="numeric"
|
|
|
|
returnKeyType="done"
|
|
|
|
/>
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
<View style={styles.filterButtons}>
|
|
|
|
<TouchableOpacity
|
|
|
|
style={styles.resetButton}
|
|
|
|
onPress={resetPriceFilter}
|
|
|
|
>
|
|
|
|
<Text style={styles.resetButtonText}>{t('reset')}</Text>
|
|
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
|
|
style={styles.applyButton}
|
|
|
|
onPress={handlePriceFilter}
|
|
|
|
>
|
|
|
|
<Text style={styles.applyButtonText}>{t('apply')}</Text>
|
|
|
|
</TouchableOpacity>
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* 搜索结果标题栏和排序选项 */}
|
|
|
|
<View style={styles.resultsHeader}>
|
|
|
|
<ScrollView
|
|
|
|
horizontal
|
|
|
|
showsHorizontalScrollIndicator={false}
|
|
|
|
style={styles.sortScrollView}
|
|
|
|
>
|
|
|
|
<View style={styles.sortGroup}>
|
|
|
|
<Text style={styles.sortLabel}>{t('price')}:</Text>
|
|
|
|
<View style={styles.sortButtons}>
|
|
|
|
<TouchableOpacity
|
|
|
|
style={[
|
|
|
|
styles.sortButton,
|
|
|
|
sortField === "price" && sortOrder === "asc"
|
|
|
|
? styles.sortButtonActive
|
|
|
|
: {},
|
|
|
|
]}
|
|
|
|
onPress={() => handleSort("price", "asc")}
|
|
|
|
>
|
|
|
|
<Text
|
|
|
|
style={[
|
|
|
|
styles.sortButtonText,
|
|
|
|
sortField === "price" && sortOrder === "asc"
|
|
|
|
? styles.sortButtonTextActive
|
|
|
|
: {},
|
|
|
|
]}
|
|
|
|
>
|
|
|
|
{t('lowToHigh')}
|
|
|
|
</Text>
|
|
|
|
{sortField === "price" && sortOrder === "asc" && (
|
|
|
|
<IconComponent
|
|
|
|
name="chevron-up"
|
|
|
|
size={16}
|
|
|
|
color="#ff6600"
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
|
|
style={[
|
|
|
|
styles.sortButton,
|
|
|
|
sortField === "price" && sortOrder === "desc"
|
|
|
|
? styles.sortButtonActive
|
|
|
|
: {},
|
|
|
|
]}
|
|
|
|
onPress={() => handleSort("price", "desc")}
|
|
|
|
>
|
|
|
|
<Text
|
|
|
|
style={[
|
|
|
|
styles.sortButtonText,
|
|
|
|
sortField === "price" && sortOrder === "desc"
|
|
|
|
? styles.sortButtonTextActive
|
|
|
|
: {},
|
|
|
|
]}
|
|
|
|
>
|
|
|
|
{t('highToLow')}
|
|
|
|
</Text>
|
|
|
|
{sortField === "price" && sortOrder === "desc" && (
|
|
|
|
<IconComponent
|
|
|
|
name="chevron-down"
|
|
|
|
size={16}
|
|
|
|
color="#ff6600"
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</TouchableOpacity>
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
<View style={styles.sortDivider} />
|
|
|
|
|
|
|
|
<View style={styles.sortGroup}>
|
|
|
|
<Text style={styles.sortLabel}>{t('time')}:</Text>
|
|
|
|
<View style={styles.sortButtons}>
|
|
|
|
<TouchableOpacity
|
|
|
|
style={[
|
|
|
|
styles.sortButton,
|
|
|
|
sortField === "time" && sortOrder === "asc"
|
|
|
|
? styles.sortButtonActive
|
|
|
|
: {},
|
|
|
|
]}
|
|
|
|
onPress={() => handleSort("time", "asc")}
|
|
|
|
>
|
|
|
|
<Text
|
|
|
|
style={[
|
|
|
|
styles.sortButtonText,
|
|
|
|
sortField === "time" && sortOrder === "asc"
|
|
|
|
? styles.sortButtonTextActive
|
|
|
|
: {},
|
|
|
|
]}
|
|
|
|
>
|
|
|
|
{t('oldest')}
|
|
|
|
</Text>
|
|
|
|
{sortField === "time" && sortOrder === "asc" && (
|
|
|
|
<IconComponent
|
|
|
|
name="chevron-up"
|
|
|
|
size={16}
|
|
|
|
color="#ff6600"
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
|
|
style={[
|
|
|
|
styles.sortButton,
|
|
|
|
sortField === "time" && sortOrder === "desc"
|
|
|
|
? styles.sortButtonActive
|
|
|
|
: {},
|
|
|
|
]}
|
|
|
|
onPress={() => handleSort("time", "desc")}
|
|
|
|
>
|
|
|
|
<Text
|
|
|
|
style={[
|
|
|
|
styles.sortButtonText,
|
|
|
|
sortField === "time" && sortOrder === "desc"
|
|
|
|
? styles.sortButtonTextActive
|
|
|
|
: {},
|
|
|
|
]}
|
|
|
|
>
|
|
|
|
{t('newest')}
|
|
|
|
</Text>
|
|
|
|
{sortField === "time" && sortOrder === "desc" && (
|
|
|
|
<IconComponent
|
|
|
|
name="chevron-down"
|
|
|
|
size={16}
|
|
|
|
color="#ff6600"
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</TouchableOpacity>
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
</ScrollView>
|
|
|
|
</View>
|
|
|
|
|
|
|
|
{/* 加载指示器 */}
|
|
|
|
{loading ? (
|
|
|
|
<View style={styles.loadingContainer}>
|
|
|
|
<ActivityIndicator size="large" color="#0066FF" />
|
|
|
|
</View>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
<FlatList
|
|
|
|
ref={flatListRef}
|
|
|
|
data={products}
|
|
|
|
renderItem={renderProductItem}
|
|
|
|
keyExtractor={(item) => String(item.offer_id)}
|
|
|
|
numColumns={2}
|
|
|
|
contentContainerStyle={styles.productGrid}
|
|
|
|
ListEmptyComponent={renderEmptyList}
|
|
|
|
ListFooterComponent={renderFooter}
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
initialNumToRender={8}
|
|
|
|
maxToRenderPerBatch={10}
|
|
|
|
windowSize={5}
|
|
|
|
removeClippedSubviews={true}
|
|
|
|
onEndReached={handleLoadMore}
|
|
|
|
onEndReachedThreshold={0.2}
|
|
|
|
onScroll={handleScroll}
|
|
|
|
scrollEventThrottle={16}
|
|
|
|
/>
|
|
|
|
{showBackToTop && (
|
|
|
|
<TouchableOpacity
|
|
|
|
style={styles.backToTopButton}
|
|
|
|
onPress={scrollToTop}
|
|
|
|
activeOpacity={0.7}
|
|
|
|
>
|
|
|
|
<IconComponent name="arrow-up" size={24} color="#fff" />
|
|
|
|
</TouchableOpacity>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</View>
|
|
|
|
</KeyboardAvoidingView>
|
|
|
|
</SafeAreaView>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
|
safeArea: {
|
|
|
|
flex: 1,
|
|
|
|
backgroundColor: "#ffffff",
|
|
|
|
},
|
|
|
|
container: {
|
|
|
|
flex: 1,
|
|
|
|
backgroundColor: "#f5f5f5",
|
|
|
|
},
|
|
|
|
searchHeader: {
|
|
|
|
flexDirection: "row",
|
|
|
|
alignItems: "center",
|
|
|
|
paddingHorizontal: 15,
|
|
|
|
paddingVertical: 10,
|
|
|
|
backgroundColor: "#fff",
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
borderBottomColor: "#f0f0f0",
|
|
|
|
},
|
|
|
|
backButton: {
|
|
|
|
marginRight: 10,
|
|
|
|
padding: 5,
|
|
|
|
},
|
|
|
|
searchBar: {
|
|
|
|
flex: 1,
|
|
|
|
flexDirection: "row",
|
|
|
|
alignItems: "center",
|
|
|
|
backgroundColor: "#f5f5f5",
|
|
|
|
borderRadius: 20,
|
|
|
|
paddingHorizontal: 15,
|
|
|
|
height: 40,
|
|
|
|
},
|
|
|
|
searchInput: {
|
|
|
|
flex: 1,
|
|
|
|
marginLeft: 8,
|
|
|
|
fontSize: 16,
|
|
|
|
color: "#333",
|
|
|
|
height: 40,
|
|
|
|
},
|
|
|
|
resultsContainer: {
|
|
|
|
flex: 1,
|
|
|
|
},
|
|
|
|
resultsHeader: {
|
|
|
|
backgroundColor: "#fff",
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
borderBottomColor: "#f0f0f0",
|
|
|
|
paddingVertical: 10,
|
|
|
|
},
|
|
|
|
resultsTitle: {
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: "bold",
|
|
|
|
color: "#333",
|
|
|
|
flex: 1,
|
|
|
|
},
|
|
|
|
resultsCount: {
|
|
|
|
fontSize: 14,
|
|
|
|
color: "#999",
|
|
|
|
},
|
|
|
|
loadingContainer: {
|
|
|
|
flex: 1,
|
|
|
|
justifyContent: "center",
|
|
|
|
alignItems: "center",
|
|
|
|
},
|
|
|
|
productGrid: {
|
|
|
|
padding: 8,
|
|
|
|
},
|
|
|
|
productCard: {
|
|
|
|
flex: 1,
|
|
|
|
margin: 8,
|
|
|
|
backgroundColor: "#fff",
|
|
|
|
borderRadius: 8,
|
|
|
|
overflow: "hidden",
|
|
|
|
elevation: 2, // Android shadow
|
|
|
|
shadowColor: "#000", // iOS shadow
|
|
|
|
shadowOffset: { width: 0, height: 1 },
|
|
|
|
shadowOpacity: 0.1,
|
|
|
|
shadowRadius: 2,
|
|
|
|
},
|
|
|
|
productImageContainer: {
|
|
|
|
height: 150,
|
|
|
|
backgroundColor: "#f9f9f9",
|
|
|
|
alignItems: "center",
|
|
|
|
justifyContent: "center",
|
|
|
|
},
|
|
|
|
productImage: {
|
|
|
|
width: "100%",
|
|
|
|
height: "100%",
|
|
|
|
},
|
|
|
|
placeholderText: {
|
|
|
|
color: "#999",
|
|
|
|
fontSize: 16,
|
|
|
|
},
|
|
|
|
productInfo: {
|
|
|
|
padding: 10,
|
|
|
|
},
|
|
|
|
productPrice: {
|
|
|
|
fontSize: 18,
|
|
|
|
fontWeight: "bold",
|
|
|
|
color: "#ff6600",
|
|
|
|
marginBottom: 5,
|
|
|
|
},
|
|
|
|
productTitle: {
|
|
|
|
fontSize: 14,
|
|
|
|
color: "#333",
|
|
|
|
marginBottom: 5,
|
|
|
|
},
|
|
|
|
productSales: {
|
|
|
|
fontSize: 12,
|
|
|
|
color: "#999",
|
|
|
|
},
|
|
|
|
emptyContainer: {
|
|
|
|
flex: 1,
|
|
|
|
minHeight: 300,
|
|
|
|
justifyContent: "center",
|
|
|
|
alignItems: "center",
|
|
|
|
padding: 20,
|
|
|
|
},
|
|
|
|
emptyText: {
|
|
|
|
fontSize: 16,
|
|
|
|
fontWeight: "bold",
|
|
|
|
color: "#333",
|
|
|
|
marginTop: 15,
|
|
|
|
marginBottom: 8,
|
|
|
|
},
|
|
|
|
emptySubtext: {
|
|
|
|
fontSize: 14,
|
|
|
|
color: "#999",
|
|
|
|
textAlign: "center",
|
|
|
|
},
|
|
|
|
filterButton: {
|
|
|
|
marginLeft: 10,
|
|
|
|
padding: 5,
|
|
|
|
},
|
|
|
|
filterContainer: {
|
|
|
|
backgroundColor: "#fff",
|
|
|
|
paddingHorizontal: 15,
|
|
|
|
paddingVertical: 10,
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
borderBottomColor: "#f0f0f0",
|
|
|
|
},
|
|
|
|
priceFilterRow: {
|
|
|
|
flexDirection: "row",
|
|
|
|
alignItems: "center",
|
|
|
|
marginBottom: 10,
|
|
|
|
},
|
|
|
|
filterLabel: {
|
|
|
|
fontSize: 14,
|
|
|
|
fontWeight: "bold",
|
|
|
|
color: "#333",
|
|
|
|
marginRight: 10,
|
|
|
|
},
|
|
|
|
priceInputContainer: {
|
|
|
|
flex: 1,
|
|
|
|
flexDirection: "row",
|
|
|
|
alignItems: "center",
|
|
|
|
},
|
|
|
|
priceInput: {
|
|
|
|
flex: 1,
|
|
|
|
height: 36,
|
|
|
|
backgroundColor: "#f5f5f5",
|
|
|
|
borderRadius: 4,
|
|
|
|
paddingHorizontal: 10,
|
|
|
|
fontSize: 14,
|
|
|
|
color: "#333",
|
|
|
|
},
|
|
|
|
priceDivider: {
|
|
|
|
marginHorizontal: 8,
|
|
|
|
color: "#333",
|
|
|
|
},
|
|
|
|
filterButtons: {
|
|
|
|
flexDirection: "row",
|
|
|
|
justifyContent: "flex-end",
|
|
|
|
},
|
|
|
|
resetButton: {
|
|
|
|
paddingHorizontal: 15,
|
|
|
|
paddingVertical: 6,
|
|
|
|
borderRadius: 4,
|
|
|
|
backgroundColor: "#f5f5f5",
|
|
|
|
marginRight: 10,
|
|
|
|
},
|
|
|
|
resetButtonText: {
|
|
|
|
fontSize: 14,
|
|
|
|
color: "#666",
|
|
|
|
},
|
|
|
|
applyButton: {
|
|
|
|
paddingHorizontal: 15,
|
|
|
|
paddingVertical: 6,
|
|
|
|
borderRadius: 4,
|
|
|
|
backgroundColor: "#0066FF",
|
|
|
|
},
|
|
|
|
applyButtonText: {
|
|
|
|
fontSize: 14,
|
|
|
|
color: "#fff",
|
|
|
|
fontWeight: "bold",
|
|
|
|
},
|
|
|
|
sortScrollView: {
|
|
|
|
flexGrow: 0,
|
|
|
|
},
|
|
|
|
sortGroup: {
|
|
|
|
flexDirection: "row",
|
|
|
|
alignItems: "center",
|
|
|
|
paddingHorizontal: 15,
|
|
|
|
},
|
|
|
|
sortLabel: {
|
|
|
|
fontSize: 14,
|
|
|
|
color: "#666",
|
|
|
|
marginRight: 8,
|
|
|
|
},
|
|
|
|
sortButtons: {
|
|
|
|
flexDirection: "row",
|
|
|
|
alignItems: "center",
|
|
|
|
},
|
|
|
|
sortButton: {
|
|
|
|
flexDirection: "row",
|
|
|
|
alignItems: "center",
|
|
|
|
paddingVertical: 4,
|
|
|
|
paddingHorizontal: 8,
|
|
|
|
borderRadius: 4,
|
|
|
|
marginLeft: 6,
|
|
|
|
borderWidth: 1,
|
|
|
|
borderColor: "#e0e0e0",
|
|
|
|
},
|
|
|
|
sortButtonActive: {
|
|
|
|
borderColor: "#ff6600",
|
|
|
|
backgroundColor: "#fff8f5",
|
|
|
|
},
|
|
|
|
sortButtonText: {
|
|
|
|
fontSize: 12,
|
|
|
|
color: "#666",
|
|
|
|
},
|
|
|
|
sortButtonTextActive: {
|
|
|
|
color: "#ff6600",
|
|
|
|
fontWeight: "bold",
|
|
|
|
},
|
|
|
|
sortDivider: {
|
|
|
|
width: 1,
|
|
|
|
height: 20,
|
|
|
|
backgroundColor: "#e0e0e0",
|
|
|
|
marginHorizontal: 15,
|
|
|
|
},
|
|
|
|
footerContainer: {
|
|
|
|
padding: 15,
|
|
|
|
alignItems: "center",
|
|
|
|
flexDirection: "row",
|
|
|
|
justifyContent: "center",
|
|
|
|
},
|
|
|
|
footerText: {
|
|
|
|
fontSize: 14,
|
|
|
|
color: "#666",
|
|
|
|
marginLeft: 8,
|
|
|
|
},
|
|
|
|
footerSpace: {
|
|
|
|
height: 20,
|
|
|
|
},
|
|
|
|
backToTopButton: {
|
|
|
|
position: "absolute",
|
|
|
|
bottom: 20,
|
|
|
|
right: 20,
|
|
|
|
width: 44,
|
|
|
|
height: 44,
|
|
|
|
borderRadius: 22,
|
|
|
|
backgroundColor: "#0066FF",
|
|
|
|
justifyContent: "center",
|
|
|
|
alignItems: "center",
|
|
|
|
elevation: 5,
|
|
|
|
shadowColor: "#000",
|
|
|
|
shadowOffset: { width: 0, height: 2 },
|
|
|
|
shadowOpacity: 0.3,
|
|
|
|
shadowRadius: 3,
|
|
|
|
},
|
|
|
|
});
|