Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
91
suixinkan_ios/Features/Schedule/API/ScheduleAPI.swift
Normal file
91
suixinkan_ios/Features/Schedule/API/ScheduleAPI.swift
Normal file
@ -0,0 +1,91 @@
|
||||
//
|
||||
// ScheduleAPI.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 排班服务协议,抽象排班列表、新增、删除和可关联订单接口。
|
||||
@MainActor
|
||||
protocol ScheduleServing {
|
||||
/// 获取指定月份有排班的日期集合。
|
||||
func monthScheduleDays(scenicId: Int, yearMonth: String) async throws -> [String]
|
||||
|
||||
/// 获取指定日期的排班列表。
|
||||
func dayScheduleList(scenicId: Int, date: String) async throws -> [ScheduleItem]
|
||||
|
||||
/// 新增排班。
|
||||
func addSchedule(_ request: AddScheduleRequest) async throws
|
||||
|
||||
/// 删除排班。
|
||||
func deleteSchedule(id: Int) async throws
|
||||
|
||||
/// 获取排班可关联订单。
|
||||
func availableOrderList(scenicId: Int) async throws -> [AvailableOrderResponse]
|
||||
}
|
||||
|
||||
/// 排班 API,封装摄影师排班相关接口。
|
||||
@MainActor
|
||||
final class ScheduleAPI: ScheduleServing {
|
||||
private let client: APIClient
|
||||
|
||||
/// 初始化排班 API,并注入共享网络客户端。
|
||||
init(client: APIClient) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// 获取指定月份有排班的日期集合。
|
||||
func monthScheduleDays(scenicId: Int, yearMonth: String) async throws -> [String] {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/schedule/list-date",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "scenic_id", value: "\(scenicId)"),
|
||||
URLQueryItem(name: "date", value: yearMonth)
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 获取指定日期的排班列表。
|
||||
func dayScheduleList(scenicId: Int, date: String) async throws -> [ScheduleItem] {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/schedule/list",
|
||||
queryItems: [
|
||||
URLQueryItem(name: "scenic_id", value: "\(scenicId)"),
|
||||
URLQueryItem(name: "date", value: date)
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// 新增排班。
|
||||
func addSchedule(_ request: AddScheduleRequest) async throws {
|
||||
_ = try await client.send(
|
||||
APIRequest(method: .post, path: "/api/yf-handset-app/photog/schedule/add", body: request)
|
||||
) as EmptyPayload
|
||||
}
|
||||
|
||||
/// 删除排班。
|
||||
func deleteSchedule(id: Int) async throws {
|
||||
_ = try await client.send(
|
||||
APIRequest(method: .post, path: "/api/yf-handset-app/photog/schedule/delete", body: ScheduleDeleteRequest(id: id))
|
||||
) as EmptyPayload
|
||||
}
|
||||
|
||||
/// 获取排班可关联订单。
|
||||
func availableOrderList(scenicId: Int) async throws -> [AvailableOrderResponse] {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .get,
|
||||
path: "/api/yf-handset-app/photog/task/available-order",
|
||||
queryItems: [URLQueryItem(name: "scenic_id", value: "\(scenicId)")]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
140
suixinkan_ios/Features/Schedule/Models/ScheduleModels.swift
Normal file
140
suixinkan_ios/Features/Schedule/Models/ScheduleModels.swift
Normal file
@ -0,0 +1,140 @@
|
||||
//
|
||||
// ScheduleModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 排班日程实体,表示某一天的一条工作安排。
|
||||
struct ScheduleItem: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let remark: String
|
||||
let startTime: String
|
||||
let endTime: String
|
||||
let scheduleDate: String
|
||||
let orderNumber: String?
|
||||
let userNickname: String?
|
||||
let userPhone: String?
|
||||
let orderStatus: Int?
|
||||
let orderStatusName: String?
|
||||
|
||||
/// 字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
case remark
|
||||
case startTime = "start_time"
|
||||
case endTime = "end_time"
|
||||
case scheduleDate = "schedule_date"
|
||||
case orderNumber = "order_number"
|
||||
case userNickname = "user_nickname"
|
||||
case userPhone = "user_phone"
|
||||
case orderStatus = "order_status"
|
||||
case orderStatusName = "order_status_name"
|
||||
}
|
||||
|
||||
/// 宽松解码排班字段。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeScheduleLossyInt(forKey: .id) ?? 0
|
||||
name = try container.decodeScheduleLossyString(forKey: .name)
|
||||
remark = try container.decodeScheduleLossyString(forKey: .remark)
|
||||
startTime = try container.decodeScheduleLossyString(forKey: .startTime)
|
||||
endTime = try container.decodeScheduleLossyString(forKey: .endTime)
|
||||
scheduleDate = try container.decodeScheduleLossyString(forKey: .scheduleDate)
|
||||
orderNumber = container.decodeScheduleLossyOptionalString(forKey: .orderNumber)
|
||||
userNickname = container.decodeScheduleLossyOptionalString(forKey: .userNickname)
|
||||
userPhone = container.decodeScheduleLossyOptionalString(forKey: .userPhone)
|
||||
orderStatus = try container.decodeScheduleLossyInt(forKey: .orderStatus)
|
||||
orderStatusName = container.decodeScheduleLossyOptionalString(forKey: .orderStatusName)
|
||||
}
|
||||
}
|
||||
|
||||
/// 新增排班请求实体。
|
||||
struct AddScheduleRequest: Encodable, Equatable {
|
||||
let scenicId: String
|
||||
let name: String
|
||||
let remark: String
|
||||
let startTime: String
|
||||
let endTime: String
|
||||
let scheduleDate: String
|
||||
let orderNumber: String?
|
||||
|
||||
/// 请求字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case scenicId = "scenic_id"
|
||||
case name
|
||||
case remark
|
||||
case startTime = "start_time"
|
||||
case endTime = "end_time"
|
||||
case scheduleDate = "schedule_date"
|
||||
case orderNumber = "order_number"
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除排班请求实体。
|
||||
struct ScheduleDeleteRequest: Encodable, Equatable {
|
||||
let id: Int
|
||||
}
|
||||
|
||||
/// 排班编辑表单实体,用于 ViewModel 校验和生成请求。
|
||||
struct ScheduleDraft: Equatable {
|
||||
var name = ""
|
||||
var remark = ""
|
||||
var scheduleDate = ""
|
||||
var startTime = ""
|
||||
var endTime = ""
|
||||
var manualOrderNumber = ""
|
||||
var selectedOrder: AvailableOrderResponse?
|
||||
|
||||
/// 计算最终订单号,优先使用选择的订单,手工输入作为兜底。
|
||||
func finalOrderNumber() -> String? {
|
||||
let picked = selectedOrder?.orderNumber.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let manual = manualOrderNumber.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let value = picked.isEmpty ? manual : picked
|
||||
return value.isEmpty ? nil : value
|
||||
}
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
/// 将 String、数字和 Bool 宽松解码为字符串。
|
||||
func decodeScheduleLossyString(forKey key: Key) throws -> String {
|
||||
decodeScheduleLossyOptionalString(forKey: key) ?? ""
|
||||
}
|
||||
|
||||
/// 将 String、数字和 Bool 宽松解码为可选字符串。
|
||||
func decodeScheduleLossyOptionalString(forKey key: Key) -> String? {
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : value
|
||||
}
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return String(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||||
return String(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
|
||||
return value ? "1" : "0"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// 将 String、Double 和 Int 宽松解码为 Int。
|
||||
func decodeScheduleLossyInt(forKey key: Key) throws -> Int? {
|
||||
if let value = try? decodeIfPresent(Int.self, forKey: key) {
|
||||
return value
|
||||
}
|
||||
if let value = try? decodeIfPresent(Double.self, forKey: key) {
|
||||
return Int(value)
|
||||
}
|
||||
if let value = try? decodeIfPresent(String.self, forKey: key) {
|
||||
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return Int(text) ?? Double(text).map(Int.init)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
27
suixinkan_ios/Features/Schedule/Schedule.md
Normal file
27
suixinkan_ios/Features/Schedule/Schedule.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Schedule 模块业务逻辑
|
||||
|
||||
## 模块职责
|
||||
|
||||
Schedule 模块负责首页 `schedule_management` 入口,提供排班日历、某日排班列表、新增排班和删除排班能力。
|
||||
|
||||
模块状态只保存在 `ScheduleManagementViewModel` 和 `ScheduleAddViewModel` 内,不写入全局登录态、账号上下文或首页状态。
|
||||
|
||||
## 排班列表
|
||||
|
||||
`ScheduleManagementViewModel` 按当前月份加载有排班的日期标记,并按选中日期加载日排班列表。切换月份或日期时只刷新当前模块数据。
|
||||
|
||||
缺少当前景区时,ViewModel 会清空月份标记和日程列表,并展示缺少经营上下文的空状态。
|
||||
|
||||
## 新增排班
|
||||
|
||||
`ScheduleAddViewModel` 管理名称、备注、日期、开始/结束时间和关联订单。关联订单可以从接口返回的可选订单中选择,也可以手填订单号兜底。
|
||||
|
||||
提交前会校验名称、景区、日期和时间顺序;重复提交会被忽略。提交成功后返回排班列表并刷新当前日期。
|
||||
|
||||
## 接口边界
|
||||
|
||||
`ScheduleAPI` 封装月排班日期、日排班列表、新增排班、删除排班和可关联订单接口。可关联订单模型放在共享业务模型中,避免 Schedule 直接依赖 Tasks 模块。
|
||||
|
||||
## 缓存边界
|
||||
|
||||
排班表单、关联订单、月标记和日程列表都只保存在内存中,不做本地缓存。
|
||||
@ -0,0 +1,149 @@
|
||||
//
|
||||
// ScheduleViewControllers.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/26.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 排班管理页。
|
||||
final class ScheduleManagementViewController: ModuleTableViewController {
|
||||
private let viewModel = ScheduleManagementViewModel()
|
||||
|
||||
override func viewDidLoad() {
|
||||
title = "排班管理"
|
||||
navigationItem.rightBarButtonItems = [
|
||||
UIBarButtonItem(title: "新增", style: .plain, target: self, action: #selector(addSchedule)),
|
||||
UIBarButtonItem(title: "下月", style: .plain, target: self, action: #selector(nextMonth))
|
||||
]
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
title: "上月",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(previousMonth)
|
||||
)
|
||||
super.viewDidLoad()
|
||||
wireViewModel(viewModel) { [weak self] in self?.updateTitle() }
|
||||
}
|
||||
|
||||
override func tableRowCount() -> Int {
|
||||
viewModel.items.count
|
||||
}
|
||||
|
||||
override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {
|
||||
let item = viewModel.items[indexPath.row]
|
||||
cell.configure(
|
||||
title: item.name,
|
||||
subtitle: "\(item.startTime) - \(item.endTime)",
|
||||
detail: item.orderNumber ?? item.remark
|
||||
)
|
||||
}
|
||||
|
||||
override func didSelectTableRow(at indexPath: IndexPath) {
|
||||
let item = viewModel.items[indexPath.row]
|
||||
let alert = UIAlertController(title: item.name, message: "删除该排班?", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: "删除", style: .destructive) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.delete(item: item, api: self.services.scheduleAPI, scenicId: self.services.currentScenicId) }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
override func reloadContent() async {
|
||||
await viewModel.reload(api: services.scheduleAPI, scenicId: services.currentScenicId)
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
private func updateTitle() {
|
||||
let month = viewModel.monthDate.scheduleYearMonthText
|
||||
title = "排班 \(month)"
|
||||
}
|
||||
|
||||
@objc private func previousMonth() {
|
||||
Task { await viewModel.previousMonth(api: services.scheduleAPI, scenicId: services.currentScenicId) }
|
||||
}
|
||||
|
||||
@objc private func nextMonth() {
|
||||
Task { await viewModel.nextMonth(api: services.scheduleAPI, scenicId: services.currentScenicId) }
|
||||
}
|
||||
|
||||
@objc private func addSchedule() {
|
||||
navigationController?.pushViewController(ScheduleAddViewController(), animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension ScheduleManagementViewModel: ViewModelBindable {}
|
||||
|
||||
/// 新增排班页。
|
||||
final class ScheduleAddViewController: ModuleTableViewController {
|
||||
private let viewModel = ScheduleAddViewModel()
|
||||
private let nameField = UITextField()
|
||||
private let remarkField = UITextField()
|
||||
|
||||
override func viewDidLoad() {
|
||||
title = "新增排班"
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(
|
||||
title: "提交",
|
||||
style: .done,
|
||||
target: self,
|
||||
action: #selector(submit)
|
||||
)
|
||||
super.viewDidLoad()
|
||||
wireViewModel(viewModel) { }
|
||||
setupFormHeader()
|
||||
}
|
||||
|
||||
private func setupFormHeader() {
|
||||
nameField.placeholder = "日程名称"
|
||||
nameField.borderStyle = .roundedRect
|
||||
remarkField.placeholder = "备注"
|
||||
remarkField.borderStyle = .roundedRect
|
||||
let stack = UIStackView(arrangedSubviews: [nameField, remarkField])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 8
|
||||
stack.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 96)
|
||||
stack.layoutMargins = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16)
|
||||
stack.isLayoutMarginsRelativeArrangement = true
|
||||
tableView.tableHeaderView = stack
|
||||
}
|
||||
|
||||
override func tableRowCount() -> Int {
|
||||
viewModel.availableOrders.count
|
||||
}
|
||||
|
||||
override func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {
|
||||
let order = viewModel.availableOrders[indexPath.row]
|
||||
let selected = viewModel.draft.selectedOrder?.orderNumber == order.orderNumber
|
||||
cell.configure(title: order.orderNumber, subtitle: order.projectName, detail: selected ? "已选择" : nil)
|
||||
cell.accessoryType = selected ? .checkmark : .none
|
||||
}
|
||||
|
||||
override func didSelectTableRow(at indexPath: IndexPath) {
|
||||
viewModel.draft.selectedOrder = viewModel.availableOrders[indexPath.row]
|
||||
reloadTable()
|
||||
}
|
||||
|
||||
override func reloadContent() async {
|
||||
await viewModel.loadOrders(api: services.scheduleAPI, scenicId: services.currentScenicId)
|
||||
}
|
||||
|
||||
@objc private func submit() {
|
||||
viewModel.draft.name = nameField.text ?? ""
|
||||
viewModel.draft.remark = remarkField.text ?? ""
|
||||
Task {
|
||||
let start = Date()
|
||||
let end = Calendar.current.date(byAdding: .hour, value: 1, to: start) ?? start
|
||||
let success = await viewModel.submit(
|
||||
api: services.scheduleAPI,
|
||||
scenicId: services.currentScenicId,
|
||||
startDate: start,
|
||||
endDate: end
|
||||
)
|
||||
if success { navigationController?.popViewController(animated: true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ScheduleAddViewModel: ViewModelBindable {}
|
||||
@ -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