refactor(ui): enhance TradeDetailScreen with responsive design and image gallery
- 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:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user