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 {
|
struct TeachMateApp: App {
|
||||||
var sharedModelContainer: ModelContainer = {
|
var sharedModelContainer: ModelContainer = {
|
||||||
let schema = Schema([
|
let schema = Schema([
|
||||||
Item.self,
|
Semester.self,
|
||||||
|
CourseModel.self,
|
||||||
|
ClassSession.self,
|
||||||
])
|
])
|
||||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
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