feat(navigation): add schedule tab and stack navigator
Add new ScheduleTab to main navigation with ScheduleStackNavigator for managing course schedule screens. Includes CourseDetailScreen with modal presentation. Also exports scheduleService and related types for the new schedule feature. fix(message): add group_id parameter to invite and request handlers Pass group_id from extra_data when calling respondInvite and reviewJoinRequest APIs, as the backend now requires this parameter. refactor(message): extract mergeMessagesById helper method Centralize message merging logic into a reusable private method to avoid duplication and ensure consistent message deduplication behavior.
This commit is contained in:
239
src/screens/schedule/CourseDetailScreen.tsx
Normal file
239
src/screens/schedule/CourseDetailScreen.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } 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';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import type { ScheduleStackParamList } from '../../navigation/types';
|
||||
import { colors, spacing, borderRadius, fontSizes, shadows } from '../../theme';
|
||||
import { scheduleService } from '../../services/scheduleService';
|
||||
|
||||
type CourseDetailRouteProp = RouteProp<ScheduleStackParamList, 'CourseDetail'>;
|
||||
|
||||
const WEEKDAY_NAMES = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
const getMergedSectionIndex = (section: number) => Math.ceil(section / 2);
|
||||
|
||||
const formatWeekRanges = (weeks: number[]) => {
|
||||
if (weeks.length === 0) return '';
|
||||
const sorted = [...weeks].sort((a, b) => a - b);
|
||||
const ranges: string[] = [];
|
||||
let start = sorted[0];
|
||||
let prev = sorted[0];
|
||||
|
||||
for (let i = 1; i < sorted.length; i += 1) {
|
||||
const current = sorted[i];
|
||||
if (current === prev + 1) {
|
||||
prev = current;
|
||||
continue;
|
||||
}
|
||||
ranges.push(start === prev ? `${start}` : `${start}-${prev}`);
|
||||
start = current;
|
||||
prev = current;
|
||||
}
|
||||
ranges.push(start === prev ? `${start}` : `${start}-${prev}`);
|
||||
return ranges.join(', ');
|
||||
};
|
||||
|
||||
const CourseDetailScreen: React.FC = () => {
|
||||
const route = useRoute<CourseDetailRouteProp>();
|
||||
const navigation = useNavigation<NativeStackNavigationProp<ScheduleStackParamList>>();
|
||||
const { course, relatedCourses } = route.params;
|
||||
|
||||
const timeLines = relatedCourses.length > 0 ? relatedCourses : [course];
|
||||
|
||||
const handleDeleteCourse = () => {
|
||||
Alert.alert('删除课程', '确认删除这条课程安排吗?', [
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '删除',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await scheduleService.deleteCourse(course.id);
|
||||
Alert.alert('删除成功', '课程已删除');
|
||||
navigation.goBack();
|
||||
} catch (error) {
|
||||
console.error('删除课程失败:', error);
|
||||
Alert.alert('删除失败', '删除课程失败,请稍后重试');
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => navigation.goBack()}>
|
||||
<TouchableOpacity activeOpacity={1} style={styles.card} onPress={e => e.stopPropagation()}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<View style={styles.iconBadge}>
|
||||
<MaterialCommunityIcons name="book-open-page-variant" size={18} color={colors.primary.main} />
|
||||
</View>
|
||||
<Text style={styles.title}>课程详情</Text>
|
||||
</View>
|
||||
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.closeButton} hitSlop={10}>
|
||||
<MaterialCommunityIcons name="close" size={18} color={colors.text.secondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.label}>课程名称</Text>
|
||||
<Text style={styles.value} numberOfLines={2}>{course.name}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.label}>教师</Text>
|
||||
<Text style={styles.value}>{course.teacher || '未填写'}</Text>
|
||||
</View>
|
||||
|
||||
<View style={[styles.row, styles.rowLast]}>
|
||||
<Text style={styles.label}>上课时间</Text>
|
||||
<View style={styles.timeList}>
|
||||
{timeLines.map((item, index) => (
|
||||
<View key={`${item.id}-${index}`} style={styles.timeItemCard}>
|
||||
<Text style={styles.timeItemMain}>
|
||||
第{formatWeekRanges(item.weeks)}周 · {WEEKDAY_NAMES[item.dayOfWeek]} 第
|
||||
{getMergedSectionIndex(item.startSection)}节
|
||||
</Text>
|
||||
{item.location ? <Text style={styles.timeItemSub}>{item.location}</Text> : null}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
<TouchableOpacity style={styles.deleteButton} onPress={handleDeleteCourse} activeOpacity={0.85}>
|
||||
<MaterialCommunityIcons name="delete-outline" size={16} color={colors.error.main} />
|
||||
<Text style={styles.deleteButtonText}>删除本条课程</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
mask: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.32)',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.xl,
|
||||
paddingVertical: spacing.lg,
|
||||
paddingHorizontal: spacing.lg,
|
||||
...shadows.lg,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: `${colors.divider}AA`,
|
||||
paddingBottom: spacing.md,
|
||||
},
|
||||
headerLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
iconBadge: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: `${colors.primary.main}12`,
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
title: {
|
||||
fontSize: fontSizes.xl,
|
||||
fontWeight: '700',
|
||||
color: colors.text.primary,
|
||||
},
|
||||
closeButton: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: borderRadius.full,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
row: {
|
||||
marginBottom: spacing.lg,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
rowLast: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
label: {
|
||||
width: 76,
|
||||
fontSize: fontSizes.md,
|
||||
fontWeight: '600',
|
||||
color: colors.text.secondary,
|
||||
lineHeight: 22,
|
||||
marginTop: 1,
|
||||
},
|
||||
value: {
|
||||
flex: 1,
|
||||
fontSize: fontSizes.lg,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
lineHeight: 24,
|
||||
},
|
||||
timeList: {
|
||||
flex: 1,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
timeItemCard: {
|
||||
borderWidth: 1,
|
||||
borderColor: `${colors.primary.main}20`,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: `${colors.primary.main}08`,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
timeItemMain: {
|
||||
fontSize: fontSizes.md,
|
||||
fontWeight: '600',
|
||||
color: colors.text.primary,
|
||||
lineHeight: 20,
|
||||
},
|
||||
timeItemSub: {
|
||||
marginTop: spacing.xs,
|
||||
fontSize: fontSizes.sm,
|
||||
color: colors.text.secondary,
|
||||
lineHeight: 18,
|
||||
},
|
||||
actionRow: {
|
||||
marginTop: spacing.lg,
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
deleteButton: {
|
||||
height: 38,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderRadius: borderRadius.full,
|
||||
backgroundColor: `${colors.error.main}10`,
|
||||
borderWidth: 1,
|
||||
borderColor: `${colors.error.main}25`,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
deleteButtonText: {
|
||||
fontSize: fontSizes.sm,
|
||||
fontWeight: '700',
|
||||
color: colors.error.main,
|
||||
},
|
||||
});
|
||||
|
||||
export default CourseDetailScreen;
|
||||
1065
src/screens/schedule/ScheduleScreen.tsx
Normal file
1065
src/screens/schedule/ScheduleScreen.tsx
Normal file
File diff suppressed because it is too large
Load Diff
7
src/screens/schedule/index.ts
Normal file
7
src/screens/schedule/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 课程表模块导出
|
||||
*/
|
||||
|
||||
export { default as ScheduleScreen } from './ScheduleScreen';
|
||||
export { default as CourseDetailScreen } from './CourseDetailScreen';
|
||||
export { default } from './ScheduleScreen';
|
||||
Reference in New Issue
Block a user