diff --git a/.idea/.gitignore b/.idea/.gitignore index 35410ca..f5fb94d 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -6,3 +6,5 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +# Screenshots +screenshots/ diff --git a/src/navigation/MobileTabNavigatorWithDelay.tsx b/src/navigation/MobileTabNavigatorWithDelay.tsx index 5eb1ac3..2417596 100644 --- a/src/navigation/MobileTabNavigatorWithDelay.tsx +++ b/src/navigation/MobileTabNavigatorWithDelay.tsx @@ -63,6 +63,7 @@ export type MainTabParamList = { export type HomeStackParamList = { Home: undefined; Search: undefined; + CreatePost: undefined; }; export type MessageStackParamList = { @@ -133,6 +134,15 @@ function HomeStackNavigatorComponent() { headerShown: false, }} /> + ); } diff --git a/src/screens/create/CreatePostScreen.tsx b/src/screens/create/CreatePostScreen.tsx index 7d0d597..314ab40 100644 --- a/src/screens/create/CreatePostScreen.tsx +++ b/src/screens/create/CreatePostScreen.tsx @@ -33,6 +33,11 @@ import VoteEditor from '../../components/business/VoteEditor'; import { useResponsive, useResponsiveValue } from '../../hooks'; import { RootStackParamList } from '../../navigation/types'; +// Props 接口 +interface CreatePostScreenProps { + onClose?: () => void; +} + const MAX_TITLE_LENGTH = 100; const MAX_CONTENT_LENGTH = 2000; @@ -73,7 +78,8 @@ const getPublishErrorMessage = (error: unknown): string => { return '发布失败,请重试'; }; -export const CreatePostScreen: React.FC = () => { +export const CreatePostScreen: React.FC = (props) => { + const { onClose } = props; const navigation = useNavigation(); const route = useRoute>(); const isEditMode = route.params?.mode === 'edit' && !!route.params?.postId; @@ -130,8 +136,14 @@ export const CreatePostScreen: React.FC = () => { React.useLayoutEffect(() => { navigation.setOptions({ title: isEditMode ? '编辑帖子' : '发布帖子', + // 当作为 Modal 使用时,显示关闭按钮 + headerLeft: onClose ? () => ( + + + + ) : undefined, }); - }, [navigation, isEditMode]); + }, [navigation, isEditMode, onClose]); React.useEffect(() => { if (!isEditMode || !editPostID) { diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index f576dc1..6a74b3d 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -17,6 +17,7 @@ import { NativeSyntheticEvent, Alert, Clipboard, + Modal, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; @@ -33,6 +34,7 @@ import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid import { HomeStackParamList, RootStackParamList } from '../../navigation/types'; import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive'; import { SearchScreen } from './SearchScreen'; +import { CreatePostScreen } from '../create/CreatePostScreen'; type NavigationProp = NativeStackNavigationProp & NativeStackNavigationProp; @@ -88,6 +90,9 @@ export const HomeScreen: React.FC = () => { // 搜索显示状态(用于内嵌搜索页面) const [showSearch, setShowSearch] = useState(false); + // 发帖弹窗状态 + const [showCreatePost, setShowCreatePost] = useState(false); + // 用于跟踪当前页面显示的帖子 ID,以便从 store 同步状态 const postIdsRef = React.useRef>(new Set()); const inFlightRequestKeysRef = React.useRef>(new Set()); @@ -144,7 +149,8 @@ export const HomeScreen: React.FC = () => { if (!isMobile) { return undefined; } - return insets.bottom + MOBILE_TAB_BAR_HEIGHT + MOBILE_TAB_FLOATING_MARGIN + MOBILE_FAB_GAP; + // 往底部导航栏靠近一个按钮的位置 + return insets.bottom + MOBILE_TAB_BAR_HEIGHT + MOBILE_TAB_FLOATING_MARGIN + MOBILE_FAB_GAP - MOBILE_TAB_BAR_HEIGHT; }, [isMobile, insets.bottom]); const appendUniquePosts = useCallback((prevPosts: Post[], incomingPosts: Post[]) => { @@ -398,9 +404,9 @@ export const HomeScreen: React.FC = () => { setShowImageViewer(true); }; - // 跳转到发帖页面 + // 跳转到发帖页面(使用 Modal 方式) const handleCreatePost = () => { - navigation.getParent()?.navigate('CreatePost'); + setShowCreatePost(true); }; // 渲染帖子卡片(列表模式) @@ -733,6 +739,18 @@ export const HomeScreen: React.FC = () => { onClose={() => setShowImageViewer(false)} enableSave /> + + {/* 发帖弹窗 */} + setShowCreatePost(false)} + > + setShowCreatePost(false)} + /> + ); }; diff --git a/src/screens/schedule/ScheduleScreen.tsx b/src/screens/schedule/ScheduleScreen.tsx index b9e44b0..42639e4 100644 --- a/src/screens/schedule/ScheduleScreen.tsx +++ b/src/screens/schedule/ScheduleScreen.tsx @@ -24,13 +24,13 @@ import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { colors, fontSizes, spacing, borderRadius, shadows } from '../../theme'; +import { useResponsive } from '../../hooks/useResponsive'; import { Course, COURSE_COLORS, TimeSlot, getCourseColor, hasCourseInWeek, - getCurrentWeek, } from '../../types/schedule'; import type { ScheduleStackParamList } from '../../navigation/types'; import { scheduleService } from '../../services/scheduleService'; @@ -44,8 +44,8 @@ const VISIBLE_WEEKDAY_NAMES = ['周一', '周二', '周三', '周四', '周五', // 星期列宽度(根据屏幕宽度自动计算) const getDayColumnWidth = () => Math.max(46, (SCREEN_WIDTH - TIME_COLUMN_WIDTH - 2) / VISIBLE_DAY_VALUES.length); -// 每个大节(两小节)高度 -const SECTION_HEIGHT = 105; +// 每个大节(两小节)高度 - 默认值,会在组件中动态调整 +const DEFAULT_SECTION_HEIGHT = 105; // 顶部周选择器高度 const WEEK_SELECTOR_HEIGHT = 44; // 星期标题行高度 @@ -106,6 +106,18 @@ const getTodayColumnIndex = () => { // 第1周固定从2026年3月9日(周一)开始 const FIRST_WEEK_START = new Date(2026, 2, 9); // 2026年3月9日 +// 计算当前日期对应的周数(返回1-20) +const getCurrentWeekNumber = (): number => { + const now = new Date(); + // 计算当前日期与第一周周一的差距天数 + const diffTime = now.getTime() - FIRST_WEEK_START.getTime(); + const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + // 计算周数(从1开始) + const weekNumber = Math.floor(diffDays / 7) + 1; + // 确保周数在有效范围内 + return Math.max(1, Math.min(TOTAL_WEEKS, weekNumber)); +}; + // 获取指定周的起始日期、每天的日期和月份 // weekIndex: 0表示第1周,1表示第2周,以此类推 const getWeekInfo = (weekIndex: number = 0) => { @@ -131,18 +143,41 @@ const getWeekDates = (weekOffset: number = 0) => { return getWeekInfo(weekOffset).dates; }; -// 学期起始日期字符串(用于 getCurrentWeek 函数) -const SEMESTER_START_DATE = '2026-03-09'; - -// 计算当前实际周数 -const getInitialWeek = (): number => { - return getCurrentWeek(SEMESTER_START_DATE, TOTAL_WEEKS); -}; +// 获取初始周数(仅在组件初始化时计算一次) +const INITIAL_WEEK = getCurrentWeekNumber(); export const ScheduleScreen: React.FC = () => { const navigation = useNavigation>(); - const [currentWeek, setCurrentWeek] = useState(getInitialWeek); + // 使用响应式 hook 检测屏幕尺寸 + const { width: screenWidth, height: screenHeight, isMobile, platform } = useResponsive(); + const isWeb = platform.isWeb; + const [currentWeek, setCurrentWeek] = useState(INITIAL_WEEK); const [courses, setCourses] = useState([]); + + // 动态计算日列宽度:移动端填满屏幕宽度,电脑端也填满 + // Web端需要特殊处理,确保周一到周日刚好横向填满 + const dayColumnWidth = useMemo(() => { + if (isWeb) { + // Web端:使用更大的宽度确保填满 + return Math.max(80, (screenWidth - TIME_COLUMN_WIDTH) / VISIBLE_DAY_VALUES.length); + } + // 移动端和原生桌面端 + return Math.max(46, (screenWidth - TIME_COLUMN_WIDTH - 2) / VISIBLE_DAY_VALUES.length); + }, [screenWidth, isWeb]); + + // 动态计算每节课的高度 + // 手机端:第六节课底部刚好跟底部导航栏持平(可用高度 = 屏幕高度 - 周选择器 - 星期标题 - 底部Tab栏) + // 电脑端:第六节课刚好填到底部(可用高度 = 屏幕高度 - 周选择器 - 星期标题) + const sectionHeight = useMemo((): number => { + const totalHeaderHeight = WEEK_SELECTOR_HEIGHT + HEADER_HEIGHT; + const bottomOffset = isMobile ? TAB_BAR_HEIGHT : 0; + // 可用高度 = 屏幕高度 - 顶部区域 - 底部偏移 + const availableHeight = screenHeight - totalHeaderHeight - bottomOffset; + // 6节课,每节课高度 = 可用高度 / 6 + const calculatedHeight = Math.floor(availableHeight / 6); + // 确保高度不低于默认值 + return Math.max(DEFAULT_SECTION_HEIGHT, calculatedHeight); + }, [screenHeight, isMobile]); const [isAddModalVisible, setIsAddModalVisible] = useState(false); const [pendingDay, setPendingDay] = useState(0); const [pendingMergedSection, setPendingMergedSection] = useState(1); @@ -172,13 +207,10 @@ export const ScheduleScreen: React.FC = () => { }; }, []); const todayColumnIndex = getTodayColumnIndex(); - // 获取当前实际周数,用于判断是否在查看当前周 - const actualCurrentWeek = getInitialWeek(); - // 只有在查看当前实际周时才高亮今日列 - const isViewingCurrentWeek = currentWeek === actualCurrentWeek; + // 当前正在查看的周是否为真实的当前周 + const isViewingCurrentWeek = currentWeek === INITIAL_WEEK; const activeTodayColumn = isViewingCurrentWeek ? todayColumnIndex : -1; - const dayColumnWidth = getDayColumnWidth(); const usedCourseColors = useMemo( () => new Set(courses.map(item => normalizeHexColor(item.color || '')).filter(Boolean)), [courses] @@ -396,7 +428,7 @@ export const ScheduleScreen: React.FC = () => { style={[ styles.dayHeader, { - width: dayColumnWidth, + flex: 1, backgroundColor: index === activeTodayColumn ? `${colors.primary.main}10` : 'transparent', }, ]} @@ -433,7 +465,7 @@ export const ScheduleScreen: React.FC = () => { key={slot.section} style={[ styles.timeCell, - { height: SECTION_HEIGHT }, + { height: sectionHeight }, ]} > @@ -558,8 +590,8 @@ export const ScheduleScreen: React.FC = () => { const startMergedSection = getMergedSectionIndex(course.startSection); const endMergedSection = getMergedSectionIndex(course.endSection); const duration = endMergedSection - startMergedSection + 1; - const top = (startMergedSection - 1) * SECTION_HEIGHT; - const height = duration * SECTION_HEIGHT - 4; + const top = (startMergedSection - 1) * sectionHeight; + const height = duration * sectionHeight - 4; const laneGap = 2; const baseLeft = 2; const availableWidth = dayColumnWidth - baseLeft * 2; @@ -618,9 +650,8 @@ export const ScheduleScreen: React.FC = () => { style={[ styles.dayColumn, { - width: dayColumnWidth, borderRightWidth: isLastColumn ? 0 : 1, - backgroundColor: columnIndex === activeTodayColumn ? `${colors.primary.main}08` : 'transparent', + backgroundColor: columnIndex === activeTodayColumn ? `${colors.primary.main}08` : colors.background.default, }, ]} > @@ -633,7 +664,7 @@ export const ScheduleScreen: React.FC = () => { key={slot.section} style={[ styles.gridCell, - { height: SECTION_HEIGHT }, + { height: sectionHeight }, ]} activeOpacity={1} delayLongPress={280} @@ -652,7 +683,11 @@ export const ScheduleScreen: React.FC = () => { }; // 渲染课程表主体 - const renderScheduleBody = () => ( + const renderScheduleBody = () => { + // 移动端和电脑端都不添加额外底部间距,让内容填满整个区域 + // 动态计算的高度已经确保第六节课显示在正确位置 + + return ( { {renderTimeColumn()} {VISIBLE_DAY_VALUES.map((dayOfWeek, index) => @@ -678,7 +714,8 @@ export const ScheduleScreen: React.FC = () => { - ); + ); + }; return ( @@ -1063,11 +1100,11 @@ const styles = StyleSheet.create({ flexDirection: 'row', height: HEADER_HEIGHT, backgroundColor: colors.background.paper, - borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomWidth: 1, borderBottomColor: colors.divider, }, timeHeader: { - borderRightWidth: StyleSheet.hairlineWidth, + borderRightWidth: 1, borderRightColor: colors.divider, alignItems: 'center', justifyContent: 'center', @@ -1079,6 +1116,7 @@ const styles = StyleSheet.create({ letterSpacing: 0.3, }, dayHeader: { + flex: 1, alignItems: 'center', justifyContent: 'center', paddingVertical: 3, @@ -1118,7 +1156,6 @@ const styles = StyleSheet.create({ scheduleBody: { flex: 1, backgroundColor: colors.background.default, - marginBottom: TAB_BAR_HEIGHT, }, scheduleBodyGestureLayer: { flex: 1, @@ -1164,11 +1201,16 @@ const styles = StyleSheet.create({ // ── 日期列容器 ──────────────────────────────────────── daysContainer: { flexDirection: 'row', + flex: 1, + backgroundColor: colors.background.default, + minWidth: TIME_COLUMN_WIDTH, // 确保最小宽度 }, // ── 单日列 ──────────────────────────────────────────── dayColumn: { - borderRightWidth: StyleSheet.hairlineWidth, + flex: 1, + backgroundColor: colors.background.default, // 使用主题默认背景色 + borderRightWidth: 1, borderRightColor: '#EBEBEB', }, gridCell: { diff --git a/src/services/scheduleService.ts b/src/services/scheduleService.ts index 1b6c551..875d20c 100644 --- a/src/services/scheduleService.ts +++ b/src/services/scheduleService.ts @@ -45,42 +45,22 @@ interface SyncScheduleResponse { error_message?: string; } -const toCourse = (dto: ScheduleCourseDTO): Course => { - // 调试日志:检查后端返回的 weeks 字段 - console.log('[ScheduleService] 转换课程 DTO:', { - id: dto.id, - name: dto.name, - day_of_week: dto.day_of_week, - weeks: dto.weeks, - weeksType: typeof dto.weeks, - weeksLength: dto.weeks?.length, - }); - return { - id: dto.id, - name: dto.name, - teacher: dto.teacher, - location: dto.location, - dayOfWeek: dto.day_of_week, - startSection: dto.start_section, - endSection: dto.end_section, - weeks: dto.weeks ?? [], - color: dto.color, - }; -}; +const toCourse = (dto: ScheduleCourseDTO): Course => ({ + id: dto.id, + name: dto.name, + teacher: dto.teacher, + location: dto.location, + dayOfWeek: dto.day_of_week, + startSection: dto.start_section, + endSection: dto.end_section, + weeks: dto.weeks ?? [], + color: dto.color, +}); class ScheduleService { async getCourses(week?: number): Promise { const params = week ? { week } : undefined; - console.log('[ScheduleService] 获取课程列表, params:', params); const response = await api.get('/schedule/courses', params); - console.log('[ScheduleService] 课程列表响应, 数量:', response.data.list.length); - // 打印原始响应数据中的 weeks 字段 - response.data.list.forEach((item, index) => { - console.log(`[ScheduleService] 课程[${index}] ${item.name}:`, { - day_of_week: item.day_of_week, - weeks: item.weeks, - }); - }); return response.data.list.map(toCourse); } @@ -99,9 +79,7 @@ class ScheduleService { } async syncSchedule(req: SyncScheduleRequest): Promise { - console.log('[ScheduleService] 开始同步教务系统, username:', req.username); const response = await api.post('/schedule/sync', req); - console.log('[ScheduleService] 同步响应:', response.data); return response.data; } }