You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1010 lines
28 KiB
1010 lines
28 KiB
import React, { useState, useEffect, useCallback, useRef } from "react"; |
|
import { |
|
View, |
|
Text, |
|
StyleSheet, |
|
FlatList, |
|
Image, |
|
TouchableOpacity, |
|
TextInput, |
|
SafeAreaView, |
|
StatusBar, |
|
ActivityIndicator, |
|
KeyboardAvoidingView, |
|
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, |
|
ProductParams, |
|
type Product, |
|
} 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"; |
|
// @ts-ignore |
|
import vipIcon from "../../assets/vip.png"; |
|
|
|
|
|
// 图标组件 - 使用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; |
|
}; |
|
|
|
// 懒加载图片组件 |
|
const LazyImage = React.memo( |
|
({ |
|
uri, |
|
style, |
|
resizeMode, |
|
}: { |
|
uri: string; |
|
style: any; |
|
resizeMode: any; |
|
}) => { |
|
const [isVisible, setIsVisible] = useState(false); |
|
const [hasError, setHasError] = useState(false); |
|
|
|
// // 缩略图处理 - 为原图创建更低质量的缩略版本以更快加载 |
|
// const getThumbnailUrl = useCallback((originalUrl: string) => { |
|
// // 如果有可能,使用CDN参数来获取更小的图片 |
|
// // 这里是一个简单的实现,实际上需要根据具体的CDN服务来调整 |
|
// if (originalUrl.includes('?')) { |
|
// return `${originalUrl}&quality=10&width=100`; |
|
// } |
|
// return `${originalUrl}?quality=10&width=100`; |
|
// }, []); |
|
|
|
const onError = useCallback(() => { |
|
setHasError(true); |
|
}, []); |
|
|
|
// 使用IntersectionObserver的替代方案,在组件挂载时显示图片 |
|
useEffect(() => { |
|
// 延迟一小段时间后开始加载图片 |
|
setIsVisible(true); |
|
}, []); |
|
|
|
return ( |
|
<View style={[style, { backgroundColor: "#f9f9f9", overflow: "hidden" }]}> |
|
{hasError && ( |
|
<View |
|
style={[ |
|
style, |
|
{ |
|
justifyContent: "center", |
|
alignItems: "center", |
|
backgroundColor: "#f5f5f5", |
|
}, |
|
]} |
|
> |
|
<IconComponent name="image-outline" size={24} color="#999" /> |
|
<Text |
|
style={{ |
|
fontSize: isSmallScreen ? 12 : 14, |
|
color: "#999", |
|
marginTop: 4, |
|
}} |
|
> |
|
加载失败 |
|
</Text> |
|
</View> |
|
)} |
|
|
|
{isVisible && !hasError && ( |
|
<Image |
|
source={{ uri }} |
|
style={style} |
|
resizeMode={resizeMode} |
|
onError={onError} |
|
/> |
|
)} |
|
</View> |
|
); |
|
} |
|
); |
|
|
|
// 产品项组件 - 使用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] ? ( |
|
<LazyImage |
|
uri={product.product_image_urls[0]} |
|
style={styles.productImage} |
|
resizeMode="cover" |
|
/> |
|
) : ( |
|
<Text style={styles.placeholderText}>product picture</Text> |
|
)} |
|
<Image |
|
source={vipIcon} |
|
style={styles.vipIcon} |
|
/> |
|
</View> |
|
{/* 产品分类 */} |
|
<View style={styles.productInfo}> |
|
<Text style={styles.categoryText} numberOfLines={2}> |
|
{product.subject_trans} |
|
</Text> |
|
{/* 价格信息 */} |
|
<View style={styles.priceRow}> |
|
<Text style={styles.currentPrice}> |
|
{product?.min_price?.toFixed(0)}{" "} |
|
<Text style={styles.currency}>FCFA</Text> |
|
</Text> |
|
<Text style={styles.originalPrice}> |
|
3000<Text style={styles.currencySmall}>FCFA</Text> |
|
</Text> |
|
</View> |
|
{/* 销售量 */} |
|
<Text style={styles.productSales}> |
|
{product.sold_out}+ {t("monthlySales")} |
|
</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 [originalProducts, setOriginalProducts] = useState<Product[]>([]); |
|
const [loading, setLoading] = useState(true); |
|
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 [showBackToTop, setShowBackToTop] = useState(false); |
|
const flatListRef = useRef<FlatList>(null); |
|
const [activeTab, setActiveTab] = useState<"default" | "volume" | "price">( |
|
"default" |
|
); |
|
|
|
const [searchParams, setSearchParams] = useState<ProductParams>({ |
|
keyword: route.params?.keyword || "", |
|
page: 1, |
|
page_size: 20, |
|
sort_order: "desc", |
|
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); |
|
// 保存原始排序的数据,以便默认排序时恢复 |
|
setOriginalProducts(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); |
|
// 重置到默认标签 |
|
setActiveTab("default"); |
|
|
|
const newParams = { |
|
...searchParams, |
|
keyword: searchText.trim(), |
|
page: 1, // 重置到第一页 |
|
}; |
|
setSearchParams(newParams); |
|
searchProducts(newParams); |
|
} |
|
}, [searchText, 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] |
|
); |
|
|
|
// 创建产品列表项的key提取器 |
|
const keyExtractor = useCallback( |
|
(item: Product) => String(item.offer_id), |
|
[] |
|
); |
|
|
|
// 处理排序 |
|
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 }); |
|
}, []); |
|
|
|
// 处理标签切换 |
|
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] |
|
); |
|
|
|
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}> |
|
|
|
<Svg width="11" height="18" viewBox="0 0 11 18" fill="none"> |
|
<Path |
|
d="M8.52018 17.1171L10.0867 15.6172L3.19348 8.93139L10.2127 2.37801L8.67501 0.848572L0.0893813 8.90185L8.52018 17.1171Z" |
|
fill={"black"} // 动态修改颜色 |
|
/> |
|
</Svg> |
|
</TouchableOpacity> |
|
<View style={styles.searchBar}> |
|
<View style={{marginRight: 8,marginLeft: 4}}> |
|
<SearchIcon color="#373737" size={20} /> |
|
</View> |
|
<TextInput |
|
style={styles.searchInput} |
|
placeholder={t("searchProducts")} |
|
placeholderTextColor="#999" |
|
value={searchText} |
|
onChangeText={setSearchText} |
|
returnKeyType="search" |
|
onSubmitEditing={handleSearch} |
|
/> |
|
{searchText.length > 0 && ( |
|
<TouchableOpacity |
|
onPress={() => setSearchText("")} |
|
style={styles.clearButton} |
|
> |
|
<IconComponent name="close-circle" size={18} color="#999" /> |
|
</TouchableOpacity> |
|
)} |
|
</View> |
|
<TouchableOpacity style={styles.searchButton} onPress={handleSearch}> |
|
<Text style={styles.searchButtonText}>{t("cancel")}</Text> |
|
</TouchableOpacity> |
|
</View> |
|
|
|
{/* 标签筛选 */} |
|
<View style={styles.tabContainer}> |
|
<TouchableOpacity |
|
style={[ |
|
styles.tabButton, |
|
activeTab === "default" && styles.activeTabButton, |
|
]} |
|
onPress={() => handleTabChange("default")} |
|
> |
|
<Text |
|
style={[ |
|
styles.tabText, |
|
activeTab === "default" && styles.activeTabText, |
|
]} |
|
> |
|
{t("default")} |
|
</Text> |
|
</TouchableOpacity> |
|
<TouchableOpacity |
|
style={[ |
|
styles.tabButton, |
|
activeTab === "volume" && styles.activeTabButton, |
|
]} |
|
onPress={() => handleTabChange("volume")} |
|
> |
|
<Text |
|
style={[ |
|
styles.tabText, |
|
activeTab === "volume" && styles.activeTabText, |
|
]} |
|
> |
|
{t("volume")} |
|
</Text> |
|
</TouchableOpacity> |
|
<TouchableOpacity |
|
style={[ |
|
styles.tabButton, |
|
activeTab === "price" && styles.activeTabButton, |
|
]} |
|
onPress={() => handleTabChange("price")} |
|
> |
|
<View style={styles.tabButtonContent}> |
|
<Text |
|
style={[ |
|
styles.tabText, |
|
activeTab === "price" && styles.activeTabText, |
|
]} |
|
> |
|
{t("price")} |
|
</Text> |
|
{activeTab === "price" && ( |
|
<View style={styles.tabIcon}> |
|
<IconComponent |
|
name={sortOrder === "desc" ? "chevron-down" : "chevron-up"} |
|
size={16} |
|
color="#000" |
|
/> |
|
</View> |
|
)} |
|
</View> |
|
</TouchableOpacity> |
|
</View> |
|
|
|
{/* 搜索结果 */} |
|
<View style={styles.resultsContainer}> |
|
{/* 搜索结果标题栏和排序选项 */} |
|
{isFilterVisible && ( |
|
<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={keyExtractor} |
|
numColumns={2} |
|
contentContainerStyle={styles.productGrid} |
|
ListEmptyComponent={renderEmptyList} |
|
ListFooterComponent={renderFooter} |
|
showsVerticalScrollIndicator={false} |
|
initialNumToRender={4} |
|
maxToRenderPerBatch={8} |
|
windowSize={3} |
|
removeClippedSubviews={Platform.OS !== "web"} |
|
updateCellsBatchingPeriod={50} |
|
onEndReached={handleLoadMore} |
|
onEndReachedThreshold={0.5} |
|
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: "#ffffff", |
|
}, |
|
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: 40, |
|
marginHorizontal: 8, |
|
position: "relative", |
|
}, |
|
searchInput: { |
|
flex: 1, |
|
marginLeft: 4, |
|
fontSize: isSmallScreen ? 14 : 16, |
|
color: "#333", |
|
height: 40, |
|
paddingRight: 32, |
|
}, |
|
clearButton: { |
|
position: "absolute", |
|
right: 8, |
|
top: "50%", |
|
transform: [{ translateY: -9 }], |
|
padding: 4, |
|
}, |
|
searchButton: { |
|
paddingVertical: 4, |
|
paddingHorizontal: 8, |
|
}, |
|
searchButtonText: { |
|
fontSize: isSmallScreen ? 14 : 16, |
|
color: "#333", |
|
fontWeight: "500", |
|
}, |
|
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: isSmallScreen ? 12 : 16, |
|
color: "#000", |
|
}, |
|
activeTabText: { |
|
color: "#0933a1", |
|
fontWeight: "bold", |
|
}, |
|
activeTabButton: { |
|
// borderBottomColor: "#0933a1", |
|
}, |
|
resultsContainer: { |
|
flex: 1, |
|
}, |
|
resultsHeader: { |
|
backgroundColor: "#fff", |
|
borderBottomWidth: 1, |
|
borderBottomColor: "#f0f0f0", |
|
paddingVertical: 8, |
|
}, |
|
loadingContainer: { |
|
flex: 1, |
|
justifyContent: "center", |
|
alignItems: "center", |
|
}, |
|
productGrid: { |
|
padding: 8, |
|
}, |
|
productCard: { |
|
flex: 1, |
|
margin: 8, |
|
backgroundColor: "#fff", |
|
borderRadius: 8, |
|
overflow: "hidden", |
|
}, |
|
productImageContainer: { |
|
height: 190, |
|
backgroundColor: "#f9f9f9", |
|
alignItems: "center", |
|
justifyContent: "center", |
|
}, |
|
productImage: { |
|
width: "100%", |
|
height: "100%", |
|
}, |
|
placeholderText: { |
|
color: "#999", |
|
fontSize: isSmallScreen ? 12 : 14, |
|
}, |
|
productInfo: { |
|
padding: 8, |
|
}, |
|
categoryText: { |
|
fontSize: isSmallScreen ? 12 : 14, |
|
color: "#000000", |
|
fontWeight: "600", |
|
marginBottom: 4, |
|
fontFamily: "PingFang SC", |
|
letterSpacing: 0, |
|
}, |
|
priceRow: { |
|
flexDirection: "row", |
|
alignItems: "baseline", |
|
marginBottom: 2, |
|
}, |
|
currentPrice: { |
|
fontSize: isSmallScreen ? 12 : 24, |
|
fontWeight: "600", |
|
color: "#ff6600", |
|
marginRight: 4, |
|
}, |
|
currency: { |
|
fontSize: isSmallScreen ? 12 : 14, |
|
fontWeight: "600", |
|
fontFamily: "PingFang SC", |
|
color: "#ff6600", |
|
}, |
|
originalPrice: { |
|
fontSize: isSmallScreen ? 10 : 14, |
|
color: "#999", |
|
textDecorationLine: "line-through", |
|
}, |
|
currencySmall: { |
|
fontSize: isSmallScreen ? 12 : 14, |
|
color: "#9a9a9a", |
|
fontWeight: "600", |
|
fontFamily: "PingFang SC", |
|
}, |
|
productSales: { |
|
fontSize: isSmallScreen ? 12 : 14, |
|
fontWeight: "600", |
|
fontFamily: "PingFang SC", |
|
color: "#7c7c7c", |
|
}, |
|
sortScrollView: { |
|
flexGrow: 0, |
|
}, |
|
sortGroup: { |
|
flexDirection: "row", |
|
alignItems: "center", |
|
paddingHorizontal: 16, |
|
}, |
|
sortLabel: { |
|
fontSize: isSmallScreen ? 12 : 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: isSmallScreen ? 12 : 14, |
|
color: "#666", |
|
}, |
|
sortButtonTextActive: { |
|
color: "#ff6600", |
|
fontWeight: "bold", |
|
}, |
|
sortDivider: { |
|
width: 1, |
|
height: 20, |
|
backgroundColor: "#e0e0e0", |
|
marginHorizontal: 16, |
|
}, |
|
footerContainer: { |
|
padding: 16, |
|
alignItems: "center", |
|
flexDirection: "row", |
|
justifyContent: "center", |
|
}, |
|
footerText: { |
|
fontSize: isSmallScreen ? 12 : 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", |
|
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: isSmallScreen ? 12 : 14, |
|
fontWeight: "bold", |
|
color: "#333", |
|
marginTop: 16, |
|
marginBottom: 8, |
|
}, |
|
emptySubtext: { |
|
fontSize: isSmallScreen ? 12 : 14, |
|
color: "#999", |
|
textAlign: "center", |
|
}, |
|
resultsTitle: { |
|
fontSize: isSmallScreen ? 12 : 14, |
|
fontWeight: "bold", |
|
color: "#333", |
|
flex: 1, |
|
}, |
|
resultsCount: { |
|
fontSize: isSmallScreen ? 12 : 14, |
|
color: "#999", |
|
}, |
|
filterButton: { |
|
marginLeft: 8, |
|
padding: 4, |
|
}, |
|
imagePlaceholder: { |
|
justifyContent: "center", |
|
alignItems: "center", |
|
backgroundColor: "#f5f5f5", |
|
}, |
|
vipIcon: { |
|
position: 'absolute', |
|
top: 0, |
|
right: 0, |
|
width: 66, |
|
height: 30, |
|
}, |
|
});
|
|
|