refactor(ui): enhance TradeDetailScreen with responsive design and image gallery
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 2m44s
Frontend CI / ota-android (push) Successful in 10m26s
Frontend CI / build-android-apk (push) Successful in 39m50s

- Add responsive hooks (useResponsive, useResponsiveValue, useResponsiveSpacing)
- Replace custom image handling with ImageGrid and ImageGallery components
- Update styling with modern typography and spacing adjustments
- Add image modal for fullscreen gallery view
This commit is contained in:
lafay
2026-04-27 10:32:24 +08:00
parent e0b43ce60d
commit f478fa3fd3

View File

@@ -1,22 +1,23 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { View, ScrollView, Image, StyleSheet, Pressable, Alert, Modal } from 'react-native'; import { View, ScrollView, Image, StyleSheet, Pressable, Alert, Modal } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { useAppColors, spacing, borderRadius, fontSizes, shadows, type AppColors } from '../../theme'; import { useAppColors, spacing, borderRadius, fontSizes, shadows, type AppColors } from '../../theme';
import { Text, Loading, AppBackButton } from '../../components/common'; import { Text, Loading, AppBackButton, ImageGrid, ImageGallery } from '../../components/common';
import { tradeService } from '../../services/trade/tradeService'; import { tradeService } from '../../services/trade/tradeService';
import { messageService } from '../../services/message/messageService'; import { messageService } from '../../services/message/messageService';
import { useIsAuthenticated, useCurrentUser } from '../../stores/auth'; import { useIsAuthenticated, useCurrentUser } from '../../stores/auth';
import type { TradeItemDetailDTO } from '../../types/trade'; import type { TradeItemDetailDTO } from '../../types/trade';
import { TRADE_CONDITION_MAP } from '../../types/trade'; import { TRADE_CONDITION_MAP } from '../../types/trade';
import { hrefChat, hrefEditTrade } from '../../navigation/hrefs'; import { hrefChat, hrefEditTrade } from '../../navigation/hrefs';
import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../hooks/useResponsive';
function createStyles(colors: AppColors) { function createStyles(colors: AppColors) {
return StyleSheet.create({ return StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.background.paper, backgroundColor: colors.background.default,
}, },
header: { header: {
flexDirection: 'row', flexDirection: 'row',
@@ -39,17 +40,6 @@ function createStyles(colors: AppColors) {
headerRight: { headerRight: {
width: 40, width: 40,
}, },
imageContainer: {
width: '100%',
aspectRatio: 1,
backgroundColor: colors.background.paper,
position: 'relative',
},
image: {
width: '100%',
height: '100%',
resizeMode: 'contain',
},
// 降价标签 // 降价标签
discountBadge: { discountBadge: {
position: 'absolute', position: 'absolute',
@@ -130,7 +120,7 @@ function createStyles(colors: AppColors) {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'flex-start', alignItems: 'flex-start',
gap: spacing.sm, gap: spacing.sm,
marginBottom: spacing.md, marginBottom: spacing.lg,
}, },
conditionBadge: { conditionBadge: {
paddingHorizontal: 8, paddingHorizontal: 8,
@@ -189,10 +179,9 @@ function createStyles(colors: AppColors) {
color: '#FF7875', color: '#FF7875',
}, },
title: { title: {
fontSize: fontSizes['3xl'], fontWeight: '800',
fontWeight: '600',
color: colors.text.primary, color: colors.text.primary,
lineHeight: 34, letterSpacing: -0.3,
flex: 1, flex: 1,
}, },
// 商品属性行 // 商品属性行
@@ -217,12 +206,11 @@ function createStyles(colors: AppColors) {
color: colors.text.primary, color: colors.text.primary,
fontWeight: '500', fontWeight: '500',
}, },
// 描述 // 描述 - 与帖子正文同步
contentText: { contentText: {
fontSize: fontSizes.lg,
color: colors.text.primary, color: colors.text.primary,
lineHeight: 28, letterSpacing: 0.2,
marginBottom: spacing.md, marginBottom: spacing.lg,
}, },
// 底部操作栏 // 底部操作栏
bottomBar: { bottomBar: {
@@ -362,12 +350,23 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
const [item, setItem] = useState<TradeItemDetailDTO | null>(null); const [item, setItem] = useState<TradeItemDetailDTO | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [manageModalVisible, setManageModalVisible] = useState(false); const [manageModalVisible, setManageModalVisible] = useState(false);
const [showImageModal, setShowImageModal] = useState(false);
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const isOwner = item?.user_id === currentUser?.id; const isOwner = item?.user_id === currentUser?.id;
// 响应式 hook与帖子同步
const {
width,
isMobile,
isTablet,
isDesktop,
isWideScreen,
} = useResponsive();
const responsivePadding = useResponsiveSpacing({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, '2xl': 28, '3xl': 32, '4xl': 40 });
const responsiveGap = useResponsiveSpacing({ xs: 8, sm: 10, md: 12, lg: 16, xl: 20, '2xl': 24, '3xl': 28, '4xl': 32 });
const fetchItem = useCallback(async () => { const fetchItem = useCallback(async () => {
try { try {
@@ -456,6 +455,28 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
} }
}, [isAuth, item?.author?.id, router]); }, [isAuth, item?.author?.id, router]);
// 响应式图片间距和圆角(与帖子同步)
const imageGap = isDesktop ? 12 : isTablet ? 10 : 8;
const imageBorderRadius = isDesktop ? borderRadius.xl : borderRadius.lg;
const images = item?.images || [];
// 转换图片格式给 ImageGrid 使用
const tradeImages = useMemo(() => {
return images.map(img => ({
id: img.id,
url: img.url || img.preview_url_large || img.preview_url || '',
thumbnail_url: img.preview_url || img.preview_url_large || img.url || '',
width: img.width,
height: img.height,
}));
}, [images]);
const handleImagePress = useCallback((allImages: any[], index: number) => {
setSelectedImageIndex(index);
setShowImageModal(true);
}, []);
if (loading) { if (loading) {
return <Loading fullScreen />; return <Loading fullScreen />;
} }
@@ -470,9 +491,6 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
); );
} }
const images = item.images || [];
const currentImage = images[currentImageIndex];
return ( return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}> <SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<View style={styles.header}> <View style={styles.header}>
@@ -484,7 +502,7 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
</View> </View>
<ScrollView> <ScrollView>
<View style={styles.content}> <View style={[styles.content, { paddingHorizontal: responsivePadding, paddingTop: responsivePadding }]}>
{/* 卖家信息头部 */} {/* 卖家信息头部 */}
{item.author && ( {item.author && (
<View> <View>
@@ -522,34 +540,56 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
</View> </View>
)} )}
{/* 标题 */} {/* 标题 - 与帖子同步 */}
<View style={styles.titleRow}> <View style={styles.titleRow}>
<Text style={styles.title} numberOfLines={2}>{item.title}</Text> <Text
style={[
styles.title,
{
fontSize: isDesktop ? fontSizes['3xl'] : isTablet ? fontSizes['2xl'] : fontSizes['2xl'],
lineHeight: isDesktop ? 40 : isTablet ? 36 : 32,
},
]}
numberOfLines={2}
>
{item.title}
</Text>
</View> </View>
{/* 商品描述 */} {/* 商品描述 - 与帖子正文同步 */}
{item.content && ( {item.content && (
<Text style={styles.contentText}>{item.content}</Text> <Text
style={[
styles.contentText,
{
fontSize: isDesktop ? fontSizes.xl : isTablet ? fontSizes.lg : fontSizes.lg,
lineHeight: isDesktop ? 32 : isTablet ? 30 : 28,
},
]}
>
{item.content}
</Text>
)} )}
</View> </View>
{/* 图片放在文字下方 */} {/* 图片 - 使用 ImageGrid 与帖子同步 */}
{images.length > 0 && ( {tradeImages.length > 0 && (
<View style={{ paddingHorizontal: spacing.md, paddingBottom: spacing.md }}> <View style={{ paddingHorizontal: responsivePadding, paddingBottom: responsivePadding }}>
{images.map((img, idx) => ( <ImageGrid
<View key={img.id || idx} style={[styles.imageContainer, { marginBottom: spacing.md, borderRadius: borderRadius.lg, overflow: 'hidden' }]}> images={tradeImages}
<Image mode="auto"
source={{ uri: img.url || img.preview_url_large || img.preview_url || '' }} gap={imageGap}
style={styles.image} borderRadius={imageBorderRadius}
maxDisplayCount={isWideScreen ? 20 : 100}
showMoreOverlay={false}
onImagePress={handleImagePress}
/> />
</View> </View>
))}
</View>
)} )}
{/* 内容区右下角属性文字(仅出售商品显示) */} {/* 内容区右下角属性文字(仅出售商品显示) */}
{item.trade_type === 'sell' && ( {item.trade_type === 'sell' && (
<View style={{ paddingHorizontal: spacing.md, paddingBottom: spacing.md }}> <View style={{ paddingHorizontal: responsivePadding, paddingBottom: responsivePadding }}>
<View style={styles.contentAttrs}> <View style={styles.contentAttrs}>
<Text style={styles.contentAttrText}>{item.brand || '其他'}</Text> <Text style={styles.contentAttrText}>{item.brand || '其他'}</Text>
<Text style={styles.contentAttrText}> <Text style={styles.contentAttrText}>
@@ -617,6 +657,18 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
</View> </View>
</Pressable> </Pressable>
</Modal> </Modal>
{/* 图片预览 */}
<ImageGallery
visible={showImageModal}
images={tradeImages.map(img => ({
id: img.id || img.url || String(Math.random()),
url: img.url || '',
}))}
initialIndex={selectedImageIndex}
onClose={() => setShowImageModal(false)}
enableSave
/>
</SafeAreaView> </SafeAreaView>
); );
} }