refactor(App, navigation): migrate to Expo Router and clean up navigation structure
- Updated App entry point to utilize Expo Router, simplifying the navigation setup. - Removed legacy navigation components and services to streamline the codebase. - Adjusted package.json to reflect the new entry point and updated dependencies for compatibility. - Enhanced overall application structure by consolidating navigation logic and improving maintainability.
This commit is contained in:
239
src/app-navigation/AppDesktopShell.tsx
Normal file
239
src/app-navigation/AppDesktopShell.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Text,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { usePathname, useRouter } from 'expo-router';
|
||||
|
||||
import { colors, shadows } from '../theme';
|
||||
import { useTotalUnreadCount } from '../stores';
|
||||
import { AppRouteStack } from './AppRouteStack';
|
||||
|
||||
type TabName = 'HomeTab' | 'MessageTab' | 'ScheduleTab' | 'ProfileTab';
|
||||
|
||||
const SIDEBAR_WIDTH_DESKTOP = 240;
|
||||
const SIDEBAR_WIDTH_TABLET = 200;
|
||||
const SIDEBAR_COLLAPSED_WIDTH = 72;
|
||||
|
||||
const NAV_ITEMS: { name: TabName; label: string; href: string; icon: string; iconOutline: string }[] = [
|
||||
{ name: 'HomeTab', label: '首页', href: '/home', icon: 'home', iconOutline: 'home-outline' },
|
||||
{ name: 'MessageTab', label: '消息', href: '/messages', icon: 'message-text', iconOutline: 'message-text-outline' },
|
||||
{ name: 'ScheduleTab', label: '课表', href: '/schedule', icon: 'calendar-today', iconOutline: 'calendar-today' },
|
||||
{ name: 'ProfileTab', label: '我的', href: '/profile', icon: 'account', iconOutline: 'account-outline' },
|
||||
];
|
||||
|
||||
function pathToTab(pathname: string): TabName {
|
||||
if (pathname.startsWith('/messages')) return 'MessageTab';
|
||||
if (pathname.startsWith('/schedule')) return 'ScheduleTab';
|
||||
if (pathname.startsWith('/profile')) return 'ProfileTab';
|
||||
if (pathname.startsWith('/home')) return 'HomeTab';
|
||||
return 'HomeTab';
|
||||
}
|
||||
|
||||
export function AppDesktopShell() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const unreadCount = useTotalUnreadCount();
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isDesktop, setIsDesktop] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== 'web') return;
|
||||
const check = () => {
|
||||
const w = window.innerWidth || document.documentElement.clientWidth;
|
||||
setIsDesktop(w >= SIDEBAR_WIDTH_DESKTOP);
|
||||
};
|
||||
check();
|
||||
window.addEventListener('resize', check);
|
||||
return () => window.removeEventListener('resize', check);
|
||||
}, []);
|
||||
|
||||
const sidebarWidth = isCollapsed
|
||||
? SIDEBAR_COLLAPSED_WIDTH
|
||||
: isDesktop
|
||||
? SIDEBAR_WIDTH_DESKTOP
|
||||
: SIDEBAR_WIDTH_TABLET;
|
||||
|
||||
const currentTab = pathToTab(pathname || '/home');
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(href: string) => {
|
||||
router.replace(href);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<SafeAreaView
|
||||
style={[
|
||||
styles.sidebar,
|
||||
{ width: sidebarWidth, paddingTop: insets.top, paddingBottom: insets.bottom },
|
||||
]}
|
||||
>
|
||||
<View style={styles.sidebarHeader}>
|
||||
<MaterialCommunityIcons name="carrot" size={32} color={colors.primary.main} />
|
||||
{!isCollapsed && <Text style={styles.logoText}>胡萝卜BBS</Text>}
|
||||
</View>
|
||||
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive = currentTab === item.name;
|
||||
const showBadge = item.name === 'MessageTab' && unreadCount > 0;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={item.name}
|
||||
style={[
|
||||
styles.sidebarItem,
|
||||
isActive && styles.sidebarItemActive,
|
||||
isCollapsed && styles.sidebarItemCollapsed,
|
||||
]}
|
||||
onPress={() => handleTabChange(item.href)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.sidebarIconContainer}>
|
||||
<MaterialCommunityIcons
|
||||
name={(isActive ? item.icon : item.iconOutline) as any}
|
||||
size={24}
|
||||
color={isActive ? colors.primary.main : colors.text.secondary}
|
||||
/>
|
||||
{showBadge ? (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>{unreadCount > 99 ? '99+' : unreadCount}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
{!isCollapsed ? (
|
||||
<Text style={[styles.sidebarLabel, isActive && styles.sidebarLabelActive]}>{item.label}</Text>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
<TouchableOpacity
|
||||
style={[styles.collapseButton, isCollapsed && styles.collapseButtonCollapsed]}
|
||||
onPress={() => setIsCollapsed((c) => !c)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={isCollapsed ? 'chevron-right' : 'chevron-left'}
|
||||
size={24}
|
||||
color={colors.text.secondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
<View style={styles.mainContent}>
|
||||
<AppRouteStack />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
sidebar: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: colors.divider,
|
||||
flexDirection: 'column',
|
||||
...shadows.md,
|
||||
},
|
||||
sidebarHeader: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
logoText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: colors.primary.main,
|
||||
marginLeft: 8,
|
||||
},
|
||||
sidebarContent: {
|
||||
flex: 1,
|
||||
paddingTop: 8,
|
||||
},
|
||||
sidebarItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
marginHorizontal: 8,
|
||||
marginVertical: 4,
|
||||
borderRadius: 12,
|
||||
},
|
||||
sidebarItemActive: {
|
||||
backgroundColor: `${colors.primary.main}15`,
|
||||
},
|
||||
sidebarItemCollapsed: {
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
sidebarIconContainer: {
|
||||
position: 'relative',
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 12,
|
||||
},
|
||||
sidebarLabel: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
color: colors.text.secondary,
|
||||
marginLeft: 12,
|
||||
flex: 1,
|
||||
},
|
||||
sidebarLabelActive: {
|
||||
color: colors.primary.main,
|
||||
fontWeight: '600',
|
||||
},
|
||||
collapseButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.divider,
|
||||
},
|
||||
collapseButtonCollapsed: {
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
right: 2,
|
||||
backgroundColor: colors.error.main,
|
||||
borderRadius: 10,
|
||||
minWidth: 18,
|
||||
height: 18,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
badgeText: {
|
||||
color: colors.primary.contrast,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
mainContent: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
});
|
||||
8
src/app-navigation/AppRouteStack.tsx
Normal file
8
src/app-navigation/AppRouteStack.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Stack } from 'expo-router';
|
||||
|
||||
/**
|
||||
* 已登录区内栈:由 app/(app) 下文件系统自动注册子路由
|
||||
*/
|
||||
export function AppRouteStack() {
|
||||
return <Stack screenOptions={{ headerShown: false }} />;
|
||||
}
|
||||
Reference in New Issue
Block a user