2026-03-18 12:11:49 +08:00
|
|
|
/**
|
|
|
|
|
* 桌面端导航器
|
|
|
|
|
* 为平板/桌面设备提供侧边栏导航体验
|
|
|
|
|
*/
|
|
|
|
|
import React, { useState, useCallback, useEffect } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
View,
|
|
|
|
|
StyleSheet,
|
|
|
|
|
ScrollView,
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
Text,
|
|
|
|
|
Animated,
|
|
|
|
|
Platform,
|
|
|
|
|
} from 'react-native';
|
|
|
|
|
import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context';
|
|
|
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
|
|
|
|
|
|
|
|
import type { TabName, NavItemConfig } from '../infrastructure/navigation/types';
|
|
|
|
|
import { NAVIGATION_CONSTANTS } from '../infrastructure/navigation/types';
|
|
|
|
|
import { colors, shadows } from '../theme';
|
|
|
|
|
import { useNavigationState } from '../infrastructure/navigation/hooks/useNavigationState';
|
|
|
|
|
|
|
|
|
|
// 导入屏幕
|
|
|
|
|
import { HomeScreen } from '../screens/home';
|
|
|
|
|
import { ScheduleScreen } from '../screens/schedule';
|
|
|
|
|
import { MessageListScreen } from '../screens/message';
|
|
|
|
|
import { ProfileScreen } from '../screens/profile';
|
|
|
|
|
|
|
|
|
|
// 侧边栏常量
|
|
|
|
|
const { SIDEBAR_WIDTH_DESKTOP, SIDEBAR_WIDTH_TABLET, SIDEBAR_COLLAPSED_WIDTH } = NAVIGATION_CONSTANTS;
|
|
|
|
|
|
|
|
|
|
// 导航项配置
|
|
|
|
|
const NAV_ITEMS: NavItemConfig[] = [
|
|
|
|
|
{ name: 'HomeTab', label: '首页', icon: 'home', iconOutline: 'home-outline' },
|
|
|
|
|
{ name: 'MessageTab', label: '消息', icon: 'message-text', iconOutline: 'message-text-outline' },
|
|
|
|
|
{ name: 'ScheduleTab', label: '课表', icon: 'calendar-today', iconOutline: 'calendar-today' },
|
|
|
|
|
{ name: 'ProfileTab', label: '我的', icon: 'account', iconOutline: 'account-outline' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
interface DesktopNavigatorProps {
|
|
|
|
|
unreadCount?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function DesktopNavigator({ unreadCount = 0 }: DesktopNavigatorProps) {
|
|
|
|
|
const insets = useSafeAreaInsets();
|
|
|
|
|
const { currentTab, isCollapsed, setCurrentTab, toggleCollapse, setIsReady } = useNavigationState();
|
|
|
|
|
const [isDesktop, setIsDesktop] = useState(false);
|
|
|
|
|
|
|
|
|
|
// 检测是否是桌面尺寸
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const checkDesktop = () => {
|
|
|
|
|
const width = window.innerWidth || document.documentElement.clientWidth;
|
|
|
|
|
setIsDesktop(width >= SIDEBAR_WIDTH_DESKTOP);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
checkDesktop();
|
|
|
|
|
window.addEventListener('resize', checkDesktop);
|
|
|
|
|
setIsReady(true);
|
|
|
|
|
|
|
|
|
|
return () => window.removeEventListener('resize', checkDesktop);
|
|
|
|
|
}, [setIsReady]);
|
|
|
|
|
|
|
|
|
|
// 计算侧边栏宽度
|
|
|
|
|
const sidebarWidth = isCollapsed ? SIDEBAR_COLLAPSED_WIDTH : (isDesktop ? SIDEBAR_WIDTH_DESKTOP : SIDEBAR_WIDTH_TABLET);
|
|
|
|
|
|
|
|
|
|
// 处理 Tab 切换
|
|
|
|
|
const handleTabChange = useCallback((tab: TabName) => {
|
|
|
|
|
setCurrentTab(tab);
|
|
|
|
|
}, [setCurrentTab]);
|
|
|
|
|
|
|
|
|
|
// 渲染当前 Tab 的内容
|
|
|
|
|
const renderTabContent = () => {
|
|
|
|
|
switch (currentTab) {
|
|
|
|
|
case 'HomeTab':
|
|
|
|
|
return <HomeScreen />;
|
|
|
|
|
case 'MessageTab':
|
|
|
|
|
return <MessageListScreen />;
|
|
|
|
|
case 'ScheduleTab':
|
|
|
|
|
return <ScheduleScreen />;
|
|
|
|
|
case 'ProfileTab':
|
|
|
|
|
return <ProfileScreen />;
|
|
|
|
|
default:
|
|
|
|
|
return <HomeScreen />;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<View style={styles.container}>
|
|
|
|
|
{/* 侧边栏 */}
|
|
|
|
|
<SafeAreaView style={[styles.sidebar, { width: sidebarWidth, paddingTop: insets.top, paddingBottom: insets.bottom }]}>
|
|
|
|
|
{/* Logo 区域 */}
|
|
|
|
|
<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.name)}
|
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
>
|
|
|
|
|
<View style={styles.sidebarIconContainer}>
|
|
|
|
|
<MaterialCommunityIcons
|
2026-03-19 10:53:09 +08:00
|
|
|
name={isActive ? item.icon as any : item.iconOutline as any}
|
2026-03-18 12:11:49 +08:00
|
|
|
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>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
{!isCollapsed && (
|
|
|
|
|
<Text style={[styles.sidebarLabel, isActive && styles.sidebarLabelActive]}>
|
|
|
|
|
{item.label}
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ScrollView>
|
|
|
|
|
|
|
|
|
|
{/* 折叠按钮 */}
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
style={[styles.collapseButton, isCollapsed && styles.collapseButtonCollapsed]}
|
|
|
|
|
onPress={toggleCollapse}
|
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
>
|
|
|
|
|
<MaterialCommunityIcons
|
|
|
|
|
name={isCollapsed ? 'chevron-right' : 'chevron-left'}
|
|
|
|
|
size={24}
|
|
|
|
|
color={colors.text.secondary}
|
|
|
|
|
/>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
|
|
|
|
{/* 主内容区域 */}
|
|
|
|
|
<View style={styles.mainContent}>
|
|
|
|
|
{renderTabContent()}
|
|
|
|
|
</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,
|
|
|
|
|
},
|
|
|
|
|
});
|