225 lines
7.9 KiB
Swift
225 lines
7.9 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|
||
}
|