diff --git a/TeachMate/ContentView.swift b/TeachMate/ContentView.swift deleted file mode 100644 index 2ea0f3f..0000000 --- a/TeachMate/ContentView.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// ContentView.swift -// TeachMate -// -// Created by Hongli on 2025/3/12. -// - -import SwiftUI -import SwiftData - -struct ContentView: View { - @Environment(\.modelContext) private var modelContext - @Query private var items: [Item] - - var body: some View { - NavigationSplitView { - List { - ForEach(items) { item in - NavigationLink { - Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") - } label: { - Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) - } - } - .onDelete(perform: deleteItems) - } -#if os(macOS) - .navigationSplitViewColumnWidth(min: 180, ideal: 200) -#endif - .toolbar { -#if os(iOS) - ToolbarItem(placement: .navigationBarTrailing) { - EditButton() - } -#endif - ToolbarItem { - Button(action: addItem) { - Label("Add Item", systemImage: "plus") - } - } - } - } detail: { - Text("Select an item") - } - } - - private func addItem() { - withAnimation { - let newItem = Item(timestamp: Date()) - modelContext.insert(newItem) - } - } - - private func deleteItems(offsets: IndexSet) { - withAnimation { - for index in offsets { - modelContext.delete(items[index]) - } - } - } -} - -#Preview { - ContentView() - .modelContainer(for: Item.self, inMemory: true) -} diff --git a/TeachMate/DEV.md b/TeachMate/DEV.md new file mode 100644 index 0000000..e69de29 diff --git a/TeachMate/Item.swift b/TeachMate/Item.swift deleted file mode 100644 index 042ca5a..0000000 --- a/TeachMate/Item.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Item.swift -// TeachMate -// -// Created by Hongli on 2025/3/12. -// - -import Foundation -import SwiftData - -@Model -final class Item { - var timestamp: Date - - init(timestamp: Date) { - self.timestamp = timestamp - } -} diff --git a/TeachMate/Models/CourseModel.swift b/TeachMate/Models/CourseModel.swift new file mode 100644 index 0000000..9c5eab3 --- /dev/null +++ b/TeachMate/Models/CourseModel.swift @@ -0,0 +1,60 @@ +// +// CourseModel.swift +// TeachMate +// +// Created by Hongli on 2025/3/13. +// + +import Foundation +import SwiftData +import SwiftUI +import Observation + +@SwiftData.Model +final class ClassSession { + var weekday: Int // 1-7 表示周一到周日 + var timeSlot: Int // 1-5 表示不同的时间段 + var location: String + var schoolCalss: String + var course: CourseModel? + + init(weekday: Int, timeSlot: Int, location: String, schoolClass: String) { + self.weekday = weekday + self.timeSlot = timeSlot + self.location = location + self.schoolCalss = schoolClass + } +} + +@SwiftData.Model +final class CourseModel { + var name: String + @Relationship(deleteRule: .cascade, inverse: \ClassSession.course) + var sessions: [ClassSession] = [] + var colorHex: String + var semester: Semester? + + init(name: String, colorHex: String, isNew: Bool = false) { + self.name = name + self.colorHex = colorHex + } + + // Convenience initializer for backward compatibility + convenience init(name: String, location: String, weekday: Int, timeSlot: Int, colorHex: String, isNew: Bool = false, schoolClass: String) { + self.init(name: name, colorHex: colorHex, isNew: isNew) + let session = ClassSession(weekday: weekday, timeSlot: timeSlot, location: location, schoolClass: schoolClass) + self.sessions.append(session) + session.course = self + } + + var backgroundColor: Color { + Color(hex: colorHex) ?? Color.gray.opacity(0.3) + } + + // Helper method to add a new class session + func addSession(weekday: Int, timeSlot: Int, location: String, schoolClass: String) { + let session = ClassSession(weekday: weekday, timeSlot: timeSlot, location: location, schoolClass: schoolClass) + sessions.append(session) + session.course = self + } +} diff --git a/TeachMate/Models/Semester.swift b/TeachMate/Models/Semester.swift new file mode 100644 index 0000000..7b207fd --- /dev/null +++ b/TeachMate/Models/Semester.swift @@ -0,0 +1,46 @@ +// +// Semester.swift +// TeachMate +// +// Created by Hongli on 2025/3/12. +// + +import Foundation +import SwiftData + +@Model +final class Semester { + var title: String + var startDate: Date + var endDate: Date + var weeksCount: Int + var isCurrent: Bool = false + var createdAt: Date = Date() + + init(title: String, startDate: Date, endDate: Date, weeksCount: Int, isCurrent: Bool = false) { + self.title = title + self.startDate = startDate + self.endDate = endDate + self.weeksCount = weeksCount + self.isCurrent = isCurrent + self.createdAt = Date() + } + + // Helper method to create a semester with a standard format title + static func create(startYear: Int, endYear: Int, semesterNumber: Int, startDate: Date, endDate: Date, weeksCount: Int) -> Semester { + let title = "\(startYear)-\(endYear)-\(semesterNumber)" + return Semester(title: title, startDate: startDate, endDate: endDate, weeksCount: weeksCount) + } + + // Helper to get current academic week number + func currentWeekNumber(from date: Date = Date()) -> Int? { + guard date >= startDate && date <= endDate else { return nil } + + let calendar = Calendar.current + let components = calendar.dateComponents([.day], from: startDate, to: date) + guard let days = components.day else { return nil } + + let weekNumber = (days / 7) + 1 + return weekNumber <= weeksCount ? weekNumber : nil + } +} diff --git a/TeachMate/Preview Content/PreviewData.swift b/TeachMate/Preview Content/PreviewData.swift new file mode 100644 index 0000000..267c352 --- /dev/null +++ b/TeachMate/Preview Content/PreviewData.swift @@ -0,0 +1,291 @@ +// +// PreviewData.swift +// TeachMate +// +// Created by Hongli on 2025/3/18. +// + +import SwiftUI +import SwiftData + +/// 提供预览所需的示例数据 +@MainActor +enum PreviewData { + + // MARK: - 共享的日期和时间 + + /// 当前学期开始日期(2025年春季学期) + static let currentSemesterStartDate = Calendar.current.date(from: DateComponents(year: 2025, month: 2, day: 17))! + + /// 当前学期结束日期 + static let currentSemesterEndDate = Calendar.current.date(byAdding: .day, value: 18 * 7, to: currentSemesterStartDate)! + + /// 上一学期开始日期(2024年秋季学期) + static let previousSemesterStartDate = Calendar.current.date(from: DateComponents(year: 2024, month: 9, day: 1))! + + /// 上一学期结束日期 + static let previousSemesterEndDate = Calendar.current.date(from: DateComponents(year: 2025, month: 1, day: 15))! + + // MARK: - 示例颜色 + + /// 示例课程颜色 + static let courseColors = [ + "#D6EAF8", // 浅蓝色 + "#FADBD8", // 浅红色 + "#D5F5E3", // 浅绿色 + "#FCF3CF", // 浅黄色 + "#F5EEF8", // 浅紫色 + "#EAEDED", // 浅灰色 + "#FDEBD0" // 浅橙色 + ] + + // MARK: - 示例课程名称和位置 + + /// 常见课程名称 + static let courseNames = [ + "高等数学", "线性代数", "程序设计", "数据结构", + "计算机网络", "操作系统", "软件工程", "数据库原理", + "计算机组成原理", "编译原理", "人工智能", "机器学习", + "离散数学", "概率论与数理统计", "算法设计与分析" + ] + + /// 常见教室位置 + static let locations = [ + "教学楼A-101", "教学楼A-102", "教学楼A-201", "教学楼A-202", + "教学楼B-101", "教学楼B-102", "教学楼B-201", "教学楼B-202", + "实验楼C-301", "实验楼C-302", "实验楼C-303", "实验楼C-304", + "图书馆-多媒体教室", "综合楼-报告厅", "科技楼-机房" + ] + + /// 常见班级 + static let classes = [ + "计算机1班", "计算机2班", "软件工程1班", "软件工程2班", + "网络工程1班", "人工智能1班", "数据科学1班", "信息安全1班" + ] + + // MARK: - 创建预览容器 + + /// 创建预览用的ModelContainer(包含所有示例数据) + static func createContainer() -> ModelContainer { + let container = try! ModelContainer(for: Semester.self, CourseModel.self, ClassSession.self, + configurations: ModelConfiguration(isStoredInMemoryOnly: true)) + + // 创建示例学期 + let currentSemester = Semester( + title: "2024-2025-2", + startDate: currentSemesterStartDate, + endDate: currentSemesterEndDate, + weeksCount: 18, + isCurrent: true + ) + + let previousSemester = Semester( + title: "2024-2025-1", + startDate: previousSemesterStartDate, + endDate: previousSemesterEndDate, + weeksCount: 18 + ) + + container.mainContext.insert(currentSemester) + container.mainContext.insert(previousSemester) + + // 创建示例课程 + let course1 = CourseModel(name: courseNames[0], colorHex: courseColors[0]) + course1.semester = currentSemester + course1.addSession(weekday: 1, timeSlot: 1, location: locations[0], schoolClass: classes[0]) + course1.addSession(weekday: 3, timeSlot: 2, location: locations[0], schoolClass: classes[0]) + + let course2 = CourseModel(name: courseNames[1], colorHex: courseColors[1]) + course2.semester = currentSemester + course2.addSession(weekday: 2, timeSlot: 3, location: locations[4], schoolClass: classes[0]) + + let course3 = CourseModel(name: courseNames[2], colorHex: courseColors[2], isNew: true) + course3.semester = currentSemester + course3.addSession(weekday: 4, timeSlot: 4, location: locations[8], schoolClass: classes[0]) + course3.addSession(weekday: 5, timeSlot: 5, location: locations[8], schoolClass: classes[0]) + + let course4 = CourseModel(name: courseNames[3], colorHex: courseColors[3]) + course4.semester = previousSemester + course4.addSession(weekday: 1, timeSlot: 2, location: locations[1], schoolClass: classes[0]) + + // 添加更多课程以展示丰富的数据 + let course5 = CourseModel(name: courseNames[4], colorHex: courseColors[4]) + course5.semester = currentSemester + course5.addSession(weekday: 2, timeSlot: 1, location: locations[5], schoolClass: classes[1]) + + let course6 = CourseModel(name: courseNames[5], colorHex: courseColors[5]) + course6.semester = currentSemester + course6.addSession(weekday: 3, timeSlot: 3, location: locations[9], schoolClass: classes[1]) + course6.addSession(weekday: 5, timeSlot: 4, location: locations[9], schoolClass: classes[1]) + + container.mainContext.insert(course1) + container.mainContext.insert(course2) + container.mainContext.insert(course3) + container.mainContext.insert(course4) + container.mainContext.insert(course5) + container.mainContext.insert(course6) + + return container + } + + /// 创建空的预览容器(用于添加新数据的表单) + static func createEmptyContainer() -> ModelContainer { + return try! ModelContainer(for: Semester.self, CourseModel.self, ClassSession.self, + configurations: ModelConfiguration(isStoredInMemoryOnly: true)) + } + + /// 创建包含大量数据的预览容器(用于性能测试) + static func createLargeDataContainer() -> ModelContainer { + let container = try! ModelContainer(for: Semester.self, CourseModel.self, ClassSession.self, + configurations: ModelConfiguration(isStoredInMemoryOnly: true)) + + // 创建多个学期 + let oldStartDate1 = Calendar.current.date(byAdding: .year, value: -2, to: currentSemesterStartDate)! + let oldEndDate1 = Calendar.current.date(byAdding: .day, value: 18 * 7, to: oldStartDate1)! + + let oldStartDate2 = Calendar.current.date(byAdding: .year, value: -1, to: currentSemesterStartDate)! + let oldEndDate2 = Calendar.current.date(byAdding: .day, value: 18 * 7, to: oldStartDate2)! + + let prevEndDate = Calendar.current.date(byAdding: .day, value: 18 * 7, to: previousSemesterStartDate)! + + let semesters = [ + Semester(title: "2023-2024-1", startDate: oldStartDate1, endDate: oldEndDate1, weeksCount: 18), + Semester(title: "2023-2024-2", startDate: oldStartDate2, endDate: oldEndDate2, weeksCount: 18), + Semester(title: "2024-2025-1", startDate: previousSemesterStartDate, endDate: prevEndDate, weeksCount: 18), + Semester(title: "2024-2025-2", startDate: currentSemesterStartDate, endDate: currentSemesterEndDate, weeksCount: 18, isCurrent: true) + ] + + for semester in semesters { + container.mainContext.insert(semester) + } + + // 为每个学期创建多个课程 + for semester in semesters { + for i in 0..<10 { + let courseIndex = i % courseNames.count + let colorIndex = i % courseColors.count + + let course = CourseModel(name: courseNames[courseIndex], colorHex: courseColors[colorIndex]) + course.semester = semester + + // 为每个课程添加1-3个课时 + let sessionCount = (i % 3) + 1 + for j in 0.. ModelContainer { + let config = ModelConfiguration(isStoredInMemoryOnly: true) + let container = try! ModelContainer(for: Semester.self, configurations: config) + + // 创建示例学期 + let currentSemester = Semester( + title: "2024-2025-2", + startDate: currentSemesterStartDate, + endDate: currentSemesterEndDate, + weeksCount: 18, + isCurrent: true + ) + + let previousSemester = Semester( + title: "2024-2025-1", + startDate: previousSemesterStartDate, + endDate: previousSemesterEndDate, + weeksCount: 18, + isCurrent: false + ) + + container.mainContext.insert(currentSemester) + container.mainContext.insert(previousSemester) + + return container + } + + // MARK: - 示例模型 + + /// 创建示例学期(用于日历视图和课程表视图) + static func createSampleSemester() -> Semester { + return Semester( + title: "2024-2025-2", + startDate: currentSemesterStartDate, + endDate: currentSemesterEndDate, + weeksCount: 18, + isCurrent: true + ) + } + + /// 创建示例课程(用于课程详情视图) + static func createSampleCourse() -> CourseModel { + let course = CourseModel(name: courseNames[0], colorHex: courseColors[0]) + course.addSession(weekday: 1, timeSlot: 1, location: locations[0], schoolClass: classes[0]) + course.addSession(weekday: 3, timeSlot: 2, location: locations[0], schoolClass: classes[0]) + return course + } + + /// 创建示例课时(用于课时详情视图) + static func createSampleSession() -> ClassSession { + let session = ClassSession( + weekday: 1, + timeSlot: 1, + location: locations[0], + schoolClass: classes[0] + ) + + // 设置关联的课程 + let course = createSampleCourse() + session.course = course + + return session + } + + // MARK: - 辅助方法 + + /// 获取随机课程颜色 + static func randomCourseColor() -> String { + return courseColors.randomElement() ?? courseColors[0] + } + + /// 获取随机课程名称 + static func randomCourseName() -> String { + return courseNames.randomElement() ?? courseNames[0] + } + + /// 获取随机教室位置 + static func randomLocation() -> String { + return locations.randomElement() ?? locations[0] + } + + /// 获取随机班级 + static func randomClass() -> String { + return classes.randomElement() ?? classes[0] + } + + // MARK: - 预览修饰器 + + /// 为预览添加标准边距和背景 + static func standardPreviewStyle(_ content: Content) -> some View { + content + .padding() + .background(Color.white) // 使用简单的白色背景 + } +} diff --git a/TeachMate/TeachMateApp.swift b/TeachMate/TeachMateApp.swift index d0aac5d..e5a7142 100644 --- a/TeachMate/TeachMateApp.swift +++ b/TeachMate/TeachMateApp.swift @@ -12,7 +12,9 @@ import SwiftData struct TeachMateApp: App { var sharedModelContainer: ModelContainer = { let schema = Schema([ - Item.self, + Semester.self, + CourseModel.self, + ClassSession.self, ]) let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) diff --git a/TeachMate/Utilities/ColorExtension.swift b/TeachMate/Utilities/ColorExtension.swift new file mode 100644 index 0000000..59040da --- /dev/null +++ b/TeachMate/Utilities/ColorExtension.swift @@ -0,0 +1,26 @@ +// +// ColorExtension.swift +// TeachMate +// +// Created by Hongli on 2025/3/13. +// + +import SwiftUI + +// 颜色扩展,用于十六进制颜色转换 +public extension Color { + init?(hex: String) { + var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) + hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") + + var rgb: UInt64 = 0 + + guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { return nil } + + let r = Double((rgb & 0xFF0000) >> 16) / 255.0 + let g = Double((rgb & 0x00FF00) >> 8) / 255.0 + let b = Double(rgb & 0x0000FF) / 255.0 + + self.init(red: r, green: g, blue: b) + } +} diff --git a/TeachMate/Utilities/TimeSlot.swift b/TeachMate/Utilities/TimeSlot.swift new file mode 100644 index 0000000..0107a47 --- /dev/null +++ b/TeachMate/Utilities/TimeSlot.swift @@ -0,0 +1,23 @@ +// +// TimeSlot.swift +// TeachMate +// +// Created by Hongli on 2025/3/13. +// + +import Foundation + +// 时间段数据 +public struct TimeSlot: Identifiable { + public let id: Int + public let name: String + public let timeRange: String + + public static let defaultSlots = [ + TimeSlot(id: 1, name: "1/2 节", timeRange: "8:30~10:00"), + TimeSlot(id: 2, name: "3/4 节", timeRange: "10:30~12:00"), + TimeSlot(id: 3, name: "5/6 节", timeRange: "13:10~14:40"), + TimeSlot(id: 4, name: "7/8 节", timeRange: "15:00~16:30"), + TimeSlot(id: 5, name: "9/10 节", timeRange: "16:40~18:10") + ] +} diff --git a/TeachMate/Utilities/WindowSizeManager.swift b/TeachMate/Utilities/WindowSizeManager.swift new file mode 100644 index 0000000..093bc0d --- /dev/null +++ b/TeachMate/Utilities/WindowSizeManager.swift @@ -0,0 +1,68 @@ +// +// WindowSizeManager.swift +// TeachMate +// +// Created by Hongli on 2025/3/13. +// + +import SwiftUI + +#if os(macOS) +// 窗口尺寸管理器 +struct WindowSizeManager { + enum WindowSizePreset { + case calendar // 教学历视图 - 竖长 + case schedule // 课表视图 - 横长 + + var size: CGSize { + switch self { + case .calendar: + return CGSize(width: 500, height: 800) // 竖长窗口 + case .schedule: + return CGSize(width: 1200, height: 680) // 横长窗口 + } + } + } + + // 调整窗口大小 + static func resizeWindow(to preset: WindowSizePreset) { + guard let window = NSApplication.shared.windows.first else { return } + + let newSize = preset.size + let currentFrame = window.frame + + // 计算新的窗口位置,保持窗口中心点不变 + let newOriginX = currentFrame.origin.x + (currentFrame.width - newSize.width) / 2 + let newOriginY = currentFrame.origin.y + (currentFrame.height - newSize.height) / 2 + + let newFrame = NSRect( + x: newOriginX, + y: newOriginY, + width: newSize.width, + height: newSize.height + ) + + // 平滑动画调整窗口大小 + window.animator().setFrame(newFrame, display: true, animate: true) + } +} + +// 视图修饰符,用于调整窗口大小 +struct WindowSizeModifier: ViewModifier { + let preset: WindowSizeManager.WindowSizePreset + + func body(content: Content) -> some View { + content + .onAppear { + WindowSizeManager.resizeWindow(to: preset) + } + } +} + +// 扩展View以便于使用 +extension View { + func adjustWindowSize(to preset: WindowSizeManager.WindowSizePreset) -> some View { + modifier(WindowSizeModifier(preset: preset)) + } +} +#endif diff --git a/TeachMate/Views/CalendarView.swift b/TeachMate/Views/CalendarView.swift new file mode 100644 index 0000000..fd9f743 --- /dev/null +++ b/TeachMate/Views/CalendarView.swift @@ -0,0 +1,163 @@ +// +// AcademicCalendarView.swift +// TeachMate +// +// Created by Hongli on 2025/3/12. +// + +import SwiftUI + +struct AcademicCalendarView: View { + let semester: Semester + + // 布局常量 + private let weekdays = ["一", "二", "三", "四", "五", "六", "日"] + private let cellHeight: CGFloat = 35 + private let weekColumnWidth: CGFloat = 50 + private let dayCellWidth: CGFloat = 60 + private let padding: CGFloat = 10 + + var body: some View { + ScrollView { + calendarGrid + .padding(padding) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + // 日历网格 + private var calendarGrid: some View { + VStack(spacing: 0) { + // 表头行 + headerRow + + // 日期行 + ForEach(0.. some View { + let dates = weekDates(for: weekIndex) + + return HStack(spacing: 0) { + // 周数 + Text("\(weekIndex + 1)") + .font(.headline) + .frame(width: weekColumnWidth, height: cellHeight) + .background(Color.gray.opacity(0.3)) + .border(Color.gray.opacity(0.5), width: 0.5) + + // 日期单元格 + ForEach(0..<7, id: \.self) { dayIndex in + dateCell(date: dates[dayIndex], isWeekend: dayIndex >= 5) + } + } + } + + // 日期单元格 + private func dateCell(date: Date?, isWeekend: Bool) -> some View { + Group { + if let date = date { + Text(formatDate(date)) + .frame(width: dayCellWidth, height: cellHeight) + .background(isWeekend ? Color.gray.opacity(0.2) : Color.white) + .border(Color.gray.opacity(0.5), width: 0.5) + } else { + Text("") + .frame(width: dayCellWidth, height: cellHeight) + .background(Color.gray.opacity(0.1)) + .border(Color.gray.opacity(0.5), width: 0.5) + } + } + } + + // 格式化日期显示 + private func formatDate(_ date: Date) -> String { + let formatter = DateFormatter() + let calendar = Calendar.current + + // 如果是月份的第一天,显示"月日"格式 + if calendar.component(.day, from: date) == 1 { + formatter.dateFormat = "M月d日" + return formatter.string(from: date) + } + + // 否则只显示日期 + formatter.dateFormat = "d" + return formatter.string(from: date) + } + + // 获取指定周的日期数组 + private func weekDates(for weekIndex: Int) -> [Date?] { + let calendar = Calendar.current + + // 计算该周的第一天 + let weekStartDate = calendar.date(byAdding: .day, value: weekIndex * 7, to: firstMondayDate)! + + // 创建该周的日期数组 + var dates: [Date?] = [] + for dayIndex in 0..<7 { + let currentDate = calendar.date(byAdding: .day, value: dayIndex, to: weekStartDate)! + + // 检查日期是否在学期范围内(包含结束日期) + // 使用 Calendar 的 compare 方法进行更准确的日期比较 + let startComparison = calendar.compare(currentDate, to: semester.startDate, toGranularity: .day) + let endComparison = calendar.compare(currentDate, to: semester.endDate, toGranularity: .day) + + if (startComparison == .orderedSame || startComparison == .orderedDescending) && + (endComparison == .orderedSame || endComparison == .orderedAscending) { + dates.append(currentDate) + } else { + dates.append(nil) + } + } + + return dates + } + + // 计算学期第一周的周一日期 + private var firstMondayDate: Date { + let calendar = Calendar.current + let startDate = semester.startDate + + // 获取开始日期是周几(1=周一,2=周二,...,7=周日) + let weekday = calendar.component(.weekday, from: startDate) + // 转换为中国周历(周一是1,周日是7) + let chineseWeekday = weekday == 1 ? 7 : weekday - 1 + + // 如果不是周一,则回退到该周的周一 + if chineseWeekday > 1 { + return calendar.date(byAdding: .day, value: -(chineseWeekday - 1), to: startDate)! + } + + return startDate + } +} + +#Preview { + AcademicCalendarView(semester: PreviewData.createSampleSemester()) +} diff --git a/TeachMate/Views/ContentView.swift b/TeachMate/Views/ContentView.swift new file mode 100644 index 0000000..71327a0 --- /dev/null +++ b/TeachMate/Views/ContentView.swift @@ -0,0 +1,231 @@ +// +// ContentView.swift +// TeachMate +// +// Created by Hongli on 2025/3/12. +// + +import SwiftUI +import SwiftData + +struct ContentView: View { + @Environment(\.modelContext) private var modelContext + @Query private var semesters: [Semester] + + // Add a state to track the selected navigation item + @State private var selectedNavItem: NavItem? = .schedule + @State private var selectedSemester: Semester? + @State private var isShowingSemesterForm = false + + // Define navigation items + enum NavItem: String, Identifiable, CaseIterable { + case schedule = "课程表" + case calendar = "教学历" + case reminders = "提醒事项" + case courseManager = "课程管理" + + var id: String { self.rawValue } + + var icon: String { + switch self { + case .schedule: return "calendar.day.timeline.left" + case .calendar: return "calendar" + case .reminders: return "bell" + case .courseManager: return "list.bullet.below.rectangle" + } + } + } + + var body: some View { + NavigationSplitView { + List(selection: $selectedNavItem) { + // Top section with main navigation items + Section { + ForEach(NavItem.allCases) { item in + NavigationLink(value: item) { + Label(item.rawValue, systemImage: item.icon) + } + } + } + + // Bottom section with semester list + Section("学期") { + ForEach(semesters) { semester in + NavigationLink(value: semester) { + HStack { + if semester.isCurrent { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + .font(.caption) + } else { + Image(systemName: "star") + .foregroundColor(.secondary) + .font(.caption) + } + + VStack(alignment: .leading) { + Text(semester.title) + .font(.headline) + Text("第1周 - 第\(semester.weeksCount)周") + .font(.caption) + .foregroundColor(.secondary) + } + } + } + .contextMenu { + Button { + setAsCurrentSemester(semester) + } label: { + Label("设置为当前学期", systemImage: "star") + } + + Divider() + + Button(role: .destructive) { + if let index = semesters.firstIndex(where: { $0.id == semester.id }) { + deleteSemesters(offsets: IndexSet(integer: index)) + } + } label: { + Label("删除", systemImage: "trash") + } + } + } + .onDelete(perform: deleteSemesters) + } + } +#if os(macOS) + .navigationSplitViewColumnWidth(min: 180, ideal: 200) +#endif + .toolbar { +#if os(iOS) + ToolbarItem(placement: .navigationBarTrailing) { + EditButton() + } +#endif + ToolbarItem { + Button(action: { isShowingSemesterForm = true }) { + Label("Add Semester", systemImage: "plus") + } + } + } + .sheet(isPresented: $isShowingSemesterForm) { + SemesterFormView() + } + } detail: { + if let selectedNavItem = selectedNavItem { + // Show content based on selected navigation item + switch selectedNavItem { + case .schedule: + if let currentSemester = semesters.first(where: { $0.isCurrent }) { + CourseScheduleView(semester: currentSemester) + .frame(maxWidth: .infinity, maxHeight: .infinity) + #if os(macOS) + .adjustWindowSize(to: .schedule) + #endif + } else if !semesters.isEmpty { + CourseScheduleView(semester: semesters[0]) + .frame(maxWidth: .infinity, maxHeight: .infinity) + #if os(macOS) + .adjustWindowSize(to: .schedule) + #endif + } else { + VStack { + Text("请先添加学期") + .font(.title2) + Text("在添加学期后,将自动显示课程表") + .foregroundColor(.secondary) + } + } + case .calendar: + if let currentSemester = semesters.first(where: { $0.isCurrent }) { + AcademicCalendarView(semester: currentSemester) + .frame(maxWidth: .infinity, maxHeight: .infinity) + #if os(macOS) + .adjustWindowSize(to: .calendar) + #endif + } else if !semesters.isEmpty { + AcademicCalendarView(semester: semesters[0]) + .frame(maxWidth: .infinity, maxHeight: .infinity) + #if os(macOS) + .adjustWindowSize(to: .calendar) + #endif + } else { + VStack { + Text("请先添加学期") + .font(.title2) + Text("在添加学期后,将自动显示教学日历") + .foregroundColor(.secondary) + } + } + case .reminders: + Text("提醒事项内容") + case .courseManager: + CourseManagerView() + } + } else if let semester = selectedSemester { + // Show semester detail + VStack(alignment: .leading, spacing: 16) { + Text(semester.title) + .font(.largeTitle) + .bold() + + Group { + HStack { + Text("开始日期:") + Text(semester.startDate, format: .dateTime.day().month().year()) + } + + HStack { + Text("结束日期:") + Text(semester.endDate, format: .dateTime.day().month().year()) + } + + HStack { + Text("总周数:") + Text("\(semester.weeksCount)周") + } + + if let currentWeek = semester.currentWeekNumber() { + HStack { + Text("当前周数:") + Text("第\(currentWeek)周") + .foregroundColor(.blue) + .bold() + } + } + } + .font(.body) + + Spacer() + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } else { + Text("请选择一个项目") + } + } + } + + private func deleteSemesters(offsets: IndexSet) { + withAnimation { + for index in offsets { + modelContext.delete(semesters[index]) + } + } + } + + private func setAsCurrentSemester(_ semester: Semester) { + // First, set all semesters to not current + for existingSemester in semesters { + existingSemester.isCurrent = false + } + + // Then set the selected semester as current + semester.isCurrent = true + } +} + +#Preview { + ContentView() + .modelContainer(PreviewData.createSemesterContainer()) +} diff --git a/TeachMate/Views/CourseFormView.swift b/TeachMate/Views/CourseFormView.swift new file mode 100644 index 0000000..09fe207 --- /dev/null +++ b/TeachMate/Views/CourseFormView.swift @@ -0,0 +1,221 @@ +// +// CourseFormView.swift +// TeachMate +// +// Created by Hongli on 2025/3/18. +// + +import SwiftUI +import SwiftData + +// 课程表单视图 +struct CourseFormView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var modelContext + @Environment(\.colorScheme) private var colorScheme + @Query private var semesters: [Semester] + + enum FormMode { + case add + case edit(CourseModel) + } + + let mode: FormMode + let onSave: (CourseModel) -> Void + + @State private var name: String = "" + @State private var selectedColor: String = "#FCF3CF" // 默认黄色 + @State private var selectedSemester: Semester? + + // 可选颜色 + private let colorOptions = [ + "#FCF3CF", // 黄色 + "#D6EAF8", // 蓝色 + "#FADBD8", // 红色 + "#D5F5E3", // 绿色 + "#E8DAEF" // 紫色 + ] + + private var isFormValid: Bool { + !name.isEmpty && selectedSemester != nil + } + + var body: some View { + NavigationStack { + courseFormContent + .navigationTitle(navigationTitle) + .toolbarRole(.editor) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("取消") { + dismiss() + } + } + + ToolbarItem(placement: .confirmationAction) { + Button("保存") { + saveCourse() + dismiss() + } + .disabled(!isFormValid) + } + } + .onAppear { + setupInitialValues() + } + } + } + + // Main content view + private var courseFormContent: some View { + ZStack { + // Background + (colorScheme == .dark ? Color.black : Color.gray.opacity(0.1)) + .ignoresSafeArea() + + // Main content with horizontal layout + HStack(spacing: 0) { + // Left side - Form content + formFieldsSection + } + .frame(minWidth: 600, idealWidth: 700, maxHeight: .infinity) + } + } + + // Form fields section + private var formFieldsSection: some View { + ScrollView(.vertical) { + VStack(spacing: 24) { + // 课程信息 + FormSection(title: "课程信息") { + courseInfoSection + } + + // 颜色选择 + FormSection(title: "课程颜色") { + colorSelectionSection + } + } + .padding() + .frame(maxWidth: .infinity) + } + .frame(minWidth: 300, idealWidth: 350, maxWidth: .infinity) + } + + // Course information section + private var courseInfoSection: some View { + VStack(alignment: .leading, spacing: 16) { + // Course name input + VStack(alignment: .leading, spacing: 8) { + TextField("课程名称", text: $name) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocorrectionDisabled() + .frame(maxWidth: .infinity) + + Text("请输入课程的完整名称") + .font(.caption) + .foregroundStyle(.secondary) + } + + Divider() + + // 学期选择 + HStack { + Text("学期:") + .foregroundStyle(.secondary) + + Spacer() + + Picker("学期", selection: $selectedSemester) { + if semesters.isEmpty { + Text("请先创建学期").tag(nil as Semester?) + } else { + ForEach(semesters) { semester in + Text(semester.title).tag(semester as Semester?) + } + } + } + .pickerStyle(.menu) + } + + Divider() + } + .frame(maxWidth: .infinity) + } + + // Color selection section + private var colorSelectionSection: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + ForEach(colorOptions, id: \.self) { colorHex in + Circle() + .fill(Color(hex: colorHex) ?? .gray) + .frame(width: 30, height: 30) + .overlay( + Circle() + .stroke(selectedColor == colorHex ? Color.black : Color.clear, lineWidth: 2) + ) + .onTapGesture { + selectedColor = colorHex + } + .padding(5) + } + } + + Text("选择一个颜色来标识课程") + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + } + + private var navigationTitle: String { + switch mode { + case .add: + return "添加课程" + case .edit: + return "编辑课程" + } + } + + private func setupInitialValues() { + switch mode { + case .add: + // 如果有当前学期,默认选择它 + selectedSemester = semesters.first(where: { $0.isCurrent }) ?? semesters.first + case .edit(let course): + name = course.name + selectedColor = course.colorHex + selectedSemester = course.semester + } + } + + private func saveCourse() { + switch mode { + case .add: + guard let semester = selectedSemester else { return } + + let newCourse = CourseModel( + name: name, + colorHex: selectedColor + ) + newCourse.semester = semester + onSave(newCourse) + + case .edit(let course): + course.name = name + course.colorHex = selectedColor + course.semester = selectedSemester + onSave(course) + } + } +} +#Preview("添加课程") { + CourseFormView(mode: .add, onSave: { _ in }) + .modelContainer(PreviewData.createContainer()) +} + +#Preview("编辑课程") { + CourseFormView(mode: .edit(PreviewData.createSampleCourse()), onSave: { _ in }) + .modelContainer(PreviewData.createContainer()) +} diff --git a/TeachMate/Views/CourseManagerView.swift b/TeachMate/Views/CourseManagerView.swift new file mode 100644 index 0000000..069ffd4 --- /dev/null +++ b/TeachMate/Views/CourseManagerView.swift @@ -0,0 +1,310 @@ +// +// CourseManagerView.swift +// TeachMate +// +// Created by Hongli on 2025/3/17. +// + +import SwiftUI +import SwiftData + +struct CourseManagerView: View { + @Environment(\.modelContext) private var modelContext + @Query(sort: \CourseModel.name) private var courses: [CourseModel] + @Query(sort: \Semester.title) private var semesters: [Semester] + + @State private var selectedCourse: CourseModel? + @State private var showingCourseForm = false + @State private var courseToEdit: CourseModel? + + var filteredCourses: [CourseModel] { + var result = courses + + // 按当前学期筛选 + if let currentSemester = semesters.first(where: { $0.isCurrent }) { + result = result.filter { $0.semester?.id == currentSemester.id } + } + return result + } + + var body: some View { + NavigationSplitView { + VStack(spacing: 0) { + + // 课程列表 + List(selection: $selectedCourse) { + ForEach(filteredCourses) { course in + CourseRow(course: course) + .tag(course) + } + .onDelete(perform: deleteCourses) + } + .listStyle(.plain) + .overlay { + if filteredCourses.isEmpty { + ContentUnavailableView { + Label("没有课程", systemImage: "book.closed") + } description: { + Text("点击右上角的 + 按钮添加课程") + } + } + } + } + .navigationTitle("课程管理") + .toolbar { + ToolbarItem(placement: .automatic) { + Button(action: { + courseToEdit = nil + showingCourseForm = true + }) { + Image(systemName: "plus") + } + } + + ToolbarItem(placement: .automatic) { + Button(action: { + if let course = selectedCourse { + courseToEdit = course + showingCourseForm = true + } + }) { + Image(systemName: "pencil") + } + .disabled(selectedCourse == nil) + } + } + } detail: { + if let course = selectedCourse { + CourseDetailView(course: course) + } else { + Text("请选择一门课程查看详情") + .foregroundColor(.secondary) + } + } + .sheet(isPresented: $showingCourseForm) { + if let course = courseToEdit { + CourseFormView( + mode: .edit(course), + onSave: { _ in } + ) + .frame(width: 600) + } else { + CourseFormView( + mode: .add, + onSave: { newCourse in + modelContext.insert(newCourse) + selectedCourse = newCourse + } + ) + .frame(width: 600) + } + } + .onAppear { + // 如果没有选中的课程且有可用课程,自动选择第一个课程(对预览特别有用) + if selectedCourse == nil && !filteredCourses.isEmpty { + selectedCourse = filteredCourses.first + } + } + } + + private func deleteCourses(at offsets: IndexSet) { + for index in offsets { + let course = filteredCourses[index] + modelContext.delete(course) + } + } +} + +// 课程行视图 +struct CourseRow: View { + let course: CourseModel + + var body: some View { + HStack { + // 颜色标记 + RoundedRectangle(cornerRadius: 4) + .fill(course.backgroundColor) + .frame(width: 4, height: 40) + + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(course.name) + .font(.headline) + } + + Text("周 \(course.sessions.count) 节") + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.leading, 8) + + Spacer() + } + .padding(.vertical, 4) + } +} + +// 课程详情视图 +struct CourseDetailView: View { + @Environment(\.modelContext) private var modelContext + let course: CourseModel + + @State private var showingSessionForm = false + @State private var sessionToEdit: ClassSession? + + // 星期标题 + private let weekdays = ["一", "二", "三", "四", "五", "六", "日"] + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + // 课程基本信息 + HStack(alignment: .center) { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(course.name) + .font(.title) + .fontWeight(.bold) + } + + if let semester = course.semester { + Text("学期: \(semester.title)") + .foregroundColor(.secondary) + } + } + + Spacer() + + // 颜色标记 + Circle() + .fill(course.backgroundColor) + .frame(width: 24, height: 24) + } + .padding(.horizontal) + + Divider() + + // 课时列表 + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("课时列表") + .font(.headline) + + Spacer() + + Button(action: { + sessionToEdit = nil + showingSessionForm = true + }) { + Label("添加课时", systemImage: "plus.circle") + } + .buttonStyle(.borderless) + } + .padding(.horizontal) + + if course.sessions.isEmpty { + Text("没有课时信息") + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .center) + .padding() + } else { + List { + ForEach(course.sessions, id: \.self) { session in + SessionRow(session: session, weekdays: weekdays) + .contextMenu { + Button(action: { + sessionToEdit = session + showingSessionForm = true + }) { + Label("编辑", systemImage: "pencil") + } + + Button(role: .destructive, action: { + deleteSession(session) + }) { + Label("删除", systemImage: "trash") + } + } + } + } + .listStyle(.plain) + } + } + } + .padding(.vertical) + .sheet(isPresented: $showingSessionForm) { + if let session = sessionToEdit { + SessionFormView(mode: .edit(session), course: course) + } else { + SessionFormView(mode: .add, course: course) + } + } + } + + private func deleteSession(_ session: ClassSession) { + modelContext.delete(session) + } +} + +// 课时行视图 +struct SessionRow: View { + let session: ClassSession + let weekdays: [String] + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("周\(weekdays[session.weekday - 1])") + .font(.headline) + + Text("第\(session.timeSlot)节") + .foregroundColor(.secondary) + + Spacer() + + Text(timeSlotString(for: session.timeSlot)) + .font(.caption) + .foregroundColor(.secondary) + } + + HStack { + Image(systemName: "mappin.and.ellipse") + .foregroundColor(.secondary) + .font(.caption) + + Text(session.location) + .font(.subheadline) + + Spacer() + + if !session.schoolCalss.isEmpty { + Text(session.schoolCalss) + .font(.caption) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.gray.opacity(0.2)) + .cornerRadius(4) + } + } + } + .padding(.vertical, 4) + } + + private func timeSlotString(for slot: Int) -> String { + let slots = TimeSlot.defaultSlots + if slot >= 1 && slot <= slots.count { + return slots[slot - 1].timeRange + } + return "" + } +} + +#Preview("课程管理") { + CourseManagerView() + .modelContainer(PreviewData.createContainer()) + .onAppear { + // 预览时给一些时间让数据加载 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + // 这个空的闭包会触发视图刷新,有助于在预览中显示数据 + } + } +} diff --git a/TeachMate/Views/CourseScheduleView.swift b/TeachMate/Views/CourseScheduleView.swift new file mode 100644 index 0000000..29a120e --- /dev/null +++ b/TeachMate/Views/CourseScheduleView.swift @@ -0,0 +1,350 @@ +// +// CourseScheduleView.swift +// TeachMate +// +// Created by Hongli on 2025/3/13. +// + +import SwiftUI +import SwiftData + +struct CourseScheduleView: View { + let semester: Semester + @State private var currentWeek: Int = 1 + @State private var showingAddCourse = false + @State private var selectedTimeSlot: (weekday: Int, timeSlot: Int)? = nil + + @Environment(\.modelContext) private var modelContext + @Query private var courses: [CourseModel] + + // 星期标题 + private let weekdays = ["一", "二", "三", "四", "五", "六", "日"] + + // 布局常量 + private let timeColumnWidth: CGFloat = 100 + private let dayCellWidth: CGFloat = 120 + private let cellHeight: CGFloat = 100 + private let headerHeight: CGFloat = 40 + + // 初始化查询 + init(semester: Semester) { + self.semester = semester + + // 不使用谓词,而是在视图中过滤课程 + self._courses = Query() + } + + // 过滤后的课程列表 + private var filteredCourses: [CourseModel] { + courses.filter { $0.semester?.id == semester.id } + } + + // 获取特定单元格的课程 + private func getCoursesForCell(weekday: Int, timeSlot: Int) -> [CourseModel] { + return filteredCourses.filter { course in + course.sessions.contains { session in + session.weekday == weekday && session.timeSlot == timeSlot + } + } + } + + var body: some View { + VStack(spacing: 0) { + // 周次选择器 + weekSelector + .padding(.horizontal) + .padding(.vertical, 8) + + // 课程表 + ScrollView([.horizontal, .vertical]) { + VStack(spacing: 0) { + // 表头行 + headerRow + + // 课程行 + ForEach(TimeSlot.defaultSlots) { timeSlot in + courseRow(timeSlot: timeSlot) + } + } + .padding() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onAppear { + // 设置当前周 + if let weekNumber = semester.currentWeekNumber() { + currentWeek = weekNumber + } + + // 如果没有课程数据,添加示例数据 + if filteredCourses.isEmpty { + addSampleCourses() + } + } + .sheet(isPresented: $showingAddCourse) { + if let selected = selectedTimeSlot { + AddCourseView(semester: semester, weekday: selected.weekday, timeSlot: selected.timeSlot) + } + } + } + + // 周次选择器 + private var weekSelector: some View { + HStack { + Button(action: { + if currentWeek > 1 { + currentWeek -= 1 + } + }) { + Image(systemName: "chevron.left") + } + .disabled(currentWeek <= 1) + + Spacer() + + Picker("周次", selection: $currentWeek) { + ForEach(1...semester.weeksCount, id: \.self) { week in + Text("第\(week)周").tag(week) + } + } + .pickerStyle(.menu) + .frame(width: 120) + + Spacer() + + Button(action: { + if currentWeek < semester.weeksCount { + currentWeek += 1 + } + }) { + Image(systemName: "chevron.right") + } + .disabled(currentWeek >= semester.weeksCount) + } + } + + // 表头行 + private var headerRow: some View { + HStack(spacing: 0) { + // 左上角空白单元格 + Text("第\(currentWeek)周") + .font(.headline) + .frame(width: timeColumnWidth, height: headerHeight) + .background(Color.gray.opacity(0.3)) + .border(Color.gray.opacity(0.5), width: 0.5) + + // 星期表头 + ForEach(0..<7, id: \.self) { index in + Text(weekdays[index]) + .font(.headline) + .frame(width: dayCellWidth, height: headerHeight) + .background(Color.gray.opacity(0.3)) + .border(Color.gray.opacity(0.5), width: 0.5) + } + } + } + + // 课程行 + private func courseRow(timeSlot: TimeSlot) -> some View { + HStack(spacing: 0) { + // 时间段信息 + VStack(alignment: .center, spacing: 2) { + Text(timeSlot.name) + .font(.system(size: 13, weight: .medium)) + Text(timeSlot.timeRange) + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + .frame(width: timeColumnWidth, height: cellHeight) + .background(Color.gray.opacity(0.1)) + .border(Color.gray.opacity(0.5), width: 0.5) + + // 每天的课程单元格 + ForEach(1...7, id: \.self) { weekday in + courseCell(weekday: weekday, timeSlot: timeSlot.id) + } + } + } + + // 课程单元格 + private func courseCell(weekday: Int, timeSlot: Int) -> some View { + let coursesForCell = getCoursesForCell(weekday: weekday, timeSlot: timeSlot) + + return Group { + if let course = coursesForCell.first { + courseDisplayView(course: course, weekday: weekday, timeSlot: timeSlot) + .padding(5) + .frame(width: dayCellWidth, height: cellHeight) + .background(course.backgroundColor) + .border(Color.gray.opacity(0.5), width: 0.5) + .contextMenu { + Button(role: .destructive) { + deleteCourse(course) + } label: { + Label("删除课程", systemImage: "trash") + } + } + } else { + Text("") + .frame(width: dayCellWidth, height: cellHeight) + .background(Color.white) + .border(Color.gray.opacity(0.5), width: 0.5) + .onTapGesture { + selectedTimeSlot = (weekday, timeSlot) + showingAddCourse = true + } + } + } + } + + // 课程显示视图 + private func courseDisplayView(course: CourseModel, weekday: Int, timeSlot: Int) -> some View { + // 获取当前单元格对应的课程会话 + let session = course.sessions.first { $0.weekday == weekday && $0.timeSlot == timeSlot } + + return VStack(spacing: 2) { + Text(course.name) + .font(.system(size: 11)) + .foregroundColor(.black) + .lineLimit(1) + Text(session?.schoolCalss ?? "未知班级") + .font(.system(size: 10)) + .foregroundColor(.black) + + if let location = session?.location { + Text(location) + .font(.system(size: 10)) + .foregroundColor(.black) + .lineLimit(1) + } + } + } + + // 添加示例课程数据 + private func addSampleCourses() { + var sampleCourses: [CourseModel] = [] + + // 创建有多个课时的课程 + let aiCourse = CourseModel(name: "人工智能技术与应用", colorHex: "#FADBD8", isNew: false) + + aiCourse.addSession(weekday: 4, timeSlot: 5, location: "CMA101陈栋教室", schoolClass: "环艺G24-1,环艺G24-2,电竞G24-1") + aiCourse.addSession(weekday: 5, timeSlot: 5, location: "CMA101陈栋教室", schoolClass: "视传G24-1,视传G24-2,视传G24-3") + + let dockerCourse = CourseModel(name: "容器云架构与运维", colorHex: "#D6EAF8", isNew: true) + dockerCourse.addSession(weekday: 3, timeSlot: 3, location: "XXGY402", schoolClass: "云计算G23-1") + dockerCourse.addSession(weekday: 5, timeSlot: 1, location: "XXGY404", schoolClass: "云计算G23-1") + + let networkCourse = CourseModel(name: "网络组建与维护", colorHex: "#FCF3CF", isNew: true) + networkCourse.addSession(weekday: 1, timeSlot: 3, location: "XXA2305", schoolClass: "软件G23-3") + networkCourse.addSession(weekday: 2, timeSlot: 3, location: "XXA2401", schoolClass: "软件G23-4") + networkCourse.addSession(weekday: 3, timeSlot: 2, location: "XXA2303", schoolClass: "软件G23-3") + networkCourse.addSession(weekday: 3, timeSlot: 4, location: "XXA2504", schoolClass: "软件G23-4") + + sampleCourses.append(aiCourse) + sampleCourses.append(dockerCourse) + sampleCourses.append(networkCourse) + + for course in sampleCourses { + course.semester = semester + modelContext.insert(course) + } + } + + // 删除课程 + private func deleteCourse(_ course: CourseModel) { + modelContext.delete(course) + } +} + +// 添加课程视图 +struct AddCourseView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var modelContext + + let semester: Semester + let weekday: Int + let timeSlot: Int + + @State private var name: String = "" + @State private var location: String = "" + @State private var schoolClass: String = "" + @State private var isNew: Bool = false + @State private var selectedColor: String = "#FCF3CF" // 默认黄色 + + // 可选颜色 + private let colorOptions = [ + "#FCF3CF", // 黄色 + "#D6EAF8", // 蓝色 + "#FADBD8", // 红色 + "#D5F5E3", // 绿色 + "#E8DAEF" // 紫色 + ] + + var body: some View { + NavigationView { + Form { + Section(header: Text("课程信息")) { + TextField("课程名称", text: $name) + TextField("班级", text: $schoolClass) + TextField("上课地点", text: $location) + Toggle("标记为新课程", isOn: $isNew) + } + + Section(header: Text("颜色")) { + HStack { + ForEach(colorOptions, id: \.self) { colorHex in + Circle() + .fill(Color(hex: colorHex) ?? .gray) + .frame(width: 30, height: 30) + .overlay( + Circle() + .stroke(selectedColor == colorHex ? Color.black : Color.clear, lineWidth: 2) + ) + .onTapGesture { + selectedColor = colorHex + } + .padding(5) + } + } + } + + Section { + Button("添加课程") { + addCourse() + } + .frame(maxWidth: .infinity, alignment: .center) + .disabled(name.isEmpty || location.isEmpty || location.isEmpty) + } + } + .navigationTitle("添加课程") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("取消") { + dismiss() + } + } + } + } + } + + private func addCourse() { + let course = CourseModel( + name: name, + location: location, + weekday: weekday, + timeSlot: timeSlot, + colorHex: selectedColor, + isNew: isNew, + schoolClass: schoolClass + ) + + course.semester = semester + modelContext.insert(course) + dismiss() + } +} + +#Preview { + CourseScheduleView(semester: PreviewData.createSampleSemester()) + .modelContainer(PreviewData.createContainer()) +} diff --git a/TeachMate/Views/SemesterFormView.swift b/TeachMate/Views/SemesterFormView.swift new file mode 100644 index 0000000..01d7159 --- /dev/null +++ b/TeachMate/Views/SemesterFormView.swift @@ -0,0 +1,207 @@ +// +// SemesterFormView.swift +// TeachMate +// +// Created by Hongli on 2025/3/12. +// + +import SwiftUI +import SwiftData + +struct SemesterFormView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var modelContext + @Environment(\.colorScheme) private var colorScheme + + @State private var title: String = "" + @State private var startDate: Date = Date() + @State private var endDate: Date = Date() + @State private var weeksCount: Int = 18 + + @State private var showingAlert = false + @State private var alertMessage = "" + + // Calculate the end of the week (Sunday) for a given date + private func endOfWeek(for date: Date) -> Date { + let calendar = Calendar.current + let components = calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date) + guard let sunday = calendar.date(from: components) else { return date } + // Add 6 days to get to Sunday (assuming first day of week is Monday) + return calendar.date(byAdding: .day, value: 6, to: sunday) ?? date + } + + // Calculate the end date (last Sunday of the semester) + private func calculateEndDate() -> Date { + let calendar = Calendar.current + // Add (weeksCount - 1) weeks to the start date to get to the beginning of the last week + guard let lastWeekStart = calendar.date(byAdding: .day, value: (weeksCount - 1) * 7, to: startDate) else { + return endDate + } + // Get the Sunday of the last week + return endOfWeek(for: lastWeekStart) + } + + // Format date for display + private func formattedDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter.string(from: date) + } + + var body: some View { + NavigationStack { + ZStack { + // Background + (colorScheme == .dark ? Color.black : Color.gray.opacity(0.1)) + .ignoresSafeArea() + + // Main content with horizontal layout + HStack(spacing: 0) { + // Left side - Form content + ScrollView { + VStack(spacing: 24) { + // Title Section + FormSection(title: "学期标题") { + VStack(alignment: .leading, spacing: 8) { + TextField("请输入标题", text: $title) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocorrectionDisabled() + .frame(maxWidth: .infinity) + + Text("标题格式: YYYY-YYYY-N") + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + } + + // Combined Teaching Weeks Section + FormSection(title: "教学周") { + VStack(spacing: 16) { + // Weeks count + HStack { + Text("周数:") + .foregroundStyle(.secondary) + + Spacer() + + Picker("周数", selection: $weeksCount) { + ForEach(1...30, id: \.self) { week in + Text("\(week) 周").tag(week) + } + } + .pickerStyle(.menu) + .onChange(of: weeksCount) { _, _ in + // Update end date when weeks count changes + endDate = calculateEndDate() + } + } + + Divider() + + // Start date + HStack { + Text("开始日期:") + .foregroundStyle(.secondary) + + Spacer() + + DatePicker( + "", + selection: $startDate, + displayedComponents: [.date] + ) + .labelsHidden() + .onChange(of: startDate) { _, _ in + // Update end date when start date changes + endDate = calculateEndDate() + } + } + } + .frame(maxWidth: .infinity) + } + } + .padding() + .frame(maxWidth: .infinity) + } + .frame(minWidth: 300, idealWidth: 350, maxWidth: .infinity) + + // Vertical divider + Divider() + .padding(.vertical) + + // Right side - Summary + VStack { + FormSection(title: "学期概览") { + VStack(alignment: .leading, spacing: 12) { + SummaryRow(label: "标题", value: title.isEmpty ? "未设置" : title) + SummaryRow(label: "周数", value: "\(weeksCount) 周") + SummaryRow(label: "开始日期", value: formattedDate(startDate)) + SummaryRow(label: "结束日期", value: formattedDate(endDate)) + SummaryRow( + label: "总时长", + value: "\(Calendar.current.dateComponents([.day], from: startDate, to: endDate).day ?? 0 + 1) 天" + ) + } + } + + Spacer() + } + .padding() + .frame(minWidth: 250, idealWidth: 300, maxWidth: .infinity) + .background(colorScheme == .dark ? Color.black : Color.white) + } + .frame(minWidth: 600, idealWidth: 700, maxHeight: .infinity) + } + .navigationTitle("添加学期") + .toolbarRole(.editor) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("取消") { + dismiss() + } + } + + ToolbarItem(placement: .confirmationAction) { + Button("保存") { + saveSemester() + } + } + } + .onAppear { + // Set default end date + endDate = calculateEndDate() + } + .alert("错误", isPresented: $showingAlert) { + Button("确定", role: .cancel) { } + } message: { + Text(alertMessage) + } + } + } + + private func saveSemester() { + // Validate inputs + if title.isEmpty { + alertMessage = "请输入学期标题" + showingAlert = true + return + } + + // Create and save the semester + let semester = Semester( + title: title, + startDate: startDate, + endDate: endDate, + weeksCount: weeksCount + ) + + modelContext.insert(semester) + dismiss() + } +} + +#Preview { + SemesterFormView() + .modelContainer(PreviewData.createSemesterContainer()) +} diff --git a/TeachMate/Views/SessionFormView.swift b/TeachMate/Views/SessionFormView.swift new file mode 100644 index 0000000..d0cead2 --- /dev/null +++ b/TeachMate/Views/SessionFormView.swift @@ -0,0 +1,219 @@ +// +// SessionFormView.swift +// TeachMate +// +// Created by Hongli on 2025/3/18. +// + +import SwiftUI +import SwiftData + +// 课时弹窗 +struct SessionFormView: View { + @Environment(\.dismiss) private var dismiss + @Environment(\.modelContext) private var modelContext + @Environment(\.colorScheme) private var colorScheme + + enum FormMode { + case add + case edit(ClassSession) + } + + let mode: FormMode + let course: CourseModel + + @State private var weekday: Int = 1 + @State private var timeSlot: Int = 1 + @State private var location: String = "" + @State private var schoolClass: String = "" + + // 星期标题 + private let weekdays = ["一", "二", "三", "四", "五", "六", "日"] + + private var isFormValid: Bool { + !location.isEmpty + } + + var body: some View { + NavigationStack { + sessionFormContent + .navigationTitle(navigationTitle) + .toolbarRole(.editor) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("取消") { + dismiss() + } + } + + ToolbarItem(placement: .confirmationAction) { + Button("保存") { + saveSession() + dismiss() + } + .disabled(!isFormValid) + } + } + .onAppear { + setupInitialValues() + } + } + } + + // Main content view + private var sessionFormContent: some View { + ZStack { + // Background + (colorScheme == .dark ? Color.black : Color.gray.opacity(0.1)) + .ignoresSafeArea() + + // Main content with horizontal layout + HStack(spacing: 0) { + // Left side - Form content + formFieldsSection + + // Vertical divider + Divider() + .padding(.vertical) + } + .frame(minWidth: 600, idealWidth: 700, maxHeight: .infinity) + } + } + + // Form fields section + private var formFieldsSection: some View { + ScrollView(.vertical) { + VStack(spacing: 24) { + // 课时信息 + FormSection(title: "课时信息") { + weekdayAndTimeSection + } + + // 位置信息 + FormSection(title: "位置信息") { + locationAndClassSection + } + } + .padding() + .frame(maxWidth: .infinity) + } + .frame(minWidth: 300, idealWidth: 350, maxWidth: .infinity) + } + + // Weekday and time slot section + private var weekdayAndTimeSection: some View { + VStack(alignment: .leading, spacing: 16) { + // 星期选择 + HStack { + Picker("星期", selection: $weekday) { + ForEach(1...7, id: \.self) { day in + Text("周\(weekdays[day-1])").tag(day) + } + } + .pickerStyle(.menu) + } + + Divider() + + // 节次选择 + HStack { + Picker("节次", selection: $timeSlot) { + ForEach(TimeSlot.defaultSlots) { slot in + Text("\(slot.name) (\(slot.timeRange))").tag(slot.id) + } + } + .pickerStyle(.menu) + } + } + .frame(maxWidth: .infinity) + } + + // Location and class input section + private var locationAndClassSection: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + TextField("上课地点", text: $location) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocorrectionDisabled() + .frame(maxWidth: .infinity) + + Text("请输入教室或上课地点") + .font(.caption) + .foregroundStyle(.secondary) + } + + Divider() + + VStack(alignment: .leading, spacing: 8) { + TextField("班级", text: $schoolClass) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocorrectionDisabled() + .frame(maxWidth: .infinity) + + Text("可选,请输入班级名称") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity) + } + + private var navigationTitle: String { + switch mode { + case .add: + return "添加课时" + case .edit: + return "编辑课时" + } + } + + private func setupInitialValues() { + switch mode { + case .add: + // 使用默认值 + break + case .edit(let session): + weekday = session.weekday + timeSlot = session.timeSlot + location = session.location + schoolClass = session.schoolCalss + } + } + + private func saveSession() { + switch mode { + case .add: + course.addSession( + weekday: weekday, + timeSlot: timeSlot, + location: location, + schoolClass: schoolClass + ) + + case .edit(let session): + session.weekday = weekday + session.timeSlot = timeSlot + session.location = location + session.schoolCalss = schoolClass + } + } + + private func timeSlotString(for slot: Int) -> String { + let slots = TimeSlot.defaultSlots + if slot >= 1 && slot <= slots.count { + let timeSlot = slots[slot - 1] + return "\(timeSlot.name) (\(timeSlot.timeRange))" + } + return "" + } +} + +#Preview("添加课时") { + SessionFormView(mode: .add, course: PreviewData.createSampleCourse()) + .modelContainer(PreviewData.createContainer()) +} + +#Preview("编辑课时") { + SessionFormView(mode: .edit(PreviewData.createSampleSession()), course: PreviewData.createSampleCourse()) + .modelContainer(PreviewData.createContainer()) +} diff --git a/TeachMate/Views/Shared/PreviewExampleView.swift b/TeachMate/Views/Shared/PreviewExampleView.swift new file mode 100644 index 0000000..38cdd80 --- /dev/null +++ b/TeachMate/Views/Shared/PreviewExampleView.swift @@ -0,0 +1,130 @@ +// +// PreviewExampleView.swift +// TeachMate +// +// Created by Hongli on 2025/3/18. +// + +import SwiftUI +import SwiftData + +/// 示例视图,展示如何使用预览数据 +/// 此视图仅用于开发和测试,不会包含在最终应用中 +struct PreviewExampleView: View { + @Environment(\.modelContext) private var modelContext + @Query private var semesters: [Semester] + + var body: some View { + NavigationStack { + List { + Section("学期") { + ForEach(semesters) { semester in + HStack { + Text(semester.title) + .font(.headline) + + Spacer() + + if semester.isCurrent { + Text("当前") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + + Section("使用预览数据的示例") { + NavigationLink("标准预览容器") { + Text("此视图使用 PreviewData.createContainer()") + .font(.headline) + } + + NavigationLink("大数据集预览") { + Text("此视图使用 PreviewData.createLargeDataContainer()") + .font(.headline) + } + + NavigationLink("空容器预览") { + Text("此视图使用 PreviewData.createEmptyContainer()") + .font(.headline) + } + } + + Section("辅助方法示例") { + HStack { + Text("随机课程颜色:") + + RoundedRectangle(cornerRadius: 4) + .fill(ColorFromHex(hex: PreviewData.randomCourseColor())) + .frame(width: 20, height: 20) + } + + HStack { + Text("随机课程名称:") + Text(PreviewData.randomCourseName()) + .foregroundStyle(.secondary) + } + + HStack { + Text("随机教室位置:") + Text(PreviewData.randomLocation()) + .foregroundStyle(.secondary) + } + + HStack { + Text("随机班级:") + Text(PreviewData.randomClass()) + .foregroundStyle(.secondary) + } + } + } + .navigationTitle("预览数据示例") + } + } + + // 内部函数,用于从十六进制字符串创建颜色 + // 避免与 ColorExtension.swift 中的扩展冲突 + private func ColorFromHex(hex: String) -> Color { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (1, 1, 1, 0) + } + + return Color( + .sRGB, + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + opacity: Double(a) / 255 + ) + } +} + +// 标准预览 +#Preview("标准预览") { + PreviewExampleView() + .modelContainer(PreviewData.createContainer()) +} + +// 大数据集预览 +#Preview("大数据集") { + PreviewExampleView() + .modelContainer(PreviewData.createLargeDataContainer()) +} + +// 空容器预览 +#Preview("空容器") { + PreviewExampleView() + .modelContainer(PreviewData.createEmptyContainer()) +} diff --git a/TeachMate/Views/Shared/UIComponents.swift b/TeachMate/Views/Shared/UIComponents.swift new file mode 100644 index 0000000..9f45ade --- /dev/null +++ b/TeachMate/Views/Shared/UIComponents.swift @@ -0,0 +1,54 @@ +// +// UIComponents.swift +// TeachMate +// +// Created by Hongli on 2025/3/18. +// + +import SwiftUI + +// Shared UI components for forms across the app +public struct FormSection: View { + public let title: String + public let content: Content + @Environment(\.colorScheme) private var colorScheme + + public init(title: String, @ViewBuilder content: () -> Content) { + self.title = title + self.content = content() + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.headline) + .foregroundStyle(.primary) + + content + .padding() + .frame(maxWidth: .infinity) + .background(colorScheme == .dark ? Color.black : Color.white) + .cornerRadius(12) + } + .frame(maxWidth: .infinity) + } +} + +public struct SummaryRow: View { + public let label: String + public let value: String + + public var body: some View { + HStack(alignment: .top) { + Text(label) + .foregroundStyle(.secondary) + .frame(width: 80, alignment: .leading) + + Text(value) + .foregroundStyle(.primary) + .fontWeight(.medium) + + Spacer() + } + } +}