chore: 重构导航页面

This commit is contained in:
Junhui Chen 2025-08-05 23:52:30 +08:00
parent ec83f9ce34
commit 3904f8da66
21 changed files with 198 additions and 427 deletions

23
app/(auth)/_layout.tsx Normal file
View File

@ -0,0 +1,23 @@
import { Stack } from 'expo-router';
import React from 'react';
export default function AuthLayout() {
return (
<Stack>
<Stack.Screen
name="login"
options={{
headerShown: false,
animation: 'fade'
}}
/>
<Stack.Screen
name="reset-password"
options={{
headerShown: false,
animation: 'fade'
}}
/>
</Stack>
);
}

View File

@ -0,0 +1,76 @@
import { Tabs } from 'expo-router';
import React from 'react';
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { HapticTab } from '@/components/HapticTab';
import { TransitionPresets } from '@react-navigation/bottom-tabs';
import { Platform } from 'react-native';
import AskNavbar from '@/components/layout/ask';
import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util';
import { useEffect, useState } from 'react';
// 只在iOS平台上导入TabBarBackground组件
const TabBarBackground = Platform.OS === 'ios' ? require('@/components/ui/TabBarBackground').default : null;
export default function TabLayout() {
const colorScheme = useColorScheme();
const [wsStatus, setWsStatus] = useState<WebSocketStatus>('disconnected');
useEffect(() => {
const handleStatusChange = (status: WebSocketStatus) => {
setWsStatus(status);
};
webSocketManager.subscribeStatus(handleStatusChange);
return () => {
webSocketManager.unsubscribeStatus(handleStatusChange);
};
}, []);
// 只在iOS平台上使用TabBarBackground
const renderTabBarBackground = () => {
if (Platform.OS === 'ios' && TabBarBackground) {
return <TabBarBackground />;
}
return null;
};
return (
<>
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarBackground: renderTabBarBackground, // 使用自定义背景
tabBarButton: HapticTab, // 添加触觉反馈
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
...TransitionPresets.ShiftTransition,
}}
/>
<Tabs.Screen
name="memo-list"
options={{
title: 'Memos',
tabBarIcon: ({ color }) => <TabBarIcon name="document-text" color={color} />,
...TransitionPresets.ShiftTransition,
}}
/>
<Tabs.Screen
name="owner"
options={{
title: 'Profile',
tabBarIcon: ({ color }) => <TabBarIcon name="person" color={color} />,
...TransitionPresets.ShiftTransition,
}}
/>
</Tabs>
<AskNavbar wsStatus={wsStatus} />
</>
);
}

57
app/(main)/_layout.tsx Normal file
View File

@ -0,0 +1,57 @@
import { Tabs } from 'expo-router';
import React from 'react';
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { HapticTab } from '@/components/HapticTab';
import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util';
import { useEffect, useState } from 'react';
import { Platform } from 'react-native';
// 只在iOS平台上导入TabBarBackground组件
const TabBarBackground = Platform.OS === 'ios' ? require('@/components/ui/TabBarBackground').default : null;
export default function MainLayout() {
const colorScheme = useColorScheme();
const [wsStatus, setWsStatus] = useState<WebSocketStatus>('disconnected');
useEffect(() => {
const handleStatusChange = (status: WebSocketStatus) => {
setWsStatus(status);
};
webSocketManager.subscribeStatus(handleStatusChange);
return () => {
webSocketManager.unsubscribeStatus(handleStatusChange);
};
}, []);
// 只在iOS平台上使用TabBarBackground
const renderTabBarBackground = () => {
if (Platform.OS === 'ios' && TabBarBackground) {
return <TabBarBackground />;
}
return null;
};
return (
<>
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarBackground: renderTabBarBackground, // 使用自定义背景
tabBarButton: HapticTab, // 添加触觉反馈
}}
>
<Tabs.Screen
name="(tabs)" // 这将渲染 (tabs) 目录下的 _layout.tsx
options={{
headerShown: false,
title: 'Home',
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
}}
/>
</Tabs>
</>
);
}

View File

@ -0,0 +1,39 @@
import { Stack } from 'expo-router';
import React from 'react';
export default function SettingsLayout() {
return (
<Stack>
<Stack.Screen
name="setting"
options={{
headerShown: false
}}
/>
<Stack.Screen
name="privacy-policy"
options={{
headerShown: false
}}
/>
<Stack.Screen
name="support"
options={{
headerShown: false
}}
/>
<Stack.Screen
name="rights"
options={{
headerShown: false
}}
/>
<Stack.Screen
name="user-message"
options={{
headerShown: false
}}
/>
</Stack>
);
}

