feat: 同步dev分支并保留当前修改
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 1m35s
Frontend CI / ota-android (push) Successful in 13m3s
Frontend CI / build-android-apk (push) Successful in 39m23s

This commit is contained in:
lafay
2026-03-21 03:22:28 +08:00
parent 2c65330837
commit a8c78f0c3f
16 changed files with 461 additions and 128 deletions

View File

@@ -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(() => {

View File

@@ -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,