diff --git a/src/screens/schedule/CourseDetailScreen.tsx b/src/screens/schedule/CourseDetailScreen.tsx
index f3431ab..b59455f 100644
--- a/src/screens/schedule/CourseDetailScreen.tsx
+++ b/src/screens/schedule/CourseDetailScreen.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
+import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView, Pressable } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { MaterialCommunityIcons } from '@expo/vector-icons';
@@ -64,8 +64,9 @@ const CourseDetailScreen: React.FC = () => {
return (
- navigation.goBack()}>
- e.stopPropagation()}>
+
+ navigation.goBack()} />
+
@@ -78,39 +79,44 @@ const CourseDetailScreen: React.FC = () => {
-
- 课程名称
- {course.name}
-
-
-
- 教师
- {course.teacher || '未填写'}
-
-
-
- 上课时间
-
- {timeLines.map((item, index) => (
-
-
- 第{formatWeekRanges(item.weeks)}周 · {WEEKDAY_NAMES[item.dayOfWeek]} 第
- {getMergedSectionIndex(item.startSection)}节
-
- {item.location ? {item.location} : null}
-
- ))}
+
+
+ 课程名称
+ {course.name}
-
-
-
-
- 删除本条课程
-
-
-
-
+
+ 上课时间
+
+ {timeLines.map((item, index) => (
+
+
+ 第{formatWeekRanges(item.weeks)}周 · {WEEKDAY_NAMES[item.dayOfWeek]} 第
+ {getMergedSectionIndex(item.startSection)}节
+
+
+ 任课教师:{item.teacher || '未填写'}
+
+ {item.location ? {item.location} : null}
+
+ ))}
+
+
+
+
+
+
+ 删除本条课程
+
+
+
+
+
);
};
@@ -130,8 +136,15 @@ const styles = StyleSheet.create({
borderRadius: borderRadius.xl,
paddingVertical: spacing.lg,
paddingHorizontal: spacing.lg,
+ maxHeight: '82%',
...shadows.lg,
},
+ contentScroll: {
+ maxHeight: '100%',
+ },
+ contentScrollContainer: {
+ paddingBottom: spacing.xs,
+ },
header: {
flexDirection: 'row',
justifyContent: 'space-between',
diff --git a/src/screens/schedule/ScheduleScreen.tsx b/src/screens/schedule/ScheduleScreen.tsx
index 16a4995..f15dc5b 100644
--- a/src/screens/schedule/ScheduleScreen.tsx
+++ b/src/screens/schedule/ScheduleScreen.tsx
@@ -15,6 +15,7 @@ import {
Modal,
TextInput,
Alert,
+ ActivityIndicator,
} from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import type { PanGestureHandlerStateChangeEvent } from 'react-native-gesture-handler';
@@ -71,6 +72,13 @@ const getMergedSectionEnd = (mergedSection: number) => mergedSection * 2;
const SWIPE_THRESHOLD = 18;
type RepeatMode = 'single' | 'weekly' | 'odd' | 'even';
+type CourseDisplayItem = {
+ key: string;
+ course: Course;
+ locationText: string;
+ laneIndex: number;
+ laneCount: number;
+};
const TOTAL_WEEKS = 20;
const HEX_COLOR_REGEX = /^#?[0-9A-Fa-f]{6}$/;
const parseWeekInput = (value: string, fallback: number) => {
@@ -137,6 +145,14 @@ export const ScheduleScreen: React.FC = () => {
const [startWeekInput, setStartWeekInput] = useState(String(currentWeek));
const [endWeekInput, setEndWeekInput] = useState(String(TOTAL_WEEKS));
const weekScrollRef = useRef(null);
+
+ // 设置弹窗状态
+ const [isSettingsModalVisible, setIsSettingsModalVisible] = useState(false);
+ // 同步教务系统弹窗状态
+ const [isSyncModalVisible, setIsSyncModalVisible] = useState(false);
+ const [syncUsername, setSyncUsername] = useState('');
+ const [syncPassword, setSyncPassword] = useState('');
+ const [isSyncing, setIsSyncing] = useState(false);
const todayColumnIndex = getTodayColumnIndex();
// currentWeek === 1 对应今天所在的真实周(offset 0),其他周不高亮今日列
const isViewingCurrentWeek = currentWeek === 1;
@@ -308,7 +324,10 @@ export const ScheduleScreen: React.FC = () => {
// 渲染周选择器
const renderWeekSelector = () => (
-
+ setIsSettingsModalVisible(true)}
+ >
{
);
+ // 处理长按课程删除
+ const handleCourseLongPress = useCallback((course: Course) => {
+ const relatedCourses = getRelatedCourses(course);
+ const hasMultiple = relatedCourses.length > 1;
+
+ Alert.alert(
+ '删除课程',
+ `课程:${course.name}`,
+ [
+ {
+ text: '取消',
+ style: 'cancel',
+ },
+ {
+ text: '删除本节',
+ style: 'destructive',
+ onPress: async () => {
+ try {
+ await scheduleService.deleteCourse(course.id);
+ setCourses(prev => prev.filter(c => c.id !== course.id));
+ Alert.alert('成功', '已删除本节课程');
+ } catch (error) {
+ console.error('删除课程失败:', error);
+ Alert.alert('删除失败', '请稍后重试');
+ }
+ },
+ },
+ ...(hasMultiple
+ ? [
+ {
+ text: '删除全部',
+ style: 'destructive' as const,
+ onPress: async () => {
+ try {
+ // 删除所有相关课程
+ for (const relatedCourse of relatedCourses) {
+ await scheduleService.deleteCourse(relatedCourse.id);
+ }
+ // 从本地状态中移除
+ const relatedIds = new Set(relatedCourses.map(c => c.id));
+ setCourses(prev => prev.filter(c => !relatedIds.has(c.id)));
+ Alert.alert('成功', `已删除 ${relatedCourses.length} 节课程`);
+ } catch (error) {
+ console.error('批量删除课程失败:', error);
+ Alert.alert('删除失败', '请稍后重试');
+ }
+ },
+ },
+ ]
+ : []),
+ ],
+ { cancelable: true }
+ );
+ }, [getRelatedCourses]);
+
+ const buildDayDisplayCourses = useCallback((dayCourses: Course[]): CourseDisplayItem[] => {
+ const groupedByNameAndSlot = new Map();
+
+ dayCourses.forEach(course => {
+ // 同名 + 同时间段分组;若出现多条记录,则拆分并排展示
+ const key = `${course.name}|${course.dayOfWeek}|${course.startSection}|${course.endSection}`;
+ const group = groupedByNameAndSlot.get(key);
+ if (group) {
+ group.push(course);
+ } else {
+ groupedByNameAndSlot.set(key, [course]);
+ }
+ });
+
+ const items: CourseDisplayItem[] = [];
+ groupedByNameAndSlot.forEach((groupCourses, groupKey) => {
+ groupCourses.forEach((course, index) => {
+ items.push({
+ key: `${groupKey}|${course.id}|${index}`,
+ course,
+ locationText: course.location?.trim() ?? '',
+ laneIndex: index,
+ laneCount: groupCourses.length,
+ });
+ });
+ });
+
+ return items;
+ }, []);
+
// 渲染课程卡片
- const renderCourseCard = (course: Course) => {
- const color = getCourseColor(course.id, course.color);
+ const renderCourseCard = ({ key, course, locationText, laneIndex, laneCount }: CourseDisplayItem) => {
+ // 颜色仅按课程名计算,确保同名课在 UI 上颜色一致
+ const color = getCourseColor(course.name);
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 laneGap = 2;
+ const baseLeft = 2;
+ const availableWidth = dayColumnWidth - baseLeft * 2;
+ const cardWidth =
+ laneCount > 1
+ ? (availableWidth - laneGap * (laneCount - 1)) / laneCount
+ : availableWidth;
+ const left = baseLeft + laneIndex * (cardWidth + laneGap);
return (
@@ -438,14 +551,16 @@ export const ScheduleScreen: React.FC = () => {
relatedCourses: getRelatedCourses(course),
})
}
+ onLongPress={() => handleCourseLongPress(course)}
+ delayLongPress={500}
activeOpacity={0.82}
>
{formatCourseName(course.name)}
- {course.location ? (
+ {locationText ? (
- @{course.location}
+ @{locationText}
) : null}
@@ -458,6 +573,7 @@ export const ScheduleScreen: React.FC = () => {
const dayCourses = currentWeekCourses.filter(
course => course.dayOfWeek === dayOfWeek
);
+ const displayCourses = buildDayDisplayCourses(dayCourses);
return (
{
);
})}
{/* 课程卡片 */}
- {dayCourses.map(renderCourseCard)}
+ {displayCourses.map(renderCourseCard)}
);
};
@@ -540,6 +656,154 @@ export const ScheduleScreen: React.FC = () => {
{/* 课程表主体 */}
{renderScheduleBody()}
+ {/* 设置弹窗 */}
+ setIsSettingsModalVisible(false)}
+ >
+
+
+ 设置
+
+ {
+ setIsSettingsModalVisible(false);
+ setIsSyncModalVisible(true);
+ }}
+ >
+
+ 同步教务系统
+
+
+
+ setIsSettingsModalVisible(false)}
+ >
+ 关闭
+
+
+
+
+
+ {/* 同步教务系统弹窗 */}
+ {
+ if (!isSyncing) {
+ setIsSyncModalVisible(false);
+ }
+ }}
+ >
+
+
+ 同步教务系统
+
+ 输入教务系统账号密码,自动同步课表数据
+
+
+
+
+
+ {isSyncing && (
+
+
+ 正在同步课表数据...
+
+ )}
+
+
+ {
+ if (!isSyncing) {
+ setIsSyncModalVisible(false);
+ setSyncUsername('');
+ setSyncPassword('');
+ }
+ }}
+ activeOpacity={0.8}
+ disabled={isSyncing}
+ >
+ 取消
+
+ {
+ if (!syncUsername.trim() || !syncPassword.trim()) return;
+
+ setIsSyncing(true);
+ try {
+ const result = await scheduleService.syncSchedule({
+ username: syncUsername.trim(),
+ password: syncPassword.trim(),
+ });
+
+ if (result.success) {
+ Alert.alert(
+ '同步成功',
+ `成功同步 ${result.synced_count} 门课程`,
+ [
+ {
+ text: '确定',
+ onPress: () => {
+ setIsSyncModalVisible(false);
+ setSyncUsername('');
+ setSyncPassword('');
+ loadCourses(); // 刷新课表
+ },
+ },
+ ]
+ );
+ } else {
+ Alert.alert(
+ '同步失败',
+ result.error_message || result.message || '请检查账号密码是否正确'
+ );
+ }
+ } catch (error) {
+ console.error('同步课表失败:', error);
+ Alert.alert('同步失败', '网络错误,请稍后重试');
+ } finally {
+ setIsSyncing(false);
+ }
+ }}
+ activeOpacity={0.8}
+ disabled={!syncUsername.trim() || !syncPassword.trim() || isSyncing}
+ >
+
+ {isSyncing ? '同步中...' : '开始同步'}
+
+
+
+
+
+
+
({
id: dto.id,
name: dto.name,
@@ -64,7 +77,12 @@ class ScheduleService {
async deleteCourse(courseId: string): Promise {
await api.delete(`/schedule/courses/${courseId}`);
}
+
+ async syncSchedule(req: SyncScheduleRequest): Promise {
+ const response = await api.post('/schedule/sync', req);
+ return response.data;
+ }
}
export const scheduleService = new ScheduleService();
-export type { CreateScheduleCourseRequest };
+export type { CreateScheduleCourseRequest, SyncScheduleRequest };