Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user