Migrate project management, schedule management, and photographer invitation flows from placeholders, including project image OSS upload and shared business models. Co-authored-by: Cursor <cursoragent@cursor.com>
292 lines
11 KiB
Swift
292 lines
11 KiB
Swift
//
|
||
// 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
|
||
}
|
||
}
|