refactor(home): redesign home screen with sort bar and add recommendation feed
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 2m49s
Frontend CI / ota-android (push) Successful in 10m29s
Frontend CI / build-android-apk (push) Successful in 40m32s

- Replace PagerView-based tabs with modern sort bar navigation
- Add "推荐" (recommended) feed option with "hot" sort type
- Update tab icons and labels to match new sort paradigm
- Optimize PostCard, MessageBubble conditional styles

perf(chat): migrate FlatList to FlashList for emoji/sticker panels

- Improve virtualized rendering performance for emoji and sticker grids
- Fix jump-to-latest scroll behavior for inverted FlashList

perf(profile): extract user header and tab bar into separate memoized renders

- Prevent unnecessary re-renders when switching tabs

feat(api): add channel_id filter support for post queries

- Include channel_id in cursor pagination requests
- Update CursorPaginationRequest type definition

style(chat-info): redesign group and private chat info screens with flat layout

- Remove card borders and shadows for cleaner appearance
- Adjust avatar sizes and spacing for consistency
This commit is contained in:
lafay
2026-04-26 12:04:27 +08:00
parent e9d7098ad0
commit 5815f1a5f7
12 changed files with 293 additions and 337 deletions

View File

@@ -605,7 +605,7 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
size={14} size={14}
color={post.is_liked ? colors.primary.main : colors.text.secondary} color={post.is_liked ? colors.primary.main : colors.text.secondary}
/> />
<Text style={[styles.gridLikeCount, post.is_liked && { color: colors.primary.main }]}> <Text style={[styles.gridLikeCount, post.is_liked ? { color: colors.primary.main } : {}]}>
{formatNumber(post.likes_count || 0)} {formatNumber(post.likes_count || 0)}
</Text> </Text>
</View> </View>

View File

