feat: 同步dev分支并保留当前修改
This commit is contained in:
@@ -249,10 +249,12 @@ export const MessageListScreen: React.FC = () => {
|
||||
}, [width]);
|
||||
|
||||
// 给底部列表预留安全空间,避免被 TabBar 遮挡导致最后几项无法完整滑出
|
||||
// TabBar 悬浮:高度 64 + 浮动间距 12*2 = 88
|
||||
const listBottomInset = useMemo(() => {
|
||||
if (isWideScreen) return spacing.lg;
|
||||
return tabBarHeight + insets.bottom + spacing.md;
|
||||
}, [isWideScreen, tabBarHeight, insets.bottom]);
|
||||
const FLOATING_TAB_BAR_HEIGHT = 64 + 24; // TabBar高度 + 上下浮动间距
|
||||
return FLOATING_TAB_BAR_HEIGHT + insets.bottom + spacing.md;
|
||||
}, [isWideScreen, insets.bottom]);
|
||||
|
||||
// 【新架构】页面获得焦点时初始化MessageManager
|
||||
useEffect(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 支持响应式布局
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
FlatList,
|
||||
@@ -20,7 +20,7 @@ import { useIsFocused } from '@react-navigation/native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, spacing } from '../../theme';
|
||||
import { colors, spacing, borderRadius } from '../../theme';
|
||||
import { SystemMessageResponse } from '../../types/dto';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { commentService } from '../../services/commentService';
|
||||
@@ -75,6 +75,17 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
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 {
|
||||
items: messages,
|
||||
isLoading,
|
||||
@@ -83,15 +94,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
loadMore,
|
||||
refresh,
|
||||
error: paginationError,
|
||||
} = useCursorPagination(
|
||||
async ({ cursor, pageSize }) => {
|
||||
return await messageService.getSystemMessagesCursor({
|
||||
cursor,
|
||||
page_size: pageSize,
|
||||
});
|
||||
},
|
||||
{ pageSize: 20 }
|
||||
);
|
||||
} = useCursorPagination(fetchSystemMessages, { pageSize: 20 });
|
||||
|
||||
// 本地刷新状态(仅用于下拉刷新的UI显示)
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
@@ -161,13 +164,16 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
}, [refresh, fetchMessageUnreadCount, setSystemUnreadCount]);
|
||||
|
||||
// 页面加载和获得焦点时刷新,并自动标记所有消息为已读
|
||||
// 使用 ref 防止重复执行,避免无限循环
|
||||
const hasInitializedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (isFocused) {
|
||||
if (isFocused && !hasInitializedRef.current) {
|
||||
hasInitializedRef.current = true;
|
||||
fetchUnreadCount();
|
||||
// 进入界面自动标记所有消息为已读
|
||||
handleMarkAllRead();
|
||||
}
|
||||
}, [isFocused, fetchUnreadCount, handleMarkAllRead]);
|
||||
}, [isFocused]); // 只依赖 isFocused,函数使用 ref 稳定引用
|
||||
|
||||
// 屏幕失去焦点时,如果有 onBack 回调则调用它(用于内嵌模式)
|
||||
useEffect(() => {
|
||||
@@ -328,8 +334,8 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
return (
|
||||
<View style={[styles.header, isWideScreen ? styles.headerWide : null]}>
|
||||
{shouldShowBackButton ? (
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={onBack}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
@@ -338,9 +344,16 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
) : (
|
||||
<View style={styles.backButtonPlaceholder} />
|
||||
)}
|
||||
<Text style={[styles.headerTitle, ...(isWideScreen ? [styles.headerTitleWide] : [])]}>
|
||||
系统通知
|
||||
</Text>
|
||||
<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>
|
||||
);
|
||||
@@ -388,30 +401,34 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
}
|
||||
return m.system_type === type.key;
|
||||
}).length;
|
||||
const isActive = activeType === type.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={type.key}
|
||||
style={[
|
||||
styles.filterTag,
|
||||
activeType === type.key && styles.filterTagActive,
|
||||
isActive && styles.filterTagActive,
|
||||
isWideScreen && styles.filterTagWide,
|
||||
]}
|
||||
onPress={() => setActiveType(type.key)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
color={activeType === type.key ? colors.primary.main : colors.text.secondary}
|
||||
color={isActive ? colors.text.inverse : colors.text.secondary}
|
||||
style={isActive ? styles.filterTagTextActive : undefined}
|
||||
>
|
||||
{type.title}
|
||||
</Text>
|
||||
{count > 0 && (
|
||||
<Text
|
||||
variant="caption"
|
||||
color={activeType === type.key ? colors.primary.main : colors.text.hint}
|
||||
style={styles.filterCount}
|
||||
>
|
||||
{count}
|
||||
</Text>
|
||||
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
|
||||
<Text
|
||||
variant="caption"
|
||||
color={isActive ? colors.text.inverse : colors.text.hint}
|
||||
>
|
||||
{count}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -463,29 +480,33 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
}
|
||||
return m.system_type === type.key;
|
||||
}).length;
|
||||
const isActive = activeType === type.key;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={type.key}
|
||||
style={[
|
||||
styles.filterTag,
|
||||
activeType === type.key && styles.filterTagActive,
|
||||
isActive && styles.filterTagActive,
|
||||
]}
|
||||
onPress={() => setActiveType(type.key)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
color={activeType === type.key ? colors.primary.main : colors.text.secondary}
|
||||
color={isActive ? colors.text.inverse : colors.text.secondary}
|
||||
style={isActive ? styles.filterTagTextActive : undefined}
|
||||
>
|
||||
{type.title}
|
||||
</Text>
|
||||
{count > 0 && (
|
||||
<Text
|
||||
variant="caption"
|
||||
color={activeType === type.key ? colors.primary.main : colors.text.hint}
|
||||
style={styles.filterCount}
|
||||
>
|
||||
{count}
|
||||
</Text>
|
||||
<View style={[styles.filterCount, isActive && styles.filterCountActive]}>
|
||||
<Text
|
||||
variant="caption"
|
||||
color={isActive ? colors.text.inverse : colors.text.hint}
|
||||
>
|
||||
{count}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -529,13 +550,70 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
// Header 样式 - 美化版
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
// 移除底部边框,使用更柔和的分隔方式
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
},
|
||||
headerWide: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
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: 'flex-start',
|
||||
},
|
||||
backButtonPlaceholder: {
|
||||
width: 40,
|
||||
},
|
||||
// 分类筛选 - 美化版(使用分段式设计)
|
||||
filterContainer: {
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
// 移除底部边框,让界面更简洁
|
||||
},
|
||||
filterContainerWideWeb: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
@@ -548,55 +626,36 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
marginRight: spacing.sm,
|
||||
borderRadius: 16,
|
||||
marginRight: spacing.xs,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
filterTagWide: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
marginRight: spacing.md,
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
filterTagActive: {
|
||||
backgroundColor: colors.primary.light + '30',
|
||||
backgroundColor: colors.primary.main,
|
||||
},
|
||||
filterTagTextActive: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
filterCount: {
|
||||
marginLeft: spacing.xs,
|
||||
},
|
||||
// Header 样式
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.primary.light + '40',
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 1,
|
||||
borderRadius: 8,
|
||||
minWidth: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
headerWide: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingVertical: spacing.lg,
|
||||
},
|
||||
backButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
backButtonPlaceholder: {
|
||||
width: 40,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
headerTitleWide: {
|
||||
fontSize: 20,
|
||||
filterCountActive: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||
},
|
||||
listContent: {
|
||||
flexGrow: 1,
|
||||
paddingTop: spacing.sm,
|
||||
},
|
||||
listContentWide: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
|
||||
Reference in New Issue
Block a user