From e2171b50eadad03057ab1bbba75ae69e36416c94 Mon Sep 17 00:00:00 2001 From: Mac Date: Tue, 1 Apr 2025 16:48:20 +0800 Subject: [PATCH] init --- app/components/ResponsiveContainer.tsx | 58 ++ app/components/ResponsiveGrid.tsx | 74 +++ app/constants/styles.ts | 118 ++++ app/screens/SearchResultScreen.tsx | 751 ++++++++++++++----------- app/utils/dimensions.ts | 45 ++ app/utils/size.ts | 1 + babel.config.js | 17 + package-lock.json | 35 ++ package.json | 2 + yarn.lock | 27 +- 10 files changed, 788 insertions(+), 340 deletions(-) create mode 100644 app/components/ResponsiveContainer.tsx create mode 100644 app/components/ResponsiveGrid.tsx create mode 100644 app/constants/styles.ts create mode 100644 app/utils/dimensions.ts create mode 100644 app/utils/size.ts create mode 100644 babel.config.js diff --git a/app/components/ResponsiveContainer.tsx b/app/components/ResponsiveContainer.tsx new file mode 100644 index 0000000..f676f1b --- /dev/null +++ b/app/components/ResponsiveContainer.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { View, ViewStyle, StyleSheet } from 'react-native'; +import { getScreenWidth, isTablet } from '../utils/dimensions'; +import { spacing } from '../constants/styles'; + +interface ResponsiveContainerProps { + children: React.ReactNode; + style?: ViewStyle; + maxWidth?: number; + padding?: 'none' | 'sm' | 'md' | 'lg'; +} + +export const ResponsiveContainer: React.FC = ({ + children, + style, + maxWidth = 1200, + padding = 'md', +}) => { + const screenWidth = getScreenWidth(); + const isTabletDevice = isTablet(); + + // 计算容器宽度 + const containerWidth = Math.min(screenWidth, maxWidth); + + // 计算水平内边距 + const horizontalPadding = padding === 'none' + ? 0 + : padding === 'sm' + ? spacing.sm + : padding === 'lg' + ? spacing.lg + : spacing.md; + + return ( + + + {children} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + width: '100%', + alignItems: 'center', + }, + content: { + flex: 1, + }, +}); \ No newline at end of file diff --git a/app/components/ResponsiveGrid.tsx b/app/components/ResponsiveGrid.tsx new file mode 100644 index 0000000..3c3f82e --- /dev/null +++ b/app/components/ResponsiveGrid.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { View, ViewStyle, StyleSheet } from 'react-native'; +import { getScreenWidth, isTablet } from '../utils/dimensions'; +import { spacing } from '../constants/styles'; + +interface ResponsiveGridProps { + children: React.ReactNode; + style?: ViewStyle; + columns?: number; + gap?: 'none' | 'sm' | 'md' | 'lg'; + padding?: 'none' | 'sm' | 'md' | 'lg'; +} + +export const ResponsiveGrid: React.FC = ({ + children, + style, + columns = 2, + gap = 'md', + padding = 'md', +}) => { + const screenWidth = getScreenWidth(); + const isTabletDevice = isTablet(); + + // 根据设备类型调整列数 + const adjustedColumns = isTabletDevice ? Math.min(columns * 1.5, 4) : columns; + + // 计算间距 + const gapSize = gap === 'none' + ? 0 + : gap === 'sm' + ? spacing.sm + : gap === 'lg' + ? spacing.lg + : spacing.md; + + // 计算内边距 + const paddingSize = padding === 'none' + ? 0 + : padding === 'sm' + ? spacing.sm + : padding === 'lg' + ? spacing.lg + : spacing.md; + + // 计算每个项目的宽度 + const itemWidth = (screenWidth - (paddingSize * 2) - (gapSize * (adjustedColumns - 1))) / adjustedColumns; + + // 将子元素转换为数组 + const childrenArray = React.Children.toArray(children); + + return ( + + + {childrenArray.map((child, index) => ( + + {child} + + ))} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + width: '100%', + }, + grid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'flex-start', + }, +}); \ No newline at end of file diff --git a/app/constants/styles.ts b/app/constants/styles.ts new file mode 100644 index 0000000..4d0e6d3 --- /dev/null +++ b/app/constants/styles.ts @@ -0,0 +1,118 @@ +import { StyleSheet } from 'react-native'; +import { scale, verticalScale, moderateScale } from '../utils/dimensions'; + +export const globalStyles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFFFFF', + }, + safeArea: { + flex: 1, + }, + row: { + flexDirection: 'row', + }, + column: { + flexDirection: 'column', + }, + center: { + justifyContent: 'center', + alignItems: 'center', + }, + spaceBetween: { + justifyContent: 'space-between', + }, + spaceAround: { + justifyContent: 'space-around', + }, + flexStart: { + justifyContent: 'flex-start', + }, + flexEnd: { + justifyContent: 'flex-end', + }, +}); + +export const spacing = { + xs: scale(4), + sm: scale(8), + md: scale(16), + lg: scale(24), + xl: scale(32), + xxl: scale(40), +}; + +export const fontSize = { + xs: moderateScale(12), + sm: moderateScale(14), + md: moderateScale(16), + lg: moderateScale(18), + xl: moderateScale(20), + xxl: moderateScale(24), +}; + +export const borderRadius = { + sm: scale(4), + md: scale(8), + lg: scale(12), + xl: scale(16), + round: scale(9999), +}; + +export const iconSize = { + sm: scale(16), + md: scale(24), + lg: scale(32), + xl: scale(40), +}; + +export const buttonHeight = { + sm: verticalScale(32), + md: verticalScale(44), + lg: verticalScale(56), +}; + +export const inputHeight = { + sm: verticalScale(32), + md: verticalScale(44), + lg: verticalScale(56), +}; + +export const cardPadding = { + sm: scale(8), + md: scale(16), + lg: scale(24), +}; + +export const shadow = { + small: { + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 2, + }, + medium: { + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 4, + }, + shadowOpacity: 0.30, + shadowRadius: 4.65, + elevation: 4, + }, + large: { + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 6, + }, + shadowOpacity: 0.37, + shadowRadius: 7.49, + elevation: 6, + }, +}; \ No newline at end of file diff --git a/app/screens/SearchResultScreen.tsx b/app/screens/SearchResultScreen.tsx index ed977ac..c610736 100644 --- a/app/screens/SearchResultScreen.tsx +++ b/app/screens/SearchResultScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useRef } from "react"; import { View, Text, @@ -12,7 +12,6 @@ import { ActivityIndicator, KeyboardAvoidingView, Platform, - Keyboard, ScrollView, } from "react-native"; import Ionicons from "@expo/vector-icons/Ionicons"; @@ -39,6 +38,56 @@ type SearchResultRouteParams = { keyword: string; }; +// 懒加载图片组件 +const LazyImage = React.memo(({ uri, style, resizeMode }: { uri: string, style: any, resizeMode: any }) => { + const [isVisible, setIsVisible] = useState(false); + const [hasError, setHasError] = useState(false); + + // // 缩略图处理 - 为原图创建更低质量的缩略版本以更快加载 + // const getThumbnailUrl = useCallback((originalUrl: string) => { + // // 如果有可能,使用CDN参数来获取更小的图片 + // // 这里是一个简单的实现,实际上需要根据具体的CDN服务来调整 + // if (originalUrl.includes('?')) { + // return `${originalUrl}&quality=10&width=100`; + // } + // return `${originalUrl}?quality=10&width=100`; + // }, []); + + const onError = useCallback(() => { + setHasError(true); + }, []); + + // 使用IntersectionObserver的替代方案,在组件挂载时显示图片 + useEffect(() => { + // 延迟一小段时间后开始加载图片 + const timer = setTimeout(() => { + setIsVisible(true); + }, 100); + + return () => clearTimeout(timer); + }, []); + + return ( + + {hasError && ( + + + 加载失败 + + )} + + {isVisible && !hasError && ( + + )} + + ); +}); + // 产品项组件 - 使用React.memo优化渲染 const ProductItem = React.memo( ({ @@ -57,8 +106,8 @@ const ProductItem = React.memo( > {product.product_image_urls[0] ? ( - @@ -66,15 +115,20 @@ const ProductItem = React.memo( product picture )} - {/* 价格 */} + {/* 产品分类 */} - - {product?.min_price?.toFixed(2)} - - {/* 产品标题 */} - - {product.subject} + + {product.subject_trans} + {/* 价格信息 */} + + + {product?.min_price?.toFixed(0)} FCFA + + + 3000FCFA + + {/* 销售量 */} {t('monthlySales')}: {product.sold_out} @@ -91,24 +145,22 @@ export const SearchResultScreen = () => { useRoute, string>>(); const [searchText, setSearchText] = useState(""); const [products, setProducts] = useState([]); + const [originalProducts, setOriginalProducts] = useState([]); const [loading, setLoading] = useState(true); const [hasMore, setHasMore] = useState(true); const [loadingMore, setLoadingMore] = useState(false); - const [minPrice, setMinPrice] = useState(""); - const [maxPrice, setMaxPrice] = useState(""); const [isFilterVisible, setIsFilterVisible] = useState(false); const [sortOrder, setSortOrder] = useState<"asc" | "desc" | null>(null); const [sortField, setSortField] = useState<"price" | "time">("price"); const [showBackToTop, setShowBackToTop] = useState(false); - const flatListRef = React.useRef(null); + const flatListRef = useRef(null); + const [activeTab, setActiveTab] = useState<"default" | "volume" | "price">("default"); const [searchParams, setSearchParams] = useState({ keyword: route.params?.keyword || "", page: 1, page_size: 20, sort_order: "desc", - max_price: null, - min_price: null, category_id: null, sort_by: "create_date", }); @@ -136,6 +188,8 @@ export const SearchResultScreen = () => { setProducts((prev) => [...prev, ...res.products]); } else { setProducts(res.products); + // 保存原始排序的数据,以便默认排序时恢复 + setOriginalProducts(res.products); } // 如果返回的数据少于页面大小,说明没有更多数据了 setHasMore(res.products.length === params.page_size); @@ -155,6 +209,8 @@ export const SearchResultScreen = () => { // 重置排序状态 setSortField("price"); setSortOrder(null); + // 重置到默认标签 + setActiveTab("default"); const newParams = { ...searchParams, @@ -166,43 +222,6 @@ export const SearchResultScreen = () => { } }, [searchText, searchParams, searchProducts]); - // 处理价格筛选 - const handlePriceFilter = useCallback(() => { - Keyboard.dismiss(); - const newParams = { ...searchParams }; - - if (minPrice.trim()) { - newParams.min_price = parseFloat(minPrice); - } else { - newParams.min_price = null; - } - - if (maxPrice.trim()) { - newParams.max_price = parseFloat(maxPrice); - } else { - newParams.max_price = null; - } - - newParams.page = 1; // 重置到第一页 - setSearchParams(newParams); - searchProducts(newParams); - console.log(newParams); - }, [minPrice, maxPrice, searchParams, searchProducts]); - - // 重置价格筛选 - const resetPriceFilter = useCallback(() => { - setMinPrice(""); - setMaxPrice(""); - const newParams = { - ...searchParams, - min_price: null, - max_price: null, - page: 1, - }; - setSearchParams(newParams); - searchProducts(newParams); - }, [searchParams, searchProducts]); - // 切换筛选器显示状态 const toggleFilter = useCallback(() => { setIsFilterVisible(!isFilterVisible); @@ -250,6 +269,9 @@ export const SearchResultScreen = () => { [handleProductPress, t] ); + // 创建产品列表项的key提取器 + const keyExtractor = useCallback((item: Product) => String(item.offer_id), []); + // 处理排序 const handleSort = useCallback( (field: "price" | "time", order: "asc" | "desc") => { @@ -327,6 +349,40 @@ export const SearchResultScreen = () => { flatListRef.current?.scrollToOffset({ offset: 0, animated: true }); }, []); + // 处理标签切换 + const handleTabChange = useCallback((tab: "default" | "volume" | "price") => { + // 如果点击的是已经激活的价格标签,则切换排序顺序 + if (tab === "price" && activeTab === "price") { + // 如果当前是价格升序,则切换为降序;如果是降序或未设置,则切换为升序 + const newOrder = sortOrder === "asc" ? "desc" : "asc"; + handleSort("price", newOrder); + scrollToTop(); + } else { + setActiveTab(tab); + + // 根据标签类型设置排序规则 + if (tab === "price") { + // 默认价格从低到高 + handleSort("price", "asc"); + scrollToTop(); + } else if (tab === "volume") { + // 按销量排序 + const sortedProducts = [...originalProducts]; + sortedProducts.sort((a, b) => { + const volumeA = a.sold_out || 0; + const volumeB = b.sold_out || 0; + return volumeB - volumeA; // 从高到低排序 + }); + setProducts(sortedProducts); + scrollToTop(); + } else { + // 默认排序 - 恢复到原始数据顺序 + setProducts([...originalProducts]); + scrollToTop(); + } + } + }, [handleSort, activeTab, sortOrder, originalProducts, scrollToTop]); + return ( @@ -340,7 +396,7 @@ export const SearchResultScreen = () => { - + { onSubmitEditing={handleSearch} /> {searchText.length > 0 && ( - setSearchText("")}> - + setSearchText("")} + style={styles.clearButton} + > + )} - - + + {t('cancel')} - {/* 搜索结果 */} - - {/* 价格筛选器 */} - {isFilterVisible && ( - - - {t('priceRange')}: - - - - - + handleTabChange("default")} + > + + {t('default')} + + + handleTabChange("volume")} + > + + {t('volume')} + + + handleTabChange("price")} + > + + + {t('price')} + + {activeTab === "price" && ( + + - - - - {t('reset')} - - - {t('apply')} - - + )} - )} + + + {/* 搜索结果 */} + {/* 搜索结果标题栏和排序选项 */} - - - - {t('price')}: - - handleSort("price", "asc")} - > - + + + {t('price')}: + + handleSort("price", "asc")} > - {t('lowToHigh')} - - {sortField === "price" && sortOrder === "asc" && ( - - )} - - handleSort("price", "desc")} - > - + {t('lowToHigh')} + + {sortField === "price" && sortOrder === "asc" && ( + + )} + + handleSort("price", "desc")} > - {t('highToLow')} - - {sortField === "price" && sortOrder === "desc" && ( - - )} - + + {t('highToLow')} + + {sortField === "price" && sortOrder === "desc" && ( + + )} + + - - - - - - {t('time')}: - - handleSort("time", "asc")} - > - + + + {t('time')}: + + handleSort("time", "asc")} > - {t('oldest')} - - {sortField === "time" && sortOrder === "asc" && ( - - )} - - handleSort("time", "desc")} - > - + {t('oldest')} + + {sortField === "time" && sortOrder === "asc" && ( + + )} + + handleSort("time", "desc")} > - {t('newest')} - - {sortField === "time" && sortOrder === "desc" && ( - - )} - + + {t('newest')} + + {sortField === "time" && sortOrder === "desc" && ( + + )} + + - - - + + + )} {/* 加载指示器 */} {loading ? ( @@ -553,18 +605,19 @@ export const SearchResultScreen = () => { ref={flatListRef} data={products} renderItem={renderProductItem} - keyExtractor={(item) => String(item.offer_id)} + keyExtractor={keyExtractor} numColumns={2} contentContainerStyle={styles.productGrid} ListEmptyComponent={renderEmptyList} ListFooterComponent={renderFooter} showsVerticalScrollIndicator={false} - initialNumToRender={8} - maxToRenderPerBatch={10} - windowSize={5} - removeClippedSubviews={true} + initialNumToRender={4} + maxToRenderPerBatch={8} + windowSize={3} + removeClippedSubviews={Platform.OS !== 'web'} + updateCellsBatchingPeriod={50} onEndReached={handleLoadMore} - onEndReachedThreshold={0.2} + onEndReachedThreshold={0.5} onScroll={handleScroll} scrollEventThrottle={16} /> @@ -597,15 +650,14 @@ const styles = StyleSheet.create({ searchHeader: { flexDirection: "row", alignItems: "center", - paddingHorizontal: 15, - paddingVertical: 10, + paddingHorizontal: 12, + paddingVertical: 8, backgroundColor: "#fff", borderBottomWidth: 1, borderBottomColor: "#f0f0f0", }, backButton: { - marginRight: 10, - padding: 5, + padding: 4, }, searchBar: { flex: 1, @@ -613,34 +665,75 @@ const styles = StyleSheet.create({ alignItems: "center", backgroundColor: "#f5f5f5", borderRadius: 20, - paddingHorizontal: 15, + paddingHorizontal: 8, height: 40, + marginHorizontal: 8, + position: "relative", }, searchInput: { flex: 1, - marginLeft: 8, + marginLeft: 4, fontSize: 16, color: "#333", height: 40, + paddingRight: 32, }, - resultsContainer: { - flex: 1, + clearButton: { + position: "absolute", + right: 8, + top: "50%", + transform: [{ translateY: -9 }], + padding: 4, }, - resultsHeader: { + searchButton: { + paddingVertical: 4, + paddingHorizontal: 8, + }, + searchButtonText: { + fontSize: 16, + color: "#333", + fontWeight: "500", + }, + tabContainer: { + flexDirection: "row", backgroundColor: "#fff", borderBottomWidth: 1, borderBottomColor: "#f0f0f0", - paddingVertical: 10, }, - resultsTitle: { - fontSize: 16, - fontWeight: "bold", - color: "#333", + tabButton: { flex: 1, + alignItems: "center", + justifyContent: "center", + paddingVertical: 12, + position: "relative", }, - resultsCount: { + tabButtonContent: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + }, + tabIcon: { + marginLeft: 4, + }, + tabText: { fontSize: 14, - color: "#999", + color: "#666", + }, + activeTabText: { + color: "#000", + fontWeight: "bold", + }, + activeTabButton: { + // borderBottomColor: "#007AFF", + }, + resultsContainer: { + flex: 1, + }, + resultsHeader: { + backgroundColor: "#fff", + borderBottomWidth: 1, + borderBottomColor: "#f0f0f0", + paddingVertical: 8, }, loadingContainer: { flex: 1, @@ -656,11 +749,14 @@ const styles = StyleSheet.create({ backgroundColor: "#fff", borderRadius: 8, overflow: "hidden", - elevation: 2, // Android shadow - shadowColor: "#000", // iOS shadow - shadowOffset: { width: 0, height: 1 }, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, shadowOpacity: 0.1, - shadowRadius: 2, + shadowRadius: 4, + elevation: 3, }, productImageContainer: { height: 150, @@ -677,107 +773,47 @@ const styles = StyleSheet.create({ fontSize: 16, }, productInfo: { - padding: 10, - }, - productPrice: { - fontSize: 18, - fontWeight: "bold", - color: "#ff6600", - marginBottom: 5, - }, - productTitle: { - fontSize: 14, - color: "#333", - marginBottom: 5, - }, - productSales: { - fontSize: 12, - color: "#999", - }, - emptyContainer: { - flex: 1, - minHeight: 300, - justifyContent: "center", - alignItems: "center", - padding: 20, - }, - emptyText: { - fontSize: 16, - fontWeight: "bold", - color: "#333", - marginTop: 15, - marginBottom: 8, - }, - emptySubtext: { - fontSize: 14, - color: "#999", - textAlign: "center", - }, - filterButton: { - marginLeft: 10, - padding: 5, - }, - filterContainer: { - backgroundColor: "#fff", - paddingHorizontal: 15, - paddingVertical: 10, - borderBottomWidth: 1, - borderBottomColor: "#f0f0f0", - }, - priceFilterRow: { - flexDirection: "row", - alignItems: "center", - marginBottom: 10, + padding: 8, }, - filterLabel: { - fontSize: 14, - fontWeight: "bold", - color: "#333", - marginRight: 10, + categoryText: { + fontSize: 10, + color: "#000000", + fontWeight: "600", + marginBottom: 4, + fontFamily: 'PingFang SC' }, - priceInputContainer: { - flex: 1, + priceRow: { flexDirection: "row", - alignItems: "center", + alignItems: "baseline", + marginBottom: 4, }, - priceInput: { - flex: 1, - height: 36, - backgroundColor: "#f5f5f5", - borderRadius: 4, - paddingHorizontal: 10, + currentPrice: { fontSize: 14, - color: "#333", - }, - priceDivider: { - marginHorizontal: 8, - color: "#333", - }, - filterButtons: { - flexDirection: "row", - justifyContent: "flex-end", + fontWeight: "600", + color: "#ff6600", + marginRight: 4, }, - resetButton: { - paddingHorizontal: 15, - paddingVertical: 6, - borderRadius: 4, - backgroundColor: "#f5f5f5", - marginRight: 10, + currency: { + fontSize: 10, + fontWeight: "600", + fontFamily: 'PingFang SC', }, - resetButtonText: { - fontSize: 14, - color: "#666", + originalPrice: { + fontSize: 10, + color: "#999", + textDecorationLine: "line-through", }, - applyButton: { - paddingHorizontal: 15, - paddingVertical: 6, - borderRadius: 4, - backgroundColor: "#0066FF", + currencySmall: { + fontSize: 10, + color: '#9a9a9a', + fontWeight: "600", + fontFamily: 'PingFang SC', }, - applyButtonText: { - fontSize: 14, - color: "#fff", - fontWeight: "bold", + productSales: { + fontSize: 10, + fontWeight: "600", + fontFamily: 'PingFang SC', + color: "#7c7c7c", }, sortScrollView: { flexGrow: 0, @@ -785,7 +821,7 @@ const styles = StyleSheet.create({ sortGroup: { flexDirection: "row", alignItems: "center", - paddingHorizontal: 15, + paddingHorizontal: 16, }, sortLabel: { fontSize: 14, @@ -802,7 +838,7 @@ const styles = StyleSheet.create({ paddingVertical: 4, paddingHorizontal: 8, borderRadius: 4, - marginLeft: 6, + marginLeft: 4, borderWidth: 1, borderColor: "#e0e0e0", }, @@ -822,10 +858,10 @@ const styles = StyleSheet.create({ width: 1, height: 20, backgroundColor: "#e0e0e0", - marginHorizontal: 15, + marginHorizontal: 16, }, footerContainer: { - padding: 15, + padding: 16, alignItems: "center", flexDirection: "row", justifyContent: "center", @@ -848,10 +884,51 @@ const styles = StyleSheet.create({ backgroundColor: "#0066FF", justifyContent: "center", alignItems: "center", - elevation: 5, shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.3, - shadowRadius: 3, + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + emptyContainer: { + flex: 1, + minHeight: 300, + justifyContent: "center", + alignItems: "center", + padding: 16, + }, + emptyText: { + fontSize: 16, + fontWeight: "bold", + color: "#333", + marginTop: 16, + marginBottom: 8, + }, + emptySubtext: { + fontSize: 14, + color: "#999", + textAlign: "center", + }, + resultsTitle: { + fontSize: 16, + fontWeight: "bold", + color: "#333", + flex: 1, + }, + resultsCount: { + fontSize: 14, + color: "#999", + }, + filterButton: { + marginLeft: 8, + padding: 4, + }, + imagePlaceholder: { + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#f5f5f5', }, }); diff --git a/app/utils/dimensions.ts b/app/utils/dimensions.ts new file mode 100644 index 0000000..15c19a2 --- /dev/null +++ b/app/utils/dimensions.ts @@ -0,0 +1,45 @@ +import { Dimensions, Platform, StatusBar, PixelRatio } from 'react-native'; + +const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); + +// 基准尺寸(以 iPhone 11 为基准) +const baseWidth = 375; +const baseHeight = 812; + +// 计算缩放比例 +const widthRatio = SCREEN_WIDTH / baseWidth; +const heightRatio = SCREEN_HEIGHT / baseHeight; + +// 获取状态栏高度 +const statusBarHeight = Platform.OS === 'ios' ? 44 : StatusBar.currentHeight || 0; + +// 响应式尺寸计算函数 +export const scale = (size: number) => { + return Math.round(size * widthRatio); +}; + +export const verticalScale = (size: number) => { + return Math.round(size * heightRatio); +}; + +export const moderateScale = (size: number, factor = 0.5) => { + return Math.round(size + (scale(size) - size) * factor); +}; + +// 获取屏幕尺寸 +export const getScreenWidth = () => SCREEN_WIDTH; +export const getScreenHeight = () => SCREEN_HEIGHT; + +// 获取状态栏高度 +export const getStatusBarHeight = () => statusBarHeight; + +// 判断是否为小屏幕设备 +export const isSmallDevice = () => SCREEN_WIDTH < 375; + +// 判断是否为平板设备 +export const isTablet = () => { + const pixelDensity = PixelRatio.get(); + const adjustedWidth = SCREEN_WIDTH * pixelDensity; + const adjustedHeight = SCREEN_HEIGHT * pixelDensity; + return Math.sqrt(Math.pow(adjustedWidth, 2) + Math.pow(adjustedHeight, 2)) >= 1000; +}; \ No newline at end of file diff --git a/app/utils/size.ts b/app/utils/size.ts new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/app/utils/size.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..eed7541 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,17 @@ +module.exports = function (api) { + api.cache(true); + return { + presets: ['babel-preset-expo'], + plugins: [ + [ + '@hancleee/babel-plugin-react-native-pxtodp', + { + designWidth: 430, // 设计稿宽度 + designHeight: 932, // 设计稿高度 + exclude: /node_modules/, // 排除 node_modules 目录 + include: /src|app/, // 只处理 src 和 app 目录 + }, + ], + ], + }; +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4d4f915..a17e2f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "react-i18next": "^15.4.1", "react-native": "0.76.7", "react-native-localize": "^3.4.1", + "react-native-responsive-fontsize": "^0.5.1", "react-native-safe-area-context": "^5.3.0", "react-native-screens": "^4.10.0", "react-native-vector-icons": "^10.2.0", @@ -31,6 +32,7 @@ }, "devDependencies": { "@babel/core": "^7.25.2", + "@hancleee/babel-plugin-react-native-pxtodp": "^1.0.8", "@types/react": "~18.3.12", "@types/react-native-vector-icons": "^6.4.18", "typescript": "^5.3.3" @@ -2820,6 +2822,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@hancleee/babel-plugin-react-native-pxtodp": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@hancleee/babel-plugin-react-native-pxtodp/-/babel-plugin-react-native-pxtodp-1.0.8.tgz", + "integrity": "sha512-WCy1faXgMVu71tvLWl/jVY+nGhy/D67ersa/slalRzSSCRFibIbE5sDb4PMQOn8Eka3N6tfZYeIMp6ggoW8+sg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@babel/core": "^7.12.9", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/parser": "^7.21.3" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -9107,6 +9121,15 @@ } } }, + "node_modules/react-native-iphone-x-helper": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz", + "integrity": "sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==", + "license": "MIT", + "peerDependencies": { + "react-native": ">=0.42.0" + } + }, "node_modules/react-native-localize": { "version": "3.4.1", "resolved": "https://registry.npmmirror.com/react-native-localize/-/react-native-localize-3.4.1.tgz", @@ -9123,6 +9146,18 @@ } } }, + "node_modules/react-native-responsive-fontsize": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/react-native-responsive-fontsize/-/react-native-responsive-fontsize-0.5.1.tgz", + "integrity": "sha512-G77iPzrf3BHxMxVm6G3Mw3vPImIdq+jLLXhYoOjWei6i9J3/jzUNUhNdRWvp49Csb5prhbVBLPM+pYZz+b3ESQ==", + "license": "MIT", + "dependencies": { + "react-native-iphone-x-helper": "^1.3.1" + }, + "peerDependencies": { + "react-native": ">=0.42.0" + } + }, "node_modules/react-native-safe-area-context": { "version": "5.3.0", "resolved": "https://registry.npmmirror.com/react-native-safe-area-context/-/react-native-safe-area-context-5.3.0.tgz", diff --git a/package.json b/package.json index 6ac3002..294818c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "react-i18next": "^15.4.1", "react-native": "0.76.7", "react-native-localize": "^3.4.1", + "react-native-responsive-fontsize": "^0.5.1", "react-native-safe-area-context": "^5.3.0", "react-native-screens": "^4.10.0", "react-native-vector-icons": "^10.2.0", @@ -32,6 +33,7 @@ }, "devDependencies": { "@babel/core": "^7.25.2", + "@hancleee/babel-plugin-react-native-pxtodp": "^1.0.8", "@types/react": "~18.3.12", "@types/react-native-vector-icons": "^6.4.18", "typescript": "^5.3.3" diff --git a/yarn.lock b/yarn.lock index e5acdf1..0c3c4cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -43,7 +43,7 @@ resolved "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.26.8.tgz" integrity sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.0", "@babel/core@^7.12.3", "@babel/core@^7.13.0", "@babel/core@^7.13.16", "@babel/core@^7.20.0", "@babel/core@^7.25.2", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.8.0": +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.0", "@babel/core@^7.12.3", "@babel/core@^7.12.9", "@babel/core@^7.13.0", "@babel/core@^7.13.16", "@babel/core@^7.20.0", "@babel/core@^7.25.2", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.8.0": version "7.26.10" resolved "https://registry.npmmirror.com/@babel/core/-/core-7.26.10.tgz" integrity sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ== @@ -231,7 +231,7 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.25.3", "@babel/parser@^7.26.10", "@babel/parser@^7.27.0": +"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.7", "@babel/parser@^7.20.0", "@babel/parser@^7.20.7", "@babel/parser@^7.21.3", "@babel/parser@^7.25.3", "@babel/parser@^7.26.10", "@babel/parser@^7.27.0": version "7.27.0" resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.0.tgz" integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== @@ -1462,6 +1462,15 @@ find-up "^5.0.0" js-yaml "^4.1.0" +"@hancleee/babel-plugin-react-native-pxtodp@^1.0.8": + version "1.0.8" + resolved "https://registry.npmmirror.com/@hancleee/babel-plugin-react-native-pxtodp/-/babel-plugin-react-native-pxtodp-1.0.8.tgz" + integrity sha512-WCy1faXgMVu71tvLWl/jVY+nGhy/D67ersa/slalRzSSCRFibIbE5sDb4PMQOn8Eka3N6tfZYeIMp6ggoW8+sg== + dependencies: + "@babel/core" "^7.12.9" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/parser" "^7.21.3" + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz" @@ -5159,11 +5168,23 @@ react-is@^18.0.0, react-is@^18.2.0: resolved "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +react-native-iphone-x-helper@^1.3.1: + version "1.3.1" + resolved "https://registry.npmmirror.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz" + integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg== + react-native-localize@^3.4.1: version "3.4.1" resolved "https://registry.npmmirror.com/react-native-localize/-/react-native-localize-3.4.1.tgz" integrity sha512-NJqJGBUpHtD/MpLCCkrNiqNZ+xFwbHCivxaoN1Oeb8tBAiZr/IqgP3E+MgnqmmdTMOJ33llUfiW3EM6pEIb33w== +react-native-responsive-fontsize@^0.5.1: + version "0.5.1" + resolved "https://registry.npmmirror.com/react-native-responsive-fontsize/-/react-native-responsive-fontsize-0.5.1.tgz" + integrity sha512-G77iPzrf3BHxMxVm6G3Mw3vPImIdq+jLLXhYoOjWei6i9J3/jzUNUhNdRWvp49Csb5prhbVBLPM+pYZz+b3ESQ== + dependencies: + react-native-iphone-x-helper "^1.3.1" + react-native-safe-area-context@^5.3.0, "react-native-safe-area-context@>= 4.0.0": version "5.3.0" resolved "https://registry.npmmirror.com/react-native-safe-area-context/-/react-native-safe-area-context-5.3.0.tgz" @@ -5199,7 +5220,7 @@ react-native-web@~0.19.13: postcss-value-parser "^4.2.0" styleq "^0.1.3" -react-native@*, "react-native@^0.0.0-0 || >=0.65 <1.0", react-native@>=0.70.0, react-native@0.76.7: +react-native@*, "react-native@^0.0.0-0 || >=0.65 <1.0", react-native@>=0.42.0, react-native@>=0.70.0, react-native@0.76.7: version "0.76.7" resolved "https://registry.npmmirror.com/react-native/-/react-native-0.76.7.tgz" integrity sha512-GPJcQeO3qUi1MvuhsC2DC6tH8gJQ4uc4JWPORrdeuCGFWE3QLsN8/hiChTEvJREHLfQSV61YPI8gIOtAQ8c37g==