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.
 
 
 

1973 lines
57 KiB

import React, {
useCallback,
useState,
useRef,
useEffect,
useMemo,
} from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
FlatList,
InteractionManager,
Image,
ScrollView,
Modal,
RefreshControl,
Dimensions,
Animated,
Platform,
StatusBar,
SafeAreaView,
ViewStyle,
TextStyle,
ImageStyle,
Linking,
Alert,
} from "react-native";
import {
productApi,
ProductParams,
type Product,
} from "../services/api/productApi";
import Carousel from "react-native-reanimated-carousel";
import Ionicons from "@expo/vector-icons/Ionicons";
import { useNavigation } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useTranslation } from "react-i18next";
import widthUtils from "../utils/widthUtils";
import DownArrowIcon from "../components/DownArrowIcon";
import { LinearGradient } from "expo-linear-gradient";
import fontSize from "../utils/fontsizeUtils";
import CloseIcon from "../components/CloseIcon";
import CheckmarkIcon from "../components/CheckmarkIcon";
import { getSubjectTransLanguage } from "../utils/languageUtils";
import useUserStore from "../store/user";
import * as ImagePicker from "expo-image-picker";
import * as FileSystem from "expo-file-system";
import { useGlobalStore } from "../store/useGlobalStore";
import { getCurrentLanguage } from "../i18n";
import { eventBus } from "../utils/eventBus";
// 为图标定义类型
type IconProps = {
name: string;
size: number;
color: string;
};
// 图标组件辅助函数 - 使用React.memo优化渲染
const IconComponent = React.memo(({ name, size, color }: IconProps) => {
const Icon = Ionicons as any;
return <Icon name={name} size={size} color={color} />;
});
type SubcategoryItem = {
id: string | number;
title: string;
icon: string;
};
type CategoryContentType = {
[key: string]: SubcategoryItem[];
};
// 懒加载图片组件 - 改进版本
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 (
<View style={[style, { overflow: "hidden" }]}>
{/* Show placeholder while image is loading */}
{!isLoaded && !hasError && (
<View
style={[
style,
styles.imagePlaceholder,
{ position: "absolute", zIndex: 1 },
]}
/>
)}
{/* Show error state if image failed to load */}
{hasError && (
<View
style={[
style,
styles.imagePlaceholder,
{ position: "absolute", zIndex: 1 },
]}
>
<IconComponent name="image-outline" size={24} color="#999" />
<Text
style={{ fontSize: fontSize(12), color: "#999", marginTop: 4 }}
>
</Text>
</View>
)}
{/* Actual image */}
<Image
source={{ uri }}
style={[style, { opacity: isLoaded ? 1 : 0 }]}
resizeMode={resizeMode}
onLoad={onLoad}
onError={onError}
/>
</View>
);
}
);
// 产品骨架屏组件 - 用于加载状态
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.beautyProductCard1}>
<View style={styles.skeletonImage}>
<Animated.View
style={[
styles.shimmer,
{
transform: [{ translateX: shimmerTranslate }],
},
]}
/>
</View>
<View style={styles.beautyProductCard}>
<View style={styles.skeletonTitle}>
<Animated.View
style={[
styles.shimmer,
{
transform: [{ translateX: shimmerTranslate }],
},
]}
/>
</View>
<View style={[styles.skeletonTitle, { width: "60%" }]}>
<Animated.View
style={[
styles.shimmer,
{
transform: [{ translateX: shimmerTranslate }],
},
]}
/>
</View>
<View style={styles.beautyProductInfoRow}>
<View style={styles.flexRowCentered}>
<View style={styles.skeletonPrice}>
<Animated.View
style={[
styles.shimmer,
{
transform: [{ translateX: shimmerTranslate }],
},
]}
/>
</View>
</View>
</View>
<View style={styles.skeletonSales}>
<Animated.View
style={[
styles.shimmer,
{
transform: [{ translateX: shimmerTranslate }],
},
]}
/>
</View>
</View>
</View>
);
});
// Define the styles type to fix TypeScript errors
type StylesType = {
safeArea: ViewStyle;
safeAreaContent: ViewStyle;
container: ViewStyle;
swpImg: ImageStyle;
searchOverlay: ViewStyle;
searchBar: ViewStyle;
searchPlaceholder: TextStyle;
cameraButton: ViewStyle;
bannerContainer: ViewStyle;
leftContainer: ViewStyle;
leftTopItem: ViewStyle;
leftBottomItem: ViewStyle;
rightContainer: ViewStyle;
bannerIcon: ImageStyle;
bigbannerIcon: ImageStyle;
category: ViewStyle;
categoryScrollContainer: ViewStyle;
categoryScroll: ViewStyle;
categoryItem: ViewStyle;
categoryItemActive: ViewStyle;
categoryText: TextStyle;
categoryTextActive: TextStyle;
swiperContainer: ViewStyle;
swiper: ViewStyle;
dot: ViewStyle;
activeDot: ViewStyle;
slide: ViewStyle;
slideImage: ImageStyle;
fadeGradient: ViewStyle;
categoryArrowContainer: ViewStyle;
modalOverlay: ViewStyle;
modalContent: ViewStyle;
modalHeader: ViewStyle;
modalTitleContainer: ViewStyle;
modalTitle: TextStyle;
closeButton: ViewStyle;
closeButtonText: TextStyle;
modalScrollView: ViewStyle;
categoryModalItem: ViewStyle;
categoryModalText: TextStyle;
selectedCategoryText: TextStyle;
subcategoryContainer: ViewStyle;
subcategoryScroll: ViewStyle;
subcategoryContent: ViewStyle;
subcategoryItem: ViewStyle;
subcategoryImagePlaceholder: ViewStyle;
subcategoryText: TextStyle;
productContainer: ViewStyle;
productCardList: ViewStyle;
productCardGroup: ViewStyle;
beautyProductCard1: ViewStyle;
beautyCardContainer1: ViewStyle;
vipButtonContainer: ViewStyle;
vipButton: ViewStyle;
vipButtonText: TextStyle;
vipLabelBold: TextStyle;
beautyProductCard: ViewStyle;
beautyProductTitle: TextStyle;
beautyProductInfoRow: ViewStyle;
flexRowCentered: ViewStyle;
priceContainer: ViewStyle;
highlightedText: TextStyle;
highlightedText1: TextStyle;
priceContainer1: ViewStyle;
priceLabel1: TextStyle;
beautySalesInfo: TextStyle;
indicatorContainer: ViewStyle;
indicator: ViewStyle;
activeIndicator: ViewStyle;
inactiveIndicator: ViewStyle;
skeletonContainer: ViewStyle;
skeletonImage: ViewStyle;
skeletonTitle: ViewStyle;
skeletonPrice: ViewStyle;
skeletonSales: ViewStyle;
shimmer: ViewStyle;
imagePlaceholder: ViewStyle;
productImage: ImageStyle;
imagePickerOverlay: ViewStyle;
imagePickerContent: ViewStyle;
imagePickerOption: ViewStyle;
imagePickerText: TextStyle;
imagePickerDivider: ViewStyle;
imagePickerCancelButton: ViewStyle;
imagePickerCancelText: TextStyle;
};
// 轮播图组件 - 独立提取,避免重复渲染
const CarouselBanner = React.memo(
({ onCameraPress }: { onCameraPress: () => void }) => {
const screenWidth = Dimensions.get("window").width;
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const { t } = useTranslation();
const [currentIndex, setCurrentIndex] = useState(0);
const data = useMemo(
() => [
{
imgUrl: require("../../assets/img/banner en (5)_compressed.png"),
add: "TikTokScreen",
},
{
imgUrl: require("../../assets/img/banner en (3)_compressed.png"),
add: "MemberIntroduction",
},
{
imgUrl: require("../../assets/img/banner en (4)_compressed.png"),
add: "CompanyScreen",
},
],
[]
);
const navigateToSearch = useCallback(() => {
InteractionManager.runAfterInteractions(() => {
navigation.navigate("Search");
});
}, [navigation]);
const handleBannerPress = useCallback(
(screenName: string) => {
navigation.navigate(screenName);
},
[navigation]
);
const onSnapToItem = useCallback((index: number) => {
setCurrentIndex(index);
}, []);
return (
<View style={styles.swiperContainer}>
<Carousel
loop
width={screenWidth}
data={data}
height={widthUtils(286, 286).height}
modeConfig={{
parallaxScrollingScale: 0.9,
parallaxScrollingOffset: 50,
}}
onSnapToItem={onSnapToItem}
renderItem={({ item }) => (
<TouchableOpacity
onPress={() => handleBannerPress(item.add)}
key={item.imgUrl}
activeOpacity={1}
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#f2f2f2",
borderRadius: 0,
overflow: "hidden",
}}
>
<Image
source={item.imgUrl}
style={{ width: "100%", height: "100%" }}
resizeMode="cover"
defaultSource={require("../../assets/img/banner en (3).png")}
/>
</TouchableOpacity>
)}
/>
{/* 轮播图指示灯 */}
<View style={styles.indicatorContainer}>
{data.map((_, index) => (
<View
key={index}
style={[
styles.indicator,
index === currentIndex
? styles.activeIndicator
: styles.inactiveIndicator,
]}
/>
))}
</View>
<View style={styles.searchOverlay}>
<TouchableOpacity
style={styles.searchBar}
activeOpacity={0.7}
onPress={navigateToSearch}
>
<IconComponent name="search-outline" size={20} color="#999" />
<Text style={styles.searchPlaceholder}>
{t("homePage.searchPlaceholder")}
</Text>
<TouchableOpacity
style={styles.cameraButton}
onPress={onCameraPress}
>
<IconComponent name="camera-outline" size={24} color="#333" />
</TouchableOpacity>
</TouchableOpacity>
</View>
</View>
);
},
(prevProps, nextProps) => {
// 自定义比较函数,只有当onCameraPress真正改变时才重新渲染
return prevProps.onCameraPress === nextProps.onCameraPress;
}
);
// 优化的产品项组件 - 使用React.memo避免不必要的重新渲染
const ProductItem = React.memo(
({
item,
onPress,
userStore,
t,
}: {
item: Product & { _uniqueId?: number };
onPress: (item: Product) => void;
userStore: any;
t: any;
}) => (
<TouchableOpacity
onPress={() => onPress(item)}
activeOpacity={0.9}
style={styles.beautyProductCard1}
>
<View style={styles.beautyCardContainer1}>
{item.product_image_urls && item.product_image_urls.length > 0 ? (
<LazyImage
uri={item.product_image_urls[0]}
style={styles.productImage}
resizeMode="cover"
/>
) : (
<View style={[styles.productImage as any, styles.imagePlaceholder]}>
<IconComponent name="image-outline" size={24} color="#999" />
</View>
)}
{userStore.user?.user_id && (
<View style={styles.vipButtonContainer}>
<TouchableOpacity style={styles.vipButton}>
<Text style={styles.vipButtonText}>VIP</Text>
<Text style={styles.vipLabelBold}>
{userStore.user?.vip_level}
</Text>
</TouchableOpacity>
</View>
)}
</View>
<View style={styles.beautyProductCard}>
<Text
style={styles.beautyProductTitle}
numberOfLines={2}
ellipsizeMode="tail"
>
{getSubjectTransLanguage(item) || item.subject_trans}
</Text>
<View style={styles.beautyProductInfoRow}>
<View style={styles.flexRowCentered}>
{userStore.user?.user_id && (
<Text style={styles.priceLabel1}>
{item.original_min_price || "0"}
{item.currency || "FCFA"}
</Text>
)}
<View style={styles.priceContainer}>
<Text style={styles.highlightedText}>
{item.min_price || "0"}
</Text>
<Text style={styles.highlightedText1}>
{item.currency || "FCFA"}
</Text>
</View>
</View>
</View>
<Text style={styles.beautySalesInfo}>
{item.sold_out || "0"}+ {t("homePage.sales")}
</Text>
</View>
</TouchableOpacity>
),
(prevProps, nextProps) => {
// 自定义比较函数,只有当关键属性改变时才重新渲染
return (
prevProps.item._uniqueId === nextProps.item._uniqueId &&
prevProps.item.offer_id === nextProps.item.offer_id &&
prevProps.item.min_price === nextProps.item.min_price &&
prevProps.userStore.user?.user_id === nextProps.userStore.user?.user_id
);
}
);
export const HomeScreen = () => {
const [activeIndex, setActiveIndex] = useState(0);
const screenWidth = Dimensions.get("window").width;
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const { t } = useTranslation();
const [showCategoryModal, setShowCategoryModal] = useState(false);
const [showImagePickerModal, setShowImagePickerModal] = useState(false);
const [selectedCategory, setSelectedCategory] = useState("Bijoux");
const [selectedHorizontalCategory, setSelectedHorizontalCategory] =
useState("Tous");
const userStore = useUserStore();
const { country, currency } = useGlobalStore();
const flatListRef = useRef<FlatList>(null);
const horizontalScrollRef = useRef<ScrollView>(null);
const [galleryUsed, setGalleryUsed] = useState(false);
const [hotTerms, setHotTerms] = useState<string[]>([]);
const [isLoadingHotTerms, setIsLoadingHotTerms] = useState(false);
// 直接在组件中实现分页加载逻辑
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);
// 添加用于去重的Set和唯一ID生成器
const seenProductIds = useRef(new Set<string>());
const productUniqueId = useRef(0);
// 添加防抖相关的ref
const loadMoreTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastLoadMoreTime = useRef(0);
// 添加请求状态管理
const isRequestInProgress = useRef(false);
const requestQueue = useRef<Array<() => void>>([]);
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 } : {}),
});
// 请求队列管理器
const executeNextRequest = useCallback(() => {
if (requestQueue.current.length > 0 && !isRequestInProgress.current) {
const nextRequest = requestQueue.current.shift();
if (nextRequest) {
nextRequest();
}
}
}, []);
// 添加请求到队列
const addToRequestQueue = useCallback((request: () => void) => {
requestQueue.current.push(request);
executeNextRequest();
}, [executeNextRequest]);
// 优化的产品数据处理函数
const processProductData = useCallback((newProducts: Product[]) => {
const uniqueProducts: Product[] = [];
newProducts.forEach(product => {
const productKey = `${product.offer_id}-${product.min_price}`;
if (!seenProductIds.current.has(productKey)) {
seenProductIds.current.add(productKey);
// 为每个产品添加唯一ID
const processedProduct = {
...product,
_uniqueId: ++productUniqueId.current
};
uniqueProducts.push(processedProduct);
}
});
return uniqueProducts;
}, []);
// 重置产品数据状态
const resetProductState = useCallback(() => {
setProducts([]);
setCurrentPage(1);
setHasMore(true);
seenProductIds.current.clear();
productUniqueId.current = 0;
}, []);
// 获取热门关键词并初始化产品列表
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 {
// 如果没有热门关键词,使用默认关键词"pen"
await fetchInitialProducts("pen");
}
} catch (error) {
console.error("初始化失败:", error);
// 出错时使用默认关键词
await fetchInitialProducts("pen");
}
};
initApp();
}, []);
// 监听设置变更事件,重新加载产品数据
useEffect(() => {
const handleRefreshSetting = async () => {
console.log("接收到refreshSetting事件,重新加载产品数据");
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,
language: getCurrentLanguage(), // 更新语言参数
}));
// 重置状态并重新加载
setLoading(true);
setProducts([]);
setCurrentPage(1);
setHasMore(true);
// 简化的产品加载逻辑
const initialParams = {
keyword: randomKeyword,
sort_order: "desc",
sort_by: "default",
language: getCurrentLanguage(),
page: 1,
page_size: 10,
};
const firstPageRes = await productApi.getSearchProducts(
initialParams
);
setProducts(firstPageRes.products);
setTotalItems(firstPageRes.total || 0);
setCurrentPage(1);
setHasMore(firstPageRes.products.length < (firstPageRes.total || 0));
setLoading(false);
} else {
// 如果没有热门关键词,使用默认关键词
setParams((prev) => ({
...prev,
language: getCurrentLanguage(), // 更新语言参数
}));
setLoading(true);
setProducts([]);
setCurrentPage(1);
setHasMore(true);
const initialParams = {
keyword: "pen",
sort_order: "desc",
sort_by: "default",
language: getCurrentLanguage(),
page: 1,
page_size: 10,
};
const firstPageRes = await productApi.getSearchProducts(
initialParams
);
setProducts(firstPageRes.products);
setTotalItems(firstPageRes.total || 0);
setCurrentPage(1);
setHasMore(firstPageRes.products.length < (firstPageRes.total || 0));
setLoading(false);
}
} catch (error) {
console.error("重新加载产品数据失败:", error);
setLoading(false);
}
};
// 添加事件监听器
eventBus.on("refreshSetting", handleRefreshSetting);
// 清理函数,移除事件监听器
return () => {
eventBus.off("refreshSetting", handleRefreshSetting);
};
}, []); // 空依赖数组,因为我们只需要在组件挂载时添加监听器
// 获取随机关键词
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);
resetProductState();
try {
// 第一页请求参数
const initialParams = {
...params,
keyword,
page: 1,
page_size: 10,
};
// 获取第一页数据
const firstPageRes = await productApi.getSearchProducts(initialParams);
const processedFirstPage = processProductData(firstPageRes.products);
setProducts(processedFirstPage);
setTotalItems(firstPageRes.total || 0);
if (hotTerms.length > 0) {
// 存储已使用的关键词,避免重复
const usedKeywords = new Set([keyword]);
// 创建获取唯一关键词的函数
const getUniqueKeyword = () => {
// 如果热门关键词数量不足,或者已经用完所有关键词,返回随机关键词
if (hotTerms.length <= usedKeywords.size || hotTerms.length <= 1) {
return hotTerms[Math.floor(Math.random() * hotTerms.length)];
}
// 尝试获取未使用过的关键词
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;
}
attempts++;
}
// 如果无法找到唯一关键词,返回随机关键词
return hotTerms[Math.floor(Math.random() * hotTerms.length)];
};
// 使用不同关键词加载额外的3页数据
const remainingRequests = Array.from(
{ length: 3 },
async (_, index) => {
// 获取唯一的随机关键词
const pageKeyword = getUniqueKeyword();
const pageParams = {
...params,
keyword: pageKeyword,
page: index + 2,
page_size: 10,
};
return productApi.getSearchProducts(pageParams);
}
);
// 并行获取额外数据
const additionalResults = await Promise.all(remainingRequests);
const additionalProducts = additionalResults.flatMap(
(result) => result.products
);
// 处理并合并额外的产品数据
const processedAdditionalProducts = processProductData(additionalProducts);
// 使用函数式更新避免闭包问题
setProducts(prev => [...prev, ...processedAdditionalProducts]);
setCurrentPage(4);
setHasMore(
processedFirstPage.length + processedAdditionalProducts.length <
(firstPageRes.total || 0)
);
} else {
// 如果没有热门关键词,只使用第一页数据
setCurrentPage(1);
setHasMore(processedFirstPage.length < (firstPageRes.total || 0));
}
} catch (error) {
console.error("获取产品数据失败:", error);
} finally {
setLoading(false);
}
},
[params, hotTerms, processProductData, resetProductState]
);
// 优化的加载更多函数,添加防抖机制
const handleLoadMore = useCallback(() => {
const now = Date.now();
// 防抖:如果距离上次加载时间小于1秒,则忽略
if (now - lastLoadMoreTime.current < 1000) {
return;
}
if (!hasMore || loadingMore || hotTerms.length === 0 || isRequestInProgress.current) return;
lastLoadMoreTime.current = now;
// 清除之前的定时器
if (loadMoreTimeoutRef.current) {
clearTimeout(loadMoreTimeoutRef.current);
}
// 设置新的定时器,延迟执行加载
loadMoreTimeoutRef.current = setTimeout(() => {
const loadMoreRequest = () => {
isRequestInProgress.current = true;
setLoadingMore(true);
setLoadingPlaceholders(10);
// 使用新的随机关键词
const newKeyword = getRandomKeyword();
// 准备请求参数
const loadMoreParams = {
...params,
keyword: newKeyword,
page: currentPage + 1,
page_size: 10,
};
// 获取下一页数据
productApi
.getSearchProducts(loadMoreParams)
.then((res) => {
const processedNewProducts = processProductData(res.products);
// 使用函数式更新
setProducts(prev => {
const newTotal = prev.length + processedNewProducts.length;
setHasMore(newTotal < (res.total || 0));
return [...prev, ...processedNewProducts];
});
setCurrentPage(prev => prev + 1);
})
.catch((error) => {
console.error("加载更多失败:", error);
})
.finally(() => {
setLoadingMore(false);
setLoadingPlaceholders(0);
isRequestInProgress.current = false;
executeNextRequest(); // 执行队列中的下一个请求
});
};
addToRequestQueue(loadMoreRequest);
}, 300); // 300ms延迟
}, [
hasMore,
loadingMore,
hotTerms,
getRandomKeyword,
params,
currentPage,
processProductData,
addToRequestQueue,
executeNextRequest,
]);
// 清理定时器
useEffect(() => {
return () => {
if (loadMoreTimeoutRef.current) {
clearTimeout(loadMoreTimeoutRef.current);
}
};
}, []);
// 刷新产品列表
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(
(item: Product) => {
InteractionManager.runAfterInteractions(() => {
navigation.navigate("ProductDetail", {
offer_id: item.offer_id,
searchKeyword: params.keyword,
price: item.min_price,
});
});
},
[navigation]
);
const categories = [
"Tous",
"Bijoux",
"Maison et Cuisine",
"Vêtements femme",
"Grandes tailles femme",
"Chaussures femme",
"Pyjamas et Sous-vête-ments",
"Accessoires beauté",
"Soins cheveux",
"Hygiène et Soins pour le corps",
"Maquillage",
];
const defaultSubcategories: SubcategoryItem[] = [
{ id: 1, title: "Jewelry", icon: "diamond-outline" },
{ id: 2, title: "Earrings", icon: "ear-outline" },
{ id: 3, title: "Bracelet", icon: "watch-outline" },
{ id: 4, title: "Jewelry Sets", icon: "gift-outline" },
{ id: 5, title: "Earrings", icon: "ear-outline" },
{ id: 6, title: "Bracelet", icon: "watch-outline" },
];
const categoryContent: CategoryContentType = {
Tous: [],
Bijoux: defaultSubcategories,
"Maison et Cuisine": defaultSubcategories,
"Vêtements femme": defaultSubcategories,
"Grandes tailles femme": defaultSubcategories,
"Chaussures femme": defaultSubcategories,
"Pyjamas et Sous-vête-ments": defaultSubcategories,
"Accessoires beauté": defaultSubcategories,
"Soins cheveux": defaultSubcategories,
"Hygiène et Soins pour le corps": defaultSubcategories,
Maquillage: defaultSubcategories,
};
useEffect(() => {
if (!categoryContent[selectedHorizontalCategory]) {
setSelectedHorizontalCategory("Tous");
}
}, [selectedHorizontalCategory]);
const navigateToSearch = useCallback(() => {
InteractionManager.runAfterInteractions(() => {
navigation.navigate("Search");
});
}, [navigation]);
const navigateToShippingDetails = useCallback(() => {
InteractionManager.runAfterInteractions(() => {
navigation.navigate("ShippingDetailsSection");
});
}, [navigation]);
const navigateToInquiry = useCallback(() => {
InteractionManager.runAfterInteractions(() => {
navigation.navigate("InquiryScreen");
});
}, [navigation]);
const scrollToCategory = (category: string) => {
const categoryIndex = categories.findIndex((c) => c === category);
if (categoryIndex !== -1 && horizontalScrollRef.current) {
const firstFourKeys = Object.keys(categoryContent).slice(
0,
categoryIndex - 1
);
let str = "";
firstFourKeys.forEach((key) => {
str += key;
});
horizontalScrollRef.current.scrollTo({
x: str.length * fontSize(16) + (categoryIndex - 1 + 17),
animated: true,
});
}
};
// 优化的产品项渲染函数
const renderProductItem = useCallback(({ item }: { item: Product & { _uniqueId?: number } }) => (
<ProductItem
item={item}
onPress={handleProductPress}
userStore={userStore}
t={t}
/>
), [handleProductPress, userStore, t]);
const renderSkeletonGrid = useCallback(() => {
const skeletonArray = Array(8).fill(null);
return (
<View style={styles.skeletonContainer}>
<FlatList
data={skeletonArray}
renderItem={() => <ProductSkeleton />}
keyExtractor={(_, index) => `skeleton-${index}`}
numColumns={2}
columnWrapperStyle={styles.productCardGroup}
scrollEnabled={false}
contentContainerStyle={{ paddingBottom: 15 }}
/>
</View>
);
}, []);
const cleanupImagePickerCache = async () => {
try {
if (Platform.OS === "web") {
console.log("Cache cleanup skipped on web platform");
setGalleryUsed(false);
return;
}
const cacheDir = `${FileSystem.cacheDirectory}ImagePicker`;
const dirInfo = await FileSystem.getInfoAsync(cacheDir);
if (dirInfo.exists && dirInfo.isDirectory) {
await FileSystem.deleteAsync(cacheDir, { idempotent: true });
console.log("已清理ImagePicker缓存:", cacheDir);
} else {
console.log("ImagePicker缓存目录不存在或不是目录,无需清理:", cacheDir);
}
console.log("已清理ImagePicker缓存");
setGalleryUsed(false);
} catch (error) {
console.log("清理缓存错误", error);
setGalleryUsed(false);
}
};
const handleChooseFromGallery = useCallback(async () => {
setShowImagePickerModal(false);
setTimeout(async () => {
try {
const permissionResult =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permissionResult.status !== "granted") {
console.log("相册权限被拒绝");
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
// await cleanupImagePickerCache();
navigation.navigate("ImageSearchResultScreen", {
image: result.assets[0].uri,
type: 1,
});
}
} catch (error) {
console.error("相册错误:", error);
await cleanupImagePickerCache();
}
}, 500);
}, []);
const handleTakePhoto = useCallback(async () => {
setShowImagePickerModal(false);
setTimeout(async () => {
try {
const permissionResult =
await ImagePicker.requestCameraPermissionsAsync();
if (permissionResult.status !== "granted") {
console.log("相机权限被拒绝");
return;
}
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
// await cleanupImagePickerCache();
navigation.navigate("ImageSearchResultScreen", {
image: result.assets[0].uri,
type: 1,
});
}
} catch (error) {
console.error("相机错误:", error);
await cleanupImagePickerCache();
}
}, 500);
}, []);
const resetAppState = useCallback(() => {
setGalleryUsed(false);
cleanupImagePickerCache();
Alert.alert("已重置", "现在您可以使用相机功能了");
}, []);
const handleCameraPress = useCallback(() => {
setShowImagePickerModal(true);
}, []);
const renderItem = useCallback(({ item, index }: { item: Product & { _uniqueId?: number } | null; index: number }) => {
if (
index >= products.length &&
index < products.length + loadingPlaceholders
) {
return <ProductSkeleton />;
}
if (!item) {
return <ProductSkeleton />;
}
return renderProductItem({ item });
}, [products.length, loadingPlaceholders, renderProductItem]);
const keyExtractor = useCallback((item: (Product & { _uniqueId?: number }) | null, index: number) => {
if (!item) {
return `placeholder-${index}-${Date.now()}`;
}
// 使用唯一ID作为key,确保不重复
return item._uniqueId ? `product-${item._uniqueId}` : `${item.offer_id}-${index}-${Date.now()}`;
}, []);
const flatListData = useMemo(() => {
const baseData = [...products];
if (loadingPlaceholders > 0) {
const placeholders = Array(loadingPlaceholders).fill(null);
return [...baseData, ...placeholders];
}
return baseData;
}, [products, loadingPlaceholders]);
const renderHeader = () => (
<>
<View style={styles.bannerContainer}>
<View style={styles.leftContainer}>
<TouchableOpacity
style={styles.leftTopItem}
onPress={navigateToShippingDetails}
>
<Image
source={require("../../assets/img/a_计算运费.png")}
style={styles.bannerIcon}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.leftBottomItem}
onPress={() => navigation.navigate("TikTokScreen")}
>
<Image
source={require("../../assets/img/a_tiktok.png")}
style={styles.bannerIcon}
/>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.rightContainer}
onPress={navigateToInquiry}
>
<Image
source={require("../../assets/img/a_VIP.png")}
style={styles.bigbannerIcon}
/>
</TouchableOpacity>
</View>
<View style={styles.category}>
<View style={styles.categoryScrollContainer}>
<ScrollView
bounces={false}
overScrollMode="never"
ref={horizontalScrollRef}
horizontal
showsHorizontalScrollIndicator={false}
style={styles.categoryScroll}
>
{categories.map((category, index) => (
<TouchableOpacity
key={index}
style={[
styles.categoryItem,
selectedHorizontalCategory === category &&
styles.categoryItemActive,
]}
onPress={() => setSelectedHorizontalCategory(category)}
>
<Text
style={[
styles.categoryText,
selectedHorizontalCategory === category &&
styles.categoryTextActive,
]}
>
{t(`homePage.${category.toLowerCase()}`)}
</Text>
</TouchableOpacity>
))}
</ScrollView>
<LinearGradient
colors={["rgba(255,255,255,0)", "rgba(255,255,255,1)"]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.fadeGradient}
/>
</View>
<View style={styles.categoryArrowContainer}>
<TouchableOpacity onPress={() => setShowCategoryModal(true)}>
<DownArrowIcon size={fontSize(18)} color="#666" rotation={360} />
</TouchableOpacity>
</View>
</View>
{selectedHorizontalCategory &&
categoryContent[selectedHorizontalCategory] &&
categoryContent[selectedHorizontalCategory].length > 0 ? (
<View style={styles.subcategoryContainer}>
<ScrollView
bounces={false}
overScrollMode="never"
horizontal
showsHorizontalScrollIndicator={false}
style={styles.subcategoryScroll}
contentContainerStyle={styles.subcategoryContent}
>
{categoryContent[selectedHorizontalCategory].map((item) => (
<TouchableOpacity
key={item.id}
style={styles.subcategoryItem}
onPress={() => {
// Handle subcategory selection
}}
>
<View style={styles.subcategoryImagePlaceholder}>
<IconComponent name={item.icon} size={24} color="#666" />
</View>
<Text style={styles.subcategoryText}>{item.title}</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
) : null}
</>
);
// 添加性能监控
const renderCount = useRef(0);
const lastRenderTime = useRef(Date.now());
useEffect(() => {
renderCount.current++;
const now = Date.now();
const timeSinceLastRender = now - lastRenderTime.current;
lastRenderTime.current = now;
if (__DEV__) {
console.log(`HomeScreen render #${renderCount.current}, time since last: ${timeSinceLastRender}ms`);
}
});
return (
<SafeAreaView style={styles.safeArea}>
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
<View style={styles.safeAreaContent}>
<View style={styles.container}>
{loading ? (
<ScrollView
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
colors={["#ff5100"]}
tintColor="#ff5100"
progressBackgroundColor="transparent"
/>
}
>
<CarouselBanner onCameraPress={handleCameraPress} />
{renderHeader()}
{renderSkeletonGrid()}
</ScrollView>
) : (
<FlatList
ref={flatListRef}
data={flatListData}
numColumns={2}
showsVerticalScrollIndicator={false}
columnWrapperStyle={styles.productCardGroup}
renderItem={renderItem}
keyExtractor={keyExtractor}
contentContainerStyle={{
paddingBottom: 15,
backgroundColor: "transparent",
}}
ListHeaderComponent={() => (
<>
<CarouselBanner onCameraPress={handleCameraPress} />
{renderHeader()}
</>
)}
onEndReached={handleLoadMore}
onEndReachedThreshold={3}
ListFooterComponent={() =>
!hasMore && !loadingPlaceholders ? (
<View style={{ padding: 10, alignItems: "center" }}>
<Text></Text>
</View>
) : loadingMore ? (
<View style={{ padding: 10, alignItems: "center" }}>
<Text>...</Text>
</View>
) : null
}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
colors={["#ff5100"]}
tintColor="#ff5100"
progressBackgroundColor="transparent"
/>
}
initialNumToRender={6}
maxToRenderPerBatch={8}
windowSize={10}
removeClippedSubviews={Platform.OS !== "web"}
updateCellsBatchingPeriod={50}
getItemLayout={undefined}
extraData={products.length}
/>
)}
<Modal
visible={showCategoryModal}
animationType="slide"
transparent={true}
onRequestClose={() => setShowCategoryModal(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<View style={styles.modalTitleContainer}>
<Ionicons name="flame-outline" size={24} color="#000" />
<Text style={styles.modalTitle}></Text>
</View>
<TouchableOpacity
style={styles.closeButton}
onPress={() => setShowCategoryModal(false)}
>
<Text style={styles.closeButtonText}>
<CloseIcon size={18} color="#000" />
</Text>
</TouchableOpacity>
</View>
<ScrollView
style={styles.modalScrollView}
bounces={false}
overScrollMode="never"
>
{categories.map((category, index) => (
<TouchableOpacity
key={index}
style={styles.categoryModalItem}
onPress={() => {
setSelectedCategory(category);
setSelectedHorizontalCategory(category);
setShowCategoryModal(false);
scrollToCategory(category);
}}
>
<Text
style={[
styles.categoryModalText,
selectedCategory === category &&
styles.selectedCategoryText,
]}
>
{category}
</Text>
{selectedCategory === category && (
<CheckmarkIcon size={fontSize(16)} color="#000" />
)}
</TouchableOpacity>
))}
</ScrollView>
</View>
</View>
</Modal>
<Modal
visible={showImagePickerModal}
animationType="slide"
transparent={true}
onRequestClose={() => setShowImagePickerModal(false)}
>
<TouchableOpacity
style={styles.imagePickerOverlay}
activeOpacity={1}
onPress={() => setShowImagePickerModal(false)}
>
<View style={styles.imagePickerContent}>
{!galleryUsed ? (
<TouchableOpacity
style={styles.imagePickerOption}
onPress={handleTakePhoto}
>
<IconComponent
name="camera-outline"
size={24}
color="#333"
/>
<Text style={styles.imagePickerText}>
{t("homePage.takePhoto")}
</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.imagePickerOption}
onPress={resetAppState}
>
<IconComponent
name="refresh-outline"
size={24}
color="#333"
/>
<Text style={styles.imagePickerText}></Text>
</TouchableOpacity>
)}
<View style={styles.imagePickerDivider} />
<TouchableOpacity
style={styles.imagePickerOption}
onPress={handleChooseFromGallery}
>
<IconComponent name="images-outline" size={24} color="#333" />
<Text style={styles.imagePickerText}>
{t("homePage.chooseFromGallery")}
</Text>
</TouchableOpacity>
<View style={styles.imagePickerDivider} />
<TouchableOpacity
style={styles.imagePickerCancelButton}
onPress={() => setShowImagePickerModal(false)}
>
<Text style={styles.imagePickerCancelText}>
{t("homePage.cancel")}
</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
</Modal>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create<StylesType>({
safeArea: {
flex: 1,
backgroundColor: "#fff",
},
safeAreaContent: {
flex: 1,
paddingTop: Platform.OS === "android" ? 0 : 0,
},
container: {
flex: 1,
backgroundColor: "#fff",
},
swpImg: {
width: "100%",
height: 180,
},
searchOverlay: {
position: "absolute",
top: widthUtils(20, 20).height,
left: 15,
right: 15,
zIndex: 10,
},
searchBar: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "rgba(255, 255, 255, 0.9)",
borderRadius: 20,
paddingHorizontal: 15,
height: 40,
shadowColor: "#000",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 3,
},
searchPlaceholder: {
flex: 1,
marginLeft: 8,
fontSize: fontSize(16),
},
cameraButton: {
padding: 5,
},
bannerContainer: {
flexDirection: "row",
marginTop: 20,
paddingHorizontal: 15,
width: "100%",
},
leftContainer: {
marginRight: "4%",
},
leftTopItem: {
height: widthUtils(90, 200).height,
width: widthUtils(90, 200).width,
borderRadius: 8,
padding: 0,
marginBottom: 10,
overflow: "hidden",
},
leftBottomItem: {
height: widthUtils(90, 200).height,
width: widthUtils(90, 200).width,
borderRadius: 8,
padding: 0,
overflow: "hidden",
},
rightContainer: {
width: widthUtils(190, 180).width,
height: widthUtils(190, 180).height,
borderRadius: 8,
padding: 0,
overflow: "hidden",
flex: 1,
},
bannerIcon: {
width: "100%",
height: "100%",
objectFit: "contain",
},
bigbannerIcon: {
width: "100%",
height: "100%",
objectFit: "contain",
},
category: {
width: "100%",
paddingVertical: 10,
flexDirection: "row",
alignItems: "center",
marginTop: 10,
backgroundColor: "#fff",
},
categoryScrollContainer: {
flex: 1,
position: "relative",
overflow: "hidden",
paddingRight: 55,
},
categoryScroll: {
paddingHorizontal: 15,
},
categoryItem: {
paddingHorizontal: 12,
paddingVertical: 8,
marginRight: 5,
},
categoryItemActive: {
borderBottomWidth: 2,
borderBottomColor: "#000",
},
categoryText: {
fontSize: fontSize(16),
color: "#747474",
fontFamily: "Alexandria",
fontWeight: "400",
},
categoryTextActive: {
color: "#000",
fontWeight: "500",
},
swiperContainer: {
width: "100%",
height: widthUtils(286, 430).height,
paddingHorizontal: 0,
},
swiper: {
width: "100%",
},
dot: {
backgroundColor: "rgba(255,255,255,0.5)",
width: 8,
height: 8,
borderRadius: 4,
marginLeft: 3,
marginRight: 3,
},
activeDot: {
backgroundColor: "#fff",
width: 20,
height: 8,
borderRadius: 4,
marginLeft: 3,
marginRight: 3,
},
slide: {
flex: 1,
justifyContent: "center",
alignItems: "center",
width: "100%",
},
slideImage: {
width: "100%",
height: "100%",
},
fadeGradient: {
position: "absolute",
right: 40,
top: 0,
bottom: 0,
width: 40,
zIndex: 1,
backgroundColor: "transparent",
},
categoryArrowContainer: {
position: "absolute",
right: 0,
paddingRight: 15,
height: "100%",
justifyContent: "center",
alignItems: "center",
zIndex: 2,
backgroundColor: "#fff",
},
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
justifyContent: "flex-end",
},
modalContent: {
backgroundColor: "#fff",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: "80%",
},
modalHeader: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
},
modalTitleContainer: {
flexDirection: "row",
alignItems: "center",
},
modalTitle: {
fontSize: fontSize(16),
fontWeight: "600",
marginLeft: 8,
},
closeButton: {
padding: 8,
},
closeButtonText: {
fontSize: fontSize(24),
color: "#000",
fontWeight: "300",
},
modalScrollView: {
padding: 16,
},
categoryModalItem: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 16,
},
categoryModalText: {
fontSize: fontSize(16),
color: "#666",
fontFamily: "Segoe UI",
fontWeight: "700",
},
selectedCategoryText: {
color: "#000",
fontWeight: "500",
},
subcategoryContainer: {
height: 100, // Fixed height for the subcategory container
marginTop: 15,
},
subcategoryScroll: {
flex: 1,
},
subcategoryContent: {
paddingHorizontal: 15,
},
subcategoryItem: {
alignItems: "center",
marginRight: 15,
width: 60,
},
subcategoryImagePlaceholder: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: "#f5f5f5",
justifyContent: "center",
alignItems: "center",
marginBottom: 8,
},
subcategoryText: {
fontSize: fontSize(12),
color: "#333",
textAlign: "center",
fontFamily: "Alexandria",
},
productContainer: {
flex: 1,
backgroundColor: "#fff",
},
productCardList: {
padding: 15,
paddingTop: 0,
},
productCardGroup: {
justifyContent: "space-between",
marginBottom: 15,
paddingHorizontal: 15,
},
beautyProductCard1: {
width: "48%",
},
beautyCardContainer1: {
flexDirection: "column",
alignItems: "flex-end",
justifyContent: "center",
width: "100%",
height: 160,
backgroundColor: "transparent",
borderRadius: 10,
overflow: "hidden",
position: "relative",
},
vipButtonContainer: {
position: "absolute",
top: 0,
right: 0,
zIndex: 2,
},
vipButton: {
width: widthUtils(30, 60).width,
height: widthUtils(30, 60).height,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#3b3b3b",
borderRadius: 10,
flexDirection: "row",
},
vipButtonText: {
fontStyle: "italic",
fontWeight: "900",
fontSize: fontSize(18),
color: "#f1c355",
},
vipLabelBold: {
fontStyle: "italic",
fontWeight: "900",
fontSize: fontSize(18),
color: "#f1c355",
},
beautyProductCard: {
marginTop: 9,
},
beautyProductTitle: {
fontSize: fontSize(14),
fontWeight: "600",
color: "black",
lineHeight: 18,
},
beautyProductInfoRow: {
flexDirection: "row",
alignItems: "center",
},
flexRowCentered: {},
priceContainer: {
flexDirection: "row",
},
highlightedText: {
fontWeight: "700",
fontSize: fontSize(24),
color: "#ff5100",
marginLeft: 2,
},
highlightedText1: {
fontWeight: "700",
fontSize: fontSize(14),
color: "#ff5100",
},
priceContainer1: {},
priceLabel1: {
fontSize: fontSize(12),
fontWeight: "600",
color: "#9a9a9a",
textDecorationLine: "line-through",
},
beautySalesInfo: {
marginTop: 6.75,
fontSize: fontSize(14),
fontWeight: "600",
color: "#7c7c7c",
},
indicatorContainer: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
position: "absolute",
bottom: 10,
left: 0,
right: 0,
},
indicator: {
marginHorizontal: 4,
borderRadius: 3,
},
activeIndicator: {
width: 14,
height: 6,
backgroundColor: "#fff",
},
inactiveIndicator: {
width: 6,
height: 6,
backgroundColor: "rgba(255, 255, 255, 0.5)",
},
// 骨架屏样式
skeletonContainer: {
paddingHorizontal: 0,
paddingTop: 0,
},
skeletonImage: {
width: "100%",
paddingBottom: "100%",
borderRadius: 10,
backgroundColor: "#e1e1e1",
overflow: "hidden",
position: "relative",
},
skeletonTitle: {
height: 16,
borderRadius: 4,
marginTop: 8,
marginBottom: 4,
width: "100%",
backgroundColor: "#e1e1e1",
overflow: "hidden",
position: "relative",
},
skeletonPrice: {
height: 24,
width: 80,
borderRadius: 4,
marginTop: 8,
backgroundColor: "#e1e1e1",
overflow: "hidden",
position: "relative",
},
skeletonSales: {
height: 14,
width: "40%",
borderRadius: 4,
marginTop: 8,
backgroundColor: "#e1e1e1",
overflow: "hidden",
position: "relative",
},
shimmer: {
width: "30%",
height: "100%",
backgroundColor: "rgba(255, 255, 255, 0.3)",
position: "absolute",
top: 0,
left: 0,
},
imagePlaceholder: {
backgroundColor: "#EAEAEA",
justifyContent: "center",
alignItems: "center",
borderRadius: 8,
},
productImage: {
width: "100%",
height: "100%",
borderRadius: 10,
},
// Image Picker Modal Styles
imagePickerOverlay: {
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
justifyContent: "flex-end",
},
imagePickerContent: {
backgroundColor: "#fff",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingTop: 20,
},
imagePickerOption: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 16,
paddingHorizontal: 20,
},
imagePickerText: {
fontSize: fontSize(16),
marginLeft: 12,
color: "#333",
},
imagePickerDivider: {
height: 1,
backgroundColor: "#f0f0f0",
marginHorizontal: 20,
},
imagePickerCancelButton: {
alignItems: "center",
paddingVertical: 16,
marginTop: 8,
borderTopWidth: 1,
borderTopColor: "#f0f0f0",
},
imagePickerCancelText: {
fontSize: fontSize(16),
color: "#999",
},
});