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

View File

@@ -41,6 +41,7 @@ function createTradeCardStyles(colors: AppColors) {
position: 'relative', position: 'relative',
aspectRatio: 1, aspectRatio: 1,
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
minHeight: 140,
}, },
listImageContainer: { listImageContainer: {
width: 120, width: 120,
@@ -104,7 +105,9 @@ function createTradeCardStyles(colors: AppColors) {
fontWeight: '600', fontWeight: '600',
}, },
gridContent: { gridContent: {
padding: spacing.sm, paddingHorizontal: spacing.sm,
paddingTop: spacing.xs,
paddingBottom: spacing.sm,
}, },
titleRow: { titleRow: {
flexDirection: 'row', flexDirection: 'row',
@@ -155,7 +158,7 @@ function createTradeCardStyles(colors: AppColors) {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: 4, gap: 4,
marginTop: spacing.xs, marginTop: 2,
}, },
tag: { tag: {
paddingHorizontal: 5, paddingHorizontal: 5,
@@ -191,7 +194,7 @@ function createTradeCardStyles(colors: AppColors) {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
marginTop: spacing.sm, marginTop: spacing.xs,
}, },
authorRow: { authorRow: {
flexDirection: 'row', 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 ( return (
<TouchableOpacity <TouchableOpacity
style={styles.listCard} style={styles.listCard}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,9 +79,25 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
return ( return (
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity style={styles.backButton} onPress={onBack} activeOpacity={0.7}> <View style={styles.headerLeft}>
<MaterialCommunityIcons name="chevron-left" size={28} color={iconColor} /> <TouchableOpacity style={styles.backButton} onPress={onBack} activeOpacity={0.7}>
</TouchableOpacity> <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}> <View style={styles.headerCenter}>
<TouchableOpacity <TouchableOpacity
@@ -89,43 +105,23 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
onPress={onTitlePress} onPress={onTitlePress}
activeOpacity={0.75} activeOpacity={0.75}
> >
{isGroupChat ? ( <View style={styles.titleContent}>
<> <Text style={styles.headerName} numberOfLines={1}>{getDisplayTitle()}</Text>
<View style={styles.titleLeading}> {isGroupChat ? (
<Avatar <View style={styles.subtitleRow}>
source={groupInfo?.avatar || null} <Text style={styles.memberCount}>{groupInfo?.member_count || 0}</Text>
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>
{typingHint && ( {typingHint && (
<View style={styles.subtitleRow}> <Text style={styles.typingHint} numberOfLines={1}>{typingHint}</Text>
<Text style={styles.typingHint} numberOfLines={1}>{typingHint}</Text>
</View>
)} )}
</View> </View>
</> ) : (
)} typingHint && (
<View style={styles.subtitleRow}>
<Text style={styles.typingHint} numberOfLines={1}>{typingHint}</Text>
</View>
)
)}
</View>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -120,6 +120,11 @@ function createStyles(colors: AppColors) {
fontWeight: '700', fontWeight: '700',
color: '#FF4D4F', color: '#FF4D4F',
}, },
priceSymbol: {
fontSize: fontSizes.md,
fontWeight: '700',
color: '#FF4D4F',
},
titleRow: { titleRow: {
flexDirection: 'row', flexDirection: 'row',
@@ -330,6 +335,17 @@ function createStyles(colors: AppColors) {
color: colors.text.primary, color: colors.text.primary,
fontWeight: '600', 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> </View>
{item.price != null ? ( {item.price != null ? (
<Text style={styles.priceInHeader}> <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>
) : ( ) : (
<Text style={styles.priceInHeader}></Text> <Text style={styles.priceInHeader}></Text>
@@ -509,41 +525,12 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
{/* 标题 */} {/* 标题 */}
<View style={styles.titleRow}> <View style={styles.titleRow}>
<Text style={styles.title} numberOfLines={2}>{item.title}</Text> <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> </View>
{/* 商品描述 */} {/* 商品描述 */}
{item.content && ( {item.content && (
<Text style={styles.contentText}>{item.content}</Text> <Text style={styles.contentText}>{item.content}</Text>
)} )}
<View style={styles.divider} />
</View> </View>
{/* 图片放在文字下方 */} {/* 图片放在文字下方 */}
@@ -555,11 +542,23 @@ export function TradeDetailScreen({ tradeId }: TradeDetailScreenProps) {
source={{ uri: img.url || img.preview_url_large || img.preview_url || '' }} source={{ uri: img.url || img.preview_url_large || img.preview_url || '' }}
style={styles.image} style={styles.image}
/> />
</View> </View>
))} ))}
</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> </ScrollView>
{/* 底部操作栏 */} {/* 底部操作栏 */}