diff --git a/app/_layout.tsx b/app/_layout.tsx
index b1d55b9..431e2bc 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -8,6 +8,7 @@ import { PaperProvider } from 'react-native-paper';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import * as Notifications from 'expo-notifications';
import * as SystemUI from 'expo-system-ui';
+import { useFonts } from 'expo-font';
import { registerNotificationPresentationHandler } from '@/services/notification';
import { api } from '@/services/core';
@@ -267,6 +268,16 @@ function ThemedProviders({ children }: { children: React.ReactNode }) {
}
export default function RootLayout() {
+ const [fontsLoaded] = useFonts({});
+
+ if (!fontsLoaded) {
+ return (
+
+
+
+ );
+ }
+
return (
diff --git a/app/trade/[tradeId].tsx b/app/trade/[tradeId].tsx
new file mode 100644
index 0000000..e630392
--- /dev/null
+++ b/app/trade/[tradeId].tsx
@@ -0,0 +1,7 @@
+import { TradeDetailScreen } from '../../src/screens/trade/TradeDetailScreen';
+import { useLocalSearchParams } from 'expo-router';
+
+export default function TradeDetailRoute() {
+ const { tradeId } = useLocalSearchParams<{ tradeId: string }>();
+ return ;
+}
\ No newline at end of file
diff --git a/app/trade/edit/[tradeId].tsx b/app/trade/edit/[tradeId].tsx
new file mode 100644
index 0000000..60de998
--- /dev/null
+++ b/app/trade/edit/[tradeId].tsx
@@ -0,0 +1,46 @@
+import { useCallback, useEffect, useState } from 'react';
+import { useLocalSearchParams, useRouter } from 'expo-router';
+import { CreateTradeScreen } from '../../../src/screens/trade/CreateTradeScreen';
+import { tradeService } from '../../../src/services/trade/tradeService';
+import { Loading } from '../../../src/components/common';
+import { View, Alert } from 'react-native';
+import type { TradeItemDetailDTO } from '../../../src/types/trade';
+
+export default function EditTradeRoute() {
+ const { tradeId } = useLocalSearchParams<{ tradeId: string }>();
+ const router = useRouter();
+ const [item, setItem] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (!tradeId) return;
+ tradeService.getTradeItem(tradeId)
+ .then(data => setItem(data))
+ .catch(() => Alert.alert('错误', '加载商品信息失败'))
+ .finally(() => setLoading(false));
+ }, [tradeId]);
+
+ const handleClose = useCallback(() => {
+ router.back();
+ }, [router]);
+
+ const handleSuccess = useCallback(() => {
+ router.back();
+ }, [router]);
+
+ if (loading) {
+ return ;
+ }
+
+ if (!item) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/src/components/business/CommentItem.tsx b/src/components/business/CommentItem.tsx
index 79be8a6..b8fb632 100644
--- a/src/components/business/CommentItem.tsx
+++ b/src/components/business/CommentItem.tsx
@@ -128,7 +128,7 @@ function createCommentItemStyles(colors: AppColors) {
actionButton: {
flexDirection: 'row',
alignItems: 'center',
- marginRight: spacing.lg,
+ marginRight: spacing.sm,
paddingVertical: 4,
paddingRight: spacing.xs,
},
diff --git a/src/components/business/SearchBar.tsx b/src/components/business/SearchBar.tsx
index 0d66df7..2ccc101 100644
--- a/src/components/business/SearchBar.tsx
+++ b/src/components/business/SearchBar.tsx
@@ -16,52 +16,44 @@ interface SearchBarProps {
onFocus?: () => void;
onBlur?: () => void;
autoFocus?: boolean;
+ compact?: boolean;
}
-function createSearchBarStyles(colors: AppColors) {
+function createSearchBarStyles(colors: AppColors, compact: boolean) {
return StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
- backgroundColor: colors.background.paper,
+ backgroundColor: 'transparent',
borderRadius: borderRadius.full,
- paddingHorizontal: spacing.xs,
- height: 46,
- borderWidth: 1,
+ paddingHorizontal: compact ? spacing.sm : spacing.xs,
+ height: compact ? 38 : 46,
+ borderWidth: compact ? 1 : 1,
borderColor: colors.divider,
- shadowColor: 'transparent',
- shadowOffset: { width: 0, height: 0 },
- shadowOpacity: 0,
- shadowRadius: 0,
- elevation: 0,
},
containerFocused: {
borderColor: colors.primary.main,
- shadowColor: 'transparent',
- shadowOffset: { width: 0, height: 0 },
- shadowOpacity: 0,
- shadowRadius: 0,
- elevation: 0,
+ backgroundColor: 'transparent',
},
searchIconWrap: {
- width: 30,
- height: 30,
- marginLeft: spacing.xs,
- marginRight: spacing.xs,
+ width: compact ? 22 : 30,
+ height: compact ? 22 : 30,
+ marginLeft: compact ? 2 : spacing.xs,
+ marginRight: compact ? 6 : spacing.xs,
borderRadius: borderRadius.full,
- backgroundColor: `${colors.text.secondary}12`,
+ backgroundColor: 'transparent',
alignItems: 'center',
justifyContent: 'center',
},
searchIconWrapFocused: {
- backgroundColor: `${colors.primary.main}1A`,
+ backgroundColor: 'transparent',
},
input: {
flex: 1,
- fontSize: fontSizes.md,
+ fontSize: compact ? fontSizes.md : fontSizes.md,
color: colors.text.primary,
- paddingVertical: spacing.sm + 1,
- paddingHorizontal: spacing.xs,
+ paddingVertical: compact ? 0 : spacing.sm + 1,
+ paddingHorizontal: compact ? 2 : spacing.xs,
},
clearButton: {
width: 24,
@@ -83,9 +75,10 @@ const SearchBar: React.FC = ({
onFocus,
onBlur,
autoFocus = false,
+ compact = false,
}) => {
const colors = useAppColors();
- const styles = useMemo(() => createSearchBarStyles(colors), [colors]);
+ const styles = useMemo(() => createSearchBarStyles(colors, compact), [colors, compact]);
const [isFocused, setIsFocused] = useState(false);
const handleFocus = () => {
@@ -103,7 +96,7 @@ const SearchBar: React.FC = ({
diff --git a/src/components/business/TradeCard/TradeCard.tsx b/src/components/business/TradeCard/TradeCard.tsx
new file mode 100644
index 0000000..ca9da18
--- /dev/null
+++ b/src/components/business/TradeCard/TradeCard.tsx
@@ -0,0 +1,377 @@
+import React, { memo } from 'react';
+import { View, TouchableOpacity, Image, StyleSheet, Pressable } from 'react-native';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { useAppColors, spacing, borderRadius, fontSizes, type AppColors } from '../../../theme';
+import { Text } from '../../common';
+import type { TradeItemDTO } from '../../../types/trade';
+import { TRADE_CATEGORY_MAP, TRADE_CONDITION_MAP, TRADE_TYPE_MAP } from '../../../types/trade';
+
+export interface TradeCardProps {
+ item: TradeItemDTO;
+ variant?: 'list' | 'grid';
+ onPress?: (id: string) => void;
+ onFavorite?: (id: string) => void;
+}
+
+function formatPrice(price?: number | null): string {
+ if (price == null) return '面议';
+ return `¥${price.toFixed(price % 1 === 0 ? 0 : 2)}`;
+}
+
+function createTradeCardStyles(colors: AppColors) {
+ return StyleSheet.create({
+ gridCard: {
+ backgroundColor: colors.background.paper,
+ borderRadius: borderRadius.xl,
+ overflow: 'hidden',
+ // subtle shadow for depth
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.06,
+ shadowRadius: 3,
+ elevation: 2,
+ },
+ listCard: {
+ backgroundColor: colors.background.paper,
+ borderRadius: borderRadius.lg,
+ overflow: 'hidden',
+ padding: spacing.md,
+ },
+ imageContainer: {
+ position: 'relative',
+ aspectRatio: 1,
+ backgroundColor: colors.background.default,
+ },
+ listImageContainer: {
+ width: 120,
+ height: 120,
+ borderRadius: borderRadius.md,
+ backgroundColor: colors.background.default,
+ overflow: 'hidden',
+ },
+ image: {
+ width: '100%',
+ height: '100%',
+ resizeMode: 'cover',
+ },
+ // 包邮/标签 badge 放在图片左下角,黄色背景
+ shippingBadge: {
+ position: 'absolute',
+ bottom: 6,
+ left: 6,
+ paddingHorizontal: 5,
+ paddingVertical: 2,
+ borderRadius: borderRadius.sm,
+ backgroundColor: '#FFE066',
+ },
+ shippingBadgeText: {
+ color: '#5C4B00',
+ fontSize: 10,
+ fontWeight: '700',
+ },
+ typeBadge: {
+ position: 'absolute',
+ top: 6,
+ left: 6,
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ borderRadius: borderRadius.sm,
+ overflow: 'hidden',
+ },
+ sellBadge: {
+ backgroundColor: `${colors.success.main}CC`,
+ },
+ buyBadge: {
+ backgroundColor: `${colors.primary.main}CC`,
+ },
+ typeBadgeText: {
+ color: '#fff',
+ fontSize: fontSizes.xs,
+ fontWeight: '600',
+ },
+ statusBadge: {
+ position: 'absolute',
+ top: 6,
+ right: 6,
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ borderRadius: borderRadius.sm,
+ backgroundColor: `${colors.warning.main}BB`,
+ },
+ statusBadgeText: {
+ color: '#fff',
+ fontSize: fontSizes.xs,
+ fontWeight: '600',
+ },
+ gridContent: {
+ padding: spacing.sm,
+ },
+ titleRow: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ justifyContent: 'space-between',
+ gap: spacing.xs,
+ },
+ title: {
+ fontSize: fontSizes.md,
+ fontWeight: '500',
+ color: colors.text.primary,
+ lineHeight: 20,
+ flex: 1,
+ },
+ priceInline: {
+ fontSize: fontSizes.sm,
+ fontWeight: '700',
+ color: '#FF4D4F',
+ flexShrink: 0,
+ },
+ priceRow: {
+ flexDirection: 'row',
+ alignItems: 'baseline',
+ gap: 4,
+ marginTop: spacing.xs,
+ },
+ price: {
+ fontSize: fontSizes['2xl'],
+ fontWeight: '700',
+ color: '#FF4D4F',
+ },
+ originalPrice: {
+ fontSize: fontSizes.xs,
+ color: colors.text.hint,
+ textDecorationLine: 'line-through',
+ },
+ negotiable: {
+ fontSize: fontSizes.lg,
+ fontWeight: '600',
+ color: colors.text.secondary,
+ },
+ wantCount: {
+ fontSize: fontSizes.xs,
+ color: colors.text.hint,
+ marginLeft: 4,
+ },
+ tagsRow: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 4,
+ marginTop: spacing.xs,
+ },
+ tag: {
+ paddingHorizontal: 5,
+ paddingVertical: 1,
+ borderRadius: borderRadius.sm,
+ backgroundColor: `${colors.primary.main}10`,
+ },
+ tagText: {
+ fontSize: 10,
+ color: colors.primary.main,
+ },
+ conditionTag: {
+ backgroundColor: `${colors.success.main}10`,
+ },
+ conditionTagText: {
+ fontSize: 10,
+ color: colors.success.main,
+ },
+ // 促销标签样式(如"24小时发货")
+ promoTag: {
+ paddingHorizontal: 5,
+ paddingVertical: 1,
+ borderRadius: borderRadius.sm,
+ backgroundColor: '#FFF2F0',
+ borderWidth: 0.5,
+ borderColor: '#FFCCC7',
+ },
+ promoTagText: {
+ fontSize: 10,
+ color: '#FF4D4F',
+ },
+ bottomRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginTop: spacing.sm,
+ },
+ authorRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: spacing.xs,
+ flex: 1,
+ },
+ avatar: {
+ width: 18,
+ height: 18,
+ borderRadius: 9,
+ backgroundColor: colors.background.default,
+ },
+ authorName: {
+ fontSize: 11,
+ color: colors.text.secondary,
+ flexShrink: 1,
+ },
+ statsRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
+ },
+ statItem: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 2,
+ },
+ statText: {
+ fontSize: 11,
+ color: colors.text.hint,
+ },
+ favButton: {
+ padding: 2,
+ },
+ listContent: {
+ flex: 1,
+ justifyContent: 'space-between',
+ },
+ listLayout: {
+ flexDirection: 'row',
+ gap: spacing.md,
+ },
+ imagePlaceholder: {
+ width: '100%',
+ height: '100%',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: colors.background.default,
+ },
+ });
+}
+
+const TradeCardBase: React.FC = ({ item, variant = 'list', onPress, onFavorite }) => {
+ const colors = useAppColors();
+ const styles = createTradeCardStyles(colors);
+
+ const firstImage = item.images?.[0];
+ const isSell = item.trade_type === 'sell';
+ const isActive = item.status === 'active';
+ const conditionLabel = item.condition ? TRADE_CONDITION_MAP[item.condition as keyof typeof TRADE_CONDITION_MAP] : null;
+ const categoryLabel = TRADE_CATEGORY_MAP[item.category] || item.category;
+
+ const renderImage = (containerStyle: any, imageStyle: any) => (
+
+ {firstImage ? (
+
+ ) : (
+
+
+
+ )}
+ {/* 包邮标签 */}
+ {item.is_free_shipping && (
+
+ 包邮
+
+ )}
+
+ {TRADE_TYPE_MAP[item.trade_type]}
+
+ {!isActive && (
+
+
+ {item.status === 'sold' ? '已售' : item.status === 'bought' ? '已收' : item.status === 'offline' ? '下架' : ''}
+
+
+ )}
+
+ );
+
+ const renderTitleRow = () => (
+
+ {item.title}
+
+ {item.price != null ? formatPrice(item.price) : '面议'}
+
+
+ );
+
+ const renderTags = () => {
+ if (!conditionLabel && !isSell && !item.is_quick_ship) return null;
+ return (
+
+ {conditionLabel && isSell && (
+
+ {conditionLabel}
+
+ )}
+ {item.is_quick_ship && (
+
+ 24小时发货
+
+ )}
+
+ );
+ };
+
+ const renderBottom = () => (
+
+
+ {item.author?.avatar ? (
+
+ ) : (
+
+ )}
+ {item.author?.nickname || '匿名'}
+
+
+ {(item.good_reputation || 0) > 0 && (
+
+ 回头客好评过百
+
+ )}
+
+ {categoryLabel}
+
+
+
+ );
+
+ if (variant === 'grid') {
+ return (
+ onPress?.(item.id)}
+ activeOpacity={0.7}
+ >
+ {renderImage(styles.imageContainer, styles.image)}
+
+ {renderTitleRow()}
+ {renderTags()}
+ {renderBottom()}
+
+
+ );
+ }
+
+ return (
+ onPress?.(item.id)}
+ activeOpacity={0.7}
+ >
+
+ {renderImage(styles.listImageContainer, styles.image)}
+
+ {renderTitleRow()}
+ {renderTags()}
+ {renderBottom()}
+
+
+
+ );
+};
+
+export const TradeCard = memo(TradeCardBase);
+
+export default TradeCard;
diff --git a/src/components/business/index.ts b/src/components/business/index.ts
index 4988e02..749640e 100644
--- a/src/components/business/index.ts
+++ b/src/components/business/index.ts
@@ -26,3 +26,4 @@ export { default as PostContentRenderer } from './PostContentRenderer';
export { default as PostMentionInput } from './PostMentionInput';
export { default as ReportDialog } from './ReportDialog';
export { default as ShareSheet } from './ShareSheet';
+export { TradeCard } from './TradeCard/TradeCard';
diff --git a/src/navigation/hrefs.ts b/src/navigation/hrefs.ts
index 4df90ad..c960498 100644
--- a/src/navigation/hrefs.ts
+++ b/src/navigation/hrefs.ts
@@ -218,3 +218,7 @@ export function hrefProfilePrivacySettings(): string {
export function hrefProfileDeletion(): string {
return '/profile/account-deletion';
}
+
+export function hrefEditTrade(tradeId: string): string {
+ return `/trade/edit/${encodeURIComponent(tradeId)}`;
+}
diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx
index 369dd33..1213ecc 100644
--- a/src/screens/home/HomeScreen.tsx
+++ b/src/screens/home/HomeScreen.tsx
@@ -37,12 +37,22 @@ import { useDifferentialPosts } from '../../hooks/useDifferentialPosts';
import { postSyncService } from '@/services/post';
import { SearchScreen } from './SearchScreen';
import { CreatePostScreen } from '../create/CreatePostScreen';
+import { MarketView } from './MarketView';
+import { CreateTradeScreen } from '../trade/CreateTradeScreen';
+import type { TradeCategory } from '../../types/trade';
+import { ALL_TRADE_CATEGORIES } from '../../types/trade';
import * as hrefs from '../../navigation/hrefs';
import { blurActiveElement } from '../../infrastructure/platform';
const SORT_OPTIONS = ['关注', '推荐', '最新'];
const SORT_ICONS = ['account-heart', 'fire', 'clock-outline'];
const SORT_TYPES: PostType[] = ['follow', 'hot', 'latest'];
+
+const MARKET_FILTER_OPTIONS = [
+ { key: 'all' as const, label: '全部', icon: 'view-grid-outline' },
+ { key: 'sell' as const, label: '出售', icon: 'tag-outline' },
+ { key: 'buy' as const, label: '收购', icon: 'hand-coin-outline' },
+];
const DEFAULT_PAGE_SIZE = 20;
const MOBILE_TAB_BAR_HEIGHT = 64;
const MOBILE_TAB_FLOATING_MARGIN = 12;
@@ -56,7 +66,7 @@ type ViewMode = 'list' | 'grid';
type PostType = 'follow' | 'hot' | 'latest';
type LatestCapsule = { id: string; name: string };
-function createHomeStyles(colors: AppColors) {
+function createHomeStyles(colors: AppColors, responsivePadding: number) {
return StyleSheet.create({
container: {
flex: 1,
@@ -66,13 +76,45 @@ function createHomeStyles(colors: AppColors) {
backgroundColor: colors.background.paper,
},
searchWrapper: {
- paddingTop: spacing.lg,
+ paddingTop: spacing.sm,
paddingBottom: spacing.xs,
- shadowColor: 'transparent',
- shadowOffset: { width: 0, height: 0 },
- shadowOpacity: 0,
- shadowRadius: 0,
- elevation: 0,
+ },
+ homeTabSwitcher: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ marginTop: spacing.xs,
+ marginBottom: spacing.xs,
+ paddingHorizontal: responsivePadding + 4,
+ },
+ homeTabSwitcherLeft: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 20,
+ },
+ homeTabSwitcherRight: {
+ flex: 1,
+ marginLeft: 16,
+ },
+ homeTabItem: {
+ paddingVertical: spacing.sm,
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderBottomWidth: 2,
+ borderBottomColor: 'transparent',
+ },
+ homeTabItemActive: {
+ borderBottomColor: colors.text.primary,
+ },
+ homeTabText: {
+ fontSize: 20,
+ fontWeight: '400',
+ color: colors.text.hint,
+ },
+ homeTabTextActive: {
+ fontSize: 20,
+ fontWeight: '700',
+ color: colors.text.primary,
},
homeTabBar: {
marginTop: spacing.sm,
@@ -89,7 +131,6 @@ function createHomeStyles(colors: AppColors) {
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
- backgroundColor: colors.background.paper,
},
sortBarCapsuleScroll: {
flex: 1,
@@ -182,21 +223,22 @@ export const HomeScreen: React.FC = () => {
const hasFetchedVerification = useVerificationStore((s) => s.hasFetchedStatus);
const colors = useAppColors();
const resolvedScheme = useResolvedColorScheme();
- const styles = useMemo(() => createHomeStyles(colors), [colors]);
- const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content';
// 使用响应式 hook
- const {
- width,
- isMobile,
- isTablet,
- isDesktop,
+ const {
+ width,
+ isMobile,
+ isTablet,
+ isDesktop,
isWideScreen,
} = useResponsive();
-
+
// 响应式间距
const responsiveGap = useResponsiveSpacing({ xs: 4, sm: 6, md: 8, lg: 12, xl: 16 });
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
+
+ const styles = useMemo(() => createHomeStyles(colors, responsivePadding), [colors, responsivePadding]);
+ const statusBarStyle = resolvedScheme === 'dark' ? 'light-content' : 'dark-content';
const [sortIndex, setSortIndex] = useState(0);
const [viewMode, setViewMode] = useState('list');
@@ -214,6 +256,14 @@ export const HomeScreen: React.FC = () => {
// 发帖弹窗状态
const [showCreatePost, setShowCreatePost] = useState(false);
+ // 广场/市集 Tab
+ const [homeTab, setHomeTab] = useState<'square' | 'market'>('square');
+ const [showCreateTrade, setShowCreateTrade] = useState(false);
+
+ // 市集筛选状态
+ const [marketFilterType, setMarketFilterType] = useState<'all' | 'sell' | 'buy'>('all');
+ const [marketCategory, setMarketCategory] = useState('all');
+
// 分享面板状态
const [showShareSheet, setShowShareSheet] = useState(false);
const [sharePost, setSharePost] = useState(null);
@@ -530,6 +580,13 @@ export const HomeScreen: React.FC = () => {
setSortIndex(nextIndex);
}, [sortIndex]);
+ const cycleMarketFilter = useCallback(() => {
+ setMarketFilterType(prev => {
+ const idx = MARKET_FILTER_OPTIONS.findIndex(f => f.key === prev);
+ return MARKET_FILTER_OPTIONS[(idx + 1) % MARKET_FILTER_OPTIONS.length].key;
+ });
+ }, []);
+
useEffect(() => {
const loadChannels = async () => {
const list = await channelService.list();
@@ -856,18 +913,42 @@ export const HomeScreen: React.FC = () => {
{/* 顶部Header */}
- {/* 搜索栏 */}
-
- {}}
- onSubmit={handleSearchPress}
- onFocus={handleSearchPress}
- placeholder="搜索帖子、用户"
- />
+ {/* 广场/市集切换 */}
+
+
+ setHomeTab('square')}
+ >
+
+ 广场
+
+
+ setHomeTab('market')}
+ >
+
+ 市集
+
+
+
+
+ {}}
+ onSubmit={handleSearchPress}
+ onFocus={handleSearchPress}
+ placeholder="搜索"
+ compact
+ />
+
-
- {/* 排序筛选 + 频道 */}
+
+ {/* 排序筛选 + 频道(仅广场模式显示) */}
+ {homeTab === 'square' && (
{
+ )}
+
+ {/* 市集分类 + 筛选(仅市集模式显示) */}
+ {homeTab === 'market' && (
+
+ (
+ setMarketCategory(item.key as TradeCategory | 'all')}
+ >
+
+ {item.label}
+
+
+ )}
+ keyExtractor={(item: { key: string; label: string }) => item.key}
+ showsHorizontalScrollIndicator={false}
+ contentContainerStyle={styles.sortBarCapsuleContent}
+ style={styles.sortBarCapsuleScroll}
+ />
+
+ f.key === marketFilterType)?.icon || 'view-grid-outline') as any}
+ size={18}
+ color={colors.text.secondary}
+ />
+
+ {MARKET_FILTER_OPTIONS.find(f => f.key === marketFilterType)?.label || '全部'}
+
+
+
+ )}
- {/* 帖子列表 */}
+ {/* 内容区域 */}
+ {homeTab === 'square' ? (
{isInitialLoading ? (
@@ -906,8 +1034,17 @@ export const HomeScreen: React.FC = () => {
renderResponsiveGrid()
)}
+ ) : (
+ setShowCreateTrade(true)}
+ filterType={marketFilterType}
+ onFilterTypeChange={setMarketFilterType}
+ selectedCategory={marketCategory}
+ onSelectedCategoryChange={setMarketCategory}
+ />
+ )}
- {/* 漂浮发帖按钮 */}
+ {/* 漂浮发帖/发布按钮 */}
{
isWideScreen && styles.floatingButtonWide,
floatingButtonBottom !== undefined && { bottom: floatingButtonBottom },
]}
- onPress={handleCreatePost}
+ onPress={homeTab === 'market' ? () => {
+ if (!isAuthenticated) { router.push('/login'); return; }
+ if (!isVerified) { router.push(hrefs.hrefVerificationGuide()); return; }
+ setShowCreateTrade(true);
+ } : handleCreatePost}
activeOpacity={0.8}
>
@@ -945,6 +1086,18 @@ export const HomeScreen: React.FC = () => {
/>
+ {/* 发布交易弹窗 */}
+ setShowCreateTrade(false)}
+ >
+ setShowCreateTrade(false)}
+ />
+
+
{/* 分享面板 */}
void;
+ filterType?: TradeFilterType;
+ onFilterTypeChange?: (type: TradeFilterType) => void;
+ selectedCategory?: TradeCategory | 'all';
+ onSelectedCategoryChange?: (cat: TradeCategory | 'all') => void;
+}
+
+export function MarketView({
+ onCreateTrade,
+ filterType: filterTypeProp,
+ onFilterTypeChange,
+ selectedCategory: selectedCategoryProp,
+ onSelectedCategoryChange,
+}: MarketViewProps) {
+ const colors = useAppColors();
+ const router = useRouter();
+ const isAuth = useIsAuthenticated();
+ const { width, isMobile } = useResponsive();
+
+ const gap = useResponsiveSpacing({ xs: 6, sm: 8, md: 10, lg: 12, xl: 16 });
+ const padding = useResponsiveSpacing({ xs: 8, sm: 10, md: 12, lg: 16, xl: 20 });
+ const styles = useMemo(() => createMarketStyles(colors, gap, padding), [colors, gap, padding]);
+
+ const [items, setItems] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
+ const [hasMore, setHasMore] = useState(true);
+ const [cursor, setCursor] = useState();
+ const [filterTypeLocal, setFilterTypeLocal] = useState('all');
+ const [selectedCategoryLocal, setSelectedCategoryLocal] = useState('all');
+
+ const filterType = filterTypeProp ?? filterTypeLocal;
+ const selectedCategory = selectedCategoryProp ?? selectedCategoryLocal;
+ const setFilterType = onFilterTypeChange ?? setFilterTypeLocal;
+ const setSelectedCategory = onSelectedCategoryChange ?? setSelectedCategoryLocal;
+
+ const flashListRef = useRef(null);
+
+ // 网格列数:移动端2列,平板3列,桌面4列
+ const numColumns = useMemo(() => {
+ if (width >= 1200) return 4;
+ if (width >= 768) return 3;
+ return 2;
+ }, [width]);
+
+ // 计算卡片宽度(用于固定宽高比)
+ const cardWidth = useMemo(() => {
+ const totalGap = gap * (numColumns - 1);
+ return Math.floor((width - padding * 2 - totalGap) / numColumns);
+ }, [width, padding, gap, numColumns]);
+
+ const fetchItems = useCallback(async (isRefresh = false) => {
+ try {
+ if (isRefresh) {
+ setIsRefreshing(true);
+ } else if (!isLoading) {
+ return;
+ }
+
+ const tradeType = filterType === 'all' ? undefined : (filterType as TradeType);
+ const category = selectedCategory === 'all' ? undefined : (selectedCategory as TradeCategory);
+
+ const res = await tradeService.getTradeItems({
+ trade_type: tradeType,
+ category,
+ page_size: PAGE_SIZE,
+ });
+
+ setItems(res.list || []);
+ setCursor(res.next_cursor);
+ setHasMore(res.has_more);
+ } catch (e) {
+ console.error('Failed to fetch trade items:', e);
+ } finally {
+ setIsLoading(false);
+ setIsRefreshing(false);
+ }
+ }, [filterType, selectedCategory]);
+
+ useEffect(() => {
+ setIsLoading(true);
+ setCursor(undefined);
+ fetchItems(true);
+ }, [filterType, selectedCategory]);
+
+ const loadMore = useCallback(async () => {
+ if (isLoadingMore || !hasMore || !cursor) return;
+ setIsLoadingMore(true);
+ try {
+ const tradeType = filterType === 'all' ? undefined : (filterType as TradeType);
+ const category = selectedCategory === 'all' ? undefined : (selectedCategory as TradeCategory);
+ const res = await tradeService.getTradeItems({
+ trade_type: tradeType,
+ category,
+ cursor,
+ page_size: PAGE_SIZE,
+ });
+ setItems(prev => [...prev, ...(res.list || [])]);
+ setCursor(res.next_cursor);
+ setHasMore(res.has_more);
+ } catch (e) {
+ console.error('Failed to load more trade items:', e);
+ } finally {
+ setIsLoadingMore(false);
+ }
+ }, [isLoadingMore, hasMore, cursor, filterType, selectedCategory]);
+
+ const handlePressItem = useCallback((id: string) => {
+ router.push(`/trade/${id}` as any);
+ }, [router]);
+
+ const handleFavorite = useCallback(async (id: string) => {
+ if (!isAuth) return;
+ try {
+ const item = items.find(i => i.id === id);
+ if (!item) return;
+ if (item.is_favorited) {
+ await tradeService.unfavorite(id);
+ } else {
+ await tradeService.favorite(id);
+ }
+ setItems(prev =>
+ prev.map(i =>
+ i.id === id
+ ? { ...i, is_favorited: !i.is_favorited, favorites_count: i.favorites_count + (i.is_favorited ? -1 : 1) }
+ : i
+ )
+ );
+ } catch (e) {
+ console.error('Favorite failed:', e);
+ }
+ }, [items, isAuth]);
+
+ const renderItem = useCallback>(({ item }) => (
+
+
+
+ ), [handlePressItem, handleFavorite, cardWidth, gap]);
+
+ const keyExtractor = useCallback((item: TradeItemDTO) => item.id, []);
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+ fetchItems(true)}
+ colors={[colors.primary.main]}
+ tintColor={colors.primary.main}
+ />
+ }
+ onEndReached={loadMore}
+ onEndReachedThreshold={0.3}
+ ListEmptyComponent={
+
+ }
+ ListFooterComponent={isLoadingMore ? : null}
+ drawDistance={250}
+ />
+
+ );
+}
diff --git a/src/screens/home/PostDetailScreen.tsx b/src/screens/home/PostDetailScreen.tsx
index 9503da4..c3ca3a3 100644
--- a/src/screens/home/PostDetailScreen.tsx
+++ b/src/screens/home/PostDetailScreen.tsx
@@ -1472,7 +1472,7 @@ export const PostDetailScreen: React.FC = () => {
@@ -1483,7 +1483,7 @@ export const PostDetailScreen: React.FC = () => {
@@ -1492,7 +1492,7 @@ export const PostDetailScreen: React.FC = () => {
-
+
{formatNumber(post?.shares_count || 0)}
@@ -2153,7 +2153,7 @@ function createPostDetailStyles(colors: AppColors) {
borderRadius: 20,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm + 2,
- minHeight: 40,
+ minHeight: 44,
justifyContent: 'center',
},
inputTriggerText: {
@@ -2163,14 +2163,14 @@ function createPostDetailStyles(colors: AppColors) {
bottomComposerRow: {
flexDirection: 'row',
alignItems: 'center',
- gap: spacing.xs + 2,
+ gap: spacing.xs,
},
bottomActionItem: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 2,
- paddingHorizontal: spacing.sm + 1,
- marginLeft: spacing.xs,
+ paddingHorizontal: spacing.sm,
+ marginLeft: 0,
},
bottomActionCount: {
marginTop: 1,
diff --git a/src/screens/trade/CreateTradeScreen.tsx b/src/screens/trade/CreateTradeScreen.tsx
new file mode 100644
index 0000000..34ae0fd
--- /dev/null
+++ b/src/screens/trade/CreateTradeScreen.tsx
@@ -0,0 +1,640 @@
+/**
+ * CreateTradeScreen 发布/编辑商品(现代化扁平风格)
+ * 设计风格:
+ * - 纯白背景,扁平化设计
+ * - 灰色填充输入框,圆角14px
+ * - 橙色圆角按钮
+ * - 清晰的标签 + 输入框分组
+ * - 与登录、注册、创建群聊保持一致
+ */
+
+import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
+import {
+ View,
+ TextInput,
+ ScrollView,
+ Alert,
+ TouchableOpacity,
+ Image,
+ StyleSheet,
+ Pressable,
+ KeyboardAvoidingView,
+ Platform,
+ Animated,
+ ActivityIndicator,
+} from 'react-native';
+import * as ImagePicker from 'expo-image-picker';
+import { Loading } from '../../components/common';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { useAppColors, type AppColors } from '../../theme';
+import { Text } from '../../components/common';
+import { tradeService } from '../../services/trade/tradeService';
+import { uploadService } from '../../services';
+import type { TradeType, TradeCategory, TradeCondition, TradeItemDetailDTO } from '../../types/trade';
+import { ALL_TRADE_CATEGORIES, ALL_TRADE_CONDITIONS } from '../../types/trade';
+
+interface CreateTradeScreenProps {
+ onClose: () => void;
+ onSuccess?: () => void;
+ editItem?: TradeItemDetailDTO;
+}
+
+function createStyles(colors: AppColors) {
+ return StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background.paper,
+ },
+ keyboardView: {
+ flex: 1,
+ },
+ scrollContent: {
+ flexGrow: 1,
+ paddingHorizontal: 28,
+ paddingTop: 20,
+ paddingBottom: 40,
+ },
+ content: {
+ flex: 1,
+ maxWidth: 400,
+ width: '100%',
+ alignSelf: 'center',
+ },
+
+ // 标题区域
+ titleSection: {
+ marginBottom: 32,
+ marginTop: 12,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: '700',
+ color: colors.text.primary,
+ lineHeight: 36,
+ },
+ underline: {
+ width: 40,
+ height: 4,
+ backgroundColor: colors.primary.main,
+ borderRadius: 2,
+ marginTop: 12,
+ marginBottom: 12,
+ },
+ subtitle: {
+ fontSize: 15,
+ color: colors.text.secondary,
+ },
+
+ // 输入框区域(参考登录注册风格)
+ inputContainer: {
+ marginBottom: 24,
+ },
+ inputLabel: {
+ fontSize: 14,
+ fontWeight: '500',
+ color: colors.text.secondary,
+ marginBottom: 10,
+ },
+ requiredMark: {
+ color: colors.error.main,
+ fontWeight: '600',
+ },
+ inputWrapper: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: colors.background.default,
+ borderRadius: 14,
+ paddingHorizontal: 18,
+ minHeight: 56,
+ borderWidth: 1,
+ borderColor: 'transparent',
+ },
+ input: {
+ flex: 1,
+ fontSize: 16,
+ color: colors.text.primary,
+ minHeight: 56,
+ },
+ textAreaWrapper: {
+ paddingVertical: 14,
+ alignItems: 'flex-start',
+ },
+ textArea: {
+ minHeight: 100,
+ lineHeight: 22,
+ textAlignVertical: 'top',
+ },
+ checkIcon: {
+ marginLeft: 8,
+ },
+ charCount: {
+ textAlign: 'right',
+ marginTop: 6,
+ fontSize: 12,
+ color: colors.text.hint,
+ fontWeight: '500',
+ },
+
+ // 交易类型选择
+ typeRow: {
+ flexDirection: 'row',
+ gap: 12,
+ },
+ typeButton: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 8,
+ paddingVertical: 14,
+ borderRadius: 14,
+ borderWidth: 1,
+ borderColor: colors.divider,
+ backgroundColor: colors.background.default,
+ },
+ typeButtonActive: {
+ borderColor: colors.primary.main,
+ backgroundColor: `${colors.primary.main}12`,
+ },
+ typeButtonText: {
+ fontSize: 16,
+ color: colors.text.secondary,
+ },
+ typeButtonTextActive: {
+ color: colors.primary.main,
+ fontWeight: '600',
+ },
+
+ // 分类/成色选项(紧凑横向滚动)
+ optionRow: {
+ flexDirection: 'row',
+ gap: 8,
+ },
+ optionChip: {
+ paddingHorizontal: 14,
+ paddingVertical: 8,
+ borderRadius: 10,
+ backgroundColor: colors.background.default,
+ borderWidth: 1,
+ borderColor: colors.divider,
+ },
+ optionChipActive: {
+ borderColor: colors.primary.main,
+ backgroundColor: `${colors.primary.main}12`,
+ },
+ optionChipText: {
+ fontSize: 13,
+ color: colors.text.secondary,
+ },
+ optionChipTextActive: {
+ color: colors.primary.main,
+ fontWeight: '600',
+ },
+
+ // 价格输入
+ priceInputWrapper: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: colors.background.default,
+ borderRadius: 14,
+ paddingHorizontal: 18,
+ minHeight: 56,
+ borderWidth: 1,
+ borderColor: 'transparent',
+ },
+ pricePrefix: {
+ fontSize: 18,
+ color: colors.text.hint,
+ marginRight: 4,
+ },
+ priceField: {
+ flex: 1,
+ fontSize: 16,
+ color: colors.text.primary,
+ minHeight: 56,
+ },
+
+ // 图片上传
+ imageSection: {
+ marginBottom: 24,
+ },
+ imageGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 10,
+ },
+ imageItem: {
+ width: 100,
+ height: 100,
+ borderRadius: 14,
+ overflow: 'hidden',
+ position: 'relative',
+ },
+ imagePreview: {
+ width: '100%',
+ height: '100%',
+ resizeMode: 'cover',
+ },
+ removeImageBtn: {
+ position: 'absolute',
+ top: 6,
+ right: 6,
+ width: 24,
+ height: 24,
+ borderRadius: 12,
+ backgroundColor: 'rgba(0,0,0,0.5)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ addImageBtn: {
+ width: 100,
+ height: 100,
+ borderRadius: 14,
+ borderWidth: 2,
+ borderColor: colors.divider,
+ borderStyle: 'dashed',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: colors.background.default,
+ },
+ uploadingOverlay: {
+ ...StyleSheet.absoluteFillObject,
+ backgroundColor: 'rgba(0,0,0,0.3)',
+ alignItems: 'center',
+ justifyContent: 'center',
+ borderRadius: 14,
+ },
+
+ // 提交按钮
+ submitBtn: {
+ height: 56,
+ borderRadius: 14,
+ backgroundColor: colors.primary.main,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginTop: 8,
+ marginBottom: 16,
+ },
+ submitBtnDisabled: {
+ opacity: 0.6,
+ },
+ submitBtnText: {
+ fontSize: 17,
+ fontWeight: '600',
+ color: colors.text.inverse,
+ },
+
+ // 顶部关闭按钮
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ backIconButton: {
+ width: 40,
+ height: 40,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ headerTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: colors.text.primary,
+ },
+ headerRight: {
+ width: 40,
+ },
+ });
+}
+
+export function CreateTradeScreen({ onClose, onSuccess, editItem }: CreateTradeScreenProps) {
+ const colors = useAppColors();
+ const styles = useMemo(() => createStyles(colors), [colors]);
+ const isEdit = !!editItem;
+
+ const [tradeType, setTradeType] = useState(editItem?.trade_type || 'sell');
+ const [category, setCategory] = useState(editItem?.category || 'digital');
+ const [title, setTitle] = useState(editItem?.title || '');
+ const [content, setContent] = useState(editItem?.content || '');
+ const [price, setPrice] = useState(editItem?.price != null ? String(editItem.price) : '');
+ const [condition, setCondition] = useState(
+ editItem?.condition as TradeCondition | undefined
+ );
+ const [images, setImages] = useState<{ uri: string; uploading: boolean }[]>(
+ editItem?.images?.map(img => ({ uri: img.url, uploading: false })) || []
+ );
+ const [submitting, setSubmitting] = useState(false);
+
+ // 动画值
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+ const slideAnim = useRef(new Animated.Value(20)).current;
+
+ useEffect(() => {
+ Animated.parallel([
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 400,
+ useNativeDriver: true,
+ }),
+ Animated.timing(slideAnim, {
+ toValue: 0,
+ duration: 400,
+ useNativeDriver: true,
+ }),
+ ]).start();
+ }, []);
+
+ const handleSubmit = useCallback(async () => {
+ if (!title.trim()) {
+ Alert.alert('提示', '请输入标题');
+ return;
+ }
+ if (images.some(img => img.uploading)) {
+ Alert.alert('提示', '请等待图片上传完成');
+ return;
+ }
+
+ setSubmitting(true);
+ try {
+ const payload = {
+ trade_type: tradeType,
+ category,
+ title: title.trim(),
+ content: content.trim() || title.trim(),
+ images: images.map(img => img.uri),
+ price: price ? parseFloat(price) : null,
+ condition,
+ };
+
+ if (isEdit && editItem) {
+ await tradeService.updateTradeItem(editItem.id, payload);
+ } else {
+ await tradeService.createTradeItem(payload);
+ }
+ onSuccess?.();
+ onClose();
+ } catch (e: any) {
+ Alert.alert(isEdit ? '更新失败' : '发布失败', e?.message || '请稍后重试');
+ } finally {
+ setSubmitting(false);
+ }
+ }, [title, content, tradeType, category, price, condition, images, onClose, onSuccess, isEdit, editItem]);
+
+ const handlePickImage = useCallback(async () => {
+ if (images.length >= 9) {
+ Alert.alert('提示', '最多上传9张图片');
+ return;
+ }
+
+ const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
+ if (!permissionResult.granted) {
+ Alert.alert('权限不足', '需要访问相册权限来选择图片');
+ return;
+ }
+
+ const result = await ImagePicker.launchImageLibraryAsync({
+ mediaTypes: 'images',
+ allowsMultipleSelection: true,
+ selectionLimit: 9 - images.length,
+ quality: 1,
+ });
+
+ if (!result.canceled && result.assets) {
+ const newImages = result.assets.map(asset => ({ uri: asset.uri, uploading: true }));
+ setImages(prev => [...prev, ...newImages]);
+
+ for (let i = 0; i < newImages.length; i++) {
+ const asset = result.assets[i];
+ const uploadResult = await uploadService.uploadImage({
+ uri: asset.uri,
+ type: asset.mimeType || 'image/jpeg',
+ });
+
+ if (uploadResult) {
+ setImages(prev => {
+ const updated = [...prev];
+ const index = prev.findIndex(img => img.uri === asset.uri);
+ if (index !== -1) {
+ updated[index] = { uri: uploadResult.url, uploading: false };
+ }
+ return updated;
+ });
+ } else {
+ setImages(prev => prev.filter(img => img.uri !== asset.uri));
+ }
+ }
+ }
+ }, [images.length]);
+
+ const removeImage = useCallback((uri: string) => {
+ setImages(prev => prev.filter(img => img.uri !== uri));
+ }, []);
+
+ const canSubmit = title.trim().length > 0 && !submitting && !images.some(i => i.uploading);
+
+ return (
+
+ {/* 顶部导航 */}
+
+
+
+
+ {isEdit ? '编辑商品' : '发布商品'}
+
+
+
+
+
+
+ {/* 标题区域 */}
+
+ {isEdit ? '编辑商品' : '发布商品'}
+
+ 填写商品信息,让更多人看到
+
+
+ {/* 交易类型 */}
+
+ 交易类型
+
+ setTradeType('sell')}
+ >
+
+ 出售
+
+ setTradeType('buy')}
+ >
+
+ 收购
+
+
+
+
+ {/* 分类 */}
+
+ 分类
+
+
+ {ALL_TRADE_CATEGORIES.map(cat => (
+ setCategory(cat.key)}
+ >
+
+ {cat.label}
+
+
+ ))}
+
+
+
+
+ {/* 标题 */}
+
+ 标题 *
+
+
+ {title.length > 0 && (
+
+ )}
+
+ {title.length}/100
+
+
+ {/* 描述 */}
+
+ 描述
+
+
+
+ {content.length}/2000
+
+
+ {/* 图片 */}
+
+ 图片 ({images.length}/9)
+
+ {images.map((img) => (
+
+
+ {img.uploading && (
+
+
+
+ )}
+ removeImage(img.uri)}>
+
+
+
+ ))}
+ {images.length < 9 && (
+
+
+ 添加图片
+
+ )}
+
+
+
+ {/* 价格 */}
+
+ 价格(留空为面议)
+
+ ¥
+
+
+
+
+ {/* 成色 */}
+ {tradeType === 'sell' && (
+
+ 成色
+
+
+ {ALL_TRADE_CONDITIONS.map(c => (
+ setCondition(condition === c.key ? undefined : c.key)}
+ >
+
+ {c.label}
+
+
+ ))}
+
+
+
+ )}
+
+ {/* 提交按钮 */}
+
+ {submitting ? (
+
+ ) : (
+ {isEdit ? '保存' : '发布'}
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/screens/trade/TradeDetailScreen.tsx b/src/screens/trade/TradeDetailScreen.tsx
new file mode 100644
index 0000000..9a26ce9
--- /dev/null
+++ b/src/screens/trade/TradeDetailScreen.tsx
@@ -0,0 +1,623 @@
+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',
+ },
+
+ 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',
+ },
+ });
+}
+
+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(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 ;
+ }
+
+ if (!item) {
+ return (
+
+
+ 商品不存在或已删除
+
+
+ );
+ }
+
+ const images = item.images || [];
+ const currentImage = images[currentImageIndex];
+
+ return (
+
+
+
+ router.back()} />
+
+ 商品详情
+
+
+
+
+
+ {/* 卖家信息头部 */}
+ {item.author && (
+
+
+ {item.author.avatar ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {item.author.nickname && item.author.nickname.length > 10
+ ? `${item.author.nickname.slice(0, 10)}...`
+ : item.author.nickname || '匿名'}
+
+ {(item.author?.credit_score || 0) > 80 && (
+
+ 卖家信用优秀
+
+ )}
+
+
+ {item.price != null ? (
+
+ 价格:¥{item.price % 1 === 0 ? item.price.toFixed(0) : item.price.toFixed(2)}
+
+ ) : (
+ 价格:面议
+ )}
+
+
+
+ )}
+
+ {/* 标题 */}
+
+ {item.title}
+ {item.condition && item.trade_type === 'sell' && (
+
+
+
+ {TRADE_CONDITION_MAP[item.condition as keyof typeof TRADE_CONDITION_MAP] || item.condition}
+
+
+
+ )}
+
+
+ {/* 商品属性 */}
+
+
+ 品牌
+ {item.brand || '其他'}
+
+
+ 成色
+
+ {TRADE_CONDITION_MAP[item.condition as keyof typeof TRADE_CONDITION_MAP] || item.condition || '未知'}
+
+
+
+ 功能状态
+ {item.function_status || '正常使用'}
+
+
+
+ {/* 商品描述 */}
+ {item.content && (
+ {item.content}
+ )}
+
+
+
+
+ {/* 图片放在文字下方 */}
+ {images.length > 0 && (
+
+ {images.map((img, idx) => (
+
+
+
+
+ ))}
+
+ )}
+
+
+ {/* 底部操作栏 */}
+
+ {isOwner ? (
+ /* 卖家管理按钮 */
+ <>
+
+ setManageModalVisible(true)}>
+
+ 管理
+
+ >
+ ) : (
+ <>
+ {!isOwner && item.status === 'active' && (
+ <>
+
+
+
+ 聊一聊
+
+ >
+ )}
+ {!isOwner && item.status !== 'active' && (
+
+
+
+ {item.status === 'sold' ? '已售出' : item.status === 'bought' ? '已收得' : '已下架'}
+
+
+
+ )}
+ >
+ )}
+
+
+ {/* 管理弹窗 */}
+ setManageModalVisible(false)}
+ >
+ setManageModalVisible(false)}>
+
+
+ 编辑
+
+
+ 删除
+
+ setManageModalVisible(false)}>
+ 取消
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/services/index.ts b/src/services/index.ts
index c61edb6..9345074 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -40,6 +40,8 @@ export type { SubmitVerificationResponse } from './auth';
export { postService, commentService, voteService, channelService, reportService } from './post';
export type { ChannelItem } from './post';
+export { tradeService } from './trade/tradeService';
+
export { messageService } from './message';
export { groupService } from './message';
export type {
diff --git a/src/services/trade/tradeService.ts b/src/services/trade/tradeService.ts
new file mode 100644
index 0000000..35b9245
--- /dev/null
+++ b/src/services/trade/tradeService.ts
@@ -0,0 +1,72 @@
+import { api } from '../core/api';
+import type {
+ TradeItemDTO,
+ TradeItemDetailDTO,
+ CreateTradeItemInput,
+ UpdateTradeItemInput,
+ TradeCursorPageResponse,
+ TradeType,
+ TradeCategory,
+ TradeItemStatus,
+} from '@/types/trade';
+
+class TradeService {
+ async getTradeItems(params?: {
+ trade_type?: TradeType;
+ category?: TradeCategory;
+ status?: TradeItemStatus;
+ keyword?: string;
+ cursor?: string;
+ page_size?: number;
+ }): Promise {
+ const queryParams: Record = {};
+ if (params?.trade_type) queryParams.trade_type = params.trade_type;
+ if (params?.category) queryParams.category = params.category;
+ if (params?.status) queryParams.status = params.status;
+ if (params?.keyword) queryParams.keyword = params.keyword;
+ if (params?.cursor) queryParams.cursor = params.cursor;
+ if (params?.page_size) queryParams.page_size = String(params.page_size);
+
+ const res = await api.get('/trade', queryParams);
+ return res.data;
+ }
+
+ async getTradeItem(id: string): Promise {
+ const res = await api.get(`/trade/${id}`);
+ return res.data;
+ }
+
+ async createTradeItem(data: CreateTradeItemInput): Promise {
+ const res = await api.post('/trade', data);
+ return res.data;
+ }
+
+ async updateTradeItem(id: string, data: UpdateTradeItemInput): Promise {
+ const res = await api.put(`/trade/${id}`, data);
+ return res.data;
+ }
+
+ async deleteTradeItem(id: string): Promise {
+ await api.delete(`/trade/${id}`);
+ }
+
+ async updateTradeItemStatus(id: string, status: TradeItemStatus): Promise {
+ await api.put(`/trade/${id}/status`, { status });
+ }
+
+ async recordView(id: string): Promise {
+ await api.post(`/trade/${id}/view`, {});
+ }
+
+ async favorite(id: string): Promise<{ favorited: boolean }> {
+ const res = await api.post<{ favorited: boolean }>(`/trade/${id}/favorite`, {});
+ return res.data;
+ }
+
+ async unfavorite(id: string): Promise<{ favorited: boolean }> {
+ const res = await api.delete<{ favorited: boolean }>(`/trade/${id}/favorite`);
+ return res.data;
+ }
+}
+
+export const tradeService = new TradeService();
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
index 6611650..76e1a80 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -7,6 +7,7 @@
export * from './dto';
export * from './schedule';
export * from './material';
+export * from './trade';
// 兼容旧类型(用于内部组件)
import type {
diff --git a/src/types/trade.ts b/src/types/trade.ts
new file mode 100644
index 0000000..f3be2e0
--- /dev/null
+++ b/src/types/trade.ts
@@ -0,0 +1,124 @@
+export type TradeType = 'sell' | 'buy';
+export type TradeCategory = 'digital' | 'book' | 'clothing' | 'daily' | 'cosmetics' | 'sport' | 'ticket' | 'other';
+export type TradeItemStatus = 'active' | 'sold' | 'bought' | 'offline' | 'deleted';
+export type TradeCondition = 'brand_new' | 'like_new' | 'lightly_used' | 'well_used';
+
+export interface TradeImageDTO {
+ id: string;
+ url: string;
+ thumbnail_url: string;
+ preview_url: string;
+ preview_url_large: string;
+ width: number;
+ height: number;
+}
+
+export interface TradeItemDTO {
+ id: string;
+ user_id: string;
+ trade_type: TradeType;
+ category: TradeCategory;
+ title: string;
+ content: string;
+ segments?: any[];
+ images: TradeImageDTO[];
+ price?: number | null;
+ condition?: string;
+ status: TradeItemStatus;
+ views_count: number;
+ favorites_count: number;
+ wants_count?: number;
+ comments_count?: number;
+ is_favorited: boolean;
+ is_free_shipping?: boolean;
+ is_quick_ship?: boolean;
+ is_negotiable?: boolean;
+ is_self_pickup?: boolean;
+ good_reputation?: number;
+ brand?: string;
+ function_status?: string;
+ promo_text?: string;
+ author: any | null;
+ created_at: string;
+ updated_at: string;
+}
+
+export type TradeItemDetailDTO = TradeItemDTO;
+
+export interface CreateTradeItemInput {
+ trade_type: TradeType;
+ category: TradeCategory;
+ title: string;
+ content?: string;
+ segments?: any[];
+ images?: string[];
+ price?: number | null;
+ condition?: TradeCondition;
+}
+
+export interface UpdateTradeItemInput {
+ trade_type?: TradeType;
+ category?: TradeCategory;
+ title?: string;
+ content?: string;
+ segments?: any[];
+ images?: string[];
+ price?: number | null;
+ condition?: TradeCondition;
+}
+
+export interface TradeCursorPageResponse {
+ list: TradeItemDTO[];
+ next_cursor?: string;
+ prev_cursor?: string;
+ has_more: boolean;
+}
+
+export const TRADE_CATEGORY_MAP: Record = {
+ digital: '数码电子',
+ book: '书籍教材',
+ clothing: '服饰鞋包',
+ daily: '日用百货',
+ cosmetics: '美妆护肤',
+ sport: '运动户外',
+ ticket: '票券卡券',
+ other: '其他',
+};
+
+export const TRADE_CONDITION_MAP: Record = {
+ brand_new: '全新',
+ like_new: '几乎全新',
+ lightly_used: '轻微使用痕迹',
+ well_used: '明显使用痕迹',
+};
+
+export const TRADE_TYPE_MAP: Record = {
+ sell: '出售',
+ buy: '收购',
+};
+
+export const TRADE_STATUS_MAP: Record = {
+ active: '在售',
+ sold: '已售出',
+ bought: '已收得',
+ offline: '已下架',
+ deleted: '已删除',
+};
+
+export const ALL_TRADE_CATEGORIES: { key: TradeCategory; label: string }[] = [
+ { key: 'digital', label: '数码电子' },
+ { key: 'book', label: '书籍教材' },
+ { key: 'clothing', label: '服饰鞋包' },
+ { key: 'daily', label: '日用百货' },
+ { key: 'cosmetics', label: '美妆护肤' },
+ { key: 'sport', label: '运动户外' },
+ { key: 'ticket', label: '票券卡券' },
+ { key: 'other', label: '其他' },
+];
+
+export const ALL_TRADE_CONDITIONS: { key: TradeCondition; label: string }[] = [
+ { key: 'brand_new', label: '全新' },
+ { key: 'like_new', label: '几乎全新' },
+ { key: 'lightly_used', label: '轻微使用痕迹' },
+ { key: 'well_used', label: '明显使用痕迹' },
+];
\ No newline at end of file