// // TaskModels.swift // suixinkan // // Created by Codex on 2026/6/23. // import Foundation /// 摄影师任务列表实体,表示任务管理页中的一条任务记录。 struct PhotographerTaskItem: Decodable, Identifiable, Hashable { let id: Int let name: String let type: Int let orderNumber: String let taskStatus: Int let statusName: String let createdAt: String let updatedAt: String let photogRemark: String let editorName: String let operateTime: String let orderUserNickname: String let orderUserPhone: String let orderUserId: String let media: [TaskMediaItem] /// 任务列表字段映射,兼容后端下划线命名。 enum CodingKeys: String, CodingKey { case id case name case type case orderNumber = "order_number" case taskStatus = "task_status" case statusName = "status_name" case createdAt = "created_at" case updatedAt = "updated_at" case photogRemark = "photog_remark" case editorName = "editor_name" case operateTime = "operate_time" case orderUserNickname = "order_user_nickname" case orderUserPhone = "order_user_phone" case orderUserId = "order_user_id" case media } /// 宽松解码任务列表字段,避免后端数字和字符串混用导致页面不可用。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 name = try container.decodeLossyString(forKey: .name) type = try container.decodeLossyInt(forKey: .type) ?? 0 orderNumber = try container.decodeLossyString(forKey: .orderNumber) taskStatus = try container.decodeLossyInt(forKey: .taskStatus) ?? 0 statusName = try container.decodeLossyString(forKey: .statusName) createdAt = try container.decodeLossyString(forKey: .createdAt) updatedAt = try container.decodeLossyString(forKey: .updatedAt) photogRemark = try container.decodeLossyString(forKey: .photogRemark) editorName = try container.decodeLossyString(forKey: .editorName) operateTime = try container.decodeLossyString(forKey: .operateTime) orderUserNickname = try container.decodeLossyString(forKey: .orderUserNickname) orderUserPhone = try container.decodeLossyString(forKey: .orderUserPhone) orderUserId = try container.decodeLossyString(forKey: .orderUserId) media = (try? container.decode([TaskMediaItem].self, forKey: .media)) ?? [] } } /// 任务媒体实体,表示任务附件或任务结果中的图片、视频文件。 struct TaskMediaItem: Decodable, Identifiable, Hashable { let id: Int let photogTaskId: Int let userId: Int let fileName: String let fileType: Int let fileUrl: String let fileSize: Int64 let coverUrl: String let duration: Int let remark: String let createdAt: String let updatedAt: String /// 判断当前媒体是否应按视频展示。 var isVideo: Bool { fileType == 1 || Self.hasVideoExtension(fileName) || Self.hasVideoExtension(fileUrl) } /// 返回用于缩略图展示的 URL,优先使用封面图。 var displayURL: String { coverUrl.isEmpty ? fileUrl : coverUrl } /// 根据文件扩展名判断是否是常见视频格式。 private static func hasVideoExtension(_ value: String) -> Bool { ["mp4", "mov", "m4v", "avi"].contains(URL(string: value)?.pathExtension.lowercased() ?? URL(fileURLWithPath: value).pathExtension.lowercased()) } /// 任务媒体字段映射,兼容后端下划线命名。 enum CodingKeys: String, CodingKey { case id case photogTaskId = "photog_task_id" case userId = "user_id" case fileName = "file_name" case fileType = "file_type" case fileUrl = "file_url" case fileSize = "file_size" case coverUrl = "cover_url" case duration case remark case createdAt = "created_at" case updatedAt = "updated_at" } /// 宽松解码任务媒体字段。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 photogTaskId = try container.decodeLossyInt(forKey: .photogTaskId) ?? 0 userId = try container.decodeLossyInt(forKey: .userId) ?? 0 fileName = try container.decodeLossyString(forKey: .fileName) fileType = try container.decodeLossyInt(forKey: .fileType) ?? 0 fileUrl = try container.decodeLossyString(forKey: .fileUrl) fileSize = Int64(try container.decodeLossyInt(forKey: .fileSize) ?? 0) coverUrl = try container.decodeLossyString(forKey: .coverUrl) duration = try container.decodeLossyInt(forKey: .duration) ?? 0 remark = try container.decodeLossyString(forKey: .remark) createdAt = try container.decodeLossyString(forKey: .createdAt) updatedAt = try container.decodeLossyString(forKey: .updatedAt) } } /// 任务详情实体,表示任务详情页展示的完整任务信息。 struct TaskDetailResponse: Decodable, Identifiable, Hashable { let id: Int let name: String let createdAt: String let taskStatus: Int let taskStatusLabel: String let acceptedAt: String let editorName: String let editorPhone: String let urgentHour: Int let photogRemark: String let order: TaskDetailOrder? let taskResult: [TaskMediaItem] /// 任务详情字段映射,兼容后端下划线命名。 enum CodingKeys: String, CodingKey { case id case name case createdAt = "created_at" case taskStatus = "task_status" case taskStatusLabel = "task_status_label" case acceptedAt = "accepted_at" case editorName = "editor_name" case editorPhone = "editor_phone" case urgentHour = "urgent_hour" case photogRemark = "photog_remark" case order case taskResult = "task_result" } /// 宽松解码任务详情字段。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decodeLossyInt(forKey: .id) ?? 0 name = try container.decodeLossyString(forKey: .name) createdAt = try container.decodeLossyString(forKey: .createdAt) taskStatus = try container.decodeLossyInt(forKey: .taskStatus) ?? 0 taskStatusLabel = try container.decodeLossyString(forKey: .taskStatusLabel) acceptedAt = try container.decodeLossyString(forKey: .acceptedAt) editorName = try container.decodeLossyString(forKey: .editorName) editorPhone = try container.decodeLossyString(forKey: .editorPhone) urgentHour = try container.decodeLossyInt(forKey: .urgentHour) ?? 0 photogRemark = try container.decodeLossyString(forKey: .photogRemark) order = try? container.decode(TaskDetailOrder.self, forKey: .order) taskResult = (try? container.decode([TaskMediaItem].self, forKey: .taskResult)) ?? [] } } /// 任务关联订单实体,表示任务详情中的订单摘要。 struct TaskDetailOrder: Decodable, Hashable { let projectName: String let orderNumber: String let payTime: String let userPhone: String /// 任务关联订单字段映射,兼容后端下划线命名。 enum CodingKeys: String, CodingKey { case projectName = "project_name" case orderNumber = "order_number" case payTime = "pay_time" case userPhone = "user_phone" } /// 宽松解码订单摘要字段。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) projectName = try container.decodeLossyString(forKey: .projectName) orderNumber = try container.decodeLossyString(forKey: .orderNumber) payTime = try container.decodeLossyString(forKey: .payTime) userPhone = try container.decodeLossyString(forKey: .userPhone) } } /// 发布任务请求实体,描述提交给后端的新任务内容。 struct AddTaskRequest: Encodable, Equatable { let scenicId: String let name: String let orderNumber: String? let remark: String let urgentHour: Int let cloudFile: [CloudFileItem] let uploadFile: [UploadFileItem] /// 发布任务请求字段映射,兼容后端下划线命名。 enum CodingKeys: String, CodingKey { case scenicId = "scenic_id" case name case orderNumber = "order_number" case remark = "photog_remark" case urgentHour = "urgent_hour" case cloudFile = "cloud_file" case uploadFile = "upload_file" } } /// 任务云盘附件请求实体,表示发布任务时选择的云盘文件。 struct CloudFileItem: Encodable, Equatable { let fileId: Int let remark: String /// 云盘附件请求字段映射,兼容后端下划线命名。 enum CodingKeys: String, CodingKey { case fileId = "file_id" case remark } } /// 任务上传附件请求实体,表示发布任务时上传到 OSS 的本地文件。 struct UploadFileItem: Encodable, Equatable { let fileUrl: String let fileName: String let remark: String /// 上传附件请求字段映射,兼容后端下划线命名。 enum CodingKeys: String, CodingKey { case fileUrl = "file_url" case fileName = "file_name" case remark } } private extension KeyedDecodingContainer { /// 将 String、数字和 Bool 宽松解码为字符串。 func decodeLossyString(forKey key: Key) throws -> String { if let value = try? decodeIfPresent(String.self, forKey: key) { return 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 ? "true" : "false" } return "" } /// 将 Int、Double 或数字字符串宽松解码为整数。 func decodeLossyInt(forKey key: Key) throws -> Int? { if let value = try? decodeIfPresent(Int.self, forKey: key) { return value } if let value = try? decodeIfPresent(String.self, forKey: key) { let text = value.trimmingCharacters(in: .whitespacesAndNewlines) if let intValue = Int(text) { return intValue } if let doubleValue = Double(text) { return Int(doubleValue) } } if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int(value) } return nil } }