Files
frontend/src/screens/message/NotificationsScreen.tsx

785 lines
26 KiB
TypeScript
Raw Normal View History

/**
* NotificationsScreen
* BBS -
* 使 messageService.getSystemMessagesCursor
*
*
*
* -
* -
* -
* -
*/
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import {
View,
FlatList,
StyleSheet,
TouchableOpacity,
RefreshControl,
ActivityIndicator,
2026-03-17 12:29:04 +08:00
Dimensions,
BackHandler,
Animated,
StatusBar,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useIsFocused } from '@react-navigation/native';
import { useRouter } from 'expo-router';
2026-03-16 17:47:10 +08:00
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { spacing, borderRadius, useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
import { SystemMessageResponse } from '../../types/dto';
2026-04-13 01:30:37 +08:00
import { messageService } from '@/services/message';
import { commentService } from '@/services/post';
import { SystemMessageItem } from '../../components/business';
import { Text, ResponsiveContainer } from '../../components/common';
import { useCursorPagination } from '../../hooks/useCursorPagination';
import * as hrefs from '../../navigation/hrefs';
import { useMessageManagerSystemUnreadCount } from '../../stores';
import { messageManager } from '../../stores/message';
const MESSAGE_TYPES = [
{ key: 'all', title: '全部' },
{ key: 'like_post', title: '点赞' },
{ key: 'comment', title: '评论' },
{ key: 'follow', title: '关注' },
{ key: 'group', title: '群通知' },
{ key: 'system', title: '系统' },
{ key: 'announcement', title: '公告' },
];
const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'favorite_post']);
const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']);
2026-03-16 17:47:10 +08:00
export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => {
const colors = useAppColors();
const resolved = useResolvedColorScheme();
const styles = useMemo(() => createNotificationsStyles(colors), [colors]);
const isFocused = useIsFocused();
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
const router = useRouter();
// 动画值
const fadeAnim = useRef(new Animated.Value(0)).current;
const slideAnim = useRef(new Animated.Value(20)).current;
2026-03-17 12:29:04 +08:00
// 修复竞态条件:使用本地状态管理窗口尺寸,避免 hydration 问题
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
const [isHydrated, setIsHydrated] = useState(false);
useEffect(() => {
// 延迟设置窗口尺寸,确保在客户端完全加载后再获取
const timer = setTimeout(() => {
const { width, height } = Dimensions.get('window');
setWindowSize({ width, height });
setIsHydrated(true);
}, 100);
return () => clearTimeout(timer);
}, []);
// 使用本地尺寸状态计算断点 (lg = 768)
const isWideScreen = windowSize.width >= 768;
const isDesktop = windowSize.width >= 1024;
const isTablet = windowSize.width >= 768 && windowSize.width < 1024;
// Web端使用更大的容器宽度
const containerMaxWidth = isDesktop ? 1200 : isTablet ? 1000 : 900;
const [activeType, setActiveType] = useState('all');
const [unreadCount, setUnreadCount] = useState(0);
// 【游标分页】使用 useCursorPagination hook 获取系统消息列表
// 使用 useCallback 包裹 fetchFunction避免无限重新创建导致循环
const fetchSystemMessages = useCallback(
async ({ cursor, pageSize }: { cursor?: string; pageSize: number }) => {
return await messageService.getSystemMessagesCursor({
cursor,
page_size: pageSize,
});
},
[]
);
const {
list: messages,
isLoading,
isRefreshing,
hasMore,
loadMore,
refresh,
error: paginationError,
} = useCursorPagination(fetchSystemMessages, { pageSize: 20 });
// 本地刷新状态仅用于下拉刷新的UI显示
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
// 同一 flag 只要有人审批过,就将待处理消息同步展示为已处理状态
const displayMessages = useMemo(() => {
const reviewedByFlag = new Map<string, SystemMessageResponse>();
messages.forEach((msg) => {
if (msg.system_type !== 'group_join_apply') return;
const flag = msg.extra_data?.flag;
const status = msg.extra_data?.request_status;
if (!flag) return;
if (status === 'accepted' || status === 'rejected') {
reviewedByFlag.set(flag, msg);
}
});
if (reviewedByFlag.size === 0) {
return messages;
}
return messages.map((msg) => {
if (msg.system_type !== 'group_join_apply') return msg;
const flag = msg.extra_data?.flag;
if (!flag) return msg;
const reviewedMsg = reviewedByFlag.get(flag);
if (!reviewedMsg) return msg;
const reviewedStatus = reviewedMsg.extra_data?.request_status;
if (reviewedStatus !== 'accepted' && reviewedStatus !== 'rejected') return msg;
return {
...msg,
extra_data: {
...msg.extra_data,
request_status: reviewedStatus,
actor_name: reviewedMsg.extra_data?.actor_name || reviewedMsg.extra_data?.operator_name || msg.extra_data?.actor_name,
avatar_url: reviewedMsg.extra_data?.avatar_url || reviewedMsg.extra_data?.operator_avatar || msg.extra_data?.avatar_url,
},
};
});
}, [messages]);
// 获取未读数
const fetchUnreadCount = useCallback(async () => {
try {
const response = await messageService.getSystemUnreadCount();
setUnreadCount(response.unread_count);
} catch (error) {
console.error('获取未读数失败:', error);
}
}, []);
// 一键已读
const handleMarkAllRead = useCallback(async () => {
try {
await messageService.markAllSystemMessagesRead();
// 刷新消息列表
await refresh();
setUnreadCount(0);
setSystemUnreadCount(0);
// 同步更新全局 TabBar 红点
messageManager.fetchUnreadCount();
} catch (error) {
console.error('一键已读失败:', error);
}
}, [refresh, setSystemUnreadCount]);
// 页面加载和获得焦点时刷新,并自动标记所有消息为已读
// 使用 ref 防止重复执行,避免无限循环
const hasInitializedRef = useRef(false);
useEffect(() => {
if (isFocused && !hasInitializedRef.current) {
hasInitializedRef.current = true;
fetchUnreadCount();
// 进入界面自动标记所有消息为已读
handleMarkAllRead();
}
}, [isFocused]); // 只依赖 isFocused函数使用 ref 稳定引用
// 启动入场动画
useEffect(() => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.timing(slideAnim, {
toValue: 0,
duration: 400,
useNativeDriver: true,
}),
]).start();
}, []);
2026-03-16 17:47:10 +08:00
// 屏幕失去焦点时,如果有 onBack 回调则调用它(用于内嵌模式)
useEffect(() => {
2026-03-16 17:47:10 +08:00
if (!isFocused && onBack) {
onBack();
}
2026-03-16 17:47:10 +08:00
}, [isFocused, onBack]);
// 处理 Android 物理返回键 - 当 onBack 存在时(内嵌模式),拦截返回键
useEffect(() => {
if (!onBack) return;
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
if (isFocused) {
onBack();
return true; // 阻止默认行为
}
return false;
});
return () => backHandler.remove();
}, [isFocused, onBack]);
// 筛选消息
const filteredMessages = activeType === 'all'
? displayMessages
: displayMessages.filter((m) => {
if (activeType === 'like_post') {
return LIKE_SYSTEM_TYPES.has(m.system_type);
}
if (activeType === 'group') {
return GROUP_SYSTEM_TYPES.has(m.system_type);
}
return m.system_type === activeType;
});
// 下拉刷新
const onRefresh = useCallback(async () => {
setRefreshing(true);
await Promise.all([refresh(), fetchUnreadCount()]);
setRefreshing(false);
}, [refresh, fetchUnreadCount]);
// 加载更多
const onEndReached = useCallback(async () => {
if (isLoading || loadingMore || !hasMore) return;
setLoadingMore(true);
await loadMore();
setLoadingMore(false);
}, [isLoading, loadingMore, hasMore, loadMore]);
// 标记单条消息已读并处理导航
const extractPostIdFromActionUrl = (actionUrl?: string): string | null => {
if (!actionUrl) return null;
const postPathMatch = actionUrl.match(/\/posts\/([^/?#]+)/);
if (postPathMatch?.[1]) {
return decodeURIComponent(postPathMatch[1]);
}
const query = actionUrl.split('?')[1];
if (!query) return null;
const params = new URLSearchParams(query);
return params.get('post') || params.get('post_id');
};
const resolvePostId = async (message: SystemMessageResponse): Promise<string | null> => {
const { extra_data, system_type } = message;
// 后端已统一将 target_id 设为帖子IDlike_post/comment/mention/favorite_post/like_reply/like_comment
if (
extra_data?.target_id &&
['like_post', 'like_comment', 'like_reply', 'comment', 'mention', 'favorite_post'].includes(system_type)
) {
return extra_data.target_id;
}
// reply 通知target_id 是 replyID从 action_url 解析帖子ID
const postIdFromUrl = extractPostIdFromActionUrl(extra_data?.action_url);
if (postIdFromUrl) {
return postIdFromUrl;
}
// 兜底:通过评论详情反查(仅旧数据兜底)
if (extra_data?.target_id && system_type === 'reply') {
const comment = await commentService.getComment(extra_data.target_id);
if (comment?.post_id) {
return comment.post_id;
}
}
return null;
};
const handleMessagePress = async (message: SystemMessageResponse) => {
try {
const messageId = String(message.id);
const wasUnread = message.is_read !== true;
await messageService.markSystemMessageRead(messageId);
// 【游标分页】不再直接修改 messages 状态,而是通过刷新获取最新数据
if (wasUnread) {
setUnreadCount(prev => Math.max(0, prev - 1));
decrementSystemUnreadCount(1);
}
// 更新本地未读数以及全局 TabBar 红点
fetchUnreadCount();
messageManager.fetchUnreadCount();
// 根据消息类型处理导航
const { system_type, extra_data } = message;
if (
['like_post', 'comment', 'mention', 'reply', 'like_comment', 'like_reply', 'favorite_post'].includes(system_type)
) {
const postId = await resolvePostId(message);
if (postId) {
router.push(hrefs.hrefPostDetail(postId));
}
} else if (system_type === 'follow') {
// 关注 - 跳转到用户主页
if (extra_data?.actor_id_str) {
router.push(hrefs.hrefUserProfile(extra_data.actor_id_str));
}
} else if (system_type === 'group_join_apply') {
router.push(hrefs.hrefGroupRequestDetail(message));
} else if (system_type === 'group_invite') {
router.push(hrefs.hrefGroupInviteDetail(message));
}
// 其他类型暂不处理跳转
} catch (error) {
console.error('标记已读失败:', error);
}
};
// 处理头像点击 - 跳转到用户主页
const handleAvatarPress = (message: SystemMessageResponse) => {
const { extra_data } = message;
// 优先使用 actor_id_str (UUID格式),兼容 actor_id (数字格式)
const actorId = extra_data?.actor_id_str || (extra_data?.actor_id ? String(extra_data.actor_id) : null);
if (actorId) {
router.push(hrefs.hrefUserProfile(actorId));
}
};
// 渲染消息项
const renderMessage = ({ item }: { item: SystemMessageResponse }) => (
<SystemMessageItem
message={item}
onPress={() => handleMessagePress(item)}
onAvatarPress={() => handleAvatarPress(item)}
/>
);
// 渲染底部加载指示器
const renderFooter = () => {
if (!loadingMore) return null;
return (
<View style={styles.loadingMore}>
<ActivityIndicator size="small" color={colors.primary.main} />
<Text variant="caption" color={colors.text.secondary} style={styles.loadingMoreText}>
...
</Text>
</View>
);
};
// 渲染头部(包含返回按钮)- 扁平化风格
2026-03-16 17:47:10 +08:00
const renderHeader = () => {
// 大屏模式(>= lg 断点)隐藏返回按钮
const shouldShowBackButton = onBack && !isWideScreen;
return (
2026-03-17 12:29:04 +08:00
<View style={[styles.header, isWideScreen ? styles.headerWide : null]}>
2026-03-16 17:47:10 +08:00
{shouldShowBackButton ? (
<TouchableOpacity style={styles.backButton} onPress={onBack} activeOpacity={0.8}>
<View style={styles.backIconButton}>
<MaterialCommunityIcons name="arrow-left" size={22} color={colors.text.primary} />
</View>
</TouchableOpacity>
2026-03-16 17:47:10 +08:00
) : (
<View style={styles.backButtonPlaceholder} />
)}
<View style={styles.headerTitleContainer}>
<Text style={[styles.headerTitle, ...(isWideScreen ? [styles.headerTitleWide] : [])]}>
</Text>
{unreadCount > 0 && (
<View style={styles.unreadBadge}>
<Text style={styles.unreadBadgeText}>{unreadCount > 99 ? '99+' : unreadCount}</Text>
</View>
)}
</View>
2026-03-16 17:47:10 +08:00
<View style={styles.backButtonPlaceholder} />
</View>
);
};
// 渲染空状态 - 扁平化风格
const renderEmpty = () => (
2026-03-17 12:29:04 +08:00
<View style={[styles.emptyContainer, isWideScreen && styles.emptyContainerWeb]}>
<View style={styles.emptyIconContainer}>
<MaterialCommunityIcons name="bell-outline" size={64} color={colors.text.hint} />
</View>
<Text style={styles.emptyTitle}></Text>
<Text style={styles.emptyDescription}></Text>
2026-03-17 12:29:04 +08:00
</View>
);
2026-03-17 12:29:04 +08:00
// 如果还未hydrated显示加载占位符避免竞态条件导致的渲染问题
if (!isHydrated) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
2026-03-17 12:29:04 +08:00
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
2026-03-17 12:29:04 +08:00
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
<Animated.View
style={[
styles.content,
{
opacity: fadeAnim,
transform: [{ translateY: slideAnim }],
},
]}
>
{isWideScreen ? (
<ResponsiveContainer maxWidth={containerMaxWidth}>
{/* 头部 - 大屏模式下隐藏返回按钮 */}
{renderHeader()}
{/* 分类筛选 */}
<View style={[styles.filterContainer, isWideScreen && styles.filterContainerWideWeb]}>
{MESSAGE_TYPES.map(type => {
const count = type.key === 'all'
? displayMessages.length
: displayMessages.filter((m) => {
if (type.key === 'like_post') {
return LIKE_SYSTEM_TYPES.has(m.system_type);
}
if (type.key === 'group') {
return GROUP_SYSTEM_TYPES.has(m.system_type);
}
return m.system_type === type.key;
}).length;
const isActive = activeType === type.key;
return (
<TouchableOpacity
key={type.key}
style={[
styles.filterTag,
isActive && styles.filterTagActive,
isWideScreen && styles.filterTagWide,
]}
onPress={() => setActiveType(type.key)}
activeOpacity={0.8}
>
<Text
variant="body"
color={isActive ? colors.text.inverse : colors.text.secondary}
style={isActive ? styles.filterTagTextActive : undefined}
>
{type.title}
</Text>
{count > 0 && (
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
<Text
variant="caption"
color={isActive ? colors.text.inverse : colors.text.hint}
>
{count}
</Text>
</View>
)}
</TouchableOpacity>
);
})}
</View>
{/* 消息列表 */}
{isLoading && messages.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
) : (
<FlatList
data={filteredMessages}
renderItem={renderMessage}
keyExtractor={item => item.id.toString()}
contentContainerStyle={[styles.listContent, isWideScreen && styles.listContentWide]}
showsVerticalScrollIndicator={false}
ListEmptyComponent={renderEmpty}
ListFooterComponent={renderFooter}
onEndReached={onEndReached}
onEndReachedThreshold={0.3}
<think>Let me analyze the staged changes to generate a proper conventional commit message. Looking at the changes: 1. **New files**: - `app/(app)/(tabs)/profile/account-deletion.tsx` - account deletion screen - `app/(app)/(tabs)/profile/privacy-settings.tsx` - privacy settings screen - `src/screens/profile/AccountDeletionScreen.tsx` - account deletion implementation - `src/screens/profile/PrivacySettingsScreen.tsx` - privacy settings implementation 2. **Modified files**: - Database changes: `LocalDataSource.ts`, `DatabaseManager.ts`, `DatabaseConfig.ts`, `MigrationManager.ts` - refactoring database layer (removing LockManager, simplified initialization, added retry logic) - Auth store: `authStore.ts` - changed `initDatabase` to `switchDatabase` - Auth service: `authService.ts` - added privacy settings and account deletion methods - DTOs: `dto.ts` - added PrivacySettingsDTO, DeletionStatusDTO, VoteOptionDTO, VoteResultDTO types - Navigation: `hrefs.ts` - added new href functions for privacy settings and account deletion - Profile screens: `SettingsScreen.tsx`, `index.ts`, `_layout.tsx` - added new menu items and routes - Various message screens - added keyboard handling props - `app/_layout.tsx` - added CSS for mobile touch handling - `CreatePostScreen.tsx` - fixed vote_options format - `PostDetailScreen.tsx` - added type annotations for vote handling - `MediaCacheManager.ts` - updated to use new expo-file-system API - `VerificationSettingsScreen.tsx` - fixed type safety - `AccountSecurityScreen.tsx` - styling refactoring This is a multi-faceted change involving: Database refactoring, new privacy/account deletion features, bug fixes, and UI improvements. The database changes appear to be refactoring without new functionality. The most significant changes are the privacy settings and account deletion features, plus bug fixes like the vote options format and mobile touch handling. </think> feat(profile): add privacy settings and account deletion screens Add privacy settings and account deletion functionality with new screens and APIs. Refactor database initialization to use switchDatabase for user switching. Fix vote options format in post creation and add type safety to vote handling. Improve mobile touch handling in layout and message screens. Update media cache to use new expo-file-system API. BREAKING CHANGE: Database initialization now uses switchDatabase instead of initDatabase for user switching
2026-04-08 16:48:19 +08:00
scrollEnabled={true}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
/>
)}
</ResponsiveContainer>
) : (
<>
{/* 头部 - 小屏模式下显示返回按钮 */}
{renderHeader()}
{/* 分类筛选 */}
<View style={styles.filterContainer}>
{MESSAGE_TYPES.map(type => {
const count = type.key === 'all'
? displayMessages.length
: displayMessages.filter((m) => {
if (type.key === 'like_post') {
return LIKE_SYSTEM_TYPES.has(m.system_type);
}
if (type.key === 'group') {
return GROUP_SYSTEM_TYPES.has(m.system_type);
}
return m.system_type === type.key;
}).length;
const isActive = activeType === type.key;
return (
<TouchableOpacity
key={type.key}
style={[
styles.filterTag,
isActive && styles.filterTagActive,
]}
onPress={() => setActiveType(type.key)}
activeOpacity={0.8}
>
<Text
variant="body"
color={isActive ? colors.text.inverse : colors.text.secondary}
style={isActive ? styles.filterTagTextActive : undefined}
>
{type.title}
</Text>
{count > 0 && (
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
<Text
variant="caption"
color={isActive ? colors.text.inverse : colors.text.hint}
>
{count}
</Text>
</View>
)}
</TouchableOpacity>
);
})}
</View>
{/* 消息列表 */}
{isLoading && messages.length === 0 ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
) : (
<FlatList
data={filteredMessages}
renderItem={renderMessage}
keyExtractor={item => item.id.toString()}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ListEmptyComponent={renderEmpty}
ListFooterComponent={renderFooter}
onEndReached={onEndReached}
onEndReachedThreshold={0.3}
<think>Let me analyze the staged changes to generate a proper conventional commit message. Looking at the changes: 1. **New files**: - `app/(app)/(tabs)/profile/account-deletion.tsx` - account deletion screen - `app/(app)/(tabs)/profile/privacy-settings.tsx` - privacy settings screen - `src/screens/profile/AccountDeletionScreen.tsx` - account deletion implementation - `src/screens/profile/PrivacySettingsScreen.tsx` - privacy settings implementation 2. **Modified files**: - Database changes: `LocalDataSource.ts`, `DatabaseManager.ts`, `DatabaseConfig.ts`, `MigrationManager.ts` - refactoring database layer (removing LockManager, simplified initialization, added retry logic) - Auth store: `authStore.ts` - changed `initDatabase` to `switchDatabase` - Auth service: `authService.ts` - added privacy settings and account deletion methods - DTOs: `dto.ts` - added PrivacySettingsDTO, DeletionStatusDTO, VoteOptionDTO, VoteResultDTO types - Navigation: `hrefs.ts` - added new href functions for privacy settings and account deletion - Profile screens: `SettingsScreen.tsx`, `index.ts`, `_layout.tsx` - added new menu items and routes - Various message screens - added keyboard handling props - `app/_layout.tsx` - added CSS for mobile touch handling - `CreatePostScreen.tsx` - fixed vote_options format - `PostDetailScreen.tsx` - added type annotations for vote handling - `MediaCacheManager.ts` - updated to use new expo-file-system API - `VerificationSettingsScreen.tsx` - fixed type safety - `AccountSecurityScreen.tsx` - styling refactoring This is a multi-faceted change involving: Database refactoring, new privacy/account deletion features, bug fixes, and UI improvements. The database changes appear to be refactoring without new functionality. The most significant changes are the privacy settings and account deletion features, plus bug fixes like the vote options format and mobile touch handling. </think> feat(profile): add privacy settings and account deletion screens Add privacy settings and account deletion functionality with new screens and APIs. Refactor database initialization to use switchDatabase for user switching. Fix vote options format in post creation and add type safety to vote handling. Improve mobile touch handling in layout and message screens. Update media cache to use new expo-file-system API. BREAKING CHANGE: Database initialization now uses switchDatabase instead of initDatabase for user switching
2026-04-08 16:48:19 +08:00
scrollEnabled={true}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[colors.primary.main]}
tintColor={colors.primary.main}
/>
}
/>
)}
</>
)}
</Animated.View>
</SafeAreaView>
);
};
function createNotificationsStyles(colors: AppColors) {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background.paper,
},
content: {
flex: 1,
},
// Header 样式 - 扁平化风格
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 16,
backgroundColor: colors.background.paper,
},
headerWide: {
paddingHorizontal: 32,
paddingVertical: 20,
},
headerTitleContainer: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
color: colors.text.primary,
},
headerTitleWide: {
fontSize: 20,
},
unreadBadge: {
backgroundColor: colors.primary.main,
borderRadius: 10,
paddingHorizontal: 6,
paddingVertical: 2,
marginLeft: spacing.sm,
minWidth: 20,
alignItems: 'center',
justifyContent: 'center',
},
unreadBadgeText: {
color: colors.text.inverse,
fontSize: 11,
fontWeight: '600',
},
backButton: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
backIconButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
},
backButtonPlaceholder: {
width: 40,
},
// 分类筛选 - 扁平化风格(分段式设计)
filterContainer: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: colors.background.paper,
borderBottomWidth: 1,
borderBottomColor: colors.divider || '#E5E5EA',
},
filterContainerWideWeb: {
paddingHorizontal: 32,
paddingVertical: 16,
justifyContent: 'center',
},
filterTag: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 8,
marginRight: 8,
borderRadius: 14,
backgroundColor: colors.background.default,
borderWidth: 1,
borderColor: 'transparent',
},
filterTagWide: {
paddingHorizontal: 18,
paddingVertical: 10,
marginRight: 10,
},
filterTagActive: {
backgroundColor: colors.primary.main,
borderColor: colors.primary.main,
},
filterTagTextActive: {
fontWeight: '600',
},
filterCount: {
marginLeft: 6,
backgroundColor: colors.primary.light + '40',
paddingHorizontal: 6,
paddingVertical: 1,
borderRadius: 8,
minWidth: 18,
alignItems: 'center',
},
filterCountActive: {
backgroundColor: 'rgba(255, 255, 255, 0.25)',
},
listContent: {
flexGrow: 1,
paddingTop: 8,
paddingBottom: 24,
},
listContentWide: {
paddingHorizontal: 32,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 80,
},
loadingMore: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 20,
},
loadingMoreText: {
marginLeft: 8,
},
// 空状态 - 扁平化风格
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: 80,
paddingHorizontal: 40,
},
emptyContainerWeb: {
minHeight: 500,
justifyContent: 'center',
paddingTop: 100,
},
emptyIconContainer: {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: colors.background.default,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,
},
emptyTitle: {
fontSize: 20,
fontWeight: '600',
color: colors.text.primary,
marginBottom: 8,
},
emptyDescription: {
fontSize: 15,
color: colors.text.secondary,
textAlign: 'center',
},
});
}