Initial commit: suixinkan_ios UIKit rewrite project.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
291
suixinkan_ios/Features/Tasks/Models/TaskModels.swift
Normal file
291
suixinkan_ios/Features/Tasks/Models/TaskModels.swift
Normal file
@ -0,0 +1,291 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user