refactor(ui): modernize components with flat design and UX refinements
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 42s
Frontend CI / build-android-apk (push) Failing after 7m16s
Frontend CI / ota-android (push) Successful in 10m29s

- Redesign chat screen header with improved layout and panel styling
- Update emoji panel tab bar from emoji to icon-based navigation
- Simplify trade detail view with inline content attributes
- Add masonry layout support to market view grid
- Refactor privacy settings from card-based to list-based dropdown UI
- Improve comment item actions layout and styling
- Update trade creation flow with buy/sell specific text
- Adjust padding, gaps, and font sizes across multiple components
- Clean up unused code and imports
This commit is contained in:
lafay
2026-04-27 01:02:39 +08:00
parent e519346261
commit 0e2945b86b
15 changed files with 535 additions and 384 deletions

View File

@@ -143,7 +143,6 @@ function createCommentItemStyles(colors: AppColors) {
paddingVertical: spacing.xs,
borderLeftWidth: 2,
borderLeftColor: colors.divider,
backgroundColor: colors.background.default,
},
subReplyItem: {
flexDirection: 'row',
@@ -154,8 +153,8 @@ function createCommentItemStyles(colors: AppColors) {
marginBottom: 0,
},
subReplyBody: {
fontSize: fontSizes.sm,
lineHeight: 20,
fontSize: fontSizes.md,
lineHeight: 22,
marginTop: 2,
},
subReplyContent: {
@@ -177,10 +176,10 @@ function createCommentItemStyles(colors: AppColors) {
paddingVertical: spacing.xs,
},
replyToText: {
fontSize: fontSizes.xs,
fontSize: fontSizes.sm,
},
replyToName: {
fontSize: fontSizes.xs,
fontSize: fontSizes.sm,
fontWeight: '500',
},
subReplyActions: {
@@ -483,7 +482,7 @@ const CommentItem: React.FC<CommentItemProps> = ({
{targetNickname && (
<>
<Text variant="caption" color={colors.text.hint} style={styles.replyToText}>
{' '}
{' '}{' '}
</Text>
<Text variant="caption" color={colors.primary.main} style={styles.replyToName}>
{targetNickname}
@@ -501,15 +500,6 @@ const CommentItem: React.FC<CommentItemProps> = ({
{renderSubReplyImages(reply)}
{/* 子评论操作按钮 */}
<View style={styles.subReplyActions}>
<TouchableOpacity
style={styles.subActionButton}
onPress={() => onReplyPress?.(reply)}
>
<MaterialCommunityIcons name="reply" size={12} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} style={styles.subActionText}>
</Text>
</TouchableOpacity>
{/* 点赞按钮 */}
<TouchableOpacity
style={styles.subActionButton}
@@ -518,7 +508,7 @@ const CommentItem: React.FC<CommentItemProps> = ({
>
<MaterialCommunityIcons
name={reply.is_liked ? 'heart' : 'heart-outline'}
size={12}
size={14}
color={reply.is_liked ? colors.error.main : colors.text.hint}
/>
<Text
@@ -529,6 +519,15 @@ const CommentItem: React.FC<CommentItemProps> = ({
{reply.likes_count > 0 ? formatNumber(reply.likes_count) : '赞'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.subActionButton}
onPress={() => onReplyPress?.(reply)}
>
<MaterialCommunityIcons name="reply" size={14} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} style={styles.subActionText}>
</Text>
</TouchableOpacity>
{/* 删除按钮 - 子评论作者可见 */}
{isSubReplyAuthor && (
<TouchableOpacity
@@ -538,7 +537,7 @@ const CommentItem: React.FC<CommentItemProps> = ({
>
<MaterialCommunityIcons
name={isDeleting ? 'loading' : 'delete-outline'}
size={12}
size={14}
color={colors.text.hint}
/>
<Text variant="caption" color={colors.text.hint} style={styles.subActionText}>
@@ -546,13 +545,12 @@ const CommentItem: React.FC<CommentItemProps> = ({
</Text>
</TouchableOpacity>
)}
{/* 举报按钮 - 对非子评论作者显示 */}
{!isSubReplyAuthor && onReport && (
<TouchableOpacity
style={styles.subActionButton}
onPress={() => onReport(reply)}
>
<MaterialCommunityIcons name="flag-outline" size={12} color={colors.text.hint} />
<MaterialCommunityIcons name="flag-outline" size={14} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} style={styles.subActionText}>
</Text>

View File

@@ -41,6 +41,7 @@ function createTradeCardStyles(colors: AppColors) {
position: 'relative',
aspectRatio: 1,
backgroundColor: colors.background.default,
minHeight: 140,
},
listImageContainer: {
width: 120,
@@ -104,7 +105,9 @@ function createTradeCardStyles(colors: AppColors) {
fontWeight: '600',
},
gridContent: {
padding: spacing.sm,
paddingHorizontal: spacing.sm,
paddingTop: spacing.xs,
paddingBottom: spacing.sm,
},
titleRow: {
flexDirection: 'row',
@@ -155,7 +158,7 @@ function createTradeCardStyles(colors: AppColors) {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 4,
marginTop: spacing.xs,
marginTop: 2,
},
tag: {
paddingHorizontal: 5,
@@ -191,7 +194,7 @@ function createTradeCardStyles(colors: AppColors) {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: spacing.sm,
marginTop: spacing.xs,
},
authorRow: {
flexDirection: 'row',
@@ -354,6 +357,9 @@ const TradeCardBase: React.FC<TradeCardProps> = ({ item, variant = 'list', onPre
);
}
// grid variant is used in masonry layout; list variant below is unused but kept for compatibility
return (
<TouchableOpacity
style={styles.listCard}

View File

@@ -15,7 +15,7 @@ interface CustomTextProps extends Omit<TextProps, 'style'> {
color?: string;
numberOfLines?: number;
onPress?: () => void;
style?: TextStyle | TextStyle[];
style?: TextStyle | TextStyle[] | (TextStyle | undefined)[];
weight?: 'regular' | 'medium' | 'semibold' | 'bold';
}

View File

@@ -90,9 +90,6 @@ export const AppsScreen: React.FC = () => {
const renderTiles = () => (
<>
<Text variant="caption" color={colors.text.secondary} style={styles.pageSubtitle}>
</Text>
{APP_ENTRIES.map(item => (
<TouchableOpacity
key={item.id}

View File

@@ -130,19 +130,19 @@ function createHomeStyles(colors: AppColors, responsivePadding: number) {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
paddingVertical: spacing.sm,
},
sortBarCapsuleScroll: {
flex: 1,
marginHorizontal: spacing.xs,
},
sortBarCapsuleContent: {
gap: spacing.xs,
gap: spacing.sm,
alignItems: 'center',
paddingVertical: spacing.xs,
},
capsuleItem: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
paddingVertical: spacing.sm,
borderRadius: 999,
},
capsuleText: {
@@ -156,8 +156,8 @@ function createHomeStyles(colors: AppColors, responsivePadding: number) {
sortTrigger: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 4,
paddingHorizontal: 2,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.xs,
},
sortTriggerText: {
fontSize: 13,
@@ -986,10 +986,7 @@ export const HomeScreen: React.FC = () => {
renderItem={({ item }: { item: { key: string; label: string } }) => (
<TouchableOpacity
activeOpacity={0.7}
style={[
styles.capsuleItem,
marketCategory === item.key && { backgroundColor: `${colors.primary.main}18` },
]}
style={styles.capsuleItem}
onPress={() => setMarketCategory(item.key as TradeCategory | 'all')}
>
<Text style={[

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { View, StyleSheet, RefreshControl, Pressable } from 'react-native';
import { View, StyleSheet, RefreshControl } from 'react-native';
import { FlashList, FlashListRef } from '@shopify/flash-list';
import type { ListRenderItem } from '@shopify/flash-list';
import { useAppColors, type AppColors } from '../../theme';
@@ -27,12 +27,9 @@ function createMarketStyles(colors: AppColors, gap: number, padding: number) {
},
listContent: {
paddingHorizontal: padding,
paddingTop: gap,
paddingTop: gap / 2,
paddingBottom: 80,
},
columnWrapper: {
justifyContent: 'space-between',
},
});
}
@@ -54,7 +51,7 @@ export function MarketView({
const colors = useAppColors();
const router = useRouter();
const isAuth = useIsAuthenticated();
const { width, isMobile } = useResponsive();
const { width } = 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 });
@@ -74,7 +71,7 @@ export function MarketView({
const setFilterType = onFilterTypeChange ?? setFilterTypeLocal;
const setSelectedCategory = onSelectedCategoryChange ?? setSelectedCategoryLocal;
const flashListRef = useRef<any>(null);
const flashListRef = useRef<FlashListRef<any>>(null);
// 网格列数移动端2列平板3列桌面4列
const numColumns = useMemo(() => {
@@ -83,12 +80,6 @@ export function MarketView({
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) {
@@ -172,7 +163,7 @@ export function MarketView({
}, [items, isAuth]);
const renderItem = useCallback<ListRenderItem<TradeItemDTO>>(({ item }) => (
<View style={{ width: cardWidth, marginBottom: gap }}>
<View style={{ marginBottom: gap }}>
<TradeCard
item={item}
variant="grid"
@@ -180,7 +171,7 @@ export function MarketView({
onFavorite={handleFavorite}
/>
</View>
), [handlePressItem, handleFavorite, cardWidth, gap]);
), [handlePressItem, handleFavorite, gap]);
const keyExtractor = useCallback((item: TradeItemDTO) => item.id, []);
@@ -217,6 +208,7 @@ export function MarketView({
}
ListFooterComponent={isLoadingMore ? <Loading size="sm" /> : null}
drawDistance={250}
masonry
/>
</View>
);

View File

@@ -1,5 +1,6 @@
/**
* 资料详情页:展示资料详细信息
* 现代化扁平化设计,风格统一
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
@@ -114,16 +115,17 @@ export const MaterialDetailScreen: React.FC = () => {
});
}, []);
// 扁平化卡片样式:更柔和的阴影,更大的圆角
const cardStyle = useMemo(
() => ({
backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
borderRadius: borderRadius.xl,
overflow: 'hidden' as const,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.04,
shadowRadius: 4,
elevation: 1,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
}),
[colors]
);
@@ -159,14 +161,20 @@ export const MaterialDetailScreen: React.FC = () => {
return (
<>
{/* 文件信息卡片 */}
{/* 文件信息卡片 - 现代化设计 */}
<View style={[styles.fileCard, cardStyle]}>
{/* 顶部文件类型标识区 - 扁平化渐变 */}
<View style={[styles.fileTypeBanner, { backgroundColor: fileColor }]}>
<MaterialCommunityIcons name={iconName as any} size={48} color="#FFFFFF" />
<Text variant="body" style={styles.fileTypeName}>
{fileTypeName}
</Text>
<View style={styles.fileIconWrapper}>
<MaterialCommunityIcons name={iconName as any} size={40} color="#FFFFFF" />
</View>
<View style={styles.fileTypeBadge}>
<Text variant="caption" style={styles.fileTypeName}>
{fileTypeName}
</Text>
</View>
</View>
<View style={styles.fileInfo}>
<Text variant="h3" style={styles.fileName}>
{material.title}
@@ -176,16 +184,18 @@ export const MaterialDetailScreen: React.FC = () => {
{material.description}
</Text>
)}
{/* 元数据 - 现代化标签式布局 */}
<View style={styles.fileMetaRow}>
<View style={styles.metaItem}>
<MaterialCommunityIcons name="file-outline" size={16} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint}>
<View style={[styles.metaBadge, { backgroundColor: `${colors.neutral.main}12` }]}>
<MaterialCommunityIcons name="file-outline" size={14} color={colors.neutral.main} />
<Text variant="caption" color={colors.neutral.dark} style={styles.metaBadgeText}>
{formatFileSize(material.file_size)}
</Text>
</View>
<View style={styles.metaItem}>
<MaterialCommunityIcons name="download" size={16} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint}>
<View style={[styles.metaBadge, { backgroundColor: `${colors.primary.main}12` }]}>
<MaterialCommunityIcons name="download" size={14} color={colors.primary.main} />
<Text variant="caption" color={colors.primary.dark} style={styles.metaBadgeText}>
{material.download_count}
</Text>
</View>
@@ -193,46 +203,69 @@ export const MaterialDetailScreen: React.FC = () => {
</View>
</View>
{/* 详细信息 */}
{/* 详细信息卡片 - 扁平化列表 */}
<View style={[styles.detailCard, cardStyle]}>
<Text variant="body" style={styles.sectionTitle}>
</Text>
<View style={styles.detailRow}>
<Text variant="caption" color={colors.text.hint} style={styles.detailLabel}>
</Text>
<Text variant="body" color={colors.text.primary} style={styles.detailValue}>
{material.file_name}
<View style={styles.sectionHeader}>
<View style={[styles.sectionIcon, { backgroundColor: `${colors.info.main}12` }]}>
<MaterialCommunityIcons name="information-outline" size={18} color={colors.info.main} />
</View>
<Text variant="body" style={styles.sectionTitle}>
</Text>
</View>
<View style={styles.detailRow}>
<Text variant="caption" color={colors.text.hint} style={styles.detailLabel}>
</Text>
<Text variant="body" color={colors.text.primary} style={styles.detailValue}>
{formatDate(material.created_at)}
</Text>
</View>
{material.author_name && (
<View style={styles.detailList}>
<View style={styles.detailRow}>
<Text variant="caption" color={colors.text.hint} style={styles.detailLabel}>
</Text>
<View style={styles.detailLabelWrapper}>
<MaterialCommunityIcons name="file-document-outline" size={16} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} style={styles.detailLabel}>
</Text>
</View>
<Text variant="body" color={colors.text.primary} style={styles.detailValue}>
{material.author_name}
{material.file_name}
</Text>
</View>
)}
{material.tags && material.tags.length > 0 && (
<View style={styles.tagContainer}>
<Text variant="caption" color={colors.text.hint} style={styles.detailLabel}>
<View style={styles.detailRow}>
<View style={styles.detailLabelWrapper}>
<MaterialCommunityIcons name="calendar-outline" size={16} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} style={styles.detailLabel}>
</Text>
</View>
<Text variant="body" color={colors.text.primary} style={styles.detailValue}>
{formatDate(material.created_at)}
</Text>
</View>
{material.author_name && (
<View style={styles.detailRow}>
<View style={styles.detailLabelWrapper}>
<MaterialCommunityIcons name="account-outline" size={16} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} style={styles.detailLabel}>
</Text>
</View>
<Text variant="body" color={colors.text.primary} style={styles.detailValue}>
{material.author_name}
</Text>
</View>
)}
</View>
{material.tags && material.tags.length > 0 && (
<View style={styles.tagSection}>
<View style={styles.tagSectionHeader}>
<MaterialCommunityIcons name="tag-outline" size={16} color={colors.text.hint} />
<Text variant="caption" color={colors.text.hint} style={styles.tagSectionTitle}>
</Text>
</View>
<View style={styles.tags}>
{material.tags.map((tag, index) => (
<View key={index} style={styles.tag}>
<Text variant="caption" color={colors.primary.main}>
<View key={index} style={[styles.tag, { backgroundColor: `${colors.primary.main}10` }]}>
<Text variant="caption" color={colors.primary.main} style={styles.tagText}>
{tag}
</Text>
</View>
@@ -242,24 +275,29 @@ export const MaterialDetailScreen: React.FC = () => {
)}
</View>
{/* 操作按钮 */}
{/* 操作按钮 - 现代化扁平按钮 */}
<View style={styles.actionButtons}>
<TouchableOpacity
style={[styles.previewButton, { backgroundColor: colors.background.paper }]}
onPress={handlePreview}
activeOpacity={0.8}
activeOpacity={0.7}
>
<MaterialCommunityIcons name="eye-outline" size={20} color={colors.text.primary} />
<View style={[styles.actionIconWrapper, { backgroundColor: `${colors.neutral.main}12` }]}>
<MaterialCommunityIcons name="eye-outline" size={18} color={colors.neutral.dark} />
</View>
<Text variant="body" color={colors.text.primary} style={styles.actionButtonText}>
线
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.downloadButton, { backgroundColor: colors.primary.main }]}
onPress={handleDownload}
activeOpacity={0.8}
>
<MaterialCommunityIcons name="download" size={20} color={colors.primary.contrast} />
<View style={[styles.actionIconWrapper, { backgroundColor: 'rgba(255,255,255,0.2)' }]}>
<MaterialCommunityIcons name="download" size={18} color="#FFFFFF" />
</View>
<Text variant="body" color={colors.primary.contrast} style={styles.actionButtonText}>
</Text>
@@ -273,7 +311,7 @@ export const MaterialDetailScreen: React.FC = () => {
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<StatusBar barStyle={statusBarStyle} backgroundColor={colors.background.default} />
{/* Header */}
{/* Header - 扁平化设计 */}
<View style={[styles.header, isWideScreen && styles.headerWide]}>
<View style={styles.headerLeft}>
<AppBackButton onPress={() => router.back()} />
@@ -284,15 +322,15 @@ export const MaterialDetailScreen: React.FC = () => {
</Text>
</View>
<View style={styles.headerRight}>
<TouchableOpacity style={styles.shareButton}>
<MaterialCommunityIcons name="share-variant" size={22} color={colors.text.primary} />
<TouchableOpacity style={styles.shareButton} activeOpacity={0.7}>
<MaterialCommunityIcons name="share-variant" size={20} color={colors.text.secondary} />
</TouchableOpacity>
</View>
</View>
{/* Content */}
{isWideScreen ? (
<ResponsiveContainer maxWidth={800}>
<ResponsiveContainer maxWidth={720}>
<ScrollView
style={styles.scroll}
contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset }]}
@@ -326,6 +364,7 @@ function createMaterialDetailStyles(colors: AppColors) {
flex: 1,
backgroundColor: colors.background.default,
},
// Header
header: {
flexDirection: 'row',
alignItems: 'center',
@@ -333,7 +372,8 @@ function createMaterialDetailStyles(colors: AppColors) {
paddingHorizontal: spacing.md,
paddingVertical: spacing.md,
backgroundColor: headerBg,
...shadows.sm,
borderBottomWidth: 1,
borderBottomColor: colors.chat.borderHairline,
},
headerWide: {
paddingHorizontal: spacing.lg,
@@ -348,12 +388,13 @@ function createMaterialDetailStyles(colors: AppColors) {
alignItems: 'center',
},
headerTitle: {
fontSize: 19,
fontWeight: '700',
fontSize: fontSizes.lg,
fontWeight: '600',
color: colors.text.primary,
letterSpacing: 0.3,
},
headerTitleWide: {
fontSize: 22,
fontSize: fontSizes.xl,
},
headerRight: {
width: 44,
@@ -361,16 +402,19 @@ function createMaterialDetailStyles(colors: AppColors) {
},
shareButton: {
padding: spacing.xs,
borderRadius: borderRadius.md,
},
// Scroll
scroll: {
flex: 1,
backgroundColor: headerBg,
},
scrollContent: {
paddingTop: spacing.md,
paddingTop: spacing.lg,
flexGrow: 1,
backgroundColor: headerBg,
},
// Center states
centerContainer: {
flex: 1,
justifyContent: 'center',
@@ -386,19 +430,36 @@ function createMaterialDetailStyles(colors: AppColors) {
paddingVertical: spacing.sm,
paddingHorizontal: spacing.lg,
},
// File card
fileCard: {
marginBottom: spacing.md,
marginBottom: spacing.lg,
},
fileTypeBanner: {
height: 88,
height: 120,
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
},
fileIconWrapper: {
width: 72,
height: 72,
borderRadius: borderRadius['2xl'],
backgroundColor: 'rgba(255,255,255,0.2)',
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.sm,
},
fileTypeBadge: {
paddingVertical: spacing.xs,
paddingHorizontal: spacing.md,
borderRadius: borderRadius.full,
backgroundColor: 'rgba(255,255,255,0.25)',
},
fileTypeName: {
color: '#FFFFFF',
fontWeight: '700',
fontSize: fontSizes.lg,
marginTop: spacing.sm,
fontSize: fontSizes.sm,
letterSpacing: 0.5,
},
fileInfo: {
padding: spacing.lg,
@@ -406,69 +467,114 @@ function createMaterialDetailStyles(colors: AppColors) {
fileName: {
color: colors.text.primary,
fontWeight: '700',
fontSize: fontSizes.lg,
fontSize: fontSizes.xl,
lineHeight: fontSizes.xl * 1.4,
},
fileDescription: {
marginTop: spacing.sm,
lineHeight: fontSizes.md * 1.5,
lineHeight: fontSizes.md * 1.6,
color: colors.text.secondary,
},
fileMetaRow: {
flexDirection: 'row',
marginTop: spacing.md,
gap: spacing.md,
marginTop: spacing.lg,
gap: spacing.sm,
flexWrap: 'wrap',
},
metaItem: {
metaBadge: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
paddingVertical: spacing.xs + 2,
paddingHorizontal: spacing.md,
borderRadius: borderRadius.full,
},
metaBadgeText: {
fontWeight: '500',
fontSize: fontSizes.sm,
},
// Detail card
detailCard: {
padding: spacing.lg,
marginBottom: spacing.lg,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.md,
gap: spacing.sm,
},
sectionIcon: {
width: 32,
height: 32,
borderRadius: borderRadius.md,
alignItems: 'center',
justifyContent: 'center',
},
sectionTitle: {
fontWeight: '700',
fontSize: fontSizes.md,
color: colors.text.primary,
marginBottom: spacing.md,
letterSpacing: 0.2,
},
detailList: {
gap: 0,
},
detailRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: spacing.sm,
paddingVertical: spacing.md,
borderBottomWidth: 1,
borderBottomColor: colors.divider,
borderBottomColor: colors.chat.borderHairline,
},
detailLabel: {
width: 80,
},
detailValue: {
flex: 1,
textAlign: 'right',
},
tagContainer: {
detailLabelWrapper: {
flexDirection: 'row',
alignItems: 'center',
marginTop: spacing.sm,
gap: spacing.sm,
flex: 1,
},
detailLabel: {
fontWeight: '500',
},
detailValue: {
flex: 1.5,
textAlign: 'right',
fontWeight: '500',
},
// Tags
tagSection: {
marginTop: spacing.md,
},
tagSectionHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.sm,
},
tagSectionTitle: {
fontWeight: '500',
},
tags: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.xs,
flex: 1,
gap: spacing.sm,
},
tag: {
paddingVertical: spacing.xs,
paddingHorizontal: spacing.sm,
backgroundColor: `${colors.primary.main}10`,
borderRadius: borderRadius.md,
borderWidth: 0,
paddingVertical: spacing.xs + 2,
paddingHorizontal: spacing.md,
borderRadius: borderRadius.full,
},
tagText: {
fontWeight: '600',
fontSize: fontSizes.sm,
},
// Action buttons
actionButtons: {
flexDirection: 'row',
gap: spacing.md,
marginTop: spacing.lg,
marginTop: spacing.sm,
marginBottom: spacing.lg,
},
previewButton: {
flex: 1,
@@ -476,10 +582,15 @@ function createMaterialDetailStyles(colors: AppColors) {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
borderRadius: borderRadius.md,
gap: spacing.xs,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
borderRadius: borderRadius.xl,
gap: spacing.sm,
borderWidth: 1,
borderColor: colors.chat.border,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.04,
shadowRadius: 4,
elevation: 1,
},
downloadButton: {
flex: 1,
@@ -487,11 +598,25 @@ function createMaterialDetailStyles(colors: AppColors) {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
borderRadius: borderRadius.xl,
gap: spacing.sm,
shadowColor: colors.primary.main,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 8,
elevation: 4,
},
actionIconWrapper: {
width: 32,
height: 32,
borderRadius: borderRadius.md,
gap: spacing.xs,
alignItems: 'center',
justifyContent: 'center',
},
actionButtonText: {
fontWeight: '600',
fontWeight: '700',
fontSize: fontSizes.md,
letterSpacing: 0.3,
},
});
}
}

View File

@@ -79,56 +79,52 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
return (
<View style={styles.headerContainer}>
<View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={onBack} activeOpacity={0.7}>
<MaterialCommunityIcons name="chevron-left" size={28} color={iconColor} />
</TouchableOpacity>
<View style={styles.headerLeft}>
<TouchableOpacity style={styles.backButton} onPress={onBack} activeOpacity={0.7}>
<MaterialCommunityIcons name="chevron-left" size={28} color={iconColor} />
</TouchableOpacity>
{isGroupChat ? (
<Avatar
source={groupInfo?.avatar || null}
size={36}
name={groupInfo?.name || '群'}
style={styles.groupAvatar}
/>
) : (
<Avatar
source={otherUser?.avatar || null}
size={36}
name={otherUser?.nickname || ''}
/>
)}
</View>
<View style={styles.headerCenter}>
<TouchableOpacity
style={styles.titleRow}
onPress={onTitlePress}
activeOpacity={0.75}
>
{isGroupChat ? (
<>
<View style={styles.titleLeading}>
<Avatar
source={groupInfo?.avatar || null}
size={36}
name={groupInfo?.name || '群'}
style={styles.groupAvatar}
/>
</View>
<View style={styles.titleContent}>
<Text style={styles.headerName} numberOfLines={1}>{getDisplayTitle()}</Text>
<View style={styles.subtitleRow}>
<Text style={styles.memberCount}>{groupInfo?.member_count || 0}</Text>
{typingHint && (
<Text style={styles.typingHint} numberOfLines={1}>{typingHint}</Text>
)}
</View>
</View>
</>
) : (
<>
<Avatar
source={otherUser?.avatar || null}
size={36}
name={otherUser?.nickname || ''}
/>
<View style={styles.titleContent}>
<Text style={styles.headerName} numberOfLines={1}>{getDisplayTitle()}</Text>
<View style={styles.titleContent}>
<Text style={styles.headerName} numberOfLines={1}>{getDisplayTitle()}</Text>
{isGroupChat ? (
<View style={styles.subtitleRow}>
<Text style={styles.memberCount}>{groupInfo?.member_count || 0}</Text>
{typingHint && (
<View style={styles.subtitleRow}>
<Text style={styles.typingHint} numberOfLines={1}>{typingHint}</Text>
</View>
<Text style={styles.typingHint} numberOfLines={1}>{typingHint}</Text>
)}
</View>
</>
)}
) : (
typingHint && (
<View style={styles.subtitleRow}>
<Text style={styles.typingHint} numberOfLines={1}>{typingHint}</Text>
</View>
)
)}
</View>
</TouchableOpacity>
</View>
{/* 大屏幕 + 群聊时显示群信息按钮,否则显示三点菜单 */}
{isWideScreen && isGroupChat && onGroupInfoPress ? (
<TouchableOpacity

View File

@@ -74,7 +74,9 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
...baseStyles,
emojiContainer: {
flex: 1,
padding: spacing.sm,
paddingTop: spacing.sm,
paddingHorizontal: spacing.sm,
backgroundColor: baseStyles.panelContainer.backgroundColor,
},
emojiItem: {
width: 40,
@@ -281,21 +283,19 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
);
};
// 渲染管理按钮(表情面板第一位)- 现代扁平化设计
// 渲染管理按钮(表情面板第一位)- 填满格子
const renderManageButton = () => (
<TouchableOpacity
key="manage-button"
style={[styles.stickerItem, stickerButtonStyles.manageButton]}
style={[styles.stickerItem, { alignItems: 'center', justifyContent: 'center' }]}
onPress={handleOpenManage}
activeOpacity={0.7}
>
<View style={stickerButtonStyles.iconContainer}>
<MaterialCommunityIcons
name="cog-outline"
size={28}
color={colors.chat.textSecondary}
/>
</View>
<MaterialCommunityIcons
name="cog-outline"
size={32}
color={colors.chat.textSecondary}
/>
</TouchableOpacity>
);
@@ -561,24 +561,28 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
{activeTab === 'emoji' ? renderEmojiPanel() : renderStickerPanel()}
</View>
{/* 底部 Tab 栏 - 现代胶囊式设计 */}
{/* 底部 Tab 栏 - QQ/微信风格横向导航 */}
<View style={styles.panelTabBar}>
<View style={styles.panelTabContainer}>
<View style={styles.panelTabScroll}>
<TouchableOpacity
style={[styles.panelTab, activeTab === 'emoji' && styles.panelTabActive]}
style={[styles.panelTabItem, activeTab === 'emoji' && styles.panelTabItemActive]}
onPress={() => setActiveTab('emoji')}
activeOpacity={0.8}
>
<Text style={[styles.panelTabEmoji, activeTab === 'emoji' ? { opacity: 1 } : {}]}>😊</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.panelTab, activeTab === 'sticker' && styles.panelTabActive]}
onPress={() => setActiveTab('sticker')}
activeOpacity={0.8}
>
<MaterialCommunityIcons
name="emoticon-happy-outline"
size={22}
color={activeTab === 'emoji' ? colors.primary.main : colors.chat.textSecondary}
/>
</TouchableOpacity>
<TouchableOpacity
style={[styles.panelTabItem, activeTab === 'sticker' && styles.panelTabItemActive]}
onPress={() => setActiveTab('sticker')}
activeOpacity={0.8}
>
<MaterialCommunityIcons
name="image-multiple-outline"
size={22}
color={activeTab === 'sticker' ? colors.primary.main : colors.chat.textSecondary}
/>
</TouchableOpacity>

View File

@@ -105,6 +105,12 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
backgroundColor: cardBgColor,
height: 56,
},
headerLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
zIndex: 2,
},
backButton: {
width: 40,
height: 40,
@@ -114,27 +120,21 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
padding: 0,
},
headerCenter: {
flex: 1,
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
marginHorizontal: spacing.sm,
justifyContent: 'center',
paddingHorizontal: 50,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: 8,
},
titleLeading: {
width: 36,
height: 36,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: panelBgColor,
overflow: 'hidden',
},
groupAvatar: {
width: 36,
height: 36,
@@ -429,17 +429,17 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
// 输入框区域 - 与列表背景同色底栏,顶部细分隔
inputWrapper: {
backgroundColor: screenBgColor,
backgroundColor: cardBgColor,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.chat.border,
borderTopColor: colors.chat.borderHairline,
},
inputContainer: {
backgroundColor: 'transparent',
borderWidth: 0,
borderRadius: 0,
marginHorizontal: 10,
marginBottom: 10,
marginTop: 2,
marginBottom: 6,
marginTop: 6,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.sm,
shadowColor: 'transparent',
@@ -451,7 +451,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
inputInner: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: inputBgColor,
backgroundColor: colors.chat.surfaceRaised,
borderRadius: 20,
paddingHorizontal: spacing.xs,
paddingVertical: 4,
@@ -464,7 +464,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
elevation: 0,
},
inputInnerMuted: {
backgroundColor: colors.chat.borderHairline,
backgroundColor: colors.chat.surfaceMuted,
opacity: 0.7,
},
@@ -585,14 +585,9 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
// 面板
panelWrapper: {
backgroundColor: panelBgColor,
borderTopWidth: 1,
borderTopColor: colors.chat.border,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 8,
backgroundColor: cardBgColor,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.chat.borderHairline,
},
// 表情面板包装器 - 底部 tab 栏是白色,安全区域也用白色填充
emojiPanelWrapper: {
@@ -600,7 +595,11 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
},
panelContainer: {
flex: 1,
backgroundColor: panelBgColor,
backgroundColor: cardBgColor,
},
morePanelBackground: {
flex: 1,
backgroundColor: cardBgColor,
},
// 表情面板
@@ -611,6 +610,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
paddingTop: spacing.md,
paddingBottom: spacing.xl,
alignItems: 'flex-start',
backgroundColor: cardBgColor,
},
emojiItem: {
width: '12.5%',
@@ -636,55 +636,41 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
flex: 1,
},
// Tab 栏 - 现代胶囊式设计
// Tab 栏 - 底部横向导航条QQ/微信风格)
panelTabBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm + 2,
paddingHorizontal: 0,
paddingVertical: 0,
backgroundColor: cardBgColor,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.chat.borderHairline,
backgroundColor: cardBgColor,
},
panelTabContainer: {
panelTabScroll: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: panelBgColor,
borderRadius: 12,
padding: 3,
paddingHorizontal: spacing.sm,
gap: spacing.sm,
},
panelTab: {
width: 42,
height: 36,
panelTabItem: {
alignItems: 'center',
justifyContent: 'center',
borderRadius: 10,
width: 48,
height: 44,
borderBottomWidth: 2.5,
borderBottomColor: 'transparent',
},
panelTabActive: {
backgroundColor: cardBgColor,
shadowColor: colors.chat.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
elevation: 1,
},
panelTabEmoji: {
fontSize: 22,
},
panelTabDivider: {
width: 1,
height: 20,
backgroundColor: colors.chat.border,
marginHorizontal: spacing.sm,
panelTabItemActive: {
borderBottomColor: colors.primary.main,
},
panelCloseButton: {
width: 36,
height: 36,
width: 44,
height: 44,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 10,
backgroundColor: panelBgColor,
marginRight: spacing.sm,
},
// 自定义表情面板
@@ -692,12 +678,14 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: cardBgColor,
},
stickerEmptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: spacing.xl,
backgroundColor: cardBgColor,
},
stickerEmptyText: {
fontSize: 16,
@@ -715,6 +703,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
paddingHorizontal: spacing.md,
paddingTop: spacing.md,
paddingBottom: spacing.xl,
backgroundColor: cardBgColor,
},
stickerItem: {
width: (SCREEN_WIDTH - spacing.md * 2) / 4 - 8,
@@ -734,9 +723,10 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
flexDirection: 'row',
flexWrap: 'wrap',
paddingHorizontal: spacing.lg + 4,
paddingTop: spacing.lg,
paddingTop: spacing.md,
paddingBottom: spacing.xl,
justifyContent: 'flex-start',
backgroundColor: cardBgColor,
},
moreItem: {
width: (SCREEN_WIDTH - spacing.lg * 2 - 8) / 4,
@@ -779,12 +769,6 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
fontWeight: '500',
letterSpacing: 0.3,
},
// MorePanel 整体背景
morePanelBackground: {
flex: 1,
backgroundColor: panelBgColor,
},
// @成员选择浮层 - 绝对定位浮在输入框上方
mentionPanelFloat: {
position: 'absolute',

View File

@@ -524,9 +524,6 @@ export const AboutScreen: React.FC = () => {
>
<View style={styles.content}>
<View style={styles.header}>
<View style={styles.logoContainer}>
<Text style={styles.logoText}></Text>
</View>
<Text style={styles.appName}>{APP_NAME}</Text>
<Text style={styles.appSlogan}>{APP_SLOGAN}</Text>
<Text style={styles.versionText}> {APP_VERSION}</Text>

View File

@@ -52,12 +52,8 @@ const PRIVACY_SECTIONS = [
1.2 我们在您使用服务时收集的信息
• 设备信息包括设备型号、操作系统版本、设备设置、唯一设备标识符如IMEI、Android ID、IDFA、OAID、MAC地址等、IP地址等
• 日志信息IP地址、访问时间、浏览记录、操作日志等
• 位置信息:为了向您提供"校园圈"、"附近的人"等基于位置的服务,经您授权,我们会收集您的精确地理位置信息。该信息属于敏感个人信息,拒绝提供仅会影响相关功能,不影响其他服务的使用
• 相机/相册权限:用于发布动态、更换头像。我们仅在您主动触发相关功能时申请权限,且不会在后台持续收集
• 通讯录权限:经您授权后用于查找好友
1.3 从第三方获取的信息
如您使用第三方账号登录功能(具体以应用实际提供的登录方式为准),我们会从第三方获取您授权共享的账号信息(如昵称、头像等)。`,
• 通讯录权限:经您授权后用于查找好友`,
},
{
title: '二、我们如何使用信息',

View File

@@ -18,10 +18,10 @@ import type { PrivacySettingsDTO, VisibilityLevel } from '../../types/dto';
const CONTENT_MAX_WIDTH = 720;
const VISIBILITY_OPTIONS: { value: VisibilityLevel; label: string; description: string }[] = [
{ value: 'everyone', label: '所有人可见', description: '任何人都可查看' },
{ value: 'following', label: '仅关注的人可见', description: '只有你关注的人可查看' },
{ value: 'self', label: '仅自己可见', description: '只有自己可查看' },
const VISIBILITY_OPTIONS: { value: VisibilityLevel; label: string }[] = [
{ value: 'everyone', label: '所有人可见' },
{ value: 'following', label: '仅关注的人可见' },
{ value: 'self', label: '仅自己可见' },
];
const PRIVACY_ITEMS: { key: keyof PrivacySettingsDTO; title: string; description: string }[] = [
@@ -65,55 +65,78 @@ function createPrivacySettingsStyles(colors: AppColors) {
textTransform: 'uppercase',
letterSpacing: 0.5,
},
// 隐私设置项 - 扁平化卡片风格
// 扁平式布局:整行横向排列,底部边框分隔
item: {
backgroundColor: colors.background.default,
borderRadius: 14,
padding: spacing.lg,
marginHorizontal: spacing['2xl'],
marginBottom: spacing.md,
},
itemHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: spacing.sm,
paddingVertical: spacing.lg,
marginHorizontal: spacing['2xl'],
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
},
itemLast: {
borderBottomWidth: 0,
},
itemTextWrap: {
flex: 1,
marginRight: spacing.md,
},
itemTitle: {
fontWeight: '600',
fontSize: fontSizes.md,
color: colors.text.primary,
marginBottom: spacing.xs,
},
itemDescription: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
marginBottom: spacing.md,
},
optionsRow: {
// 无背景色的选择框,紧贴右侧
selectBox: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.sm,
alignItems: 'center',
paddingVertical: spacing.xs,
},
// 选项按钮 - 扁平化风格
optionButton: {
selectBoxText: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
marginRight: spacing.xs,
},
// 原位展开的下拉菜单
dropdownMenu: {
position: 'absolute',
right: 0,
top: '100%',
marginTop: 4,
backgroundColor: colors.background.paper,
borderRadius: borderRadius.md,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.divider,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 4,
zIndex: 100,
minWidth: 160,
},
dropdownOption: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.divider,
backgroundColor: colors.background.paper,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: colors.divider,
},
optionButtonActive: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
dropdownOptionLast: {
borderBottomWidth: 0,
},
optionText: {
fontSize: 13,
dropdownOptionText: {
fontSize: fontSizes.sm,
color: colors.text.primary,
},
optionTextActive: {
color: colors.text.inverse,
fontWeight: '500',
dropdownOptionTextActive: {
color: colors.primary.main,
fontWeight: '600',
},
});
}
@@ -127,6 +150,7 @@ export const PrivacySettingsScreen: React.FC = () => {
const [settings, setSettings] = useState<PrivacySettingsDTO | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [openPicker, setOpenPicker] = useState<keyof PrivacySettingsDTO | null>(null);
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
const scrollBottomInset = isMobile ? 64 + 24 + insets.bottom + spacing.md : spacing.md;
@@ -150,10 +174,10 @@ export const PrivacySettingsScreen: React.FC = () => {
const updateSetting = useCallback(async (key: keyof PrivacySettingsDTO, value: VisibilityLevel) => {
if (!settings) return;
const newSettings = { ...settings, [key]: value };
setSettings(newSettings);
setSaving(true);
try {
const ok = await authService.updatePrivacySettings({
@@ -171,6 +195,9 @@ export const PrivacySettingsScreen: React.FC = () => {
}
}, [settings]);
const getLabel = (value?: VisibilityLevel) =>
VISIBILITY_OPTIONS.find((o) => o.value === value)?.label ?? '请选择';
if (loading) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
@@ -186,42 +213,75 @@ export const PrivacySettingsScreen: React.FC = () => {
<ScrollView contentContainerStyle={[styles.scrollContent, { paddingBottom: scrollBottomInset, paddingHorizontal: responsivePadding }]}>
<View style={styles.content}>
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text variant="caption" style={styles.sectionTitle}>
</Text>
</View>
{PRIVACY_ITEMS.map((item) => (
<View key={item.key} style={styles.item}>
<Text variant="body" style={styles.itemTitle}>
{item.title}
</Text>
<Text variant="caption" color={colors.text.secondary} style={styles.itemDescription}>
{item.description}
</Text>
<View style={styles.optionsRow}>
{VISIBILITY_OPTIONS.map((option) => {
const isActive = settings?.[item.key] === option.value;
return (
<TouchableOpacity
key={option.value}
style={[styles.optionButton, isActive && styles.optionButtonActive]}
onPress={() => updateSetting(item.key, option.value)}
activeOpacity={0.7}
>
<Text
variant="caption"
style={[styles.optionText, isActive ? styles.optionTextActive : {}]}
>
{option.label}
</Text>
</TouchableOpacity>
);
})}
{PRIVACY_ITEMS.map((item, index) => {
const isOpen = openPicker === item.key;
return (
<View
key={item.key}
style={[
styles.item,
index === PRIVACY_ITEMS.length - 1 && styles.itemLast,
]}
>
<View style={styles.itemTextWrap}>
<Text variant="body" style={styles.itemTitle}>
{item.title}
</Text>
<Text variant="caption" color={colors.text.secondary} style={styles.itemDescription}>
{item.description}
</Text>
</View>
<TouchableOpacity
style={styles.selectBox}
onPress={() => setOpenPicker(isOpen ? null : item.key)}
activeOpacity={0.7}
>
<Text style={styles.selectBoxText}>
{getLabel(settings?.[item.key])}
</Text>
<MaterialCommunityIcons
name={isOpen ? 'chevron-up' : 'chevron-down'}
size={18}
color={colors.text.secondary}
/>
</TouchableOpacity>
{/* 原位展开的下拉选项 */}
{isOpen && (
<View style={styles.dropdownMenu}>
{VISIBILITY_OPTIONS.map((option, optIndex) => {
const isActive = settings?.[item.key] === option.value;
return (
<TouchableOpacity
key={option.value}
style={[
styles.dropdownOption,
optIndex === VISIBILITY_OPTIONS.length - 1 && styles.dropdownOptionLast,
]}
onPress={() => {
updateSetting(item.key, option.value);
setOpenPicker(null);
}}
activeOpacity={0.7}
>
<Text
variant="body"
style={[
styles.dropdownOptionText,
isActive ? styles.dropdownOptionTextActive : undefined,
]}
>
{option.label}
</Text>
</TouchableOpacity>
);
})}
</View>
)}
</View>
</View>
))}
);
})}
</View>
</View>
</ScrollView>

View File

@@ -466,9 +466,9 @@ export function CreateTradeScreen({ onClose, onSuccess, editItem }: CreateTradeS
>
{/* 标题区域 */}
<View style={styles.titleSection}>
<Text style={styles.title}>{isEdit ? '编辑商品' : '发布商品'}</Text>
<Text style={styles.title}>{isEdit ? '编辑' + (tradeType === 'buy' ? '收购' : '商品') : '发布' + (tradeType === 'buy' ? '收购' : '商品')}</Text>
<View style={styles.underline} />
<Text style={styles.subtitle}></Text>
<Text style={styles.subtitle}>{tradeType === 'buy' ? '填写收购需求,让卖家主动联系你' : '填写商品信息,让更多人看到'}</Text>
</View>
{/* 交易类型 */}
@@ -520,7 +520,7 @@ export function CreateTradeScreen({ onClose, onSuccess, editItem }: CreateTradeS
style={styles.input}
value={title}
onChangeText={setTitle}
placeholder="描述你的商品"
placeholder={tradeType === 'buy' ? '描述你想收购的商品' : '描述你的商品'}
placeholderTextColor={colors.text.hint}
maxLength={100}
cursorColor={colors.primary.main}
@@ -541,7 +541,7 @@ export function CreateTradeScreen({ onClose, onSuccess, editItem }: CreateTradeS
style={[styles.input, styles.textArea]}
value={content}
onChangeText={setContent}
placeholder="详细描述商品情况..."
placeholder={tradeType === 'buy' ? '详细描述收购需求、预算、成色要求等...' : '详细描述商品情况...'}
placeholderTextColor={colors.text.hint}
multiline
maxLength={2000}

View File

@@ -120,6 +120,11 @@ function createStyles(colors: AppColors) {
fontWeight: '700',
color: '#FF4D4F',
},
priceSymbol: {
fontSize: fontSizes.md,
fontWeight: '700',
color: '#FF4D4F',
},
titleRow: {
flexDirection: 'row',
@@ -330,6 +335,17 @@ function createStyles(colors: AppColors) {
color: colors.text.primary,
fontWeight: '600',
},
contentAttrs: {
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
gap: spacing.md,
flexWrap: 'wrap',
},
contentAttrText: {
fontSize: fontSizes.sm,
color: colors.text.secondary,
},
});
}
@@ -496,7 +512,7 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
</View>
{item.price != null ? (
<Text style={styles.priceInHeader}>
¥{item.price % 1 === 0 ? item.price.toFixed(0) : item.price.toFixed(2)}
<Text style={styles.priceSymbol}>¥</Text>{item.price % 1 === 0 ? item.price.toFixed(0) : item.price.toFixed(2)}
</Text>
) : (
<Text style={styles.priceInHeader}></Text>
@@ -509,41 +525,12 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
{/* 标题 */}
<View style={styles.titleRow}>
<Text style={styles.title} numberOfLines={2}>{item.title}</Text>
{item.condition && item.trade_type === 'sell' && (
<View style={{ flexShrink: 0, marginTop: 7, marginLeft: 'auto' }}>
<View style={styles.conditionBadge}>
<Text style={styles.conditionBadgeText} numberOfLines={1}>
{TRADE_CONDITION_MAP[item.condition as keyof typeof TRADE_CONDITION_MAP] || item.condition}
</Text>
</View>
</View>
)}
</View>
{/* 商品属性 */}
<View style={styles.attrGrid}>
<View style={styles.attrItem}>
<Text style={styles.attrLabel}></Text>
<Text style={styles.attrValue}>{item.brand || '其他'}</Text>
</View>
<View style={styles.attrItem}>
<Text style={styles.attrLabel}></Text>
<Text style={styles.attrValue}>
{TRADE_CONDITION_MAP[item.condition as keyof typeof TRADE_CONDITION_MAP] || item.condition || '未知'}
</Text>
</View>
<View style={styles.attrItem}>
<Text style={styles.attrLabel}></Text>
<Text style={styles.attrValue}>{item.function_status || '正常使用'}</Text>
</View>
</View>
{/* 商品描述 */}
{item.content && (
<Text style={styles.contentText}>{item.content}</Text>
)}
<View style={styles.divider} />
</View>
{/* 图片放在文字下方 */}
@@ -555,11 +542,23 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
source={{ uri: img.url || img.preview_url_large || img.preview_url || '' }}
style={styles.image}
/>
</View>
))}
</View>
)}
{/* 内容区右下角属性文字(仅出售商品显示) */}
{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>
)}
</ScrollView>
{/* 底部操作栏 */}