feat(schedule): add academic system sync and course management improvements
- add educational system synchronization with username/password authentication - implement long-press course deletion with single/all options - add multi-lane course display for overlapping same-name courses - refactor course detail screen with ScrollView and improved layout - fix teacher display to show per-time-slot instructors instead of single field - add settings modal with sync option and loading states
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
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 { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
@@ -64,8 +64,9 @@ const CourseDetailScreen: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => navigation.goBack()}>
|
<View style={styles.mask}>
|
||||||
<TouchableOpacity activeOpacity={1} style={styles.card} onPress={e => e.stopPropagation()}>
|
<Pressable style={StyleSheet.absoluteFillObject} onPress={() => navigation.goBack()} />
|
||||||
|
<View style={styles.card}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.headerLeft}>
|
<View style={styles.headerLeft}>
|
||||||
<View style={styles.iconBadge}>
|
<View style={styles.iconBadge}>
|
||||||
@@ -78,16 +79,17 @@ const CourseDetailScreen: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
style={styles.contentScroll}
|
||||||
|
contentContainerStyle={styles.contentScrollContainer}
|
||||||
|
nestedScrollEnabled
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<Text style={styles.label}>课程名称</Text>
|
<Text style={styles.label}>课程名称</Text>
|
||||||
<Text style={styles.value} numberOfLines={2}>{course.name}</Text>
|
<Text style={styles.value} numberOfLines={2}>{course.name}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.row}>
|
|
||||||
<Text style={styles.label}>教师</Text>
|
|
||||||
<Text style={styles.value}>{course.teacher || '未填写'}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={[styles.row, styles.rowLast]}>
|
<View style={[styles.row, styles.rowLast]}>
|
||||||
<Text style={styles.label}>上课时间</Text>
|
<Text style={styles.label}>上课时间</Text>
|
||||||
<View style={styles.timeList}>
|
<View style={styles.timeList}>
|
||||||
@@ -97,6 +99,9 @@ const CourseDetailScreen: React.FC = () => {
|
|||||||
第{formatWeekRanges(item.weeks)}周 · {WEEKDAY_NAMES[item.dayOfWeek]} 第
|
第{formatWeekRanges(item.weeks)}周 · {WEEKDAY_NAMES[item.dayOfWeek]} 第
|
||||||
{getMergedSectionIndex(item.startSection)}节
|
{getMergedSectionIndex(item.startSection)}节
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text style={styles.timeItemSub}>
|
||||||
|
任课教师:{item.teacher || '未填写'}
|
||||||
|
</Text>
|
||||||
{item.location ? <Text style={styles.timeItemSub}>{item.location}</Text> : null}
|
{item.location ? <Text style={styles.timeItemSub}>{item.location}</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
@@ -109,8 +114,9 @@ const CourseDetailScreen: React.FC = () => {
|
|||||||
<Text style={styles.deleteButtonText}>删除本条课程</Text>
|
<Text style={styles.deleteButtonText}>删除本条课程</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</ScrollView>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -130,8 +136,15 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: borderRadius.xl,
|
borderRadius: borderRadius.xl,
|
||||||
paddingVertical: spacing.lg,
|
paddingVertical: spacing.lg,
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
|
maxHeight: '82%',
|
||||||
...shadows.lg,
|
...shadows.lg,
|
||||||
},
|
},
|
||||||
|
contentScroll: {
|
||||||
|
maxHeight: '100%',
|
||||||
|
},
|
||||||
|
contentScrollContainer: {
|
||||||
|
paddingBottom: spacing.xs,
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
TextInput,
|
TextInput,
|
||||||
Alert,
|
Alert,
|
||||||
|
ActivityIndicator,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
||||||
import type { PanGestureHandlerStateChangeEvent } 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;
|
const SWIPE_THRESHOLD = 18;
|
||||||
|
|
||||||
type RepeatMode = 'single' | 'weekly' | 'odd' | 'even';
|
type RepeatMode = 'single' | 'weekly' | 'odd' | 'even';
|
||||||
|
type CourseDisplayItem = {
|
||||||
|
key: string;
|
||||||
|
course: Course;
|
||||||
|
locationText: string;
|
||||||
|
laneIndex: number;
|
||||||
|
laneCount: number;
|
||||||
|
};
|
||||||
const TOTAL_WEEKS = 20;
|
const TOTAL_WEEKS = 20;
|
||||||
const HEX_COLOR_REGEX = /^#?[0-9A-Fa-f]{6}$/;
|
const HEX_COLOR_REGEX = /^#?[0-9A-Fa-f]{6}$/;
|
||||||
const parseWeekInput = (value: string, fallback: number) => {
|
const parseWeekInput = (value: string, fallback: number) => {
|
||||||
@@ -137,6 +145,14 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
const [startWeekInput, setStartWeekInput] = useState(String(currentWeek));
|
const [startWeekInput, setStartWeekInput] = useState(String(currentWeek));
|
||||||
const [endWeekInput, setEndWeekInput] = useState(String(TOTAL_WEEKS));
|
const [endWeekInput, setEndWeekInput] = useState(String(TOTAL_WEEKS));
|
||||||
const weekScrollRef = useRef<ScrollView | null>(null);
|
const weekScrollRef = useRef<ScrollView | null>(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();
|
const todayColumnIndex = getTodayColumnIndex();
|
||||||
// currentWeek === 1 对应今天所在的真实周(offset 0),其他周不高亮今日列
|
// currentWeek === 1 对应今天所在的真实周(offset 0),其他周不高亮今日列
|
||||||
const isViewingCurrentWeek = currentWeek === 1;
|
const isViewingCurrentWeek = currentWeek === 1;
|
||||||
@@ -308,7 +324,10 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
// 渲染周选择器
|
// 渲染周选择器
|
||||||
const renderWeekSelector = () => (
|
const renderWeekSelector = () => (
|
||||||
<View style={styles.weekSelector}>
|
<View style={styles.weekSelector}>
|
||||||
<TouchableOpacity style={styles.settingsButton}>
|
<TouchableOpacity
|
||||||
|
style={styles.settingsButton}
|
||||||
|
onPress={() => setIsSettingsModalVisible(true)}
|
||||||
|
>
|
||||||
<MaterialCommunityIcons name="cog" size={24} color="#FFFFFF" />
|
<MaterialCommunityIcons name="cog" size={24} color="#FFFFFF" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@@ -410,26 +429,120 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 处理长按课程删除
|
||||||
|
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<string, Course[]>();
|
||||||
|
|
||||||
|
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 renderCourseCard = ({ key, course, locationText, laneIndex, laneCount }: CourseDisplayItem) => {
|
||||||
const color = getCourseColor(course.id, course.color);
|
// 颜色仅按课程名计算,确保同名课在 UI 上颜色一致
|
||||||
|
const color = getCourseColor(course.name);
|
||||||
const startMergedSection = getMergedSectionIndex(course.startSection);
|
const startMergedSection = getMergedSectionIndex(course.startSection);
|
||||||
const endMergedSection = getMergedSectionIndex(course.endSection);
|
const endMergedSection = getMergedSectionIndex(course.endSection);
|
||||||
const duration = endMergedSection - startMergedSection + 1;
|
const duration = endMergedSection - startMergedSection + 1;
|
||||||
const top = (startMergedSection - 1) * SECTION_HEIGHT;
|
const top = (startMergedSection - 1) * SECTION_HEIGHT;
|
||||||
const height = duration * SECTION_HEIGHT - 4;
|
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 (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={course.id}
|
key={key}
|
||||||
style={[
|
style={[
|
||||||
styles.courseCard,
|
styles.courseCard,
|
||||||
{
|
{
|
||||||
top,
|
top,
|
||||||
height,
|
height,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
left: 2,
|
left,
|
||||||
right: 2,
|
width: cardWidth,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
@@ -438,14 +551,16 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
relatedCourses: getRelatedCourses(course),
|
relatedCourses: getRelatedCourses(course),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
onLongPress={() => handleCourseLongPress(course)}
|
||||||
|
delayLongPress={500}
|
||||||
activeOpacity={0.82}
|
activeOpacity={0.82}
|
||||||
>
|
>
|
||||||
<Text style={styles.courseName} numberOfLines={3}>
|
<Text style={styles.courseName} numberOfLines={3}>
|
||||||
{formatCourseName(course.name)}
|
{formatCourseName(course.name)}
|
||||||
</Text>
|
</Text>
|
||||||
{course.location ? (
|
{locationText ? (
|
||||||
<Text style={styles.courseLocation}>
|
<Text style={styles.courseLocation}>
|
||||||
@{course.location}
|
@{locationText}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -458,6 +573,7 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
const dayCourses = currentWeekCourses.filter(
|
const dayCourses = currentWeekCourses.filter(
|
||||||
course => course.dayOfWeek === dayOfWeek
|
course => course.dayOfWeek === dayOfWeek
|
||||||
);
|
);
|
||||||
|
const displayCourses = buildDayDisplayCourses(dayCourses);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -493,7 +609,7 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* 课程卡片 */}
|
{/* 课程卡片 */}
|
||||||
{dayCourses.map(renderCourseCard)}
|
{displayCourses.map(renderCourseCard)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -540,6 +656,154 @@ export const ScheduleScreen: React.FC = () => {
|
|||||||
{/* 课程表主体 */}
|
{/* 课程表主体 */}
|
||||||
{renderScheduleBody()}
|
{renderScheduleBody()}
|
||||||
|
|
||||||
|
{/* 设置弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={isSettingsModalVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => setIsSettingsModalVisible(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.addModalMask}>
|
||||||
|
<View style={styles.settingsModalCard}>
|
||||||
|
<Text style={styles.addModalTitle}>设置</Text>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.settingsItem}
|
||||||
|
onPress={() => {
|
||||||
|
setIsSettingsModalVisible(false);
|
||||||
|
setIsSyncModalVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons name="cloud-sync" size={22} color={colors.primary.main} />
|
||||||
|
<Text style={styles.settingsItemText}>同步教务系统</Text>
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.settingsCloseBtn}
|
||||||
|
onPress={() => setIsSettingsModalVisible(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.settingsCloseText}>关闭</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 同步教务系统弹窗 */}
|
||||||
|
<Modal
|
||||||
|
visible={isSyncModalVisible}
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
onRequestClose={() => {
|
||||||
|
if (!isSyncing) {
|
||||||
|
setIsSyncModalVisible(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={styles.addModalMask}>
|
||||||
|
<View style={styles.addModalCard}>
|
||||||
|
<Text style={styles.addModalTitle}>同步教务系统</Text>
|
||||||
|
<Text style={styles.addModalDesc}>
|
||||||
|
输入教务系统账号密码,自动同步课表数据
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
value={syncUsername}
|
||||||
|
onChangeText={setSyncUsername}
|
||||||
|
placeholder="学号/工号"
|
||||||
|
placeholderTextColor={colors.text.hint}
|
||||||
|
style={styles.addInput}
|
||||||
|
autoCapitalize="none"
|
||||||
|
editable={!isSyncing}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
value={syncPassword}
|
||||||
|
onChangeText={setSyncPassword}
|
||||||
|
placeholder="密码"
|
||||||
|
placeholderTextColor={colors.text.hint}
|
||||||
|
style={styles.addInput}
|
||||||
|
secureTextEntry
|
||||||
|
editable={!isSyncing}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isSyncing && (
|
||||||
|
<View style={styles.syncingContainer}>
|
||||||
|
<ActivityIndicator size="small" color={colors.primary.main} />
|
||||||
|
<Text style={styles.syncingText}>正在同步课表数据...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.addActions}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.addCancelBtn, isSyncing && styles.addConfirmBtnDisabled]}
|
||||||
|
onPress={() => {
|
||||||
|
if (!isSyncing) {
|
||||||
|
setIsSyncModalVisible(false);
|
||||||
|
setSyncUsername('');
|
||||||
|
setSyncPassword('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
disabled={isSyncing}
|
||||||
|
>
|
||||||
|
<Text style={styles.addCancelText}>取消</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.addConfirmBtn,
|
||||||
|
(!syncUsername.trim() || !syncPassword.trim() || isSyncing) && styles.addConfirmBtnDisabled,
|
||||||
|
]}
|
||||||
|
onPress={async () => {
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
<Text style={styles.addConfirmText}>
|
||||||
|
{isSyncing ? '同步中...' : '开始同步'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
visible={isAddModalVisible}
|
visible={isAddModalVisible}
|
||||||
transparent
|
transparent
|
||||||
@@ -1060,6 +1324,58 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
backgroundColor: colors.background.default,
|
backgroundColor: colors.background.default,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── 设置弹窗 ──────────────────────────────────────────
|
||||||
|
settingsModalCard: {
|
||||||
|
backgroundColor: colors.background.paper,
|
||||||
|
borderRadius: borderRadius['2xl'],
|
||||||
|
padding: spacing.xl,
|
||||||
|
...shadows.lg,
|
||||||
|
minWidth: 280,
|
||||||
|
},
|
||||||
|
settingsItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
backgroundColor: colors.background.default,
|
||||||
|
marginTop: spacing.md,
|
||||||
|
},
|
||||||
|
settingsItemText: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: fontSizes.md,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.text.primary,
|
||||||
|
marginLeft: spacing.md,
|
||||||
|
},
|
||||||
|
settingsCloseBtn: {
|
||||||
|
marginTop: spacing.lg,
|
||||||
|
height: 46,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
backgroundColor: colors.background.default,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
settingsCloseText: {
|
||||||
|
fontSize: fontSizes.md,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.text.secondary,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── 同步状态 ──────────────────────────────────────────
|
||||||
|
syncingContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginVertical: spacing.md,
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
syncingText: {
|
||||||
|
fontSize: fontSizes.sm,
|
||||||
|
color: colors.text.secondary,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ScheduleScreen;
|
export default ScheduleScreen;
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ interface CreateScheduleCourseResponse {
|
|||||||
course: ScheduleCourseDTO;
|
course: ScheduleCourseDTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SyncScheduleRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
semester?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SyncScheduleResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
synced_count: number;
|
||||||
|
error_message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const toCourse = (dto: ScheduleCourseDTO): Course => ({
|
const toCourse = (dto: ScheduleCourseDTO): Course => ({
|
||||||
id: dto.id,
|
id: dto.id,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
@@ -64,7 +77,12 @@ class ScheduleService {
|
|||||||
async deleteCourse(courseId: string): Promise<void> {
|
async deleteCourse(courseId: string): Promise<void> {
|
||||||
await api.delete(`/schedule/courses/${courseId}`);
|
await api.delete(`/schedule/courses/${courseId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncSchedule(req: SyncScheduleRequest): Promise<SyncScheduleResponse> {
|
||||||
|
const response = await api.post<SyncScheduleResponse>('/schedule/sync', req);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const scheduleService = new ScheduleService();
|
export const scheduleService = new ScheduleService();
|
||||||
export type { CreateScheduleCourseRequest };
|
export type { CreateScheduleCourseRequest, SyncScheduleRequest };
|
||||||
|
|||||||
Reference in New Issue
Block a user