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
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", |
|
}, |
|
});
|
|
|