View File

@ -1,419 +0,0 @@
import { HapticTab } from '@/components/HapticTab';
import AskNavbar from '@/components/layout/ask';
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { requestNotificationPermission } from '@/components/owner/utils';
import TabBarBackground from '@/components/ui/TabBarBackground';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { prefetchChats } from '@/lib/prefetch';
import { fetchApi } from '@/lib/server-api-util';
import { webSocketManager, WebSocketStatus } from '@/lib/websocket-util';
import { TransitionPresets } from '@react-navigation/bottom-tabs';
import * as Notifications from 'expo-notifications';
import { Tabs, useRouter } from 'expo-router';
import * as SecureStore from 'expo-secure-store';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Platform } from 'react-native';
interface PollingData {
title: string;
id: string;
content: string;
extra: any;
}
export default function TabLayout() {
const { t } = useTranslation();
const colorScheme = useColorScheme();
const [pollingData, setPollingData] = useState<PollingData[]>([]);
const pollingInterval = useRef<NodeJS.Timeout | number>(null);
const tokenInterval = useRef<NodeJS.Timeout | number>(null);
const isMounted = useRef(true);
const [token, setToken] = useState('');
const [wsStatus, setWsStatus] = useState<WebSocketStatus>('disconnected');
const [notificationPermissionRequested, setNotificationPermissionRequested] = useState(false);
const sendNotification = async (item: PollingData) => {
// 只在需要发送通知时才请求权限
if (!notificationPermissionRequested) {
const granted = await requestNotificationPermission();
setNotificationPermissionRequested(granted);
if (!granted) {
console.log('用户拒绝了通知权限');
return;
}
}
// 调度本地通知
await Notifications.scheduleNotificationAsync({
content: {
title: item.title,
body: item.content,
data: { screen: 'ask', extra: item.extra, id: item.id },
priority: 'high', // 关键:设置 high 或 max
},
trigger: {
seconds: 2, // 延迟2秒显示
type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL // 添加 type 字段
}, // 延迟2秒显示
});
};
// 监听通知点击事件
useEffect(() => {
const notificationListener = Notifications.addNotificationResponseReceivedListener(response => {
const data = response.notification.request.content.data;
console.log('通知被点击,数据:', data);
setPollingData(prev => prev.filter((item) => item.id !== data.id));
// 根据通知数据导航到指定页面
if (data.screen === 'ask') {
router.push({
pathname: '/ask',
params: {
sessionId: data.id,
extra: data.extra
}
});
}
});
// 清理监听器
return () => {
Notifications.removeNotificationSubscription(notificationListener);
};
}, []);
useEffect(() => {
const handleStatusChange = (status: WebSocketStatus) => {
setWsStatus(status);
};
webSocketManager.subscribeStatus(handleStatusChange);
return () => {
webSocketManager.unsubscribeStatus(handleStatusChange);
};
}, []);
// 轮询获取推送消息
const startPolling = useCallback(async (interval: number = 5000) => {
// 设置轮询
pollingInterval.current = setInterval(async () => {
if (isMounted.current) {
await getMessageData();
}
}, interval);
// 返回清理函数
return () => {
if (pollingInterval.current) {
clearInterval(pollingInterval.current);
pollingInterval.current = null;
}
};
}, []);
// 获取推送消息
const getMessageData = async () => {
try {
const response = await fetchApi<PollingData[]>("/notice/push/message", {
method: "POST"
});
if (response && Array.isArray(response)) {
setPollingData((prev) => ([...prev, ...response]));
}
} catch (error) {
console.error('获取轮询数据时出错:', error);
}
};
// 获取认证token
const getAuthToken = async (): Promise<string> => {
let tokenValue = '';
if (Platform.OS === 'web') {
tokenValue = localStorage.getItem('token') || '';
} else {
tokenValue = (await SecureStore.getItemAsync('token')) || '';
}
// 只在获取到新token时更新状态
if (tokenValue !== token) {
setToken(tokenValue);
}
return tokenValue;
};
// 初始化时获取一次token
useEffect(() => {
getAuthToken();
}, []);
useEffect(() => {
const checkAuthStatus = async () => {
try {
if (token) {
// 启动轮询
const cleanup = startPolling(5000);
// 将清理函数保存到ref中以便在组件卸载时调用
return cleanup;
}
} catch (error) {
console.error('获取推送消息出错:', error);
}
};
const cleanupPolling = checkAuthStatus();
return () => {
// 清理函数
if (pollingInterval.current) {
clearInterval(pollingInterval.current);
pollingInterval.current = null;
}
if (typeof cleanupPolling === 'function') {
cleanupPolling();
}
isMounted.current = false;
};
}, [token, startPolling]);
// 本地推送
useEffect(() => {
pollingData?.forEach((item) => {
sendNotification(item)
})
}, [pollingData])
// 轮询获取token
useEffect(() => {
// 如果已经有token直接返回
if (token) {
if (tokenInterval.current) {
clearInterval(tokenInterval.current);
tokenInterval.current = null;
}
return;
}
// 设置轮询
tokenInterval.current = setInterval(async () => {
if (isMounted.current) {
const currentToken = await getAuthToken();
// 如果获取到token清除定时器
if (currentToken && tokenInterval.current) {
clearInterval(tokenInterval.current);
tokenInterval.current = null;
}
}
}, 5000);
// 返回清理函数
return () => {
if (tokenInterval.current) {
clearInterval(tokenInterval.current);
tokenInterval.current = null;
}
};
}, [token]); // 添加token作为依赖
useEffect(() => {
if (token) {
prefetchChats().catch(console.error);
}
}, [token]);
return (
<>
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
tabBarStyle: Platform.select({
ios: {
// Use a transparent background on iOS to show the blur effect
position: 'absolute',
},
default: {},
}),
}}
>
{/* 落地页 */}
<Tabs.Screen
name="index"
options={{
title: 'Memo',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 登录 */}
<Tabs.Screen
name="login"
options={{
title: 'Login',
href: '/login',
// tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 重置密码 */}
<Tabs.Screen
name="reset-password"
options={{
title: 'reset-password',
href: '/reset-password',
// tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* loading页面 */}
<Tabs.Screen
name="loading"
options={{
title: 'loading',
href: '/loading',
// tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 用户信息收集 */}
<Tabs.Screen
name="user-message"
options={{
title: 'user-message',
href: '/user-message',
// tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* ask页面 */}
<Tabs.Screen
name="ask"
options={{
title: 'ask',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示
...TransitionPresets.ShiftTransition,
}}
/>
{/* memo list */}
<Tabs.Screen
name="memo-list"
options={{
title: 'memo-list',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示
...TransitionPresets.ShiftTransition,
}}
/>
{/* owner */}
<Tabs.Screen
name="owner"
options={{
title: 'owner',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示
...TransitionPresets.ShiftTransition,
}}
/>
{/* 排行榜 */}
<Tabs.Screen
name="top"
options={{
title: 'top',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 对话详情页 */}
<Tabs.Screen
name="chat-details"
options={{
title: 'chat-details',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 隐私协议 */}
<Tabs.Screen
name="privacy-policy"
options={{
title: 'privacy-policy',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* Support Screen */}
<Tabs.Screen
name="support"
options={{
title: t('tabTitle', { ns: 'support' }),
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* Debug Screen - only in development */}
{process.env.NODE_ENV === 'development' && (
<Tabs.Screen
name="debug"
options={{
title: 'Debug',
tabBarIcon: ({ color, focused }) => (
<TabBarIcon name={focused ? 'bug' : 'bug-outline'} color={color} />
),
}}
/>
)}
{/* 下载页面 */}
<Tabs.Screen
name="download"
options={{
title: 'download',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 购买权益页面 */}
<Tabs.Screen
name="rights"
options={{
title: 'rights',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
{/* 设置页面 */}
<Tabs.Screen
name="setting"
options={{
title: 'setting',
tabBarButton: () => null, // 隐藏底部标签栏
headerShown: false, // 隐藏导航栏
tabBarStyle: { display: 'none' } // 确保在标签栏中不显示
}}
/>
</Tabs >
<AskNavbar wsStatus={wsStatus} />
</>
);
}

View File

@ -34,14 +34,9 @@ export default function RootLayout() {
<PermissionProvider>
<Provider>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="login"
options={{
headerShown: false,
animation: 'fade'
}}
/>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(main)" options={{ headerShown: false }} />
<Stack.Screen name="(settings)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
</Provider>