新增项目、排期与邀请模块,并接入首页路由
将项目管理、排期管理与摄影师邀请流程从占位页迁移为完整实现,包含项目图片 OSS 上传与共享业务模型。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
166
suixinkan/Core/Models/SharedBusinessModels.swift
Normal file
166
suixinkan/Core/Models/SharedBusinessModels.swift
Normal file
@ -0,0 +1,166 @@
|
||||
//
|
||||
// SharedBusinessModels.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 可关联订单实体,表示任务、排班等业务表单中可选择的订单。
|
||||
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.decodeSharedLossyString(forKey: .projectName)
|
||||
orderNumber = try container.decodeSharedLossyString(forKey: .orderNumber)
|
||||
orderStatus = try container.decodeSharedLossyInt(forKey: .orderStatus) ?? 0
|
||||
orderStatusLabel = try container.decodeSharedLossyString(forKey: .orderStatusLabel)
|
||||
payTime = try container.decodeSharedLossyString(forKey: .payTime)
|
||||
userPhone = try container.decodeSharedLossyString(forKey: .userPhone)
|
||||
}
|
||||
}
|
||||
|
||||
/// 摄影项目列表项实体,表示项目管理列表和样片上传时可关联的项目。
|
||||
struct PhotographerProjectItem: Decodable, Identifiable, Hashable {
|
||||
let id: Int
|
||||
let type: Int
|
||||
let typeName: String
|
||||
let status: Int
|
||||
let statusName: String
|
||||
let name: String
|
||||
let coverProject: String
|
||||
let coverVideo: String
|
||||
let price: String
|
||||
let otPrice: String
|
||||
let priceDeposit: String
|
||||
let attrLabel: [String]
|
||||
let label: String
|
||||
|
||||
/// 创建摄影项目列表项,主要用于详情页摘要和测试替身。
|
||||
init(
|
||||
id: Int,
|
||||
type: Int = 0,
|
||||
typeName: String = "",
|
||||
status: Int = 0,
|
||||
statusName: String = "",
|
||||
name: String,
|
||||
coverProject: String = "",
|
||||
coverVideo: String = "",
|
||||
price: String = "",
|
||||
otPrice: String = "",
|
||||
priceDeposit: String = "",
|
||||
attrLabel: [String] = [],
|
||||
label: String = ""
|
||||
) {
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.typeName = typeName
|
||||
self.status = status
|
||||
self.statusName = statusName
|
||||
self.name = name
|
||||
self.coverProject = coverProject
|
||||
self.coverVideo = coverVideo
|
||||
self.price = price
|
||||
self.otPrice = otPrice
|
||||
self.priceDeposit = priceDeposit
|
||||
self.attrLabel = attrLabel
|
||||
self.label = label
|
||||
}
|
||||
|
||||
/// 字段映射,兼容后端下划线命名。
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case type
|
||||
case typeName = "type_name"
|
||||
case status
|
||||
case statusName = "status_name"
|
||||
case name
|
||||
case coverProject = "cover_project"
|
||||
case coverVideo = "cover_video"
|
||||
case price
|
||||
case otPrice = "ot_price"
|
||||
case priceDeposit = "price_deposit"
|
||||
case attrLabel = "attr_label"
|
||||
case label
|
||||
}
|
||||
|
||||
/// 宽松解码项目列表项。
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try container.decodeSharedLossyInt(forKey: .id) ?? 0
|
||||
type = try container.decodeSharedLossyInt(forKey: .type) ?? 0
|
||||
typeName = try container.decodeSharedLossyString(forKey: .typeName)
|
||||
status = try container.decodeSharedLossyInt(forKey: .status) ?? 0
|
||||
statusName = try container.decodeSharedLossyString(forKey: .statusName)
|
||||
name = try container.decodeSharedLossyString(forKey: .name)
|
||||
coverProject = try container.decodeSharedLossyString(forKey: .coverProject)
|
||||
coverVideo = try container.decodeSharedLossyString(forKey: .coverVideo)
|
||||
price = try container.decodeSharedLossyString(forKey: .price)
|
||||
otPrice = try container.decodeSharedLossyString(forKey: .otPrice)
|
||||
priceDeposit = try container.decodeSharedLossyString(forKey: .priceDeposit)
|
||||
label = try container.decodeSharedLossyString(forKey: .label)
|
||||
if let labels = try? container.decodeIfPresent([String].self, forKey: .attrLabel) {
|
||||
attrLabel = labels
|
||||
} else if let text = try? container.decodeIfPresent(String.self, forKey: .attrLabel) {
|
||||
attrLabel = text
|
||||
.components(separatedBy: CharacterSet(charactersIn: ",,"))
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
} else {
|
||||
attrLabel = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension KeyedDecodingContainer {
|
||||
/// 将 String、数字和 Bool 宽松解码为字符串。
|
||||
func decodeSharedLossyString(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 ? "1" : "0"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/// 将 String、Double 和 Int 宽松解码为 Int。
|
||||
func decodeSharedLossyInt(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
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,9 @@ protocol OSSUploadServing {
|
||||
/// 上传任务附件,并返回可访问的 OSS 文件 URL。
|
||||
func uploadTaskFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String
|
||||
|
||||
/// 上传项目图片,并返回可访问的 OSS 文件 URL。
|
||||
func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String
|
||||
|
||||
/// 上传打卡点图片,并返回可访问的 OSS 文件 URL。
|
||||
func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String
|
||||
|
||||
@ -77,6 +80,11 @@ final class OSSUploadService {
|
||||
try await uploadFile(data: data, fileName: fileName, fileType: fileType, scenicId: scenicId, moduleType: "task_upload", onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 上传项目图片,并返回可访问的 OSS 文件 URL。
|
||||
func uploadProjectImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await uploadFile(data: data, fileName: fileName, fileType: 2, scenicId: scenicId, moduleType: "project", onProgress: onProgress)
|
||||
}
|
||||
|
||||
/// 上传打卡点图片,并返回可访问的 OSS 文件 URL。
|
||||
func uploadPunchPointImage(data: Data, fileName: String, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
||||
try await uploadFile(data: data, fileName: fileName, fileType: 2, scenicId: scenicId, moduleType: "punch_point", onProgress: onProgress)
|
||||
@ -188,6 +196,8 @@ enum OSSUploadPolicy {
|
||||
return "cloud_driver/\(currentDate)/\(scenicId)/\(uploadId)_\(safeName)"
|
||||
case "album_upload":
|
||||
return "album/\(currentDate)/\(scenicId)/\(uploadId)_\(safeName)"
|
||||
case "project":
|
||||
return "project/\(currentDate)/\(scenicId)/\(uploadId)_\(safeName)"
|
||||
case "punch_point":
|
||||
return "punch_point/\(currentDate)/\(scenicId)/\(uploadId)_\(safeName)"
|
||||
case "scenic_apply":
|
||||
|
||||
Reference in New Issue
Block a user