Migrate from flat file organization to modular directory structure under src/stores/: - auth/: authStore, registerStore, verificationStore, sessionStore - call/: callStore - settings/: chatSettingsStore, themeStore - ui/: homeTabBarVisibilityStore, homeTabPressStore - utils/: routePayloadCache - group/sources.ts, group/profileResolver.ts - message/sources.ts - post/sources.ts Update all import paths across components and screens to use new module paths. Maintain backward compatibility through deprecated re-export files for gradual migration.
785 lines
26 KiB
TypeScript
785 lines
26 KiB
TypeScript
/**
|
||
* 通知页 NotificationsScreen(扁平化风格)
|
||
* 胡萝卜BBS - 系统消息列表
|
||
* 【游标分页】使用 messageService.getSystemMessagesCursor
|
||
* 支持响应式布局
|
||
*
|
||
* 设计风格:
|
||
* - 纯白背景,扁平化设计
|
||
* - 简洁的标题区域
|
||
* - 分段式筛选标签
|
||
* - 与登录、注册、设置页面风格一致
|
||
*/
|
||
|
||
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||
import {
|
||
View,
|
||
FlatList,
|
||
StyleSheet,
|
||
TouchableOpacity,
|
||
RefreshControl,
|
||
ActivityIndicator,
|
||
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';
|
||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||
import { spacing, borderRadius, useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
|
||
import { SystemMessageResponse } from '../../types/dto';
|
||
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']);
|
||
|
||
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;
|
||
|
||
// 修复竞态条件:使用本地状态管理窗口尺寸,避免 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();
|
||
}, []);
|
||
|
||
// 屏幕失去焦点时,如果有 onBack 回调则调用它(用于内嵌模式)
|
||
useEffect(() => {
|
||
if (!isFocused && onBack) {
|
||
onBack();
|
||
}
|
||
}, [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 设为帖子ID(like_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>
|
||
);
|
||
};
|
||
|
||
// 渲染头部(包含返回按钮)- 扁平化风格
|
||
const renderHeader = () => {
|
||
// 大屏模式(>= lg 断点)隐藏返回按钮
|
||
const shouldShowBackButton = onBack && !isWideScreen;
|
||
|
||
return (
|
||
<View style={[styles.header, isWideScreen ? styles.headerWide : null]}>
|
||
{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>
|
||
) : (
|
||
<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>
|
||
<View style={styles.backButtonPlaceholder} />
|
||
</View>
|
||
);
|
||
};
|
||
|
||
// 渲染空状态 - 扁平化风格
|
||
const renderEmpty = () => (
|
||
<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>
|
||
</View>
|
||
);
|
||
|
||
// 如果还未hydrated,显示加载占位符,避免竞态条件导致的渲染问题
|
||
if (!isHydrated) {
|
||
return (
|
||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
|
||
<View style={styles.loadingContainer}>
|
||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||
</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}
|
||
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}
|
||
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',
|
||
},
|
||
});
|
||
}
|