Initial commit: suixinkan_ios UIKit rewrite project.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 14:33:31 +08:00
commit 9edf993432
297 changed files with 47151 additions and 0 deletions

View File

@ -0,0 +1,224 @@
//
// ScheduleViewModels.swift
// suixinkan
//
// Created by Codex on 2026/6/24.
//
import Foundation
///
enum ScheduleValidationError: LocalizedError, Equatable {
case missingScenic
case missingName
case invalidTime
///
var errorDescription: String? {
switch self {
case .missingScenic: return "当前缺少景区信息"
case .missingName: return "请输入日程名称"
case .invalidTime: return "结束时间必须晚于开始时间"
}
}
}
/// ViewModel
@MainActor
final class ScheduleManagementViewModel {
var onChange: (() -> Void)?
var monthDate = Date.scheduleFirstDayOfMonth { didSet { onChange?() } }
var selectedDate = Date() { didSet { onChange?() } }
private(set) var markedDays: Set<String> = [] { didSet { onChange?() } }
private(set) var items: [ScheduleItem] = [] { didSet { onChange?() } }
private(set) var loading = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
///
func previousMonth(api: any ScheduleServing, scenicId: Int?) async {
monthDate = Calendar.current.date(byAdding: .month, value: -1, to: monthDate) ?? monthDate
selectedDate = monthDate
await reload(api: api, scenicId: scenicId)
}
///
func nextMonth(api: any ScheduleServing, scenicId: Int?) async {
monthDate = Calendar.current.date(byAdding: .month, value: 1, to: monthDate) ?? monthDate
selectedDate = monthDate
await reload(api: api, scenicId: scenicId)
}
///
func selectDate(_ date: Date, api: any ScheduleServing, scenicId: Int?) async {
selectedDate = date
await loadDay(api: api, scenicId: scenicId)
}
///
func reload(api: any ScheduleServing, scenicId: Int?) async {
guard let scenicId else {
reset()
return
}
loading = true
errorMessage = nil
defer { loading = false }
do {
async let days = api.monthScheduleDays(scenicId: scenicId, yearMonth: monthDate.scheduleYearMonthText)
async let list = api.dayScheduleList(scenicId: scenicId, date: selectedDate.scheduleDayText)
markedDays = Set(try await days)
items = try await list
} catch {
items = []
markedDays = []
errorMessage = error.localizedDescription
}
}
///
func loadDay(api: any ScheduleServing, scenicId: Int?) async {
guard let scenicId else {
reset()
return
}
do {
items = try await api.dayScheduleList(scenicId: scenicId, date: selectedDate.scheduleDayText)
} catch {
items = []
errorMessage = error.localizedDescription
}
}
///
func delete(item: ScheduleItem, api: any ScheduleServing, scenicId: Int?) async {
do {
try await api.deleteSchedule(id: item.id)
await reload(api: api, scenicId: scenicId)
} catch {
errorMessage = error.localizedDescription
}
}
private func reset() {
markedDays = []
items = []
loading = false
}
}
/// ViewModel
@MainActor
final class ScheduleAddViewModel {
var onChange: (() -> Void)?
var draft = ScheduleDraft() { didSet { onChange?() } }
private(set) var availableOrders: [AvailableOrderResponse] = [] { didSet { onChange?() } }
private(set) var loadingOrders = false { didSet { onChange?() } }
private(set) var submitting = false { didSet { onChange?() } }
var errorMessage: String? { didSet { onChange?() } }
///
func loadOrders(api: any ScheduleServing, scenicId: Int?) async {
guard let scenicId else {
availableOrders = []
return
}
loadingOrders = true
defer { loadingOrders = false }
do {
availableOrders = try await api.availableOrderList(scenicId: scenicId)
} catch {
availableOrders = []
errorMessage = error.localizedDescription
}
}
///
func submit(api: any ScheduleServing, scenicId: Int?, startDate: Date, endDate: Date) async -> Bool {
guard !submitting else { return false }
guard let scenicId else { return fail(.missingScenic) }
let normalizedName = draft.name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedName.isEmpty else { return fail(.missingName) }
guard endDate > startDate else { return fail(.invalidTime) }
submitting = true
errorMessage = nil
defer { submitting = false }
do {
try await api.addSchedule(
AddScheduleRequest(
scenicId: "\(scenicId)",
name: normalizedName,
remark: draft.remark.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? normalizedName : draft.remark.trimmingCharacters(in: .whitespacesAndNewlines),
startTime: startDate.scheduleHourMinuteText,
endTime: endDate.scheduleHourMinuteText,
scheduleDate: startDate.scheduleDayText,
orderNumber: draft.finalOrderNumber()
)
)
return true
} catch {
errorMessage = normalizedError(error)
return false
}
}
///
private func normalizedError(_ error: Error) -> String {
let text = error.localizedDescription
if text.contains("start time") || text.contains("format H:i") {
return "开始时间格式不正确,请重新选择开始时间"
}
if text.contains("end time") {
return "结束时间格式不正确,请重新选择结束时间"
}
return text
}
private func fail(_ error: ScheduleValidationError) -> Bool {
errorMessage = error.localizedDescription
return false
}
}
extension Date {
///
static var scheduleFirstDayOfMonth: Date {
Calendar.current.date(from: Calendar.current.dateComponents([.year, .month], from: Date())) ?? Date()
}
/// yyyy-MM
var scheduleYearMonthText: String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "yyyy-MM"
return formatter.string(from: self)
}
/// yyyy-MM-dd
var scheduleDayText: String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "yyyy-MM-dd"
return formatter.string(from: self)
}
/// HH:mm
var scheduleHourMinuteText: String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "zh_CN")
formatter.dateFormat = "HH:mm"
return formatter.string(from: self)
}
///
var scheduleDaysInMonth: [Date] {
let calendar = Calendar.current
let range = calendar.range(of: .day, in: .month, for: self) ?? 1..<2
let components = calendar.dateComponents([.year, .month], from: self)
return range.compactMap { day in
var next = components
next.day = day
return calendar.date(from: next)
}
}
}