Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | 8x 8x 8x 8x 8x 8x 8x 14x 14x 14x 14x 8x 6x 6x 6x 6x 6x 8x 8x 8x 8x 8x 8x | /**
* AppHeader - 统一的页面头部组件
*
* 设计规范:
* - 左侧:用户头像(点击进入个人中心)
* - 中间:页面标题
* - 右侧:消息通知图标(带未读数量badge)
*/
import { useCallback, useEffect, useState } from 'react';
import { Image, Pressable } from 'react-native';
import type { EdgeInsets } from 'react-native-safe-area-context';
import { useRouter, useFocusEffect } from 'expo-router';
import { Text, XStack, YStack } from 'tamagui';
import { IconSymbol } from '@/src/components/ui/IconSymbol';
import { primaryScale, errorScale } from '@/src/design-system/tokens';
import { useThemeColors, useIsDarkMode } from '@/src/hooks/useThemeColors';
import { useUserStore } from '@/src/store/userStore';
import { supabaseForumService, supabase } from '@/src/lib/supabase';
export interface AppHeaderProps {
/** 页面标题 */
title: string;
/** 安全区域信息 */
insets: EdgeInsets;
/** 是否显示头像 */
showAvatar?: boolean;
/** 是否显示通知图标 */
showNotification?: boolean;
/** 自定义右侧元素 */
rightElement?: React.ReactNode;
/** 背景色 */
backgroundColor?: string;
}
export function AppHeader({
title,
insets,
showAvatar = true,
showNotification = true,
rightElement,
backgroundColor = 'transparent',
}: AppHeaderProps) {
const router = useRouter();
const { user } = useUserStore();
const [unreadCount, setUnreadCount] = useState(0);
const colors = useThemeColors();
const isDark = useIsDarkMode();
// 统一头部高度常量
const HEADER_HEIGHT = 56;
// 获取未读通知数量
const fetchUnreadCount = useCallback(async () => {
try {
const result = await supabaseForumService.getNotifications(true);
Eif (result.data) {
setUnreadCount(result.data.length);
}
} catch (error) {
console.error('获取未读通知失败:', error);
}
}, []);
useEffect(() => {
Iif (!showNotification) return;
fetchUnreadCount();
// 订阅通知变化
const notificationsChannel = supabase
.channel('app_header_notifications')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'notifications',
},
() => {
fetchUnreadCount();
}
)
.subscribe();
return () => {
supabase.removeChannel(notificationsChannel);
};
}, [showNotification, fetchUnreadCount]);
// 当页面获得焦点时刷新未读数量
useFocusEffect(
useCallback(() => {
Eif (showNotification) {
fetchUnreadCount();
}
}, [showNotification, fetchUnreadCount])
);
// 跳转到个人中心
const handleAvatarPress = useCallback(() => {
router.push('/(tabs)/profile');
}, [router]);
// 跳转到通知页面
const handleNotificationPress = useCallback(() => {
router.push('/(tabs)/forum/notifications' as any);
}, [router]);
return (
<YStack paddingTop={insets.top} paddingHorizontal={16} backgroundColor={backgroundColor as any}>
<XStack alignItems="center" justifyContent="space-between" height={HEADER_HEIGHT}>
{/* 左侧:头像 */}
{showAvatar ? (
<Pressable onPress={handleAvatarPress}>
<YStack
width={40}
height={40}
borderRadius={20}
backgroundColor={(isDark ? '#3D2A1F' : primaryScale.primary2) as any}
alignItems="center"
justifyContent="center"
borderWidth={2}
borderColor={(isDark ? '#4D3A2F' : primaryScale.primary4) as any}
overflow="hidden"
>
{user?.avatarUrl ? (
<Image
source={{ uri: user.avatarUrl }}
style={{ width: 40, height: 40 }}
resizeMode="cover"
/>
) : (
<IconSymbol name="person.fill" size={20} color={colors.primary} />
)}
</YStack>
</Pressable>
) : (
<YStack width={40} />
)}
{/* 中间:标题 */}
<Text fontSize={18} fontWeight="700" color={colors.text as any} flex={1} textAlign="center">
{title}
</Text>
{/* 右侧:通知图标或自定义元素 */}
{rightElement ? (
rightElement
) : showNotification ? (
<Pressable onPress={handleNotificationPress}>
<YStack
width={40}
height={40}
borderRadius={20}
backgroundColor={colors.backgroundMuted as any}
alignItems="center"
justifyContent="center"
borderWidth={1.5}
borderColor={colors.border as any}
>
<IconSymbol name="bell.fill" size={20} color={colors.icon} />
{/* 未读消息badge */}
{unreadCount > 0 && (
<YStack
position="absolute"
top={-2}
right={-2}
minWidth={18}
height={18}
borderRadius={9}
backgroundColor={colors.error as any}
alignItems="center"
justifyContent="center"
paddingHorizontal="$1"
>
<Text fontSize={10} fontWeight="700" color="white">
{unreadCount > 99 ? '99+' : unreadCount}
</Text>
</YStack>
)}
</YStack>
</Pressable>
) : (
<YStack width={40} />
)}
</XStack>
</YStack>
);
}
|