From 3904f8da66852ed9fbb16a8e0c9daf063acba47d Mon Sep 17 00:00:00 2001 From: Junhui Chen Date: Tue, 5 Aug 2025 23:52:30 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E9=87=8D=E6=9E=84=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(auth)/_layout.tsx | 23 + app/{(tabs) => (auth)}/login.tsx | 0 app/{(tabs) => (auth)}/reset-password.tsx | 0 app/(main)/(tabs)/_layout.tsx | 76 ++++ app/{ => (main)}/(tabs)/index.tsx | 0 app/{ => (main)}/(tabs)/memo-list.tsx | 0 app/{ => (main)}/(tabs)/owner.tsx | 0 app/(main)/_layout.tsx | 57 +++ app/{(tabs) => (main)}/ask.tsx | 0 app/{(tabs) => (main)}/debug.tsx | 0 app/{(tabs) => (main)}/download.tsx | 0 app/{(tabs) => (main)}/loading.tsx | 0 app/{(tabs) => (main)}/top.tsx | 0 app/(settings)/_layout.tsx | 39 ++ app/{(tabs) => (settings)}/privacy-policy.tsx | 0 app/{(tabs) => (settings)}/rights.tsx | 0 app/{(tabs) => (settings)}/setting.tsx | 0 app/{(tabs) => (settings)}/support.tsx | 0 app/{(tabs) => (settings)}/user-message.tsx | 0 app/(tabs)/_layout.tsx | 419 ------------------ app/_layout.tsx | 11 +- 21 files changed, 198 insertions(+), 427 deletions(-) create mode 100644 app/(auth)/_layout.tsx rename app/{(tabs) => (auth)}/login.tsx (100%) rename app/{(tabs) => (auth)}/reset-password.tsx (100%) create mode 100644 app/(main)/(tabs)/_layout.tsx rename app/{ => (main)}/(tabs)/index.tsx (100%) rename app/{ => (main)}/(tabs)/memo-list.tsx (100%) rename app/{ => (main)}/(tabs)/owner.tsx (100%) create mode 100644 app/(main)/_layout.tsx rename app/{(tabs) => (main)}/ask.tsx (100%) rename app/{(tabs) => (main)}/debug.tsx (100%) rename app/{(tabs) => (main)}/download.tsx (100%) rename app/{(tabs) => (main)}/loading.tsx (100%) rename app/{(tabs) => (main)}/top.tsx (100%) create mode 100644 app/(settings)/_layout.tsx rename app/{(tabs) => (settings)}/privacy-policy.tsx (100%) rename app/{(tabs) => (settings)}/rights.tsx (100%) rename app/{(tabs) => (settings)}/setting.tsx (100%) rename app/{(tabs) => (settings)}/support.tsx (100%) rename app/{(tabs) => (settings)}/user-message.tsx (100%) delete mode 100644 app/(tabs)/_layout.tsx diff --git a/app/(auth)/_layout.tsx b/app/(auth)/_layout.tsx new file mode 100644 index 0000000..ba9343f --- /dev/null +++ b/app/(auth)/_layout.tsx @@ -0,0 +1,23 @@ +import { Stack } from 'expo-router'; +import React from 'react'; + +export default function AuthLayout() { + return ( + + + + + ); +} diff --git a/app/(tabs)/login.tsx b/app/(auth)/login.tsx similarity index 100% rename from app/(tabs)/login.tsx rename to app/(auth)/login.tsx diff --git a/app/(tabs)/reset-password.tsx b/app/(auth)/reset-password.tsx similarity index 100% rename from app/(tabs)/reset-password.tsx rename to app/(auth)/reset-password.tsx diff --git a/app/(main)/(tabs)/_layout.tsx b/app/(main)/(tabs)/_layout.tsx new file mode 100644 index 0000000..5b035aa --- /dev/null +++ b/app/(main)/(tabs)/_layout.tsx @@ -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('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 ; + } + return null; + }; + + return ( + <> + + , + ...TransitionPresets.ShiftTransition, + }} + /> + , + ...TransitionPresets.ShiftTransition, + }} + /> + , + ...TransitionPresets.ShiftTransition, + }} + /> + + + + ); +} diff --git a/app/(tabs)/index.tsx b/app/(main)/(tabs)/index.tsx similarity index 100% rename from app/(tabs)/index.tsx rename to app/(main)/(tabs)/index.tsx diff --git a/app/(tabs)/memo-list.tsx b/app/(main)/(tabs)/memo-list.tsx similarity index 100% rename from app/(tabs)/memo-list.tsx rename to app/(main)/(tabs)/memo-list.tsx diff --git a/app/(tabs)/owner.tsx b/app/(main)/(tabs)/owner.tsx similarity index 100% rename from app/(tabs)/owner.tsx rename to app/(main)/(tabs)/owner.tsx diff --git a/app/(main)/_layout.tsx b/app/(main)/_layout.tsx new file mode 100644 index 0000000..c0674fb --- /dev/null +++ b/app/(main)/_layout.tsx @@ -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('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 ; + } + return null; + }; + + return ( + <> + + , + }} + /> + + + ); +} diff --git a/app/(tabs)/ask.tsx b/app/(main)/ask.tsx similarity index 100% rename from app/(tabs)/ask.tsx rename to app/(main)/ask.tsx diff --git a/app/(tabs)/debug.tsx b/app/(main)/debug.tsx similarity index 100% rename from app/(tabs)/debug.tsx rename to app/(main)/debug.tsx diff --git a/app/(tabs)/download.tsx b/app/(main)/download.tsx similarity index 100% rename from app/(tabs)/download.tsx rename to app/(main)/download.tsx diff --git a/app/(tabs)/loading.tsx b/app/(main)/loading.tsx similarity index 100% rename from app/(tabs)/loading.tsx rename to app/(main)/loading.tsx diff --git a/app/(tabs)/top.tsx b/app/(main)/top.tsx similarity index 100% rename from app/(tabs)/top.tsx rename to app/(main)/top.tsx diff --git a/app/(settings)/_layout.tsx b/app/(settings)/_layout.tsx new file mode 100644 index 0000000..9971760 --- /dev/null +++ b/app/(settings)/_layout.tsx @@ -0,0 +1,39 @@ +import { Stack } from 'expo-router'; +import React from 'react'; + +export default function SettingsLayout() { + return ( + + + + + + + + ); +} diff --git a/app/(tabs)/privacy-policy.tsx b/app/(settings)/privacy-policy.tsx similarity index 100% rename from app/(tabs)/privacy-policy.tsx rename to app/(settings)/privacy-policy.tsx diff --git a/app/(tabs)/rights.tsx b/app/(settings)/rights.tsx similarity index 100% rename from app/(tabs)/rights.tsx rename to app/(settings)/rights.tsx diff --git a/app/(tabs)/setting.tsx b/app/(settings)/setting.tsx similarity index 100% rename from app/(tabs)/setting.tsx rename to app/(settings)/setting.tsx diff --git a/app/(tabs)/support.tsx b/app/(settings)/support.tsx similarity index 100% rename from app/(tabs)/support.tsx rename to app/(settings)/support.tsx diff --git a/app/(tabs)/user-message.tsx b/app/(settings)/user-message.tsx similarity index 100% rename from app/(tabs)/user-message.tsx rename to app/(settings)/user-message.tsx diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx deleted file mode 100644 index 07c478a..0000000 --- a/app/(tabs)/_layout.tsx +++ /dev/null @@ -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([]); - const pollingInterval = useRef(null); - const tokenInterval = useRef(null); - const isMounted = useRef(true); - const [token, setToken] = useState(''); - const [wsStatus, setWsStatus] = useState('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("/notice/push/message", { - method: "POST" - }); - if (response && Array.isArray(response)) { - setPollingData((prev) => ([...prev, ...response])); - } - } catch (error) { - console.error('获取轮询数据时出错:', error); - } - }; - - // 获取认证token - const getAuthToken = async (): Promise => { - 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 ( - <> - - {/* 落地页 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - {/* 登录 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - {/* 重置密码 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - {/* loading页面 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - {/* 用户信息收集 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - {/* ask页面 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示 - ...TransitionPresets.ShiftTransition, - }} - /> - {/* memo list */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示 - ...TransitionPresets.ShiftTransition, - }} - /> - {/* owner */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' }, // 确保在标签栏中不显示 - ...TransitionPresets.ShiftTransition, - }} - /> - {/* 排行榜 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - {/* 对话详情页 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - {/* 隐私协议 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - - {/* Support Screen */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - - {/* Debug Screen - only in development */} - {process.env.NODE_ENV === 'development' && ( - ( - - ), - }} - /> - )} - - {/* 下载页面 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - - {/* 购买权益页面 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - - {/* 设置页面 */} - null, // 隐藏底部标签栏 - headerShown: false, // 隐藏导航栏 - tabBarStyle: { display: 'none' } // 确保在标签栏中不显示 - }} - /> - - - - ); -} diff --git a/app/_layout.tsx b/app/_layout.tsx index 3f1d690..947deba 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -34,14 +34,9 @@ export default function RootLayout() { - - + + +