refactor(ui): modernize components with flat design and UX refinements
- 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:
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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={[
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: '二、我们如何使用信息',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
{/* 底部操作栏 */}
|
{/* 底部操作栏 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user