/** * 课程表主屏幕 * 参考哈课表APP的设计风格 */ import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Dimensions, StatusBar, Modal, TextInput, Alert, ActivityIndicator, } from 'react-native'; import { PanGestureHandler, State } from 'react-native-gesture-handler'; import type { PanGestureHandlerStateChangeEvent } from 'react-native-gesture-handler'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useFocusEffect } from '@react-navigation/native'; import { useRouter } from 'expo-router'; import { colors, fontSizes, spacing, borderRadius, shadows } from '../../theme'; import { useResponsive } from '../../hooks/useResponsive'; import { Course, COURSE_COLORS, TimeSlot, getCourseColor, hasCourseInWeek, } from '../../types/schedule'; import * as hrefs from '../../navigation/hrefs'; import { routePayloadCache } from '../../stores/routePayloadCache'; import { scheduleService } from '../../services/scheduleService'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); // 时间段列宽度(按需求缩短) const TIME_COLUMN_WIDTH = 38; const VISIBLE_DAY_VALUES = [0, 1, 2, 3, 4, 5, 6]; // 周一到周日(0=周一) const VISIBLE_WEEKDAY_NAMES = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; // 星期列宽度(根据屏幕宽度自动计算) const getDayColumnWidth = () => Math.max(46, (SCREEN_WIDTH - TIME_COLUMN_WIDTH - 2) / VISIBLE_DAY_VALUES.length); // 每个大节(两小节)高度 - 默认值,会在组件中动态调整 const DEFAULT_SECTION_HEIGHT = 105; // 顶部周选择器高度 const WEEK_SELECTOR_HEIGHT = 44; // 星期标题行高度 const HEADER_HEIGHT = 40; // 悬浮TabBar高度(TabBar高度64 + 浮动间距12*2 = 88) const FLOATING_TAB_BAR_HEIGHT = 88; // 大节时间配置(左侧显示 1-6) const GROUPED_TIME_SLOTS: TimeSlot[] = [ { section: 1, startTime: '8:00', endTime: '9:45' }, { section: 2, startTime: '10:05', endTime: '11:50' }, { section: 3, startTime: '14:00', endTime: '15:45' }, { section: 4, startTime: '16:05', endTime: '17:50' }, { section: 5, startTime: '18:40', endTime: '20:25' }, { section: 6, startTime: '20:45', endTime: '22:30' }, ]; const getMergedSectionIndex = (section: number) => Math.ceil(section / 2); const MAX_COURSE_NAME_CHARS = 11; const formatCourseName = (name: string) => name.length > MAX_COURSE_NAME_CHARS ? `${name.slice(0, MAX_COURSE_NAME_CHARS)}…` : name; const MERGED_SECTION_COUNT = GROUPED_TIME_SLOTS.length; const getMergedSectionStart = (mergedSection: number) => (mergedSection - 1) * 2 + 1; 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) => { const parsed = Number.parseInt(value, 10); if (Number.isNaN(parsed)) return fallback; return Math.min(Math.max(parsed, 1), TOTAL_WEEKS); }; const normalizeHexColor = (value: string) => { const trimmed = value.trim(); if (!trimmed) return ''; if (!HEX_COLOR_REGEX.test(trimmed)) return ''; return (trimmed.startsWith('#') ? trimmed : `#${trimmed}`).toUpperCase(); }; // 获取今天在课表中的列索引(周一=0 ... 周日=6) const getTodayColumnIndex = () => { const day = new Date().getDay(); // day: 0=周日, 1=周一, ..., 6=周六 // 映射到: 周一=0, 周二=1, ..., 周六=5, 周日=6 if (day === 0) return 6; // 周日在最后 return day - 1; }; // 第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) => { // 计算该周的周一日期 const monday = new Date(FIRST_WEEK_START); monday.setDate(monday.getDate() + weekIndex * 7); const dates = []; for (let i = 0; i < 7; i++) { const date = new Date(monday); date.setDate(monday.getDate() + i); dates.push(date.getDate()); } // 获取该周周一所在的月份 const month = monday.getMonth() + 1; return { dates, month }; }; // 为了向后兼容,保留 getWeekDates 函数 const getWeekDates = (weekOffset: number = 0) => { return getWeekInfo(weekOffset).dates; }; // 获取初始周数(仅在组件初始化时计算一次) const INITIAL_WEEK = getCurrentWeekNumber(); export const ScheduleScreen: React.FC = () => { const router = useRouter(); // 使用响应式 hook 检测屏幕尺寸 const { width: screenWidth, height: screenHeight, isMobile, platform } = useResponsive(); const insets = useSafeAreaInsets(); 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]); // 动态计算每节课的高度 // 手机端:第六节课底部刚好在悬浮TabBar上方(可用高度 = 屏幕高度 - 周选择器 - 星期标题 - 悬浮TabBar - 安全区域) // 电脑端:第六节课刚好填到底部(可用高度 = 屏幕高度 - 周选择器 - 星期标题) const sectionHeight = useMemo((): number => { const totalHeaderHeight = WEEK_SELECTOR_HEIGHT + HEADER_HEIGHT; // 悬浮TabBar高度 + 安全区域底部 const bottomOffset = isMobile ? FLOATING_TAB_BAR_HEIGHT + insets.bottom : 0; // 可用高度 = 屏幕高度 - 顶部区域 - 底部偏移 const availableHeight = screenHeight - totalHeaderHeight - bottomOffset; // 6节课,每节课高度 = 可用高度 / 6 const calculatedHeight = Math.floor(availableHeight / 6); // 确保高度不低于默认值 return Math.max(DEFAULT_SECTION_HEIGHT, calculatedHeight); }, [screenHeight, isMobile, insets.bottom]); const [isAddModalVisible, setIsAddModalVisible] = useState(false); const [pendingDay, setPendingDay] = useState(0); const [pendingMergedSection, setPendingMergedSection] = useState(1); const [newCourseName, setNewCourseName] = useState(''); const [newCourseTeacher, setNewCourseTeacher] = useState(''); const [newCourseLocation, setNewCourseLocation] = useState(''); const [newCourseColor, setNewCourseColor] = useState(''); const [repeatMode, setRepeatMode] = useState('single'); const [startWeekInput, setStartWeekInput] = useState(String(currentWeek)); const [endWeekInput, setEndWeekInput] = useState(String(TOTAL_WEEKS)); const weekScrollRef = useRef(null); // 组件卸载状态标记,用于防止组件卸载后仍调用 setState const isMountedRef = useRef(true); // 设置弹窗状态 const [isSettingsModalVisible, setIsSettingsModalVisible] = useState(false); // 同步教务系统弹窗状态 const [isSyncModalVisible, setIsSyncModalVisible] = useState(false); const [syncUsername, setSyncUsername] = useState(''); const [syncPassword, setSyncPassword] = useState(''); const [isSyncing, setIsSyncing] = useState(false); // 组件卸载时设置标记 useEffect(() => { return () => { isMountedRef.current = false; }; }, []); const todayColumnIndex = getTodayColumnIndex(); // 当前正在查看的周是否为真实的当前周 const isViewingCurrentWeek = currentWeek === INITIAL_WEEK; const activeTodayColumn = isViewingCurrentWeek ? todayColumnIndex : -1; const usedCourseColors = useMemo( () => new Set(courses.map(item => normalizeHexColor(item.color || '')).filter(Boolean)), [courses] ); const suggestColor = useCallback(() => { const firstAvailable = COURSE_COLORS.find(color => !usedCourseColors.has(normalizeHexColor(color))); if (firstAvailable) return normalizeHexColor(firstAvailable); return ''; }, [usedCourseColors]); const loadCourses = useCallback(async () => { try { const remoteCourses = await scheduleService.getCourses(); setCourses(remoteCourses); } catch (error) { console.error('加载课表失败:', error); Alert.alert('加载失败', '获取课表数据失败,请稍后重试'); } }, []); // 生成周数列表 const weeks = useMemo(() => { return Array.from({ length: TOTAL_WEEKS }, (_, i) => i + 1); }, []); useEffect(() => { const WEEK_ITEM_ESTIMATED_WIDTH = 92; const centerOffset = SCREEN_WIDTH / 2 - WEEK_ITEM_ESTIMATED_WIDTH / 2 - 40; const targetX = Math.max(0, (currentWeek - 1) * WEEK_ITEM_ESTIMATED_WIDTH - centerOffset); weekScrollRef.current?.scrollTo({ x: targetX, animated: true }); }, [currentWeek]); useFocusEffect( useCallback(() => { loadCourses(); }, [loadCourses]) ); // 获取当前周的课程 const getCurrentWeekCourses = useCallback(() => { return courses.filter(course => hasCourseInWeek(course, currentWeek)); }, [courses, currentWeek]); const getRelatedCourses = useCallback( (targetCourse: Course) => { return courses .filter(course => course.name === targetCourse.name) .sort((a, b) => { if (a.dayOfWeek !== b.dayOfWeek) return a.dayOfWeek - b.dayOfWeek; return a.startSection - b.startSection; }); }, [courses] ); const handleWeekSwipe = useCallback( ({ nativeEvent }: PanGestureHandlerStateChangeEvent) => { if (nativeEvent.oldState !== State.ACTIVE && nativeEvent.oldState !== State.BEGAN) return; const { translationX, translationY } = nativeEvent; if (Math.abs(translationX) < SWIPE_THRESHOLD) return; if (Math.abs(translationX) <= Math.abs(translationY) * 0.8) return; if (translationX < 0) { setCurrentWeek(prev => Math.min(prev + 1, weeks.length)); } else { setCurrentWeek(prev => Math.max(prev - 1, 1)); } }, [weeks.length] ); const isSlotOccupied = useCallback( (dayOfWeek: number, mergedSection: number, weekCourses: Course[]) => { return weekCourses.some(course => { if (course.dayOfWeek !== dayOfWeek) return false; const start = getMergedSectionIndex(course.startSection); const end = getMergedSectionIndex(course.endSection); return mergedSection >= start && mergedSection <= end; }); }, [] ); const openAddCourseModal = useCallback((dayOfWeek: number, mergedSection: number) => { setPendingDay(dayOfWeek); setPendingMergedSection(mergedSection); setNewCourseName(''); setNewCourseTeacher(''); setNewCourseLocation(''); setNewCourseColor(suggestColor()); setRepeatMode('single'); setStartWeekInput(String(currentWeek)); setEndWeekInput(String(TOTAL_WEEKS)); setIsAddModalVisible(true); }, [currentWeek, suggestColor]); const closeAddCourseModal = useCallback(() => { setIsAddModalVisible(false); }, []); const confirmAddCourse = useCallback(async () => { const name = newCourseName.trim(); if (!name) return; const startSection = getMergedSectionStart(pendingMergedSection); const endSection = getMergedSectionEnd(pendingMergedSection); const startWeek = parseWeekInput(startWeekInput, currentWeek); const endWeek = parseWeekInput(endWeekInput, TOTAL_WEEKS); const normalizedStartWeek = Math.min(startWeek, endWeek); const normalizedEndWeek = Math.max(startWeek, endWeek); const repeatWeeks = weeks.filter(week => { if (week < normalizedStartWeek || week > normalizedEndWeek) return false; if (repeatMode === 'single') return week === currentWeek; if (repeatMode === 'weekly') return true; if (repeatMode === 'odd') return week % 2 === 1; return week % 2 === 0; }); const normalizedColor = normalizeHexColor(newCourseColor); if (!normalizedColor) { Alert.alert('颜色无效', '请输入有效的颜色值,例如 #FF6B6B'); return; } if (usedCourseColors.has(normalizedColor)) { Alert.alert('颜色重复', '该颜色已被其他课程使用,请更换颜色'); return; } try { const created = await scheduleService.createCourse({ name, teacher: newCourseTeacher.trim() || undefined, location: newCourseLocation.trim() || undefined, day_of_week: pendingDay, start_section: startSection, end_section: endSection, weeks: repeatWeeks, color: normalizedColor, }); setCourses(prev => [...prev, created]); closeAddCourseModal(); } catch (error) { console.error('新增课程失败:', error); Alert.alert('保存失败', '课程保存失败,请稍后重试'); } }, [ closeAddCourseModal, currentWeek, newCourseLocation, newCourseName, newCourseTeacher, newCourseColor, pendingDay, pendingMergedSection, repeatMode, startWeekInput, usedCourseColors, endWeekInput, weeks, ]); // 渲染周选择器 const renderWeekSelector = () => ( setIsSettingsModalVisible(true)} > {weeks.map(week => ( setCurrentWeek(week)} > 第{week}周 ))} ); // 渲染星期标题行 const renderHeader = () => { const { month: currentMonth, dates: weekDates } = getWeekInfo(currentWeek - 1); return ( {/* 左上角月份显示 */} {currentMonth}月 {/* 星期标题 */} {VISIBLE_WEEKDAY_NAMES.map((day, index) => ( {day} {weekDates[index]} ))} ); }; // 渲染时间段列 const renderTimeColumn = () => ( {GROUPED_TIME_SLOTS.map(slot => ( {slot.section} {slot.startTime} {'\n|\n'} {slot.endTime} ))} ); // 处理长按课程删除 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 { // 使用 Promise.allSettled 批量删除,即使部分失败也能继续 const results = await Promise.allSettled( relatedCourses.map(course => scheduleService.deleteCourse(course.id)) ); // 统计成功和失败的数量 const fulfilledCount = results.filter(r => r.status === 'fulfilled').length; const rejectedCount = results.filter(r => r.status === 'rejected').length; // 只移除成功删除的课程 const successfullyDeletedIds = new Set( relatedCourses .filter((_, index) => results[index].status === 'fulfilled') .map(c => c.id) ); setCourses(prev => prev.filter(c => !successfullyDeletedIds.has(c.id))); if (rejectedCount > 0) { Alert.alert( '部分删除成功', `成功删除 ${fulfilledCount} 节课程,${rejectedCount} 节删除失败,请重试` ); } else { Alert.alert('成功', `已删除 ${fulfilledCount} 节课程`); } } 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 = ({ 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) * sectionHeight; const height = duration * sectionHeight - 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 ( { const related = getRelatedCourses(course); routePayloadCache.stashCourseDetail(course, related); router.push(hrefs.hrefScheduleCourse(String(course.id))); }} onLongPress={() => handleCourseLongPress(course)} delayLongPress={500} activeOpacity={0.82} > {formatCourseName(course.name)} {locationText ? ( @{locationText} ) : null} ); }; // 渲染单日课程列 const renderDayColumn = (dayOfWeek: number, columnIndex: number, isLastColumn: boolean) => { const currentWeekCourses = getCurrentWeekCourses(); const dayCourses = currentWeekCourses.filter( course => course.dayOfWeek === dayOfWeek ); const displayCourses = buildDayDisplayCourses(dayCourses); return ( {/* 课程网格线 */} {GROUPED_TIME_SLOTS.map(slot => { const occupied = isSlotOccupied(dayOfWeek, slot.section, currentWeekCourses); return ( { if (!occupied) { openAddCourseModal(dayOfWeek, slot.section); } }} /> ); })} {/* 课程卡片 */} {displayCourses.map(renderCourseCard)} ); }; // 渲染课程表主体 const renderScheduleBody = () => { // 移动端和电脑端都不添加额外底部间距,让内容填满整个区域 // 动态计算的高度已经确保第六节课显示在正确位置 return ( {renderTimeColumn()} {VISIBLE_DAY_VALUES.map((dayOfWeek, index) => renderDayColumn(dayOfWeek, index, index === VISIBLE_DAY_VALUES.length - 1) )} ); }; return ( {/* 周选择器 */} {renderWeekSelector()} {/* 星期标题 */} {renderHeader()} {/* 课程表主体 */} {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 (!isMountedRef.current) return; 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); // 检查组件是否已卸载 if (isMountedRef.current) { Alert.alert('同步失败', '网络错误,请稍后重试'); } } finally { // 检查组件是否已卸载 if (isMountedRef.current) { setIsSyncing(false); } } }} activeOpacity={0.8} disabled={!syncUsername.trim() || !syncPassword.trim() || isSyncing} > {isSyncing ? '同步中...' : '开始同步'} 添加课程 第{currentWeek}周 · {VISIBLE_WEEKDAY_NAMES[pendingDay]} · 第{pendingMergedSection}大节 课程颜色(不可重复) {COURSE_COLORS.filter(color => !usedCourseColors.has(normalizeHexColor(color))).map(color => { const normalized = normalizeHexColor(color); const selected = normalizeHexColor(newCourseColor) === normalized; return ( setNewCourseColor(normalized)} activeOpacity={0.85} > {selected ? : null} ); })} {repeatMode !== 'single' ? ( 起始周 结束周 ) : null} 重复 setRepeatMode('single')} activeOpacity={0.8} > 仅本周 setRepeatMode('weekly')} activeOpacity={0.8} > 每周 setRepeatMode('odd')} activeOpacity={0.8} > 单周 setRepeatMode('even')} activeOpacity={0.8} > 双周 {repeatMode !== 'single' ? ( 在第{parseWeekInput(startWeekInput, currentWeek)}周到第{parseWeekInput(endWeekInput, TOTAL_WEEKS)}周生效 ) : null} 取消 保存 ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background.paper, }, // ── 周选择器 ────────────────────────────────────────── weekSelector: { height: WEEK_SELECTOR_HEIGHT, backgroundColor: colors.primary.main, flexDirection: 'row', alignItems: 'center', }, settingsButton: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center', marginLeft: spacing.xs, }, weekList: { paddingHorizontal: spacing.sm, alignItems: 'center', }, weekItem: { paddingHorizontal: spacing.md, paddingVertical: 5, marginHorizontal: 3, borderRadius: borderRadius.full, }, weekItemActive: { backgroundColor: 'rgba(255,255,255,0.28)', }, weekItemText: { fontSize: fontSizes.sm, fontWeight: '500', color: 'rgba(255,255,255,0.75)', letterSpacing: 0.2, }, weekItemTextActive: { color: colors.text.inverse, fontWeight: '700', }, // ── 星期标题行 ───────────────────────────────────────── header: { flexDirection: 'row', height: HEADER_HEIGHT, backgroundColor: colors.background.paper, borderBottomWidth: 1, borderBottomColor: colors.divider, }, timeHeader: { borderRightWidth: 1, borderRightColor: colors.divider, alignItems: 'center', justifyContent: 'center', }, monthText: { fontSize: fontSizes.xs, fontWeight: '700', color: colors.primary.main, letterSpacing: 0.3, }, dayHeader: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingVertical: 3, }, dayHeaderText: { fontSize: fontSizes.xs, fontWeight: '500', color: colors.text.secondary, letterSpacing: 0.3, }, dayHeaderTextActive: { color: colors.primary.main, fontWeight: '700', }, todayDateBadge: { width: 22, height: 22, borderRadius: borderRadius.full, backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', marginTop: 2, }, dateText: { fontSize: fontSizes.xs, color: colors.text.secondary, marginTop: 2, fontWeight: '400', }, dateTextActive: { color: colors.text.inverse, fontWeight: '700', marginTop: 0, }, // ── 课程表主体 ───────────────────────────────────────── scheduleBody: { flex: 1, backgroundColor: colors.background.default, }, scheduleBodyGestureLayer: { flex: 1, }, scheduleBodyContent: { flexDirection: 'row', }, // ── 时间列 ──────────────────────────────────────────── timeColumn: { backgroundColor: colors.background.paper, borderRightWidth: StyleSheet.hairlineWidth, borderRightColor: colors.divider, }, timeCell: { alignItems: 'center', justifyContent: 'center', borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#EBEBEB', gap: 4, }, timeSectionBadge: { width: 22, height: 22, borderRadius: borderRadius.full, backgroundColor: `${colors.primary.main}12`, alignItems: 'center', justifyContent: 'center', }, timeSection: { fontSize: fontSizes.xs, fontWeight: '700', color: colors.primary.dark, }, timeText: { fontSize: 9, color: colors.text.hint, fontWeight: '400', textAlign: 'center', lineHeight: 12, }, // ── 日期列容器 ──────────────────────────────────────── daysContainer: { flexDirection: 'row', flex: 1, backgroundColor: colors.background.default, minWidth: TIME_COLUMN_WIDTH, // 确保最小宽度 }, // ── 单日列 ──────────────────────────────────────────── dayColumn: { flex: 1, backgroundColor: colors.background.default, // 使用主题默认背景色 borderRightWidth: 1, borderRightColor: '#EBEBEB', }, gridCell: { borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#EBEBEB', }, // ── 课程卡片 ────────────────────────────────────────── courseCard: { position: 'absolute', borderRadius: borderRadius.xl, paddingVertical: spacing.xs + 2, paddingHorizontal: spacing.xs + 1, ...shadows.md, }, courseName: { fontSize: 10, fontWeight: '700', color: colors.text.inverse, lineHeight: 13, letterSpacing: 0.1, }, courseLocation: { fontSize: 9, color: 'rgba(255,255,255,0.80)', marginTop: 3, fontWeight: '500', letterSpacing: 0.1, }, courseTeacher: { fontSize: 9, color: 'rgba(255,255,255,0.75)', marginTop: 2, fontWeight: '400', }, // ── 添加课程弹窗 ────────────────────────────────────── addModalMask: { flex: 1, backgroundColor: 'rgba(0,0,0,0.40)', justifyContent: 'center', paddingHorizontal: spacing.lg, }, addModalCard: { backgroundColor: colors.background.paper, borderRadius: borderRadius['2xl'], padding: spacing.xl, ...shadows.lg, }, addModalTitle: { fontSize: fontSizes.xl, fontWeight: '700', color: colors.text.primary, letterSpacing: 0.3, }, addModalDesc: { marginTop: spacing.xs, marginBottom: spacing.lg, fontSize: fontSizes.sm, color: colors.text.secondary, fontWeight: '500', }, addInput: { height: 46, borderWidth: 0, borderRadius: borderRadius.lg, paddingHorizontal: spacing.md, marginBottom: spacing.sm, fontSize: fontSizes.md, color: colors.text.primary, backgroundColor: colors.background.default, }, colorSection: { marginBottom: spacing.sm, }, colorPresetRow: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.xs, marginBottom: spacing.sm, }, colorPresetItem: { width: 24, height: 24, borderRadius: borderRadius.full, alignItems: 'center', justifyContent: 'center', }, colorPresetItemActive: { borderWidth: 2, borderColor: colors.text.inverse, }, addActions: { marginTop: spacing.md, flexDirection: 'row', justifyContent: 'flex-end', gap: spacing.sm, }, addCancelBtn: { height: 42, paddingHorizontal: spacing.xl, borderRadius: borderRadius.full, backgroundColor: colors.background.default, alignItems: 'center', justifyContent: 'center', }, addCancelText: { color: colors.text.secondary, fontSize: fontSizes.md, fontWeight: '600', }, addConfirmBtn: { height: 42, paddingHorizontal: spacing.xl, borderRadius: borderRadius.full, backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', }, addConfirmBtnDisabled: { backgroundColor: colors.primary.light, opacity: 0.7, }, addConfirmText: { color: colors.text.inverse, fontSize: fontSizes.md, fontWeight: '700', letterSpacing: 0.3, }, // ── 重复选项 ────────────────────────────────────────── repeatSection: { marginBottom: spacing.sm, }, repeatLabel: { fontSize: fontSizes.sm, fontWeight: '600', color: colors.text.secondary, marginBottom: spacing.xs, letterSpacing: 0.2, }, repeatOptions: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.xs, }, repeatChip: { paddingHorizontal: spacing.md, height: 32, borderRadius: borderRadius.full, borderWidth: 1.5, borderColor: '#E8E8E8', backgroundColor: colors.background.paper, justifyContent: 'center', alignItems: 'center', }, repeatChipActive: { borderColor: colors.primary.main, backgroundColor: `${colors.primary.main}15`, }, repeatChipText: { fontSize: fontSizes.sm, color: colors.text.secondary, fontWeight: '600', }, repeatChipTextActive: { color: colors.primary.main, fontWeight: '700', }, repeatHint: { marginTop: spacing.xs, fontSize: fontSizes.xs, color: colors.text.hint, fontWeight: '400', }, // ── 周范围输入 ──────────────────────────────────────── weekRangeRow: { flexDirection: 'row', gap: spacing.sm, marginBottom: spacing.sm, }, weekRangeItem: { flex: 1, }, weekRangeLabel: { fontSize: fontSizes.sm, fontWeight: '600', color: colors.text.secondary, marginBottom: spacing.xs, letterSpacing: 0.2, }, weekRangeInput: { height: 42, borderWidth: 0, borderRadius: borderRadius.lg, paddingHorizontal: spacing.md, fontSize: fontSizes.md, color: colors.text.primary, 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;