refactor(ui): redesign SearchBar with compact mode and add home screen tabs
Refactor SearchBar component to support compact mode with transparent styling, reduced padding, and smaller icons. Add home screen tab navigation for switching between home feed and market view with animated tab indicator. Also adjust PostDetailScreen action button sizing and CommentItem margins for better spacing.
This commit is contained in:
@@ -128,7 +128,7 @@ function createCommentItemStyles(colors: AppColors) {
|
||||
actionButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.lg,
|
||||
marginRight: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
paddingRight: spacing.xs,
|
||||
},
|
||||
|
||||
@@ -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<SearchBarProps> = ({
|
||||
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<SearchBarProps> = ({
|
||||
<View style={[styles.searchIconWrap, isFocused && styles.searchIconWrapFocused]}>
|
||||
<MaterialCommunityIcons
|
||||
name="magnify"
|
||||
size={18}
|
||||
size={compact ? 16 : 18}
|
||||
color={isFocused ? colors.primary.main : colors.text.secondary}
|
||||
/>
|
||||
</View>
|
||||
|
||||
377
src/components/business/TradeCard/TradeCard.tsx
Normal file
377
src/components/business/TradeCard/TradeCard.tsx
Normal file
@@ -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<TradeCardProps> = ({ 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) => (
|
||||
<View style={containerStyle}>
|
||||
{firstImage ? (
|
||||
<Image
|
||||
source={{ uri: firstImage.preview_url || firstImage.url }}
|
||||
style={imageStyle}
|
||||
defaultSource={undefined}
|
||||
/>
|
||||
) : (
|
||||
<View style={[imageStyle, styles.imagePlaceholder]}>
|
||||
<MaterialCommunityIcons name="image-outline" size={32} color={colors.text.hint} />
|
||||
</View>
|
||||
)}
|
||||
{/* 包邮标签 */}
|
||||
{item.is_free_shipping && (
|
||||
<View style={styles.shippingBadge}>
|
||||
<Text style={styles.shippingBadgeText}>包邮</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={[styles.typeBadge, isSell ? styles.sellBadge : styles.buyBadge]}>
|
||||
<Text style={styles.typeBadgeText}>{TRADE_TYPE_MAP[item.trade_type]}</Text>
|
||||
</View>
|
||||
{!isActive && (
|
||||
<View style={styles.statusBadge}>
|
||||
<Text style={styles.statusBadgeText}>
|
||||
{item.status === 'sold' ? '已售' : item.status === 'bought' ? '已收' : item.status === 'offline' ? '下架' : ''}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderTitleRow = () => (
|
||||
<View style={styles.titleRow}>
|
||||
<Text style={styles.title} numberOfLines={2}>{item.title}</Text>
|
||||
<Text style={styles.priceInline}>
|
||||
{item.price != null ? formatPrice(item.price) : '面议'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderTags = () => {
|
||||
if (!conditionLabel && !isSell && !item.is_quick_ship) return null;
|
||||
return (
|
||||
<View style={styles.tagsRow}>
|
||||
{conditionLabel && isSell && (
|
||||
<View style={[styles.tag, styles.conditionTag]}>
|
||||
<Text style={styles.conditionTagText}>{conditionLabel}</Text>
|
||||
</View>
|
||||
)}
|
||||
{item.is_quick_ship && (
|
||||
<View style={styles.promoTag}>
|
||||
<Text style={styles.promoTagText}>24小时发货</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBottom = () => (
|
||||
<View style={styles.bottomRow}>
|
||||
<View style={styles.authorRow}>
|
||||
{item.author?.avatar ? (
|
||||
<Image source={{ uri: item.author.avatar }} style={styles.avatar} />
|
||||
) : (
|
||||
<View style={[styles.avatar, { backgroundColor: colors.primary.main + '33' }]} />
|
||||
)}
|
||||
<Text style={styles.authorName} numberOfLines={1}>{item.author?.nickname || '匿名'}</Text>
|
||||
</View>
|
||||
<View style={styles.statsRow}>
|
||||
{(item.good_reputation || 0) > 0 && (
|
||||
<View style={[styles.promoTag, { backgroundColor: '#FFF7E6', borderColor: '#FFD591' }]}>
|
||||
<Text style={[styles.promoTagText, { color: '#FA8C16' }]}>回头客好评过百</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.tag}>
|
||||
<Text style={styles.tagText}>{categoryLabel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (variant === 'grid') {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.gridCard}
|
||||
onPress={() => onPress?.(item.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{renderImage(styles.imageContainer, styles.image)}
|
||||
<View style={styles.gridContent}>
|
||||
{renderTitleRow()}
|
||||
{renderTags()}
|
||||
{renderBottom()}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.listCard}
|
||||
onPress={() => onPress?.(item.id)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.listLayout}>
|
||||
{renderImage(styles.listImageContainer, styles.image)}
|
||||
<View style={styles.listContent}>
|
||||
{renderTitleRow()}
|
||||
{renderTags()}
|
||||
{renderBottom()}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export const TradeCard = memo(TradeCardBase);
|
||||
|
||||
export default TradeCard;
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user