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;
}
}