|
|
|
import React, { useCallback, useState, useRef, useEffect } 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';
|
|
|
|
// 为图标定义类型
|
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
|
|
|
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 [refreshing, setRefreshing] = useState(false);
|
|
|
|
const [loading, setLoading] = useState(true); // 添加加载状态
|
|
|
|
const horizontalScrollRef = useRef<ScrollView>(null);
|
|
|
|
const userStore = useUserStore();
|
|
|
|
const [searchParams, setSearchParams] = useState<ProductParams>({
|
|
|
|
keyword: "pen",
|
|
|
|
page: 1,
|
|
|
|
page_size: 20,
|
|
|
|
sort_order: "desc",
|
|
|
|
sort_by: "default",
|
|
|
|
language: "en",
|
|
|
|
});
|
|
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
|
|
const flatListRef = useRef<FlatList>(null);
|
|
|
|
const data = [
|
|
|
|
{
|
|
|
|
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 [galleryUsed, setGalleryUsed] = useState(false); // 标记是否使用过相册
|
|
|
|
const getProductData = async () => {
|
|
|
|
setLoading(true); // 开始加载,显示骨架屏
|
|
|
|
try {
|
|
|
|
const currentParams = {
|
|
|
|
...searchParams,
|
|
|
|
...(userStore.user?.user_id ? { user_id: userStore.user.user_id } : {}),
|
|
|
|
};
|
|
|
|
const res = await productApi.getSearchProducts(currentParams);
|
|
|
|
setProducts(res.products);
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error fetching products:", error);
|
|
|
|
} finally {
|
|
|
|
setTimeout(() => {
|
|
|
|
setLoading(false); // 延迟结束加载状态,让骨架屏显示更平滑
|
|
|
|
}, 300);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const handleProductPress = useCallback(
|
|
|
|
(item: Product) => {
|
|
|
|
InteractionManager.runAfterInteractions(() => {
|
|
|
|
navigation.navigate("ProductDetail", {
|
|
|
|
offer_id: item.offer_id,
|
|
|
|
searchKeyword: searchParams.keyword,
|
|
|
|
price: item.min_price,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[navigation]
|
|
|
|
);
|
|
|
|
const onRefresh = useCallback(async () => {
|
|
|
|
setRefreshing(true);
|
|
|
|
try {
|
|
|
|
await getProductData();
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Error fetching products:", error);
|
|
|
|
} finally {
|
|
|
|
setRefreshing(false);
|
|
|
|
}
|
|
|
|
}, [searchParams]);
|
|
|
|
useEffect(() => {
|
|
|
|
console.log("userStore.user", userStore.user);
|
|
|
|
getProductData();
|
|
|
|
}, [userStore.user]);
|
|
|
|
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(() => {
|
|
|
|
// Ensure the content is available when category changes
|
|
|
|
if (!categoryContent[selectedHorizontalCategory]) {
|
|
|
|
setSelectedHorizontalCategory("Tous");
|
|
|
|
}
|
|
|
|
}, [selectedHorizontalCategory]);
|
|
|
|
// 导航到搜索页面 - 使用useCallback优化函数引用
|
|
|
|
const navigateToSearch = useCallback(() => {
|
|
|
|
// 使用InteractionManager延迟执行导航操作,确保当前交互和动画已完成
|
|
|
|
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, index) => {
|
|
|
|
str += key;
|
|
|
|
});
|
|
|
|
horizontalScrollRef.current.scrollTo({
|
|
|
|
x: str.length * fontSize(16) + (categoryIndex - 1 + 17),
|
|
|
|
animated: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// 渲染产品列表项
|
|
|
|
const renderProductItem = ({ item }: { item: Product }) => (
|
|
|
|
<TouchableOpacity
|
|
|
|
onPress={() => handleProductPress(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)}
|
|
|
|
</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"}+ ventes
|
|
|
|
</Text>
|
|
|
|
</View>
|
|
|
|
</TouchableOpacity>
|
|
|
|
);
|
|
|
|
|
|
|
|
// 渲染骨架屏网格
|
|
|
|
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>
|
|
|
|
);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
// 清理expo-image-picker临时文件
|
|
|
|
const cleanupImagePickerCache = async () => {
|
|
|
|
try {
|
|
|
|
// 相册选择后清理临时缓存
|
|
|
|
const cacheDir = `${FileSystem.cacheDirectory}ImagePicker`;
|
|
|
|
await FileSystem.deleteAsync(cacheDir, { idempotent: true });
|
|
|
|
console.log('已清理ImagePicker缓存');
|
|
|
|
|
|
|
|
// 立即重置状态,无需用户干预
|
|
|
|
setGalleryUsed(false);
|
|
|
|
} catch (error) {
|
|
|
|
console.log('清理缓存错误', error);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 处理从相册选择
|
|
|
|
const handleChooseFromGallery = useCallback(async () => {
|
|
|
|
console.log('handleChooseFromGallery');
|
|
|
|
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) {
|
|
|
|
console.log('相册选择成功:', result.assets[0].uri);
|
|
|
|
// 这里可以添加后续处理代码,如图片上传等
|
|
|
|
|
|
|
|
// 相册选择完成后,立即清理缓存并重置状态
|
|
|
|
await cleanupImagePickerCache();
|
|
|
|
}
|
|
|
|
} catch (error: any) {
|
|
|
|
console.error('相册错误:', error);
|
|
|
|
// 出错时也清理缓存
|
|
|
|
await cleanupImagePickerCache();
|
|
|
|
}
|
|
|
|
}, 500);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
// 处理相机拍照 - 简化版本,不再需要处理galleryUsed
|
|
|
|
const handleTakePhoto = useCallback(async () => {
|
|
|
|
console.log('handleTakePhoto');
|
|
|
|
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,
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log(result);
|
|
|
|
|
|
|
|
if (!result.canceled && result.assets && result.assets.length > 0) {
|
|
|
|
console.log('拍照成功:', result.assets[0].uri);
|
|
|
|
// 这里可以添加后续处理代码,如图片上传等
|
|
|
|
}
|
|
|
|
|
|
|
|
// 使用后清理缓存
|
|
|
|
await cleanupImagePickerCache();
|
|
|
|
} catch (error: any) {
|
|
|
|
console.error('相机错误:', error);
|
|
|
|
// 出错时也清理缓存
|
|
|
|
await cleanupImagePickerCache();
|
|
|
|
}
|
|
|
|
}, 500);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
// 重置应用状态函数
|
|
|
|
const resetAppState = useCallback(() => {
|
|
|
|
// 重置标记
|
|
|
|
setGalleryUsed(false);
|
|
|
|
|
|
|
|
// 清理缓存
|
|
|
|
cleanupImagePickerCache();
|
|
|
|
|
|
|
|
// 提示用户
|
|
|
|
Alert.alert('已重置', '现在您可以使用相机功能了');
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
// 渲染列表头部内容
|
|
|
|
const renderHeader = () => (
|
|
|
|
<>
|
|
|
|
{/* 轮播图 */}
|
|
|
|
<View style={styles.swiperContainer}>
|
|
|
|
<Carousel
|
|
|
|
loop
|
|
|
|
width={screenWidth}
|
|
|
|
data={data}
|
|
|
|
height={widthUtils(286, 286).height}
|
|
|
|
modeConfig={{
|
|
|
|
parallaxScrollingScale: 0.9,
|
|
|
|
parallaxScrollingOffset: 50,
|
|
|
|
}}
|
|
|
|
renderItem={({ item }) => (
|
|
|
|
<TouchableOpacity
|
|
|
|
onPress={() => navigation.navigate(item.add)}
|
|
|
|
key={item.imgUrl}
|
|
|
|
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,
|
|
|
|
activeIndex === index ? 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}>搜索商品</Text>
|
|
|
|
<TouchableOpacity
|
|
|
|
style={styles.cameraButton}
|
|
|
|
onPress={() => setShowImagePickerModal(true)}
|
|
|
|
>
|
|
|
|
<IconComponent name="camera-outline" size={24} color="#333" />
|
|
|
|
</TouchableOpacity>
|
|
|
|
</TouchableOpacity>
|
|
|
|
</View>
|
|
|
|
</View>
|
|
|
|
{/* 内容区域 */}
|
|
|
|
<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,
|
|
|
|
]}
|
|
|
|
>
|
|
|
|
{category}
|
|
|
|
</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>
|
|
|
|
{/* Subcategory Content */}
|
|
|
|
{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}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<SafeAreaView style={styles.safeArea}>
|
|
|
|
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
|
|
|
<View style={styles.safeAreaContent}>
|
|
|
|
<View style={styles.container}>
|
|
|
|
{loading ? (
|
|
|
|
// 显示骨架屏
|
|
|
|
<View>
|
|
|
|
{renderHeader()}
|
|
|
|
{renderSkeletonGrid()}
|
|
|
|
</View>
|
|
|
|
) : (
|
|
|
|
// 显示正常内容
|
|
|
|
<FlatList
|
|
|
|
ref={flatListRef}
|
|
|
|
data={products}
|
|
|
|
numColumns={2}
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
columnWrapperStyle={styles.productCardGroup}
|
|
|
|
renderItem={renderProductItem}
|
|
|
|
keyExtractor={(item, index) =>
|
|
|
|
item.offer_id?.toString() || index.toString()
|
|
|
|
}
|
|
|
|
contentContainerStyle={{
|
|
|
|
paddingBottom: 15,
|
|
|
|
backgroundColor: "transparent",
|
|
|
|
}}
|
|
|
|
ListHeaderComponent={renderHeader}
|
|
|
|
refreshControl={
|
|
|
|
<RefreshControl
|
|
|
|
refreshing={refreshing}
|
|
|
|
onRefresh={onRefresh}
|
|
|
|
colors={["#ff5100"]}
|
|
|
|
tintColor="#ff5100"
|
|
|
|
progressBackgroundColor="transparent"
|
|
|
|
/>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Categories Modal */}
|
|
|
|
<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>
|
|
|
|
|
|
|
|
{/* Image Picker 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}>拍照</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}>从相册选择</Text>
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
style={styles.imagePickerCancelButton}
|
|
|
|
onPress={() => setShowImagePickerModal(false)}
|
|
|
|
>
|
|
|
|
<Text style={styles.imagePickerCancelText}>取消</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: 60,
|
|
|
|
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',
|
|
|
|
},
|
|
|
|
});
|