新增项目、排期与邀请模块,并接入首页路由

将项目管理、排期管理与摄影师邀请流程从占位页迁移为完整实现,包含项目图片 OSS 上传与共享业务模型。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 15:57:15 +08:00
parent abcac9bfdf
commit 311a70d610
40 changed files with 4496 additions and 108 deletions

View 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
}
/// StringDouble 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
}
}