1066 lines
33 KiB
TypeScript
1066 lines
33 KiB
TypeScript
|
|
/**
|
|||
|
|
* 课程表主屏幕
|
|||
|
|
* 参考哈课表APP的设计风格
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|||
|
|
import {
|
|||
|
|
View,
|
|||
|
|
Text,
|
|||
|
|
StyleSheet,
|
|||
|
|
ScrollView,
|
|||
|
|
TouchableOpacity,
|
|||
|
|
Dimensions,
|
|||
|
|
StatusBar,
|
|||
|
|
Modal,
|
|||
|
|
TextInput,
|
|||
|
|
Alert,
|
|||
|
|
} from 'react-native';
|
|||
|
|
import { PanGestureHandler, State } from 'react-native-gesture-handler';
|
|||
|
|
import type { PanGestureHandlerStateChangeEvent } from 'react-native-gesture-handler';
|
|||
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|||
|
|
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 {
|
|||
|
|
Course,
|
|||
|
|
COURSE_COLORS,
|
|||
|
|
TimeSlot,
|
|||
|
|
getCourseColor,
|
|||
|
|
hasCourseInWeek,
|
|||
|
|
} from '../../types/schedule';
|
|||
|
|
import type { ScheduleStackParamList } from '../../navigation/types';
|
|||
|
|
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 SECTION_HEIGHT = 105;
|
|||
|
|
// 顶部周选择器高度
|
|||
|
|
const WEEK_SELECTOR_HEIGHT = 44;
|
|||
|
|
// 星期标题行高度
|
|||
|
|
const HEADER_HEIGHT = 40;
|
|||
|
|
// 底部Tab栏高度(用于避让)
|
|||
|
|
const TAB_BAR_HEIGHT = 80;
|
|||
|
|
|
|||
|
|
// 大节时间配置(左侧显示 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';
|
|||
|
|
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日
|
|||
|
|
|
|||
|
|
// 获取指定周的起始日期、每天的日期和月份
|
|||
|
|
// 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;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const ScheduleScreen: React.FC = () => {
|
|||
|
|
const navigation = useNavigation<NativeStackNavigationProp<ScheduleStackParamList>>();
|
|||
|
|
const [currentWeek, setCurrentWeek] = useState(1);
|
|||
|
|
const [courses, setCourses] = useState<Course[]>([]);
|
|||
|
|
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
|||
|
|
const [pendingDay, setPendingDay] = useState<number>(0);
|
|||
|
|
const [pendingMergedSection, setPendingMergedSection] = useState<number>(1);
|
|||
|
|
const [newCourseName, setNewCourseName] = useState('');
|
|||
|
|
const [newCourseTeacher, setNewCourseTeacher] = useState('');
|
|||
|
|
const [newCourseLocation, setNewCourseLocation] = useState('');
|
|||
|
|
const [newCourseColor, setNewCourseColor] = useState('');
|
|||
|
|
const [repeatMode, setRepeatMode] = useState<RepeatMode>('single');
|
|||
|
|
const [startWeekInput, setStartWeekInput] = useState(String(currentWeek));
|
|||
|
|
const [endWeekInput, setEndWeekInput] = useState(String(TOTAL_WEEKS));
|
|||
|
|
const weekScrollRef = useRef<ScrollView | null>(null);
|
|||
|
|
const todayColumnIndex = getTodayColumnIndex();
|
|||
|
|
// currentWeek === 1 对应今天所在的真实周(offset 0),其他周不高亮今日列
|
|||
|
|
const isViewingCurrentWeek = currentWeek === 1;
|
|||
|
|
const activeTodayColumn = isViewingCurrentWeek ? todayColumnIndex : -1;
|
|||
|
|
|
|||
|
|
const dayColumnWidth = getDayColumnWidth();
|
|||
|
|
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 = () => (
|
|||
|
|
<View style={styles.weekSelector}>
|
|||
|
|
<TouchableOpacity style={styles.settingsButton}>
|
|||
|
|
<MaterialCommunityIcons name="cog" size={24} color="#FFFFFF" />
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
<ScrollView
|
|||
|
|
ref={weekScrollRef}
|
|||
|
|
horizontal
|
|||
|
|
showsHorizontalScrollIndicator={false}
|
|||
|
|
contentContainerStyle={styles.weekList}
|
|||
|
|
>
|
|||
|
|
{weeks.map(week => (
|
|||
|
|
<TouchableOpacity
|
|||
|
|
key={week}
|
|||
|
|
style={[
|
|||
|
|
styles.weekItem,
|
|||
|
|
currentWeek === week && styles.weekItemActive,
|
|||
|
|
]}
|
|||
|
|
onPress={() => setCurrentWeek(week)}
|
|||
|
|
>
|
|||
|
|
<Text
|
|||
|
|
style={[
|
|||
|
|
styles.weekItemText,
|
|||
|
|
currentWeek === week && styles.weekItemTextActive,
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
第{week}周
|
|||
|
|
</Text>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
))}
|
|||
|
|
</ScrollView>
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 渲染星期标题行
|
|||
|
|
const renderHeader = () => {
|
|||
|
|
const { month: currentMonth, dates: weekDates } = getWeekInfo(currentWeek - 1);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View style={styles.header}>
|
|||
|
|
{/* 左上角月份显示 */}
|
|||
|
|
<View style={[styles.timeHeader, { width: TIME_COLUMN_WIDTH }]}>
|
|||
|
|
<Text style={styles.monthText}>{currentMonth}月</Text>
|
|||
|
|
</View>
|
|||
|
|
{/* 星期标题 */}
|
|||
|
|
{VISIBLE_WEEKDAY_NAMES.map((day, index) => (
|
|||
|
|
<View
|
|||
|
|
key={day}
|
|||
|
|
style={[
|
|||
|
|
styles.dayHeader,
|
|||
|
|
{
|
|||
|
|
width: dayColumnWidth,
|
|||
|
|
backgroundColor: index === activeTodayColumn ? `${colors.primary.main}10` : 'transparent',
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<Text
|
|||
|
|
style={[
|
|||
|
|
styles.dayHeaderText,
|
|||
|
|
index === activeTodayColumn && styles.dayHeaderTextActive,
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{day}
|
|||
|
|
</Text>
|
|||
|
|
<View style={index === activeTodayColumn ? styles.todayDateBadge : undefined}>
|
|||
|
|
<Text
|
|||
|
|
style={[
|
|||
|
|
styles.dateText,
|
|||
|
|
index === activeTodayColumn && styles.dateTextActive,
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{weekDates[index]}
|
|||
|
|
</Text>
|
|||
|
|
</View>
|
|||
|
|
</View>
|
|||
|
|
))}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 渲染时间段列
|
|||
|
|
const renderTimeColumn = () => (
|
|||
|
|
<View style={[styles.timeColumn, { width: TIME_COLUMN_WIDTH }]}>
|
|||
|
|
{GROUPED_TIME_SLOTS.map(slot => (
|
|||
|
|
<View
|
|||
|
|
key={slot.section}
|
|||
|
|
style={[
|
|||
|
|
styles.timeCell,
|
|||
|
|
{ height: SECTION_HEIGHT },
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
<View style={styles.timeSectionBadge}>
|
|||
|
|
<Text style={styles.timeSection}>{slot.section}</Text>
|
|||
|
|
</View>
|
|||
|
|
<Text style={styles.timeText}>
|
|||
|
|
{slot.startTime}
|
|||
|
|
{'\n|\n'}
|
|||
|
|
{slot.endTime}
|
|||
|
|
</Text>
|
|||
|
|
</View>
|
|||
|
|
))}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 渲染课程卡片
|
|||
|
|
const renderCourseCard = (course: Course) => {
|
|||
|
|
const color = getCourseColor(course.id, course.color);
|
|||
|
|
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;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<TouchableOpacity
|
|||
|
|
key={course.id}
|
|||
|
|
style={[
|
|||
|
|
styles.courseCard,
|
|||
|
|
{
|
|||
|
|
top,
|
|||
|
|
height,
|
|||
|
|
backgroundColor: color,
|
|||
|
|
left: 2,
|
|||
|
|
right: 2,
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
onPress={() =>
|
|||
|
|
navigation.navigate('CourseDetail', {
|
|||
|
|
course,
|
|||
|
|
relatedCourses: getRelatedCourses(course),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
activeOpacity={0.82}
|
|||
|
|
>
|
|||
|
|
<Text style={styles.courseName} numberOfLines={3}>
|
|||
|
|
{formatCourseName(course.name)}
|
|||
|
|
</Text>
|
|||
|
|
{course.location ? (
|
|||
|
|
<Text style={styles.courseLocation}>
|
|||
|
|
@{course.location}
|
|||
|
|
</Text>
|
|||
|
|
) : null}
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 渲染单日课程列
|
|||
|
|
const renderDayColumn = (dayOfWeek: number, columnIndex: number, isLastColumn: boolean) => {
|
|||
|
|
const currentWeekCourses = getCurrentWeekCourses();
|
|||
|
|
const dayCourses = currentWeekCourses.filter(
|
|||
|
|
course => course.dayOfWeek === dayOfWeek
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<View
|
|||
|
|
key={dayOfWeek}
|
|||
|
|
style={[
|
|||
|
|
styles.dayColumn,
|
|||
|
|
{
|
|||
|
|
width: dayColumnWidth,
|
|||
|
|
borderRightWidth: isLastColumn ? 0 : 1,
|
|||
|
|
backgroundColor: columnIndex === activeTodayColumn ? `${colors.primary.main}08` : 'transparent',
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{/* 课程网格线 */}
|
|||
|
|
{GROUPED_TIME_SLOTS.map(slot => {
|
|||
|
|
const occupied = isSlotOccupied(dayOfWeek, slot.section, currentWeekCourses);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<TouchableOpacity
|
|||
|
|
key={slot.section}
|
|||
|
|
style={[
|
|||
|
|
styles.gridCell,
|
|||
|
|
{ height: SECTION_HEIGHT },
|
|||
|
|
]}
|
|||
|
|
activeOpacity={1}
|
|||
|
|
delayLongPress={280}
|
|||
|
|
onLongPress={() => {
|
|||
|
|
if (!occupied) {
|
|||
|
|
openAddCourseModal(dayOfWeek, slot.section);
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
{/* 课程卡片 */}
|
|||
|
|
{dayCourses.map(renderCourseCard)}
|
|||
|
|
</View>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 渲染课程表主体
|
|||
|
|
const renderScheduleBody = () => (
|
|||
|
|
<PanGestureHandler
|
|||
|
|
activeOffsetX={[-8, 8]}
|
|||
|
|
failOffsetY={[-80, 80]}
|
|||
|
|
onHandlerStateChange={handleWeekSwipe}
|
|||
|
|
>
|
|||
|
|
<View style={styles.scheduleBodyGestureLayer}>
|
|||
|
|
<ScrollView
|
|||
|
|
style={styles.scheduleBody}
|
|||
|
|
showsVerticalScrollIndicator={true}
|
|||
|
|
contentContainerStyle={styles.scheduleBodyContent}
|
|||
|
|
>
|
|||
|
|
{renderTimeColumn()}
|
|||
|
|
<ScrollView
|
|||
|
|
horizontal
|
|||
|
|
showsHorizontalScrollIndicator={false}
|
|||
|
|
>
|
|||
|
|
<View style={styles.daysContainer}>
|
|||
|
|
{VISIBLE_DAY_VALUES.map((dayOfWeek, index) =>
|
|||
|
|
renderDayColumn(dayOfWeek, index, index === VISIBLE_DAY_VALUES.length - 1)
|
|||
|
|
)}
|
|||
|
|
</View>
|
|||
|
|
</ScrollView>
|
|||
|
|
</ScrollView>
|
|||
|
|
</View>
|
|||
|
|
</PanGestureHandler>
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SafeAreaView style={styles.container} edges={['top']}>
|
|||
|
|
<StatusBar barStyle="light-content" backgroundColor={colors.primary.main} />
|
|||
|
|
|
|||
|
|
{/* 周选择器 */}
|
|||
|
|
{renderWeekSelector()}
|
|||
|
|
|
|||
|
|
{/* 星期标题 */}
|
|||
|
|
{renderHeader()}
|
|||
|
|
|
|||
|
|
{/* 课程表主体 */}
|
|||
|
|
{renderScheduleBody()}
|
|||
|
|
|
|||
|
|
<Modal
|
|||
|
|
visible={isAddModalVisible}
|
|||
|
|
transparent
|
|||
|
|
animationType="fade"
|
|||
|
|
onRequestClose={closeAddCourseModal}
|
|||
|
|
>
|
|||
|
|
<View style={styles.addModalMask}>
|
|||
|
|
<View style={styles.addModalCard}>
|
|||
|
|
<Text style={styles.addModalTitle}>添加课程</Text>
|
|||
|
|
<Text style={styles.addModalDesc}>
|
|||
|
|
第{currentWeek}周 · {VISIBLE_WEEKDAY_NAMES[pendingDay]} · 第{pendingMergedSection}大节
|
|||
|
|
</Text>
|
|||
|
|
|
|||
|
|
<TextInput
|
|||
|
|
value={newCourseName}
|
|||
|
|
onChangeText={setNewCourseName}
|
|||
|
|
placeholder="课程名称(必填)"
|
|||
|
|
placeholderTextColor={colors.text.hint}
|
|||
|
|
style={styles.addInput}
|
|||
|
|
/>
|
|||
|
|
<TextInput
|
|||
|
|
value={newCourseTeacher}
|
|||
|
|
onChangeText={setNewCourseTeacher}
|
|||
|
|
placeholder="教师(选填)"
|
|||
|
|
placeholderTextColor={colors.text.hint}
|
|||
|
|
style={styles.addInput}
|
|||
|
|
/>
|
|||
|
|
<TextInput
|
|||
|
|
value={newCourseLocation}
|
|||
|
|
onChangeText={setNewCourseLocation}
|
|||
|
|
placeholder="地点(选填)"
|
|||
|
|
placeholderTextColor={colors.text.hint}
|
|||
|
|
style={styles.addInput}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<View style={styles.colorSection}>
|
|||
|
|
<Text style={styles.repeatLabel}>课程颜色(不可重复)</Text>
|
|||
|
|
<View style={styles.colorPresetRow}>
|
|||
|
|
{COURSE_COLORS.filter(color => !usedCourseColors.has(normalizeHexColor(color))).map(color => {
|
|||
|
|
const normalized = normalizeHexColor(color);
|
|||
|
|
const selected = normalizeHexColor(newCourseColor) === normalized;
|
|||
|
|
return (
|
|||
|
|
<TouchableOpacity
|
|||
|
|
key={color}
|
|||
|
|
style={[
|
|||
|
|
styles.colorPresetItem,
|
|||
|
|
{ backgroundColor: normalized || color },
|
|||
|
|
selected && styles.colorPresetItemActive,
|
|||
|
|
]}
|
|||
|
|
onPress={() => setNewCourseColor(normalized)}
|
|||
|
|
activeOpacity={0.85}
|
|||
|
|
>
|
|||
|
|
{selected ? <MaterialCommunityIcons name="check" size={12} color={colors.text.inverse} /> : null}
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</View>
|
|||
|
|
<TextInput
|
|||
|
|
value={newCourseColor}
|
|||
|
|
onChangeText={setNewCourseColor}
|
|||
|
|
placeholder="#FF6B6B(支持自定义)"
|
|||
|
|
placeholderTextColor={colors.text.hint}
|
|||
|
|
style={styles.addInput}
|
|||
|
|
autoCapitalize="characters"
|
|||
|
|
maxLength={7}
|
|||
|
|
/>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
{repeatMode !== 'single' ? (
|
|||
|
|
<View style={styles.weekRangeRow}>
|
|||
|
|
<View style={styles.weekRangeItem}>
|
|||
|
|
<Text style={styles.weekRangeLabel}>起始周</Text>
|
|||
|
|
<TextInput
|
|||
|
|
value={startWeekInput}
|
|||
|
|
onChangeText={setStartWeekInput}
|
|||
|
|
placeholder={String(currentWeek)}
|
|||
|
|
placeholderTextColor={colors.text.hint}
|
|||
|
|
style={styles.weekRangeInput}
|
|||
|
|
keyboardType="number-pad"
|
|||
|
|
maxLength={2}
|
|||
|
|
/>
|
|||
|
|
</View>
|
|||
|
|
<View style={styles.weekRangeItem}>
|
|||
|
|
<Text style={styles.weekRangeLabel}>结束周</Text>
|
|||
|
|
<TextInput
|
|||
|
|
value={endWeekInput}
|
|||
|
|
onChangeText={setEndWeekInput}
|
|||
|
|
placeholder={String(TOTAL_WEEKS)}
|
|||
|
|
placeholderTextColor={colors.text.hint}
|
|||
|
|
style={styles.weekRangeInput}
|
|||
|
|
keyboardType="number-pad"
|
|||
|
|
maxLength={2}
|
|||
|
|
/>
|
|||
|
|
</View>
|
|||
|
|
</View>
|
|||
|
|
) : null}
|
|||
|
|
|
|||
|
|
<View style={styles.repeatSection}>
|
|||
|
|
<Text style={styles.repeatLabel}>重复</Text>
|
|||
|
|
<View style={styles.repeatOptions}>
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={[styles.repeatChip, repeatMode === 'single' && styles.repeatChipActive]}
|
|||
|
|
onPress={() => setRepeatMode('single')}
|
|||
|
|
activeOpacity={0.8}
|
|||
|
|
>
|
|||
|
|
<Text style={[styles.repeatChipText, repeatMode === 'single' && styles.repeatChipTextActive]}>
|
|||
|
|
仅本周
|
|||
|
|
</Text>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={[styles.repeatChip, repeatMode === 'weekly' && styles.repeatChipActive]}
|
|||
|
|
onPress={() => setRepeatMode('weekly')}
|
|||
|
|
activeOpacity={0.8}
|
|||
|
|
>
|
|||
|
|
<Text style={[styles.repeatChipText, repeatMode === 'weekly' && styles.repeatChipTextActive]}>
|
|||
|
|
每周
|
|||
|
|
</Text>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={[styles.repeatChip, repeatMode === 'odd' && styles.repeatChipActive]}
|
|||
|
|
onPress={() => setRepeatMode('odd')}
|
|||
|
|
activeOpacity={0.8}
|
|||
|
|
>
|
|||
|
|
<Text style={[styles.repeatChipText, repeatMode === 'odd' && styles.repeatChipTextActive]}>
|
|||
|
|
单周
|
|||
|
|
</Text>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={[styles.repeatChip, repeatMode === 'even' && styles.repeatChipActive]}
|
|||
|
|
onPress={() => setRepeatMode('even')}
|
|||
|
|
activeOpacity={0.8}
|
|||
|
|
>
|
|||
|
|
<Text style={[styles.repeatChipText, repeatMode === 'even' && styles.repeatChipTextActive]}>
|
|||
|
|
双周
|
|||
|
|
</Text>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
</View>
|
|||
|
|
{repeatMode !== 'single' ? (
|
|||
|
|
<Text style={styles.repeatHint}>
|
|||
|
|
在第{parseWeekInput(startWeekInput, currentWeek)}周到第{parseWeekInput(endWeekInput, TOTAL_WEEKS)}周生效
|
|||
|
|
</Text>
|
|||
|
|
) : null}
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
<View style={styles.addActions}>
|
|||
|
|
<TouchableOpacity style={styles.addCancelBtn} onPress={closeAddCourseModal} activeOpacity={0.8}>
|
|||
|
|
<Text style={styles.addCancelText}>取消</Text>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={[styles.addConfirmBtn, !newCourseName.trim() && styles.addConfirmBtnDisabled]}
|
|||
|
|
onPress={confirmAddCourse}
|
|||
|
|
activeOpacity={0.8}
|
|||
|
|
disabled={!newCourseName.trim()}
|
|||
|
|
>
|
|||
|
|
<Text style={styles.addConfirmText}>保存</Text>
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
</View>
|
|||
|
|
</View>
|
|||
|
|
</View>
|
|||
|
|
</Modal>
|
|||
|
|
</SafeAreaView>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
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: StyleSheet.hairlineWidth,
|
|||
|
|
borderBottomColor: colors.divider,
|
|||
|
|
},
|
|||
|
|
timeHeader: {
|
|||
|
|
borderRightWidth: StyleSheet.hairlineWidth,
|
|||
|
|
borderRightColor: colors.divider,
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
},
|
|||
|
|
monthText: {
|
|||
|
|
fontSize: fontSizes.xs,
|
|||
|
|
fontWeight: '700',
|
|||
|
|
color: colors.primary.main,
|
|||
|
|
letterSpacing: 0.3,
|
|||
|
|
},
|
|||
|
|
dayHeader: {
|
|||
|
|
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,
|
|||
|
|
marginBottom: TAB_BAR_HEIGHT,
|
|||
|
|
},
|
|||
|
|
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',
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// ── 单日列 ────────────────────────────────────────────
|
|||
|
|
dayColumn: {
|
|||
|
|
borderRightWidth: StyleSheet.hairlineWidth,
|
|||
|
|
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,
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
export default ScheduleScreen;
|