@@ -23,13 +23,13 @@ import { FlashList, ListRenderItem, FlashListRef } from '@shopify/flash-list';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useRouter, useFocusEffect } from 'expo-router'; import { useRouter, useFocusEffect } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { PagerView } from '../../components/common';
import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme'; import { useAppColors, useResolvedColorScheme, spacing, borderRadius, shadows, type AppColors } from '../../theme';
import { Post } from '../../types'; import { Post } from '../../types';
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores'; import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth'; import { useCurrentUser, useIsAuthenticated, useIsVerified, useVerificationStore } from '../../stores/auth';
import { channelService, postService } from '../../services'; import { channelService, postService } from '../../services';
import { PostCard, TabBar, SearchBar, ShareSheet } from '../../components/business'; import { PostCard, SearchBar, ShareSheet } from '../../components/business';
import type { PostCardAction } from '../../components/business/PostCard'; import type { PostCardAction } from '../../components/business/PostCard';
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common'; import { Loading, EmptyState, Text, ImageGallery, ImageGridItem } from '../../components/common';
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive'; import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
@@ -40,8 +40,9 @@ import { CreatePostScreen } from '../create/CreatePostScreen';
import * as hrefs from '../../navigation/hrefs'; import * as hrefs from '../../navigation/hrefs';
import { blurActiveElement } from '../../infrastructure/platform'; import { blurActiveElement } from '../../infrastructure/platform';
const TABS = ['关注', '最新', '热门']; const SORT_OPTIONS = ['关注', '推荐', '最新'];
const TAB_ICONS = ['account-heart-outline', 'clock-outline', 'fire']; const SORT_ICONS = ['account-heart', 'fire', 'clock-outline'];
const SORT_TYPES: PostType[] = ['follow', 'hot', 'latest'];
const DEFAULT_PAGE_SIZE = 20; const DEFAULT_PAGE_SIZE = 20;
const MOBILE_TAB_BAR_HEIGHT = 64; const MOBILE_TAB_BAR_HEIGHT = 64;
const MOBILE_TAB_FLOATING_MARGIN = 12; const MOBILE_TAB_FLOATING_MARGIN = 12;
@@ -83,13 +84,20 @@ function createHomeStyles(colors: AppColors) {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
capsuleWrapper: { sortBar: {
paddingBottom: spacing.sm, flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
}, },
capsuleList: { sortBarCapsuleScroll: {
flex: 1,
marginHorizontal: spacing.xs,
},
sortBarCapsuleContent: {
gap: spacing.xs, gap: spacing.xs,
paddingRight: spacing.lg, alignItems: 'center',
}, },
capsuleItem: { capsuleItem: {
paddingHorizontal: spacing.md, paddingHorizontal: spacing.md,
@@ -104,12 +112,21 @@ function createHomeStyles(colors: AppColors) {
capsuleTextActive: { capsuleTextActive: {
color: colors.primary.main, color: colors.primary.main,
}, },
sortTrigger: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 4,
paddingHorizontal: 2,
},
sortTriggerText: {
fontSize: 13,
fontWeight: '600',
color: colors.text.secondary,
marginLeft: 2,
},
listContent: { listContent: {
flexGrow: 1, flexGrow: 1,
}, },
listHeader: {
width: '100%',
},
contentContainer: { contentContainer: {
flex: 1, flex: 1,
}, },
@@ -181,7 +198,7 @@ export const HomeScreen: React.FC = () => {
const responsiveGap = useResponsiveSpacing({ xs: 4, sm: 6, md: 8, lg: 12, xl: 16 }); const responsiveGap = useResponsiveSpacing({ xs: 4, sm: 6, md: 8, lg: 12, xl: 16 });
const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
const [activeIndex, setActiveIndex] = useState(1); const [sortIndex, setSortIndex] = useState(0);
const [viewMode, setViewMode] = useState<ViewMode>('list'); const [viewMode, setViewMode] = useState<ViewMode>('list');
const [latestCapsules, setLatestCapsules] = useState<LatestCapsule[]>([{ id: '', name: '全部' }]); const [latestCapsules, setLatestCapsules] = useState<LatestCapsule[]>([{ id: '', name: '全部' }]);
const [activeCapsuleId, setActiveCapsuleId] = useState(''); const [activeCapsuleId, setActiveCapsuleId] = useState('');
@@ -217,9 +234,6 @@ export const HomeScreen: React.FC = () => {
const postIdsRef = useRef<Set<string>>(new Set()); const postIdsRef = useRef<Set<string>>(new Set());
const isLoadingMoreRef = useRef(false); const isLoadingMoreRef = useRef(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pagerRef = useRef<any>(null);
const setBottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.setBottomTabBarHiddenByScroll); const setBottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.setBottomTabBarHiddenByScroll);
const bottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.bottomTabBarHiddenByScroll); const bottomTabBarHiddenByScroll = useHomeTabBarVisibilityStore((s) => s.bottomTabBarHiddenByScroll);
@@ -325,28 +339,28 @@ export const HomeScreen: React.FC = () => {
return map; return map;
}, [storePosts]); }, [storePosts]);
// 获取当前 tab 对应的帖子类型 // 获取当前排序对应的帖子类型
const getPostType = useCallback((): PostType => { const getPostType = useCallback((): PostType => {
switch (activeIndex) { return SORT_TYPES[sortIndex] ?? 'hot';
case 0: return 'follow'; }, [sortIndex]);
case 1: return 'latest';
case 2: return 'hot';
default: return 'latest';
}
}, [activeIndex]);
const isLatestTab = activeIndex === 1; const currentChannelId = activeCapsuleId || undefined;
const currentChannelId = isLatestTab && activeCapsuleId ? activeCapsuleId : undefined;
useEffect(() => { useEffect(() => {
homeListScrollYRef.current = 0; homeListScrollYRef.current = 0;
setBottomTabBarHiddenByScroll(false); setBottomTabBarHiddenByScroll(false);
}, [activeIndex, currentChannelId, viewMode, setBottomTabBarHiddenByScroll]); }, [sortIndex, currentChannelId, setBottomTabBarHiddenByScroll]);
useEffect(() => {
requestAnimationFrame(() => {
flashListRef.current?.scrollToOffset({ offset: 0, animated: false });
scrollViewRef.current?.scrollTo({ y: 0, animated: false });
});
}, [viewMode]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!isLatestTab) return;
restoreCapsuleStripScroll(); restoreCapsuleStripScroll();
}, [isLatestTab, activeCapsuleId, latestCapsules.length, restoreCapsuleStripScroll]); }, [activeCapsuleId, latestCapsules.length, restoreCapsuleStripScroll]);
// 使用差异更新 Hook 获取帖子列表 // 使用差异更新 Hook 获取帖子列表
const listKey = useMemo( const listKey = useMemo(
@@ -419,13 +433,10 @@ export const HomeScreen: React.FC = () => {
} }
}, [listKey, getPostType, currentChannelId]); }, [listKey, getPostType, currentChannelId]);
// Tab 切换时刷新数据并重置列表
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
const run = async () => { const run = async () => {
reset(); reset();
// reset 会清空 store 中的状态,但异步订阅可能有延迟;
// 等一帧确保 store 的订阅回调已传播到 useDifferentialPosts
await new Promise(resolve => requestAnimationFrame(resolve)); await new Promise(resolve => requestAnimationFrame(resolve));
if (!cancelled) { if (!cancelled) {
await refresh(); await refresh();
@@ -434,7 +445,7 @@ export const HomeScreen: React.FC = () => {
run(); run();
return () => { cancelled = true; }; return () => { cancelled = true; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeIndex, currentChannelId]); }, [sortIndex, currentChannelId]);
// 将获取到的原始帖子与 store 中的状态合并 // 将获取到的原始帖子与 store 中的状态合并
// 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等) // 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等)
@@ -511,23 +522,13 @@ export const HomeScreen: React.FC = () => {
setViewMode(prev => prev === 'list' ? 'grid' : 'list'); setViewMode(prev => prev === 'list' ? 'grid' : 'list');
}; };
// 切换Tab点击 TabBar 或手势滑动) // 切换排序
const changeTab = useCallback((nextIndex: number) => { const changeSort = useCallback((nextIndex: number) => {
if (nextIndex < 0 || nextIndex >= TABS.length || nextIndex === activeIndex) { if (nextIndex < 0 || nextIndex >= SORT_OPTIONS.length || nextIndex === sortIndex) {
return; return;
} }
setActiveIndex(nextIndex); setSortIndex(nextIndex);
if (Platform.OS !== 'web') { }, [sortIndex]);
pagerRef.current?.setPage(nextIndex);
}
}, [activeIndex]);
const onPagerPageSelected = useCallback((e: { nativeEvent: { position: number } }) => {
const nextIndex = e.nativeEvent.position;
if (nextIndex !== activeIndex) {
setActiveIndex(nextIndex);
}
}, [activeIndex]);
useEffect(() => { useEffect(() => {
const loadChannels = async () => { const loadChannels = async () => {
@@ -669,46 +670,41 @@ export const HomeScreen: React.FC = () => {
setShowCreatePost(true); setShowCreatePost(true);
}; };
const renderLatestCapsules = () => { const renderChannelCapsules = () => {
if (!isLatestTab) {
return null;
}
return ( return (
<View style={[styles.capsuleWrapper, { paddingHorizontal: responsivePadding }]}> <ScrollView
<ScrollView ref={capsuleHScrollRef}
ref={capsuleHScrollRef} horizontal
horizontal showsHorizontalScrollIndicator={false}
showsHorizontalScrollIndicator={false} nestedScrollEnabled
nestedScrollEnabled style={styles.sortBarCapsuleScroll}
contentContainerStyle={styles.capsuleList} contentContainerStyle={styles.sortBarCapsuleContent}
onScroll={onCapsuleHorizontalScroll} onScroll={onCapsuleHorizontalScroll}
scrollEventThrottle={16} scrollEventThrottle={16}
onContentSizeChange={restoreCapsuleStripScroll} onContentSizeChange={restoreCapsuleStripScroll}
> >
{latestCapsules.map((item) => { {latestCapsules.map((item) => {
const isActive = item.id === activeCapsuleId; const isActive = item.id === activeCapsuleId;
return ( return (
<TouchableOpacity <TouchableOpacity
key={item.id || 'all'} key={item.id || 'all'}
activeOpacity={0.85} activeOpacity={0.85}
onPress={() => setActiveCapsuleId(item.id)} onPress={() => setActiveCapsuleId(item.id)}
style={styles.capsuleItem} style={styles.capsuleItem}
>
<Text
style={
isActive
? { ...styles.capsuleText, ...styles.capsuleTextActive }
: styles.capsuleText
}
> >
<Text {item.name}
style={ </Text>
isActive </TouchableOpacity>
? { ...styles.capsuleText, ...styles.capsuleTextActive } );
: styles.capsuleText })}
} </ScrollView>
>
{item.name}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
</View>
); );
}; };
@@ -758,6 +754,7 @@ export const HomeScreen: React.FC = () => {
const renderResponsiveGrid = () => { const renderResponsiveGrid = () => {
return ( return (
<FlashList <FlashList
key={`${listKey}_grid`}
ref={scrollViewRef as any} ref={scrollViewRef as any}
data={displayPosts} data={displayPosts}
renderItem={renderGridItem} renderItem={renderGridItem}
@@ -765,8 +762,6 @@ export const HomeScreen: React.FC = () => {
numColumns={gridColumns} numColumns={gridColumns}
masonry masonry
optimizeItemArrangement optimizeItemArrangement
ListHeaderComponent={renderLatestCapsules()}
ListHeaderComponentStyle={styles.listHeader}
contentContainerStyle={{ contentContainerStyle={{
paddingHorizontal: responsivePadding, paddingHorizontal: responsivePadding,
paddingBottom: 80 + responsivePadding, paddingBottom: 80 + responsivePadding,
@@ -798,10 +793,10 @@ export const HomeScreen: React.FC = () => {
return ( return (
<EmptyState <EmptyState
title="暂无内容" title="暂无内容"
description={activeIndex === 1 ? '关注一些用户来获取内容吧' : '暂无帖子,快去发布第一条内容吧'} description="暂无帖子,快去发布第一条内容吧"
/> />
); );
}, [activeIndex, isInitialLoading]); }, [isInitialLoading]);
// 渲染列表内容 // 渲染列表内容
const renderListContent = () => { const renderListContent = () => {
@@ -813,12 +808,11 @@ export const HomeScreen: React.FC = () => {
// 移动端和宽屏都使用单列 FlashList宽屏下居中显示 // 移动端和宽屏都使用单列 FlashList宽屏下居中显示
return ( return (
<FlashList <FlashList
key={listKey}
ref={flashListRef} ref={flashListRef}
data={displayPosts} data={displayPosts}
renderItem={renderPostList} renderItem={renderPostList}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
ListHeaderComponent={renderLatestCapsules()}
ListHeaderComponentStyle={styles.listHeader}
contentContainerStyle={{ contentContainerStyle={{
paddingHorizontal: listHorizontalPadding, paddingHorizontal: listHorizontalPadding,
paddingBottom: 80 + responsivePadding, paddingBottom: 80 + responsivePadding,
@@ -873,74 +867,45 @@ export const HomeScreen: React.FC = () => {
/> />
</View> </View>
{/* Tab切换 */} {/* 排序筛选 + 频道 */}
<TabBar <View style={styles.sortBar}>
tabs={TABS} <TouchableOpacity onPress={toggleViewMode} style={styles.viewToggleBtn}>
icons={TAB_ICONS} <MaterialCommunityIcons
activeIndex={activeIndex} name={viewMode === 'list' ? 'view-grid-outline' : 'view-list'}
onTabChange={changeTab} size={22}
variant="modern" color="#666"
style={styles.homeTabBar} />
rightContent={ </TouchableOpacity>
<TouchableOpacity onPress={toggleViewMode} style={styles.viewToggleBtn}>
<MaterialCommunityIcons {renderChannelCapsules()}
name={viewMode === 'list' ? 'view-grid-outline' : 'view-list'}
size={22} <TouchableOpacity
color="#666" activeOpacity={0.7}
/> onPress={() => changeSort((sortIndex + 1) % SORT_OPTIONS.length)}
</TouchableOpacity> style={styles.sortTrigger}
} >
/> <MaterialCommunityIcons
name={SORT_ICONS[sortIndex] as any}
size={18}
color={colors.text.secondary}
/>
<Text style={styles.sortTriggerText}>
{SORT_OPTIONS[sortIndex]}
</Text>
</TouchableOpacity>
</View>
</View> </View>
{/* 帖子列表 */} {/* 帖子列表 */}
{Platform.OS === 'web' ? ( <View style={styles.contentContainer}>
<View style={styles.contentContainer}> {isInitialLoading ? (
{isInitialLoading ? ( <Loading fullScreen />
<Loading fullScreen /> ) : viewMode === 'list' ? (
) : viewMode === 'list' ? ( renderListContent()
renderListContent() ) : (
) : ( renderResponsiveGrid()
renderResponsiveGrid() )}
)} </View>
</View>
) : (
<PagerView
ref={pagerRef}
style={styles.contentContainer}
initialPage={activeIndex}
onPageSelected={onPagerPageSelected}
overdrag
>
<View key="follow" collapsable={false} style={styles.contentContainer}>
{isInitialLoading && activeIndex === 0 ? (
<Loading fullScreen />
) : activeIndex === 0 ? (
viewMode === 'list' ? renderListContent() : renderResponsiveGrid()
) : (
<View style={styles.contentContainer} />
)}
</View>
<View key="latest" collapsable={false} style={styles.contentContainer}>
{isInitialLoading && activeIndex === 1 ? (
<Loading fullScreen />
) : activeIndex === 1 ? (
viewMode === 'list' ? renderListContent() : renderResponsiveGrid()
) : (
<View style={styles.contentContainer} />
)}
</View>
<View key="hot" collapsable={false} style={styles.contentContainer}>
{isInitialLoading && activeIndex === 2 ? (
<Loading fullScreen />
) : activeIndex === 2 ? (
viewMode === 'list' ? renderListContent() : renderResponsiveGrid()
) : (
<View style={styles.contentContainer} />
)}
</View>
</PagerView>
)}
{/* 漂浮发帖按钮 */} {/* 漂浮发帖按钮 */}
<TouchableOpacity <TouchableOpacity

View File

@@ -608,12 +608,12 @@ const GroupInfoScreen: React.FC = () => {
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
{/* 群组基本信息卡片 */} {/* 群组基本信息 */}
<View style={styles.headerCard}> <View style={styles.headerCard}>
<View style={styles.groupHeader}> <View style={styles.groupHeader}>
<Avatar <Avatar
source={group.avatar} source={group.avatar}
size={90} size={80}
name={group.name} name={group.name}
/> />
<View style={styles.groupInfo}> <View style={styles.groupInfo}>
@@ -621,7 +621,7 @@ const GroupInfoScreen: React.FC = () => {
{group.name} {group.name}
</Text> </Text>
<View style={styles.groupMeta}> <View style={styles.groupMeta}>
<MaterialCommunityIcons name="account-group" size={16} color={colors.text.secondary} /> <MaterialCommunityIcons name="account-group" size={14} color={colors.text.secondary} />
<Text variant="caption" color={colors.text.secondary}> <Text variant="caption" color={colors.text.secondary}>
{group.member_count} {group.member_count}
</Text> </Text>
@@ -661,7 +661,7 @@ const GroupInfoScreen: React.FC = () => {
)} )}
</View> </View>
{/* 群公告卡片 */} {/* 群公告 */}
{announcements.length > 0 && ( {announcements.length > 0 && (
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
@@ -684,7 +684,7 @@ const GroupInfoScreen: React.FC = () => {
</View> </View>
)} )}
{/* 成员预览卡片 */} {/* 成员预览 */}
<View style={styles.card}> <View style={styles.card}>
<TouchableOpacity style={styles.cardHeader} onPress={goToMembers} activeOpacity={0.7}> <TouchableOpacity style={styles.cardHeader} onPress={goToMembers} activeOpacity={0.7}>
<View style={styles.cardIconContainer}> <View style={styles.cardIconContainer}>
@@ -813,6 +813,7 @@ const GroupInfoScreen: React.FC = () => {
thumbColor={isPinned ? colors.primary.main : colors.background.paper} thumbColor={isPinned ? colors.primary.main : colors.background.paper}
/> />
</View> </View>
<Divider style={styles.settingDivider} />
<View style={styles.settingItem}> <View style={styles.settingItem}>
<View style={styles.settingIconContainer}> <View style={styles.settingIconContainer}>
<MaterialCommunityIcons name="bell-off-outline" size={20} color={colors.primary.main} /> <MaterialCommunityIcons name="bell-off-outline" size={20} color={colors.primary.main} />
@@ -1126,22 +1127,20 @@ function createGroupInfoStyles(colors: AppColors) {
return StyleSheet.create({ return StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.background.default, backgroundColor: colors.background.paper,
}, },
pageHeader: { pageHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: spacing.md, paddingHorizontal: spacing.lg,
paddingVertical: spacing.md, paddingVertical: spacing.md,
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
borderBottomWidth: 0.5, borderBottomWidth: 0,
borderBottomColor: colors.divider + '60',
}, },
pageTitle: { pageTitle: {
fontWeight: '700', fontWeight: '600',
fontSize: fontSizes.xl, fontSize: fontSizes.xl,
letterSpacing: 0.5,
}, },
pageHeaderPlaceholder: { pageHeaderPlaceholder: {
width: 40, width: 40,
@@ -1151,7 +1150,7 @@ function createGroupInfoStyles(colors: AppColors) {
flex: 1, flex: 1,
}, },
scrollContent: { scrollContent: {
padding: spacing.md, paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl * 2, paddingBottom: spacing.xl * 2,
}, },
errorContainer: { errorContainer: {
@@ -1160,29 +1159,25 @@ function createGroupInfoStyles(colors: AppColors) {
alignItems: 'center', alignItems: 'center',
}, },
// 头部卡片样式 - 扁平化设计 // 头部区域 - 扁平化无卡片
headerCard: { headerCard: {
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
borderRadius: borderRadius.xl, paddingVertical: spacing.lg,
padding: spacing.lg,
marginBottom: spacing.md, marginBottom: spacing.md,
borderWidth: 0.5,
borderColor: colors.divider + '40',
}, },
groupHeader: { groupHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: spacing.md, marginBottom: spacing.lg,
}, },
groupInfo: { groupInfo: {
marginLeft: spacing.lg, marginLeft: spacing.lg,
flex: 1, flex: 1,
}, },
groupName: { groupName: {
fontWeight: '800', fontWeight: '700',
fontSize: fontSizes.xl + 2, fontSize: fontSizes['2xl'],
marginBottom: spacing.xs, marginBottom: spacing.xs,
letterSpacing: 0.3,
}, },
groupMeta: { groupMeta: {
flexDirection: 'row', flexDirection: 'row',
@@ -1213,8 +1208,6 @@ function createGroupInfoStyles(colors: AppColors) {
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
borderRadius: borderRadius.lg, borderRadius: borderRadius.lg,
padding: spacing.md, padding: spacing.md,
borderWidth: 0.5,
borderColor: colors.divider + '30',
}, },
groupDesc: { groupDesc: {
marginLeft: spacing.sm, marginLeft: spacing.sm,
@@ -1223,19 +1216,16 @@ function createGroupInfoStyles(colors: AppColors) {
fontWeight: '400', fontWeight: '400',
}, },
// 通用卡片样式 - 扁平化 // 通用卡片样式 - 扁平化:无阴影无圆角,使用分割线
card: { card: {
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
borderRadius: borderRadius.xl,
padding: spacing.lg,
marginBottom: spacing.md, marginBottom: spacing.md,
borderWidth: 0.5,
borderColor: colors.divider + '40',
}, },
cardHeader: { cardHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: spacing.md, marginBottom: spacing.md,
paddingTop: spacing.sm,
}, },
cardIconContainer: { cardIconContainer: {
width: 36, width: 36,
@@ -1247,13 +1237,12 @@ function createGroupInfoStyles(colors: AppColors) {
marginRight: spacing.sm, marginRight: spacing.sm,
}, },
cardTitle: { cardTitle: {
fontWeight: '700', fontWeight: '600',
fontSize: fontSizes.md + 1, fontSize: fontSizes.md,
flex: 1, flex: 1,
letterSpacing: 0.3,
}, },
// 公告样式 - 更现代的卡片 // 公告样式 - 扁平化
announcementContent: { announcementContent: {
backgroundColor: colors.warning.light + '08', backgroundColor: colors.warning.light + '08',
borderRadius: borderRadius.lg, borderRadius: borderRadius.lg,
@@ -1324,7 +1313,7 @@ function createGroupInfoStyles(colors: AppColors) {
fontSize: fontSizes.sm, fontSize: fontSizes.sm,
}, },
// 邀请按钮样式 - 更加醒目 // 邀请按钮样式 - 扁平化
inviteButton: { inviteButton: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@@ -1348,25 +1337,23 @@ function createGroupInfoStyles(colors: AppColors) {
inviteButtonText: { inviteButtonText: {
fontWeight: '700', fontWeight: '700',
marginRight: spacing.xs, marginRight: spacing.xs,
letterSpacing: 0.3,
}, },
// 设置项样式 - 更加扁平 // 设置项样式 - 扁平化:无圆角背景,简洁分割线
settingsList: { settingsList: {
marginTop: spacing.sm, marginTop: 0,
}, },
settingItem: { settingItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingVertical: spacing.md, paddingVertical: spacing.md,
borderRadius: borderRadius.md, paddingHorizontal: 0,
paddingHorizontal: spacing.sm, marginLeft: 0,
marginLeft: -spacing.sm, marginRight: 0,
marginRight: -spacing.sm,
}, },
settingIconContainer: { settingIconContainer: {
width: 40, width: 36,
height: 40, height: 36,
borderRadius: borderRadius.md, borderRadius: borderRadius.md,
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
justifyContent: 'center', justifyContent: 'center',
@@ -1384,18 +1371,16 @@ function createGroupInfoStyles(colors: AppColors) {
fontWeight: '600', fontWeight: '600',
}, },
settingDivider: { settingDivider: {
marginLeft: 56, marginLeft: 52,
width: 'auto', width: 'auto',
marginVertical: spacing.xs, marginVertical: 0,
opacity: 0.5, opacity: 0.5,
}, },
// 危险操作卡片 - 更加醒目 // 危险操作区域 - 扁平化
dangerCard: { dangerCard: {
padding: 0, padding: 0,
overflow: 'hidden', overflow: 'hidden',
borderTopWidth: 1,
borderTopColor: colors.error.light + '30',
marginTop: spacing.sm, marginTop: spacing.sm,
}, },
dangerButton: { dangerButton: {
@@ -1404,14 +1389,12 @@ function createGroupInfoStyles(colors: AppColors) {
justifyContent: 'center', justifyContent: 'center',
paddingVertical: spacing.lg, paddingVertical: spacing.lg,
gap: spacing.sm, gap: spacing.sm,
backgroundColor: colors.error.light + '08',
}, },
dangerButtonText: { dangerButtonText: {
fontWeight: '700', fontWeight: '600',
letterSpacing: 0.3,
}, },
// 模态框样式 - 更现代 // 模态框样式 - 扁平化
modalOverlay: { modalOverlay: {
flex: 1, flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.45)', backgroundColor: 'rgba(0, 0, 0, 0.45)',
@@ -1440,20 +1423,18 @@ function createGroupInfoStyles(colors: AppColors) {
minWidth: 60, minWidth: 60,
}, },
modalTitle: { modalTitle: {
fontWeight: '800', fontWeight: '700',
fontSize: fontSizes.xl + 1, fontSize: fontSizes.xl,
letterSpacing: 0.5,
}, },
saveButton: { saveButton: {
fontWeight: '700', fontWeight: '600',
letterSpacing: 0.3,
}, },
confirmButton: { confirmButton: {
fontWeight: '700', fontWeight: '600',
textAlign: 'right', textAlign: 'right',
}, },
// 编辑表单样式 - 更现代 // 编辑表单样式 - 扁平化
editForm: { editForm: {
alignItems: 'center', alignItems: 'center',
}, },
@@ -1465,7 +1446,7 @@ function createGroupInfoStyles(colors: AppColors) {
width: 90, width: 90,
height: 90, height: 90,
borderRadius: 45, borderRadius: 45,
borderWidth: 3, borderWidth: 2,
borderColor: colors.primary.light + '40', borderColor: colors.primary.light + '40',
}, },
editAvatarPlaceholder: { editAvatarPlaceholder: {
@@ -1496,9 +1477,8 @@ function createGroupInfoStyles(colors: AppColors) {
}, },
editLabel: { editLabel: {
marginBottom: spacing.sm, marginBottom: spacing.sm,
fontWeight: '700', fontWeight: '600',
fontSize: fontSizes.sm + 1, fontSize: fontSizes.sm,
letterSpacing: 0.3,
}, },
editInput: { editInput: {
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
@@ -1507,7 +1487,7 @@ function createGroupInfoStyles(colors: AppColors) {
paddingVertical: spacing.md, paddingVertical: spacing.md,
fontSize: fontSizes.md, fontSize: fontSizes.md,
color: colors.text.primary, color: colors.text.primary,
borderWidth: 1.5, borderWidth: 1,
borderColor: colors.divider + '80', borderColor: colors.divider + '80',
fontWeight: '400', fontWeight: '400',
}, },
@@ -1533,7 +1513,7 @@ function createGroupInfoStyles(colors: AppColors) {
padding: spacing.md, padding: spacing.md,
fontSize: fontSizes.md, fontSize: fontSizes.md,
color: colors.text.primary, color: colors.text.primary,
borderWidth: 1.5, borderWidth: 1,
borderColor: colors.divider + '80', borderColor: colors.divider + '80',
lineHeight: 22, lineHeight: 22,
textAlignVertical: 'top', textAlignVertical: 'top',
@@ -1553,14 +1533,14 @@ function createGroupInfoStyles(colors: AppColors) {
alignItems: 'center', alignItems: 'center',
padding: spacing.md, padding: spacing.md,
borderRadius: borderRadius.lg, borderRadius: borderRadius.lg,
borderWidth: 1.5, borderWidth: 1,
borderColor: colors.divider + '80', borderColor: colors.divider + '80',
backgroundColor: colors.background.default, backgroundColor: colors.background.default,
}, },
joinTypeItemSelected: { joinTypeItemSelected: {
borderColor: colors.primary.main, borderColor: colors.primary.main,
backgroundColor: colors.primary.light + '12', backgroundColor: colors.primary.light + '12',
borderWidth: 2, borderWidth: 1.5,
}, },
joinTypeIcon: { joinTypeIcon: {
width: 48, width: 48,
@@ -1602,7 +1582,7 @@ function createGroupInfoStyles(colors: AppColors) {
}, },
transferItemSelected: { transferItemSelected: {
backgroundColor: colors.primary.light + '12', backgroundColor: colors.primary.light + '12',
borderWidth: 1.5, borderWidth: 1,
borderColor: colors.primary.light + '60', borderColor: colors.primary.light + '60',
}, },
transferItemInfo: { transferItemInfo: {
@@ -1681,7 +1661,7 @@ function createGroupInfoStyles(colors: AppColors) {
}, },
friendItemSelected: { friendItemSelected: {
backgroundColor: colors.primary.light + '12', backgroundColor: colors.primary.light + '12',
borderWidth: 1.5, borderWidth: 1,
borderColor: colors.primary.light + '60', borderColor: colors.primary.light + '60',
}, },
friendInfo: { friendInfo: {

View File

@@ -16,7 +16,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter, useLocalSearchParams } from 'expo-router'; import { useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, borderRadius, shadows, useAppColors, type AppColors } from '../../theme'; import { spacing, borderRadius, shadows, fontSizes, useAppColors, type AppColors } from '../../theme';
import { useAuthStore } from '../../stores'; import { useAuthStore } from '../../stores';
import { authService } from '@/services/auth'; import { authService } from '@/services/auth';
import { messageService } from '@/services/message'; import { messageService } from '@/services/message';
@@ -336,7 +336,7 @@ const PrivateChatInfoScreen: React.FC = () => {
contentContainerStyle={styles.scrollContent} contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
{/* 用户基本信息卡片 */} {/* 用户基本信息 */}
<View style={styles.headerCard}> <View style={styles.headerCard}>
<TouchableOpacity <TouchableOpacity
style={styles.userHeader} style={styles.userHeader}
@@ -345,7 +345,7 @@ const PrivateChatInfoScreen: React.FC = () => {
> >
<Avatar <Avatar
source={displayUser.avatar} source={displayUser.avatar}
size={90} size={80}
name={displayUser.nickname || ''} name={displayUser.nickname || ''}
/> />
<View style={styles.userInfo}> <View style={styles.userInfo}>
@@ -421,20 +421,20 @@ function createPrivateChatInfoStyles(colors: AppColors) {
return StyleSheet.create({ return StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: colors.background.default, backgroundColor: colors.background.paper,
}, },
pageHeader: { pageHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingHorizontal: spacing.md, paddingHorizontal: spacing.lg,
paddingVertical: spacing.sm, paddingVertical: spacing.md,
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
borderBottomWidth: 1, borderBottomWidth: 0,
borderBottomColor: colors.divider,
}, },
pageTitle: { pageTitle: {
fontWeight: '600', fontWeight: '600',
fontSize: fontSizes.xl,
}, },
pageHeaderPlaceholder: { pageHeaderPlaceholder: {
width: 40, width: 40,
@@ -444,15 +444,13 @@ function createPrivateChatInfoStyles(colors: AppColors) {
flex: 1, flex: 1,
}, },
scrollContent: { scrollContent: {
paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl, paddingBottom: spacing.xl,
}, },
headerCard: { headerCard: {
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
marginHorizontal: spacing.md, paddingVertical: spacing.lg,
marginTop: spacing.md, marginBottom: spacing.md,
borderRadius: borderRadius.lg,
padding: spacing.lg,
...shadows.sm,
}, },
userHeader: { userHeader: {
flexDirection: 'row', flexDirection: 'row',
@@ -460,58 +458,61 @@ function createPrivateChatInfoStyles(colors: AppColors) {
}, },
userInfo: { userInfo: {
flex: 1, flex: 1,
marginLeft: spacing.md, marginLeft: spacing.lg,
}, },
userName: { userName: {
fontWeight: '600', fontWeight: '700',
fontSize: fontSizes['2xl'],
marginBottom: spacing.xs, marginBottom: spacing.xs,
}, },
section: { section: {
marginTop: spacing.lg, marginTop: spacing.lg,
paddingHorizontal: spacing.md,
}, },
sectionTitle: { sectionTitle: {
marginBottom: spacing.sm, marginBottom: spacing.sm,
marginLeft: spacing.sm, marginLeft: spacing.sm,
fontWeight: '500', fontWeight: '500',
fontSize: fontSizes.sm,
color: colors.text.secondary,
}, },
card: { card: {
backgroundColor: colors.background.paper, backgroundColor: colors.background.paper,
borderRadius: borderRadius.lg,
...shadows.sm,
overflow: 'hidden', overflow: 'hidden',
}, },
settingItem: { settingItem: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingVertical: spacing.sm, paddingVertical: spacing.md,
paddingHorizontal: spacing.md, paddingHorizontal: 0,
}, },
settingIconContainer: { settingIconContainer: {
width: 36, width: 36,
height: 36, height: 36,
borderRadius: borderRadius.md, borderRadius: borderRadius.md,
backgroundColor: colors.primary.light + '20', backgroundColor: colors.background.default,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: spacing.md, marginRight: spacing.md,
}, },
settingIconDanger: { settingIconDanger: {
backgroundColor: colors.error.light + '20', backgroundColor: colors.error.light + '15',
}, },
settingTitle: { settingTitle: {
flex: 1, flex: 1,
}, },
dangerText: { dangerText: {
color: colors.error.main, color: colors.error.main,
fontWeight: '600',
}, },
divider: { divider: {
marginLeft: spacing.xl + 36, marginLeft: 52,
width: 'auto', width: 'auto',
marginVertical: spacing.xs, marginVertical: 0,
}, },
deleteButton: { deleteButton: {
marginTop: spacing.sm, marginTop: spacing.sm,
borderRadius: borderRadius.lg,
height: 52,
}, },
}); });
} }

View File

@@ -5,7 +5,8 @@
*/ */
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, FlatList, ListRenderItem, Platform } from 'react-native'; import { View, TouchableOpacity, ActivityIndicator, Modal, Alert, Dimensions, StyleSheet, Platform } from 'react-native';
import { FlashList, ListRenderItem } from '@shopify/flash-list';
import { Image as ExpoImage } from 'expo-image'; import { Image as ExpoImage } from 'expo-image';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
@@ -72,8 +73,7 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
return StyleSheet.create({ return StyleSheet.create({
...baseStyles, ...baseStyles,
emojiContainer: { emojiContainer: {
flexDirection: 'row', flex: 1,
flexWrap: 'wrap',
padding: spacing.sm, padding: spacing.sm,
}, },
emojiItem: { emojiItem: {
@@ -160,26 +160,19 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
</View> </View>
), [styles.emojiItem, styles.emojiText, handleEmojiPress]); ), [styles.emojiItem, styles.emojiText, handleEmojiPress]);
// 渲染 Emoji 面板(使用 FlatList 虚拟化,只渲染可见行) // 渲染 Emoji 面板(使用 FlashList 虚拟化,只渲染可见行)
const renderEmojiPanel = () => ( const renderEmojiPanel = () => (
<FlatList <View style={styles.emojiContainer}>
key="emoji-flatlist" <FlashList
data={emojiRows} key="emoji-flatlist"
keyExtractor={(item) => item.id} data={emojiRows}
renderItem={renderEmojiRow} keyExtractor={(item) => item.id}
contentContainerStyle={styles.emojiContainer} renderItem={renderEmojiRow}
showsVerticalScrollIndicator={true} showsVerticalScrollIndicator={true}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
initialNumToRender={12} style={{ flex: 1 }}
maxToRenderPerBatch={12} />
windowSize={5} </View>
removeClippedSubviews={true}
getItemLayout={(_data, index) => ({
length: 60,
offset: 60 * index,
index,
})}
/>
); );
// 添加表情(从相册选择) // 添加表情(从相册选择)
@@ -347,20 +340,18 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
} }
return ( return (
<FlatList <View style={styles.stickerGrid}>
key="sticker-flatlist" <FlashList
data={stickerListData} key="sticker-flatlist"
numColumns={4} data={stickerListData}
keyExtractor={(item) => item.type === 'manage' ? 'manage-button' : item.sticker.id} numColumns={4}
renderItem={renderStickerListItem} keyExtractor={(item) => item.type === 'manage' ? 'manage-button' : item.sticker.id}
contentContainerStyle={styles.stickerGrid} renderItem={renderStickerListItem}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
initialNumToRender={16} style={{ flex: 1 }}
maxToRenderPerBatch={16} />
windowSize={7} </View>
removeClippedSubviews={true}
/>
); );
}; };
@@ -494,19 +485,17 @@ export const EmojiPanel: React.FC<EmojiPanelProps> = ({
<ActivityIndicator size="large" color={colors.primary.main} /> <ActivityIndicator size="large" color={colors.primary.main} />
</View> </View>
) : ( ) : (
<FlatList <View style={styles.manageStickerGrid}>
data={manageListData} <FlashList
numColumns={4} data={manageListData}
keyExtractor={(item) => item.type === 'add' ? 'manage-add-button' : item.sticker.id} numColumns={4}
renderItem={renderManageListItem} keyExtractor={(item) => item.type === 'add' ? 'manage-add-button' : item.sticker.id}
contentContainerStyle={styles.manageStickerGrid} renderItem={renderManageListItem}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
initialNumToRender={16} style={{ flex: 1 }}
maxToRenderPerBatch={16} />
windowSize={7} </View>
removeClippedSubviews={true}
/>
)} )}
{/* 底部操作栏(管理模式下显示) */} {/* 底部操作栏(管理模式下显示) */}

View File

@@ -427,7 +427,7 @@ const MessageBubbleInner: React.FC<MessageBubbleProps> = ({
<View style={[styles.messageContent, isMe ? styles.myMessageContentPanel : null]}> <View style={[styles.messageContent, isMe ? styles.myMessageContentPanel : null]}>
{/* 群聊模式:显示发送者昵称 */} {/* 群聊模式:显示发送者昵称 */}
{isGroupChat && senderInfo && ( {isGroupChat && senderInfo && (
<Text style={[styles.senderName, isMe && styles.mySenderName]}>{senderInfo.nickname}</Text> <Text style={[styles.senderName, isMe ? styles.mySenderName : {}]}>{senderInfo.nickname}</Text>
)} )}
{renderMessageContent()} {renderMessageContent()}

View File

@@ -711,8 +711,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
marginTop: spacing.xs, marginTop: spacing.xs,
}, },
stickerGrid: { stickerGrid: {
flexDirection: 'row', flex: 1,
flexWrap: 'wrap',
paddingHorizontal: spacing.md, paddingHorizontal: spacing.md,
paddingTop: spacing.md, paddingTop: spacing.md,
paddingBottom: spacing.xl, paddingBottom: spacing.xl,
@@ -1259,8 +1258,7 @@ export function createChatScreenStyles(colors: AppColors, dynamicStyles?: {
color: colors.chat.textSecondary, color: colors.chat.textSecondary,
}, },
manageStickerGrid: { manageStickerGrid: {
flexDirection: 'row', flex: 1,
flexWrap: 'wrap',
paddingHorizontal: spacing.md, paddingHorizontal: spacing.md,
paddingTop: spacing.md, paddingTop: spacing.md,
paddingBottom: spacing.xl, paddingBottom: spacing.xl,

View File

@@ -562,8 +562,17 @@ export const useChatScreen = (props?: ChatScreenProps) => {
const jumpToLatestMessages = useCallback(() => { const jumpToLatestMessages = useCallback(() => {
suppressAutoFollowRef.current = false; suppressAutoFollowRef.current = false;
isBrowsingHistoryRef.current = false; isBrowsingHistoryRef.current = false;
scrollToLatest(true, true, 'jump-latest-button'); // FlashList 在 inverted 模式下 scrollToOffset({ offset: 0 }) 可能无法真正滚到最底部,
}, [scrollToLatest]); // 因为列表内容高度变化后最小 offset 可能不是 0。先尝试滚到负值确保到底再补偿回 0。
isProgrammaticScrollRef.current = true;
flatListRef.current?.scrollToOffset({ offset: -99999, animated: false });
requestAnimationFrame(() => {
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
setTimeout(() => {
isProgrammaticScrollRef.current = false;
}, 220);
});
}, [flatListRef]);
const setBrowsingHistory = useCallback((browsing: boolean) => { const setBrowsingHistory = useCallback((browsing: boolean) => {
isBrowsingHistoryRef.current = browsing; isBrowsingHistoryRef.current = browsing;

View File

@@ -110,35 +110,48 @@ export const UserProfileScreen: React.FC<UserProfileScreenProps> = ({ mode, user
); );
}, [loading, activeTab, mode, isBlockedProfile]); }, [loading, activeTab, mode, isBlockedProfile]);
// 渲染用户信息头部(与 TabBar 分离,避免切换 tab 时重渲染)
const renderUserHeader = useMemo(() => {
if (!user) return null;
return (
<UserProfileHeader
user={user}
isCurrentUser={isCurrentUser}
isBlocked={isBlocked}
onFollow={handleFollow}
onSettings={handleSettings}
onEditProfile={handleEditProfile}
onMessage={handleMessage}
onBlock={handleBlock}
onFollowingPress={handleFollowingPress}
onFollowersPress={handleFollowersPress}
/>
);
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress]);
// 渲染 TabBar
const renderTabBar = useMemo(() => (
<View style={sharedStyles.tabBarContainer}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
/>
</View>
), [activeTab, setActiveTab, sharedStyles.tabBarContainer]);
// 渲染 FlashList 头部(用户信息 + TabBar // 渲染 FlashList 头部(用户信息 + TabBar
const renderListHeader = useCallback(() => { const renderListHeader = useCallback(() => {
if (!user) return null; if (!user) return null;
return ( return (
<> <>
<UserProfileHeader {renderUserHeader}
user={user} {renderTabBar}
isCurrentUser={isCurrentUser}
isBlocked={isBlocked}
onFollow={handleFollow}
onSettings={handleSettings}
onEditProfile={handleEditProfile}
onMessage={handleMessage}
onBlock={handleBlock}
onFollowingPress={handleFollowingPress}
onFollowersPress={handleFollowersPress}
/>
<View style={sharedStyles.tabBarContainer}>
<TabBar
tabs={TABS}
activeIndex={activeTab}
onTabChange={setActiveTab}
variant="modern"
icons={TAB_ICONS}
/>
</View>
</> </>
); );
}, [user, isCurrentUser, isBlocked, handleFollow, handleSettings, handleEditProfile, handleMessage, handleBlock, handleFollowingPress, handleFollowersPress, activeTab, setActiveTab]); }, [user, renderUserHeader, renderTabBar]);
// 未登录/用户不存在状态 // 未登录/用户不存在状态
if (mode === 'self' && !currentUser) { if (mode === 'self' && !currentUser) {

View File

@@ -251,12 +251,15 @@ class PostService {
page_size: params.page_size, page_size: params.page_size,
}; };
// 将 post_type 映射为后端的 tab 参数 if (params.post_type) {
if (params.post_type) { requestParams.tab = params.post_type;
requestParams.tab = params.post_type; }
}
if (params.channel_id) {
const response = await api.get<any>('/posts', requestParams); requestParams.channel_id = params.channel_id;
}
const response = await api.get<any>('/posts', requestParams);
const data = response.data; const data = response.data;

View File

@@ -62,6 +62,7 @@ export class NetworkCursorPostListPagedSource implements IPostListPagedSource {
cursor: this.nextCursor ?? '', cursor: this.nextCursor ?? '',
page_size: this.pageSize, page_size: this.pageSize,
post_type: this.tab, post_type: this.tab,
channel_id: this.channelId,
}; };
const result = await postService.getPostsCursor(params); const result = await postService.getPostsCursor(params);

View File

@@ -520,14 +520,11 @@ export type DeviceType = 'ios' | 'android' | 'web';
* 游标分页请求参数 * 游标分页请求参数
*/ */
export interface CursorPaginationRequest { export interface CursorPaginationRequest {
/** 游标字符串(可选,首次请求不传) */
cursor?: string; cursor?: string;
/** 分页方向forward 或 backward默认 forward */
direction?: 'forward' | 'backward'; direction?: 'forward' | 'backward';
/** 每页数量(默认 20最大 100 */
page_size?: number; page_size?: number;
/** 帖子类型筛选可选follow, hot, latest */
post_type?: 'follow' | 'hot' | 'latest'; post_type?: 'follow' | 'hot' | 'latest';
channel_id?: string;
} }
/** /**