Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
227 lines
7.9 KiB
Swift
227 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
|
||
}
|
||
|
||
/// 记录校验失败并返回 false。
|
||
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)
|
||
}
|
||
}
|
||
}
|