feat 完成项目基础构架
This commit is contained in:
parent
74d2c6567a
commit
e17871e53d
@ -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)
|
||||
}
|
0
TeachMate/DEV.md
Normal file
0
TeachMate/DEV.md
Normal file
@ -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
|
||||
}
|
||||
}
|
60
TeachMate/Models/CourseModel.swift
Normal file
60
TeachMate/Models/CourseModel.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
46
TeachMate/Models/Semester.swift
Normal file
46
TeachMate/Models/Semester.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
291
TeachMate/Preview Content/PreviewData.swift
Normal file
291
TeachMate/Preview Content/PreviewData.swift
Normal file
@ -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..<sessionCount {
|
||||
let weekday = (j % 5) + 1
|
||||
let timeSlot = (j % 5) + 1
|
||||
let locationIndex = (i + j) % locations.count
|
||||
let classIndex = i % classes.count
|
||||
|
||||
course.addSession(
|
||||
weekday: weekday,
|
||||
timeSlot: timeSlot,
|
||||
location: locations[locationIndex],
|
||||
schoolClass: classes[classIndex]
|
||||
)
|
||||
}
|
||||
|
||||
container.mainContext.insert(course)
|
||||
}
|
||||
}
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
// MARK: - 专用预览容器
|
||||
|
||||
/// 创建仅包含学期的预览容器(用于ContentView预览)
|
||||
static func createSemesterContainer() -> 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: View>(_ content: Content) -> some View {
|
||||
content
|
||||
.padding()
|
||||
.background(Color.white) // 使用简单的白色背景
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
|
26
TeachMate/Utilities/ColorExtension.swift
Normal file
26
TeachMate/Utilities/ColorExtension.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
23
TeachMate/Utilities/TimeSlot.swift
Normal file
23
TeachMate/Utilities/TimeSlot.swift
Normal file
@ -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")
|
||||
]
|
||||
}
|
68
TeachMate/Utilities/WindowSizeManager.swift
Normal file
68
TeachMate/Utilities/WindowSizeManager.swift
Normal file
@ -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
|
163
TeachMate/Views/CalendarView.swift
Normal file
163
TeachMate/Views/CalendarView.swift
Normal file
@ -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..<semester.weeksCount, id: \.self) { weekIndex in
|
||||
weekRow(weekIndex: weekIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表头行
|
||||
private var headerRow: some View {
|
||||
HStack(spacing: 0) {
|
||||
// 周次表头
|
||||
Text("周次")
|
||||
.font(.headline)
|
||||
.frame(width: weekColumnWidth, height: cellHeight)
|
||||
.background(Color.gray.opacity(0.3))
|
||||
.border(Color.gray.opacity(0.5), width: 0.5)
|
||||
|
||||
// 星期表头
|
||||
ForEach(weekdays, id: \.self) { weekday in
|
||||
Text(weekday)
|
||||
.font(.headline)
|
||||
.frame(width: dayCellWidth, height: cellHeight)
|
||||
.background(Color.gray.opacity(0.3))
|
||||
.border(Color.gray.opacity(0.5), width: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 周行
|
||||
private func weekRow(weekIndex: Int) -> 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())
|
||||
}
|
231
TeachMate/Views/ContentView.swift
Normal file
231
TeachMate/Views/ContentView.swift
Normal file
@ -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())
|
||||
}
|
221
TeachMate/Views/CourseFormView.swift
Normal file
221
TeachMate/Views/CourseFormView.swift
Normal file
@ -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())
|
||||
}
|
310
TeachMate/Views/CourseManagerView.swift
Normal file
310
TeachMate/Views/CourseManagerView.swift
Normal file
@ -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) {
|
||||
// 这个空的闭包会触发视图刷新,有助于在预览中显示数据
|
||||
}
|
||||
}
|
||||
}
|
350
TeachMate/Views/CourseScheduleView.swift
Normal file
350
TeachMate/Views/CourseScheduleView.swift
Normal file
@ -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())
|
||||
}
|
207
TeachMate/Views/SemesterFormView.swift
Normal file
207
TeachMate/Views/SemesterFormView.swift
Normal file
@ -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())
|
||||
}
|
219
TeachMate/Views/SessionFormView.swift
Normal file
219
TeachMate/Views/SessionFormView.swift
Normal file
@ -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())
|
||||
}
|
130
TeachMate/Views/Shared/PreviewExampleView.swift
Normal file
130
TeachMate/Views/Shared/PreviewExampleView.swift
Normal file
@ -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())
|
||||
}
|
54
TeachMate/Views/Shared/UIComponents.swift
Normal file
54
TeachMate/Views/Shared/UIComponents.swift
Normal file
@ -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<Content: View>: 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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user