Files
汉秋 703078352c 从 iOS 17 Observation 迁移至 iOS 16 兼容的 Combine 架构
将最低部署版本降至 iOS 16,以 ObservableObject 替换 @Observable,新增导航与 UI 兼容层,并补充登录冒烟 UI 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-26 10:16:35 +08:00

224 lines
7.7 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ScheduleViewModels.swift
// suixinkan
//
// Created by Codex on 2026/6/24.
//
import Foundation
import Combine
///
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: ObservableObject {
@Published var monthDate = Date.scheduleFirstDayOfMonth
@Published var selectedDate = Date()
@Published private(set) var markedDays: Set<String> = []
@Published private(set) var items: [ScheduleItem] = []
@Published private(set) var loading = false
@Published var errorMessage: String?
///
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: ObservableObject {
@Published var draft = ScheduleDraft()
@Published private(set) var availableOrders: [AvailableOrderResponse] = []
@Published private(set) var loadingOrders = false
@Published private(set) var submitting = false
@Published var errorMessage: String?
///
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)
}
}
}