2026-03-09 21:29:03 +08:00
|
|
|
|
/**
|
2026-04-04 08:01:45 +08:00
|
|
|
|
* 通知页 NotificationsScreen(扁平化风格)
|
2026-03-09 21:29:03 +08:00
|
|
|
|
* 胡萝卜BBS - 系统消息列表
|
2026-03-20 23:00:27 +08:00
|
|
|
|
* 【游标分页】使用 messageService.getSystemMessagesCursor
|
2026-03-09 21:29:03 +08:00
|
|
|
|
* 支持响应式布局
|
2026-04-04 08:01:45 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 设计风格:
|
|
|
|
|
|
* - 纯白背景,扁平化设计
|
|
|
|
|
|
* - 简洁的标题区域
|
|
|
|
|
|
* - 分段式筛选标签
|
|
|
|
|
|
* - 与登录、注册、设置页面风格一致
|
2026-03-09 21:29:03 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-21 03:22:28 +08:00
|
|
|
|
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
View,
|
|
|
|
|
|
FlatList,
|
|
|
|
|
|
StyleSheet,
|
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
|
RefreshControl,
|
|
|
|
|
|
ActivityIndicator,
|
2026-03-17 12:29:04 +08:00
|
|
|
|
Dimensions,
|
2026-03-21 12:08:45 +08:00
|
|
|
|
BackHandler,
|
2026-04-04 08:01:45 +08:00
|
|
|
|
Animated,
|
|
|
|
|
|
StatusBar,
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} from 'react-native';
|
2026-03-25 05:16:54 +08:00
|
|
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { useIsFocused } from '@react-navigation/native';
|
2026-03-24 14:21:31 +08:00
|
|
|
|
import { useRouter } from 'expo-router';
|
2026-03-16 17:47:10 +08:00
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
2026-04-11 04:27:22 +08:00
|
|
|
|
import { spacing, borderRadius, useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { SystemMessageResponse } from '../../types/dto';
|
2026-04-13 01:30:37 +08:00
|
|
|
|
import { messageService } from '@/services/message';
|
|
|
|
|
|
import { commentService } from '@/services/post';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
import { SystemMessageItem } from '../../components/business';
|
2026-04-04 08:01:45 +08:00
|
|
|
|
import { Text, ResponsiveContainer } from '../../components/common';
|
2026-03-20 23:00:27 +08:00
|
|
|
|
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
2026-03-24 14:21:31 +08:00
|
|
|
|
import * as hrefs from '../../navigation/hrefs';
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
import { useMessageManagerSystemUnreadCount } from '../../stores';
|
2026-04-13 04:56:58 +08:00
|
|
|
|
import { messageManager } from '../../stores/message';
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
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 }) => {
|
2026-03-25 05:16:54 +08:00
|
|
|
|
const colors = useAppColors();
|
2026-04-11 04:27:22 +08:00
|
|
|
|
const resolved = useResolvedColorScheme();
|
2026-03-25 05:16:54 +08:00
|
|
|
|
const styles = useMemo(() => createNotificationsStyles(colors), [colors]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const isFocused = useIsFocused();
|
|
|
|
|
|
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
|
2026-03-24 14:21:31 +08:00
|
|
|
|
const router = useRouter();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-04-04 08:01:45 +08:00
|
|
|
|
// 动画值
|
|
|
|
|
|
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;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
const [activeType, setActiveType] = useState('all');
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
|
|
|
|
|
|
|
|
|
|
// 【游标分页】使用 useCursorPagination hook 获取系统消息列表
|
2026-03-21 03:22:28 +08:00
|
|
|
|
// 使用 useCallback 包裹 fetchFunction,避免无限重新创建导致循环
|
|
|
|
|
|
const fetchSystemMessages = useCallback(
|
|
|
|
|
|
async ({ cursor, pageSize }: { cursor?: string; pageSize: number }) => {
|
|
|
|
|
|
return await messageService.getSystemMessagesCursor({
|
|
|
|
|
|
cursor,
|
|
|
|
|
|
page_size: pageSize,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
[]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const {
|
2026-03-23 03:58:26 +08:00
|
|
|
|
list: messages,
|
2026-03-20 23:00:27 +08:00
|
|
|
|
isLoading,
|
|
|
|
|
|
isRefreshing,
|
|
|
|
|
|
hasMore,
|
|
|
|
|
|
loadMore,
|
|
|
|
|
|
refresh,
|
|
|
|
|
|
error: paginationError,
|
2026-03-21 03:22:28 +08:00
|
|
|
|
} = useCursorPagination(fetchSystemMessages, { pageSize: 20 });
|
2026-03-20 23:00:27 +08:00
|
|
|
|
|
|
|
|
|
|
// 本地刷新状态(仅用于下拉刷新的UI显示)
|
2026-03-09 21:29:03 +08:00
|
|
|
|
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();
|
2026-03-20 23:00:27 +08:00
|
|
|
|
// 刷新消息列表
|
|
|
|
|
|
await refresh();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
setUnreadCount(0);
|
|
|
|
|
|
setSystemUnreadCount(0);
|
|
|
|
|
|
// 同步更新全局 TabBar 红点
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
messageManager.fetchUnreadCount();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('一键已读失败:', error);
|
|
|
|
|
|
}
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
}, [refresh, setSystemUnreadCount]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 页面加载和获得焦点时刷新,并自动标记所有消息为已读
|
2026-03-21 03:22:28 +08:00
|
|
|
|
// 使用 ref 防止重复执行,避免无限循环
|
|
|
|
|
|
const hasInitializedRef = useRef(false);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
useEffect(() => {
|
2026-03-21 03:22:28 +08:00
|
|
|
|
if (isFocused && !hasInitializedRef.current) {
|
|
|
|
|
|
hasInitializedRef.current = true;
|
2026-03-09 21:29:03 +08:00
|
|
|
|
fetchUnreadCount();
|
|
|
|
|
|
// 进入界面自动标记所有消息为已读
|
|
|
|
|
|
handleMarkAllRead();
|
|
|
|
|
|
}
|
2026-03-21 03:22:28 +08:00
|
|
|
|
}, [isFocused]); // 只依赖 isFocused,函数使用 ref 稳定引用
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-04-04 08:01:45 +08:00
|
|
|
|
// 启动入场动画
|
|
|
|
|
|
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 回调则调用它(用于内嵌模式)
|
2026-03-09 21:29:03 +08:00
|
|
|
|
useEffect(() => {
|
2026-03-16 17:47:10 +08:00
|
|
|
|
if (!isFocused && onBack) {
|
|
|
|
|
|
onBack();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
2026-03-16 17:47:10 +08:00
|
|
|
|
}, [isFocused, onBack]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
2026-03-21 12:08:45 +08:00
|
|
|
|
// 处理 Android 物理返回键 - 当 onBack 存在时(内嵌模式),拦截返回键
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!onBack) return;
|
|
|
|
|
|
|
|
|
|
|
|
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
|
|
|
|
if (isFocused) {
|
|
|
|
|
|
onBack();
|
|
|
|
|
|
return true; // 阻止默认行为
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return () => backHandler.remove();
|
|
|
|
|
|
}, [isFocused, onBack]);
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
// 筛选消息
|
|
|
|
|
|
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);
|
2026-03-20 23:00:27 +08:00
|
|
|
|
await Promise.all([refresh(), fetchUnreadCount()]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
setRefreshing(false);
|
2026-03-20 23:00:27 +08:00
|
|
|
|
}, [refresh, fetchUnreadCount]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 加载更多
|
2026-03-20 23:00:27 +08:00
|
|
|
|
const onEndReached = useCallback(async () => {
|
|
|
|
|
|
if (isLoading || loadingMore || !hasMore) return;
|
|
|
|
|
|
setLoadingMore(true);
|
|
|
|
|
|
await loadMore();
|
|
|
|
|
|
setLoadingMore(false);
|
|
|
|
|
|
}, [isLoading, loadingMore, hasMore, loadMore]);
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 标记单条消息已读并处理导航
|
|
|
|
|
|
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);
|
2026-03-20 23:00:27 +08:00
|
|
|
|
// 【游标分页】不再直接修改 messages 状态,而是通过刷新获取最新数据
|
2026-03-09 21:29:03 +08:00
|
|
|
|
if (wasUnread) {
|
|
|
|
|
|
setUnreadCount(prev => Math.max(0, prev - 1));
|
|
|
|
|
|
decrementSystemUnreadCount(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 更新本地未读数以及全局 TabBar 红点
|
|
|
|
|
|
fetchUnreadCount();
|
refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
2026-03-21 20:55:36 +08:00
|
|
|
|
messageManager.fetchUnreadCount();
|
2026-03-09 21:29:03 +08:00
|
|
|
|
|
|
|
|
|
|
// 根据消息类型处理导航
|
|
|
|
|
|
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) {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefPostDetail(postId));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else if (system_type === 'follow') {
|
|
|
|
|
|
// 关注 - 跳转到用户主页
|
|
|
|
|
|
if (extra_data?.actor_id_str) {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefUserProfile(extra_data.actor_id_str));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else if (system_type === 'group_join_apply') {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefGroupRequestDetail(message));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
} else if (system_type === 'group_invite') {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefGroupInviteDetail(message));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
// 其他类型暂不处理跳转
|
|
|
|
|
|
} 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) {
|
2026-03-24 14:21:31 +08:00
|
|
|
|
router.push(hrefs.hrefUserProfile(actorId));
|
2026-03-09 21:29:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染消息项
|
|
|
|
|
|
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-04-04 08:01:45 +08:00
|
|
|
|
// 渲染头部(包含返回按钮)- 扁平化风格
|
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 ? (
|
2026-04-04 08:01:45 +08:00
|
|
|
|
<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} />
|
|
|
|
|
|
)}
|
2026-03-21 03:22:28 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-04 08:01:45 +08:00
|
|
|
|
// 渲染空状态 - 扁平化风格
|
2026-03-09 21:29:03 +08:00
|
|
|
|
const renderEmpty = () => (
|
2026-03-17 12:29:04 +08:00
|
|
|
|
<View style={[styles.emptyContainer, isWideScreen && styles.emptyContainerWeb]}>
|
2026-04-04 08:01:45 +08:00
|
|
|
|
<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-09 21:29:03 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-17 12:29:04 +08:00
|
|
|
|
// 如果还未hydrated,显示加载占位符,避免竞态条件导致的渲染问题
|
|
|
|
|
|
if (!isHydrated) {
|
|
|
|
|
|
return (
|
2026-03-18 10:58:41 +08:00
|
|
|
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
2026-04-11 04:27:22 +08:00
|
|
|
|
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
|
2026-03-17 12:29:04 +08:00
|
|
|
|
<View style={styles.loadingContainer}>
|
2026-04-11 04:27:22 +08:00
|
|
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
2026-03-17 12:29:04 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 21:29:03 +08:00
|
|
|
|
return (
|
2026-03-18 10:58:41 +08:00
|
|
|
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
2026-04-11 04:27:22 +08:00
|
|
|
|
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
|
2026-04-04 08:01:45 +08:00
|
|
|
|
<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}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
>
|
2026-04-04 08:01:45 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</View>
|
2026-04-04 08:01:45 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 消息列表 */}
|
|
|
|
|
|
{isLoading && messages.length === 0 ? (
|
|
|
|
|
|
<View style={styles.loadingContainer}>
|
2026-04-11 04:27:22 +08:00
|
|
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
2026-04-04 08:01:45 +08:00
|
|
|
|
</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"
|
2026-04-04 08:01:45 +08:00
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
|
|
|
|
|
refreshing={refreshing}
|
|
|
|
|
|
onRefresh={onRefresh}
|
2026-04-11 04:27:22 +08:00
|
|
|
|
colors={[colors.primary.main]}
|
|
|
|
|
|
tintColor={colors.primary.main}
|
2026-04-04 08:01:45 +08:00
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</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}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
>
|
2026-04-04 08:01:45 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</View>
|
2026-04-04 08:01:45 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 消息列表 */}
|
|
|
|
|
|
{isLoading && messages.length === 0 ? (
|
|
|
|
|
|
<View style={styles.loadingContainer}>
|
2026-04-11 04:27:22 +08:00
|
|
|
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
2026-04-04 08:01:45 +08:00
|
|
|
|
</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"
|
2026-04-04 08:01:45 +08:00
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
|
|
|
|
|
refreshing={refreshing}
|
|
|
|
|
|
onRefresh={onRefresh}
|
2026-04-11 04:27:22 +08:00
|
|
|
|
colors={[colors.primary.main]}
|
|
|
|
|
|
tintColor={colors.primary.main}
|
2026-04-04 08:01:45 +08:00
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Animated.View>
|
2026-03-09 21:29:03 +08:00
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-25 05:16:54 +08:00
|
|
|
|
function createNotificationsStyles(colors: AppColors) {
|
|
|
|
|
|
return StyleSheet.create({
|
|
|
|
|
|
container: {
|
|
|
|
|
|
flex: 1,
|
2026-04-11 04:27:22 +08:00
|
|
|
|
backgroundColor: colors.background.paper,
|
2026-04-04 08:01:45 +08:00
|
|
|
|
},
|
|
|
|
|
|
content: {
|
|
|
|
|
|
flex: 1,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
2026-04-04 08:01:45 +08:00
|
|
|
|
// Header 样式 - 扁平化风格
|
2026-03-25 05:16:54 +08:00
|
|
|
|
header: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingHorizontal: 20,
|
|
|
|
|
|
paddingVertical: 16,
|
2026-04-11 04:27:22 +08:00
|
|
|
|
backgroundColor: colors.background.paper,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
headerWide: {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingHorizontal: 32,
|
|
|
|
|
|
paddingVertical: 20,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
headerTitleContainer: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
headerTitle: {
|
|
|
|
|
|
fontSize: 18,
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: colors.text.primary,
|
|
|
|
|
|
},
|
|
|
|
|
|
headerTitleWide: {
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
},
|
|
|
|
|
|
unreadBadge: {
|
2026-04-11 04:27:22 +08:00
|
|
|
|
backgroundColor: colors.primary.main,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
borderRadius: 10,
|
|
|
|
|
|
paddingHorizontal: 6,
|
|
|
|
|
|
paddingVertical: 2,
|
|
|
|
|
|
marginLeft: spacing.sm,
|
|
|
|
|
|
minWidth: 20,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
unreadBadgeText: {
|
2026-04-11 04:27:22 +08:00
|
|
|
|
color: colors.text.inverse,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
fontSize: 11,
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
},
|
|
|
|
|
|
backButton: {
|
|
|
|
|
|
width: 40,
|
|
|
|
|
|
height: 40,
|
|
|
|
|
|
justifyContent: 'center',
|
2026-04-04 08:01:45 +08:00
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
backIconButton: {
|
|
|
|
|
|
width: 40,
|
|
|
|
|
|
height: 40,
|
|
|
|
|
|
borderRadius: 20,
|
2026-04-11 04:27:22 +08:00
|
|
|
|
backgroundColor: colors.background.default,
|
2026-04-04 08:01:45 +08:00
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
alignItems: 'center',
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
backButtonPlaceholder: {
|
|
|
|
|
|
width: 40,
|
|
|
|
|
|
},
|
2026-04-04 08:01:45 +08:00
|
|
|
|
// 分类筛选 - 扁平化风格(分段式设计)
|
2026-03-25 05:16:54 +08:00
|
|
|
|
filterContainer: {
|
|
|
|
|
|
flexDirection: 'row',
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingHorizontal: 16,
|
|
|
|
|
|
paddingVertical: 12,
|
2026-04-11 04:27:22 +08:00
|
|
|
|
backgroundColor: colors.background.paper,
|
2026-04-04 08:01:45 +08:00
|
|
|
|
borderBottomWidth: 1,
|
|
|
|
|
|
borderBottomColor: colors.divider || '#E5E5EA',
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
filterContainerWideWeb: {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingHorizontal: 32,
|
|
|
|
|
|
paddingVertical: 16,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
filterTag: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingHorizontal: 14,
|
|
|
|
|
|
paddingVertical: 8,
|
|
|
|
|
|
marginRight: 8,
|
|
|
|
|
|
borderRadius: 14,
|
2026-04-11 04:27:22 +08:00
|
|
|
|
backgroundColor: colors.background.default,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
borderColor: 'transparent',
|
|
|
|
|
|
},
|
|
|
|
|
|
filterTagWide: {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingHorizontal: 18,
|
|
|
|
|
|
paddingVertical: 10,
|
|
|
|
|
|
marginRight: 10,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
filterTagActive: {
|
2026-04-11 04:27:22 +08:00
|
|
|
|
backgroundColor: colors.primary.main,
|
|
|
|
|
|
borderColor: colors.primary.main,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
filterTagTextActive: {
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
},
|
|
|
|
|
|
filterCount: {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
marginLeft: 6,
|
2026-04-11 04:27:22 +08:00
|
|
|
|
backgroundColor: colors.primary.light + '40',
|
2026-03-25 05:16:54 +08:00
|
|
|
|
paddingHorizontal: 6,
|
|
|
|
|
|
paddingVertical: 1,
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
minWidth: 18,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
},
|
|
|
|
|
|
filterCountActive: {
|
|
|
|
|
|
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
|
|
|
|
|
},
|
|
|
|
|
|
listContent: {
|
|
|
|
|
|
flexGrow: 1,
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingTop: 8,
|
|
|
|
|
|
paddingBottom: 24,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
listContentWide: {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingHorizontal: 32,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
loadingContainer: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
alignItems: 'center',
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingVertical: 80,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
loadingMore: {
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
alignItems: 'center',
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingVertical: 20,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
loadingMoreText: {
|
2026-04-04 08:01:45 +08:00
|
|
|
|
marginLeft: 8,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
2026-04-04 08:01:45 +08:00
|
|
|
|
// 空状态 - 扁平化风格
|
2026-03-25 05:16:54 +08:00
|
|
|
|
emptyContainer: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
alignItems: 'center',
|
2026-04-04 08:01:45 +08:00
|
|
|
|
paddingTop: 80,
|
|
|
|
|
|
paddingHorizontal: 40,
|
2026-03-25 05:16:54 +08:00
|
|
|
|
},
|
|
|
|
|
|
emptyContainerWeb: {
|
|
|
|
|
|
minHeight: 500,
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
paddingTop: 100,
|
|
|
|
|
|
},
|
2026-04-04 08:01:45 +08:00
|
|
|
|
emptyIconContainer: {
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
height: 120,
|
|
|
|
|
|
borderRadius: 60,
|
2026-04-11 04:27:22 +08:00
|
|
|
|
backgroundColor: colors.background.default,
|
2026-04-04 08:01:45 +08:00
|
|
|
|
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',
|
|
|
|
|
|
},
|
2026-03-25 05:16:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|