Files
suixinkan_ios_new/suixinkan/Features/Tasks/Models/TaskModels.swift
汉秋 ffb16eca29 新增任务模块,并接入首页任务管理路由
实现任务列表、详情与创建页面,对接 API,支持云端/本地附件,并添加单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 15:26:13 +08:00

394 lines
15 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 AvailableOrderResponse: Decodable, Identifiable, Hashable {
var id: String { orderNumber }
let projectName: String
let orderNumber: String
let orderStatus: Int
let orderStatusLabel: String
let payTime: String
let userPhone: String
/// 线
enum CodingKeys: String, CodingKey {
case projectName = "project_name"
case orderNumber = "order_number"
case orderStatus = "order_status"
case orderStatusLabel = "order_status_label"
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)
orderStatus = try container.decodeLossyInt(forKey: .orderStatus) ?? 0
orderStatusLabel = try container.decodeLossyString(forKey: .orderStatusLabel)
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
}
}
///
struct CloudDriveFile: Decodable, Identifiable, Hashable {
let id: Int
let parentFolderId: Int
let fileUrl: String
let coverUrl: String
let updatedAt: String
let name: String
let createdAt: String
let childNum: Int
let type: Int
let fileSize: Int64
///
var isFolder: Bool { type == 99 }
///
var isVideo: Bool {
type == 1 || Self.hasVideoExtension(name) || Self.hasVideoExtension(fileUrl)
}
///
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 parentFolderId = "parent_folder_id"
case fileUrl = "file_url"
case coverUrl = "cover_url"
case updatedAt = "updated_at"
case name
case createdAt = "created_at"
case childNum = "child_num"
case type
case fileSize = "file_size"
}
///
init(id: Int, name: String) {
self.id = id
parentFolderId = 0
fileUrl = ""
coverUrl = ""
updatedAt = ""
self.name = name
createdAt = ""
childNum = 0
type = 99
fileSize = 0
}
///
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeLossyInt(forKey: .id) ?? 0
parentFolderId = try container.decodeLossyInt(forKey: .parentFolderId) ?? 0
fileUrl = try container.decodeLossyString(forKey: .fileUrl)
coverUrl = try container.decodeLossyString(forKey: .coverUrl)
updatedAt = try container.decodeLossyString(forKey: .updatedAt)
name = try container.decodeLossyString(forKey: .name)
createdAt = try container.decodeLossyString(forKey: .createdAt)
childNum = try container.decodeLossyInt(forKey: .childNum) ?? 0
type = try container.decodeLossyInt(forKey: .type) ?? 0
fileSize = Int64(try container.decodeLossyInt(forKey: .fileSize) ?? 0)
}
}
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 ""
}
/// IntDouble
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
}
}