2026-04-26 17:13:43 +08:00
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
|
import { View, ScrollView, Image, StyleSheet, Pressable, Alert, Modal } from 'react-native';
|
|
|
|
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
|
|
|
import { useRouter } from 'expo-router';
|
|
|
|
|
|
import { useAppColors, spacing, borderRadius, fontSizes, shadows, type AppColors } from '../../theme';
|
|
|
|
|
|
import { Text, Loading, AppBackButton } from '../../components/common';
|
|
|
|
|
|
import { tradeService } from '../../services/trade/tradeService';
|
|
|
|
|
|
import { messageService } from '../../services/message/messageService';
|
|
|
|
|
|
import { useIsAuthenticated, useCurrentUser } from '../../stores/auth';
|
|
|
|
|
|
import type { TradeItemDetailDTO } from '../../types/trade';
|
|
|
|
|
|
import { TRADE_CONDITION_MAP } from '../../types/trade';
|
|
|
|
|
|
import { hrefChat, hrefEditTrade } from '../../navigation/hrefs';
|
|
|
|
|
|
|
|
|
|
|
|
function createStyles(colors: AppColors) {
|
|
|
|
|
|
return StyleSheet.create({
|
|
|
|
|
|
container: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
},
|
|
|
|
|
|
header: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
paddingHorizontal: spacing.md,
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
...shadows.sm,
|
|
|
|
|
|
},
|
|
|
|
|
|
headerLeft: {
|
|
|
|
|
|
width: 40,
|
|
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
|
},
|
|
|
|
|
|
headerTitle: {
|
|
|
|
|
|
fontSize: fontSizes.xl,
|
|
|
|
|
|
fontWeight: '700',
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
},
|
|
|
|
|
|
headerRight: {
|
|
|
|
|
|
width: 40,
|
|
|
|
|
|
},
|
|
|
|
|
|
imageContainer: {
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
aspectRatio: 1,
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
position: 'relative',
|
|
|
|
|
|
},
|
|
|
|
|
|
image: {
|
|
|
|
|
|
width: '100%',
|
|
|
|
|
|
height: '100%',
|
|
|
|
|
|
resizeMode: 'contain',
|
|
|
|
|
|
},
|
|
|
|
|
|
// 降价标签
|
|
|
|
|
|
discountBadge: {
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
bottom: 12,
|
|
|
|
|
|
left: 12,
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: 4,
|
|
|
|
|
|
paddingHorizontal: 10,
|
|
|
|
|
|
paddingVertical: 5,
|
|
|
|
|
|
borderRadius: borderRadius.lg,
|
|
|
|
|
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
|
|
|
|
},
|
|
|
|
|
|
discountBadgeText: {
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontSize: fontSizes.sm,
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
},
|
|
|
|
|
|
content: {
|
|
|
|
|
|
padding: spacing.md,
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
},
|
|
|
|
|
|
// 顶部卖家信息栏
|
|
|
|
|
|
sellerHeader: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: spacing.sm,
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
sellerAvatar: {
|
|
|
|
|
|
width: 36,
|
|
|
|
|
|
height: 36,
|
|
|
|
|
|
borderRadius: 18,
|
|
|
|
|
|
backgroundColor: colors.background.default,
|
|
|
|
|
|
},
|
|
|
|
|
|
sellerInfo: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
sellerNameRow: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: spacing.xs,
|
|
|
|
|
|
},
|
|
|
|
|
|
sellerName: {
|
|
|
|
|
|
fontSize: fontSizes.lg,
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
},
|
|
|
|
|
|
sellerCreditBadge: {
|
|
|
|
|
|
paddingHorizontal: 6,
|
|
|
|
|
|
paddingVertical: 2,
|
|
|
|
|
|
borderRadius: borderRadius.sm,
|
|
|
|
|
|
backgroundColor: '#FFE066',
|
|
|
|
|
|
},
|
|
|
|
|
|
sellerCreditText: {
|
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
|
color: '#5C4B00',
|
|
|
|
|
|
fontWeight: '700',
|
|
|
|
|
|
},
|
|
|
|
|
|
sellerMeta: {
|
|
|
|
|
|
fontSize: fontSizes.sm,
|
|
|
|
|
|
color: colors.text.hint,
|
|
|
|
|
|
marginTop: 2,
|
|
|
|
|
|
paddingLeft: 36 + spacing.sm,
|
|
|
|
|
|
},
|
|
|
|
|
|
priceInHeader: {
|
|
|
|
|
|
fontSize: fontSizes['2xl'],
|
|
|
|
|
|
fontWeight: '700',
|
|
|
|
|
|
color: '#FF4D4F',
|
|
|
|
|
|
},
|
2026-04-27 01:02:39 +08:00
|
|
|
|
priceSymbol: {
|
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
|
fontWeight: '700',
|
|
|
|
|
|
color: '#FF4D4F',
|
|
|
|
|
|
},
|
2026-04-26 17:13:43 +08:00
|
|
|
|
|
|
|
|
|
|
titleRow: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
|
gap: spacing.sm,
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
conditionBadge: {
|
|
|
|
|
|
paddingHorizontal: 8,
|
|
|
|
|
|
paddingVertical: 3,
|
|
|
|
|
|
borderRadius: borderRadius.sm,
|
|
|
|
|
|
backgroundColor: '#FFE066',
|
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
conditionBadgeText: {
|
|
|
|
|
|
fontSize: fontSizes.sm,
|
|
|
|
|
|
color: '#5C4B00',
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
},
|
|
|
|
|
|
// 副标题信息行(包邮、自提)
|
|
|
|
|
|
subInfoRow: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: spacing.sm,
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
},
|
|
|
|
|
|
subInfoItem: {
|
|
|
|
|
|
fontSize: fontSizes.sm,
|
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
|
},
|
|
|
|
|
|
shippingTag: {
|
|
|
|
|
|
paddingHorizontal: 5,
|
|
|
|
|
|
paddingVertical: 1,
|
|
|
|
|
|
borderRadius: borderRadius.sm,
|
|
|
|
|
|
borderWidth: 0.5,
|
|
|
|
|
|
borderColor: '#FFCCC7',
|
|
|
|
|
|
backgroundColor: '#FFF2F0',
|
|
|
|
|
|
},
|
|
|
|
|
|
shippingTagText: {
|
|
|
|
|
|
fontSize: fontSizes.xs,
|
|
|
|
|
|
color: '#FF4D4F',
|
|
|
|
|
|
},
|
|
|
|
|
|
// 促销横幅
|
|
|
|
|
|
promoBanner: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: spacing.sm,
|
|
|
|
|
|
paddingHorizontal: spacing.md,
|
|
|
|
|
|
paddingVertical: spacing.sm,
|
|
|
|
|
|
backgroundColor: '#FFF5F5',
|
|
|
|
|
|
borderRadius: borderRadius.md,
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
promoBannerText: {
|
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
|
color: '#FF4D4F',
|
|
|
|
|
|
fontWeight: '700',
|
|
|
|
|
|
},
|
|
|
|
|
|
promoBannerSub: {
|
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
|
color: '#FF7875',
|
|
|
|
|
|
},
|
|
|
|
|
|
title: {
|
|
|
|
|
|
fontSize: fontSizes['3xl'],
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
lineHeight: 34,
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
// 商品属性行
|
|
|
|
|
|
attrGrid: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
justifyContent: 'flex-start',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
gap: spacing.lg,
|
|
|
|
|
|
},
|
|
|
|
|
|
attrItem: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: 4,
|
|
|
|
|
|
},
|
|
|
|
|
|
attrLabel: {
|
|
|
|
|
|
fontSize: fontSizes.sm,
|
|
|
|
|
|
color: colors.text.hint,
|
|
|
|
|
|
},
|
|
|
|
|
|
attrValue: {
|
|
|
|
|
|
fontSize: fontSizes.sm,
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
fontWeight: '500',
|
|
|
|
|
|
},
|
|
|
|
|
|
// 描述
|
|
|
|
|
|
contentText: {
|
|
|
|
|
|
fontSize: fontSizes.lg,
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
lineHeight: 28,
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
// 底部操作栏
|
|
|
|
|
|
bottomBar: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
paddingHorizontal: spacing.md,
|
|
|
|
|
|
paddingVertical: 4,
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
borderTopWidth: 1,
|
|
|
|
|
|
borderTopColor: colors.divider,
|
|
|
|
|
|
gap: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
bottomAction: {
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
gap: 2,
|
|
|
|
|
|
},
|
|
|
|
|
|
bottomActionText: {
|
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
|
},
|
|
|
|
|
|
chatButton: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
gap: spacing.xs,
|
|
|
|
|
|
height: 36,
|
|
|
|
|
|
paddingHorizontal: spacing.lg,
|
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
|
backgroundColor: colors.primary.main,
|
|
|
|
|
|
},
|
|
|
|
|
|
chatButtonText: {
|
|
|
|
|
|
color: colors.text.inverse,
|
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
},
|
|
|
|
|
|
chatButtonDisabled: {
|
|
|
|
|
|
backgroundColor: colors.text.hint,
|
|
|
|
|
|
},
|
|
|
|
|
|
// 管理按钮
|
|
|
|
|
|
editButton: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
gap: spacing.sm,
|
|
|
|
|
|
height: 48,
|
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderColor: colors.divider,
|
|
|
|
|
|
paddingHorizontal: spacing.lg,
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
},
|
|
|
|
|
|
editButtonText: {
|
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
},
|
|
|
|
|
|
statusSection: {
|
|
|
|
|
|
marginBottom: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
divider: {
|
|
|
|
|
|
height: 1,
|
|
|
|
|
|
backgroundColor: colors.divider,
|
|
|
|
|
|
marginVertical: spacing.md,
|
|
|
|
|
|
},
|
|
|
|
|
|
// 底部弹窗
|
|
|
|
|
|
modalOverlay: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
|
|
|
|
justifyContent: 'flex-end',
|
|
|
|
|
|
},
|
|
|
|
|
|
modalContent: {
|
|
|
|
|
|
backgroundColor: colors.background.paper,
|
|
|
|
|
|
borderTopLeftRadius: borderRadius.xl,
|
|
|
|
|
|
borderTopRightRadius: borderRadius.xl,
|
|
|
|
|
|
paddingBottom: spacing.xl,
|
|
|
|
|
|
},
|
|
|
|
|
|
modalItem: {
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
|
|
borderBottomColor: colors.divider,
|
|
|
|
|
|
},
|
|
|
|
|
|
modalItemText: {
|
|
|
|
|
|
fontSize: fontSizes.lg,
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
},
|
|
|
|
|
|
modalCancel: {
|
|
|
|
|
|
paddingVertical: spacing.md,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
marginTop: spacing.sm,
|
|
|
|
|
|
},
|
|
|
|
|
|
modalCancelText: {
|
|
|
|
|
|
fontSize: fontSizes.lg,
|
|
|
|
|
|
color: colors.text.hint,
|
|
|
|
|
|
},
|
|
|
|
|
|
// 管理按钮
|
|
|
|
|
|
manageButton: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
gap: spacing.xs,
|
|
|
|
|
|
height: 36,
|
|
|
|
|
|
paddingHorizontal: spacing.lg,
|
|
|
|
|
|
borderRadius: 14,
|
|
|
|
|
|
backgroundColor: 'transparent',
|
|
|
|
|
|
},
|
|
|
|
|
|
manageButtonText: {
|
|
|
|
|
|
fontSize: fontSizes.md,
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
},
|
2026-04-27 01:02:39 +08:00
|
|
|
|
contentAttrs: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
justifyContent: 'flex-end',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: spacing.md,
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
},
|
|
|
|
|
|
contentAttrText: {
|
|
|
|
|
|
fontSize: fontSizes.sm,
|
|
|
|
|
|
color: colors.text.secondary,
|
|
|
|
|
|
},
|
2026-04-26 17:13:43 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface TradeDetailScreenProps {
|
|
|
|
|
|
tradeId: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
|
|
|
|
|
|
const colors = useAppColors();
|
|
|
|
|
|
const styles = createStyles(colors);
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const isAuth = useIsAuthenticated();
|
|
|
|
|
|
const currentUser = useCurrentUser();
|
|
|
|
|
|
|
|
|
|
|
|
const [item, setItem] = useState<TradeItemDetailDTO | null>(null);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
|
|
|
|
|
const [manageModalVisible, setManageModalVisible] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const isOwner = item?.user_id === currentUser?.id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fetchItem = useCallback(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const data = await tradeService.getTradeItem(tradeId);
|
|
|
|
|
|
setItem(data);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Failed to fetch trade item:', e);
|
|
|
|
|
|
Alert.alert('错误', '加载失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [tradeId]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchItem();
|
|
|
|
|
|
}, [fetchItem]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (tradeId) {
|
|
|
|
|
|
tradeService.recordView(tradeId).catch(() => {});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [tradeId]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleFavorite = useCallback(async () => {
|
|
|
|
|
|
if (!isAuth || !item) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (item.is_favorited) {
|
|
|
|
|
|
await tradeService.unfavorite(item.id);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await tradeService.favorite(item.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
setItem(prev => prev ? {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
is_favorited: !prev.is_favorited,
|
|
|
|
|
|
favorites_count: prev.favorites_count + (prev.is_favorited ? -1 : 1),
|
|
|
|
|
|
} : prev);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Favorite failed:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isAuth, item]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleDelete = useCallback(() => {
|
|
|
|
|
|
if (!item) return;
|
|
|
|
|
|
setManageModalVisible(false);
|
|
|
|
|
|
Alert.alert('删除确认', '确定要删除这个商品吗?此操作不可恢复。', [
|
|
|
|
|
|
{ text: '取消', style: 'cancel' },
|
|
|
|
|
|
{
|
|
|
|
|
|
text: '删除',
|
|
|
|
|
|
style: 'destructive',
|
|
|
|
|
|
onPress: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await tradeService.deleteTradeItem(item.id);
|
|
|
|
|
|
router.back();
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
Alert.alert('删除失败', '请稍后重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
]);
|
|
|
|
|
|
}, [item, router]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleEdit = useCallback(() => {
|
|
|
|
|
|
setManageModalVisible(false);
|
|
|
|
|
|
router.push(hrefEditTrade(item?.id || '') as any);
|
|
|
|
|
|
}, [router, item?.id]);
|
|
|
|
|
|
|
|
|
|
|
|
const handleChat = useCallback(async () => {
|
|
|
|
|
|
if (!isAuth || !item?.author?.id) {
|
|
|
|
|
|
Alert.alert('提示', '请先登录');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
const conversation = await messageService.createConversation(item.author.id);
|
|
|
|
|
|
if (conversation) {
|
|
|
|
|
|
router.push(
|
|
|
|
|
|
hrefChat({
|
|
|
|
|
|
conversationId: conversation.id.toString(),
|
|
|
|
|
|
userId: item.author.id,
|
|
|
|
|
|
}) as any
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('创建会话失败:', error);
|
|
|
|
|
|
Alert.alert('错误', '无法发起聊天,请稍后重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isAuth, item?.author?.id, router]);
|
|
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return <Loading fullScreen />;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!item) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SafeAreaView style={styles.container}>
|
|
|
|
|
|
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
|
|
|
|
|
<Text style={{ color: colors.text.hint }}>商品不存在或已删除</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const images = item.images || [];
|
|
|
|
|
|
const currentImage = images[currentImageIndex];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
|
|
|
|
|
<View style={styles.header}>
|
|
|
|
|
|
<View style={styles.headerLeft}>
|
|
|
|
|
|
<AppBackButton onPress={() => router.back()} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<Text style={styles.headerTitle}>商品详情</Text>
|
|
|
|
|
|
<View style={styles.headerRight} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
<ScrollView>
|
|
|
|
|
|
<View style={styles.content}>
|
|
|
|
|
|
{/* 卖家信息头部 */}
|
|
|
|
|
|
{item.author && (
|
|
|
|
|
|
<View>
|
|
|
|
|
|
<View style={styles.sellerHeader}>
|
|
|
|
|
|
{item.author.avatar ? (
|
|
|
|
|
|
<Image source={{ uri: item.author.avatar }} style={styles.sellerAvatar} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<View style={[styles.sellerAvatar, { backgroundColor: colors.primary.main + '33', alignItems: 'center', justifyContent: 'center' }]}>
|
|
|
|
|
|
<MaterialCommunityIcons name="account" size={18} color={colors.primary.main} />
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<View style={styles.sellerInfo}>
|
|
|
|
|
|
<View style={styles.sellerNameRow}>
|
|
|
|
|
|
<Text style={styles.sellerName} numberOfLines={1}>
|
|
|
|
|
|
{item.author.nickname && item.author.nickname.length > 10
|
|
|
|
|
|
? `${item.author.nickname.slice(0, 10)}...`
|
|
|
|
|
|
: item.author.nickname || '匿名'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
{(item.author?.credit_score || 0) > 80 && (
|
|
|
|
|
|
<View style={styles.sellerCreditBadge}>
|
|
|
|
|
|
<Text style={styles.sellerCreditText}>卖家信用优秀</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{item.price != null ? (
|
|
|
|
|
|
<Text style={styles.priceInHeader}>
|
2026-04-27 01:02:39 +08:00
|
|
|
|
价格:<Text style={styles.priceSymbol}>¥</Text>{item.price % 1 === 0 ? item.price.toFixed(0) : item.price.toFixed(2)}
|
2026-04-26 17:13:43 +08:00
|
|
|
|
</Text>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Text style={styles.priceInHeader}>价格:面议</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 标题 */}
|
|
|
|
|
|
<View style={styles.titleRow}>
|
|
|
|
|
|
<Text style={styles.title} numberOfLines={2}>{item.title}</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 商品描述 */}
|
|
|
|
|
|
{item.content && (
|
|
|
|
|
|
<Text style={styles.contentText}>{item.content}</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 图片放在文字下方 */}
|
|
|
|
|
|
{images.length > 0 && (
|
|
|
|
|
|
<View style={{ paddingHorizontal: spacing.md, paddingBottom: spacing.md }}>
|
|
|
|
|
|
{images.map((img, idx) => (
|
|
|
|
|
|
<View key={img.id || idx} style={[styles.imageContainer, { marginBottom: spacing.md, borderRadius: borderRadius.lg, overflow: 'hidden' }]}>
|
|
|
|
|
|
<Image
|
|
|
|
|
|
source={{ uri: img.url || img.preview_url_large || img.preview_url || '' }}
|
|
|
|
|
|
style={styles.image}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
2026-04-27 01:02:39 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 内容区右下角属性文字(仅出售商品显示) */}
|
|
|
|
|
|
{item.trade_type === 'sell' && (
|
|
|
|
|
|
<View style={{ paddingHorizontal: spacing.md, paddingBottom: spacing.md }}>
|
|
|
|
|
|
<View style={styles.contentAttrs}>
|
|
|
|
|
|
<Text style={styles.contentAttrText}>品牌:{item.brand || '其他'}</Text>
|
|
|
|
|
|
<Text style={styles.contentAttrText}>
|
|
|
|
|
|
成色:{TRADE_CONDITION_MAP[item.condition as keyof typeof TRADE_CONDITION_MAP] || item.condition || '未知'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text style={styles.contentAttrText}>功能状态:{item.function_status || '正常使用'}</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
2026-04-26 17:13:43 +08:00
|
|
|
|
</ScrollView>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 底部操作栏 */}
|
|
|
|
|
|
<View style={styles.bottomBar}>
|
|
|
|
|
|
{isOwner ? (
|
|
|
|
|
|
/* 卖家管理按钮 */
|
|
|
|
|
|
<>
|
|
|
|
|
|
<View style={{ flex: 1 }} />
|
|
|
|
|
|
<Pressable style={styles.manageButton} onPress={() => setManageModalVisible(true)}>
|
|
|
|
|
|
<MaterialCommunityIcons name="cog-outline" size={18} color={colors.text.primary} />
|
|
|
|
|
|
<Text style={styles.manageButtonText}>管理</Text>
|
|
|
|
|
|
</Pressable>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{!isOwner && item.status === 'active' && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<View style={{ flex: 1 }} />
|
|
|
|
|
|
<Pressable style={styles.chatButton} onPress={handleChat}>
|
|
|
|
|
|
<MaterialCommunityIcons name="chat-outline" size={18} color={colors.text.inverse} />
|
|
|
|
|
|
<Text style={styles.chatButtonText}>聊一聊</Text>
|
|
|
|
|
|
</Pressable>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!isOwner && item.status !== 'active' && (
|
|
|
|
|
|
<View style={{ flex: 1, alignItems: 'flex-end' }}>
|
|
|
|
|
|
<View style={[styles.chatButton, styles.chatButtonDisabled]}>
|
|
|
|
|
|
<Text style={styles.chatButtonText}>
|
|
|
|
|
|
{item.status === 'sold' ? '已售出' : item.status === 'bought' ? '已收得' : '已下架'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 管理弹窗 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
visible={manageModalVisible}
|
|
|
|
|
|
transparent
|
|
|
|
|
|
animationType="slide"
|
|
|
|
|
|
onRequestClose={() => setManageModalVisible(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Pressable style={styles.modalOverlay} onPress={() => setManageModalVisible(false)}>
|
|
|
|
|
|
<View style={styles.modalContent}>
|
|
|
|
|
|
<Pressable style={styles.modalItem} onPress={handleEdit}>
|
|
|
|
|
|
<Text style={styles.modalItemText}>编辑</Text>
|
|
|
|
|
|
</Pressable>
|
|
|
|
|
|
<Pressable style={[styles.modalItem, { borderBottomWidth: 0 }]} onPress={handleDelete}>
|
|
|
|
|
|
<Text style={[styles.modalItemText, { color: colors.error.main }]}>删除</Text>
|
|
|
|
|
|
</Pressable>
|
|
|
|
|
|
<Pressable style={styles.modalCancel} onPress={() => setManageModalVisible(false)}>
|
|
|
|
|
|
<Text style={styles.modalCancelText}>取消</Text>
|
|
|
|
|
|
</Pressable>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</Pressable>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|